From 30ee7edd1276e6b09f1f0faa0ee3e2dff5932800 Mon Sep 17 00:00:00 2001 From: "Mims H. Wright" Date: Sat, 15 Mar 2025 00:23:24 +0100 Subject: [PATCH 001/134] Added an issue template (#189) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..70169028 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,64 @@ +name: Bug Report +description: Report a bug in the project +title: "[bug]: " +labels: ["bug", "needs triage"] +assignees: [] + +body: + - type: markdown + attributes: + value: "## 🐛 Bug Report\nPlease fill out the details below." + + - type: textarea + id: reproduction + attributes: + label: "Settings / Steps to Reproduce" + description: "Provide a step-by-step guide on how to reproduce the bug. Please include the plugin settings you were using." + placeholder: "Framework: Tailwind with JSX\n1. Selected design\n2. Selected Optimize Layers\n3. See error" + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: "Expected Behavior" + description: "What did you expect to happen?" + placeholder: "The button should navigate to the dashboard." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: "Actual Behavior" + description: "What actually happened?" + placeholder: "The button does nothing." + validations: + required: true + + - type: input + id: design-link + attributes: + label: "Design Reference" + description: "Provide a link to the design that you were working on (if possible)." + placeholder: "Figma design link" + validations: + required: false + + - type: textarea + id: screenshots + attributes: + label: "Screenshots or Videos" + description: "If applicable, add screenshots or screen recordings to help explain the issue." + placeholder: "Drag and drop images here or paste them from your clipboard." + validations: + required: false + + - type: textarea + id: environment + attributes: + label: "Environment" + description: "Optionally provide details about your development environment." + placeholder: "OS: macOS 14.1\nBrowser: Chrome 120\nNode.js: 18.17.0" + validations: + required: true From afcc5dc90bf2b8cdd7404a1e1ed4f3a5015b26da Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Sat, 1 Mar 2025 17:24:32 -0300 Subject: [PATCH 002/134] Total refactor part 1. --- apps/debug/package.json | 10 +- apps/plugin/package.json | 16 +- apps/plugin/plugin-src/code.ts | 52 +- package.json | 8 +- packages/backend/package.json | 10 +- .../backend/src/altNodes/altConversion.ts | 87 +- packages/backend/src/code.ts | 158 +- packages/backend/src/common/color.ts | 101 + packages/backend/src/common/nodeVisibility.ts | 4 +- .../src/flutter/builderImpl/flutterBorder.ts | 2 +- .../src/flutter/builderImpl/flutterColor.ts | 24 +- .../backend/src/flutter/flutterContainer.ts | 4 +- packages/backend/src/flutter/flutterMain.ts | 3 +- .../backend/src/html/builderImpl/htmlBlend.ts | 4 +- packages/backend/src/html/htmlMain.ts | 9 +- .../src/swiftui/builderImpl/swiftuiColor.ts | 31 +- .../src/swiftui/swiftuiDefaultBuilder.ts | 2 +- packages/backend/src/swiftui/swiftuiMain.ts | 3 +- .../builderImpl/tailwindAutoLayout.ts | 21 +- .../tailwind/builderImpl/tailwindBorder.ts | 1 - .../src/tailwind/builderImpl/tailwindColor.ts | 11 +- .../src/tailwind/tailwindDefaultBuilder.ts | 3 +- packages/backend/src/tailwind/tailwindMain.ts | 272 ++- packages/eslint-config-custom/package.json | 4 +- packages/plugin-ui/package.json | 6 +- .../plugin-ui/src/codegenPreferenceOptions.ts | 24 +- .../plugin-ui/src/components/CodePanel.tsx | 8 +- packages/types/package.json | 8 +- packages/types/src/types.ts | 1 + pnpm-lock.yaml | 2057 +++++++++-------- 30 files changed, 1662 insertions(+), 1282 deletions(-) diff --git a/apps/debug/package.json b/apps/debug/package.json index 044d2f36..1efead3a 100644 --- a/apps/debug/package.json +++ b/apps/debug/package.json @@ -11,21 +11,21 @@ }, "dependencies": { "backend": "workspace:*", - "next": "^14.2.20", + "next": "^14.2.24", "plugin-ui": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { - "@types/node": "^20.17.10", - "@types/react": "^18.3.17", + "@types/node": "^20.17.21", + "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "autoprefixer": "^10.4.20", "eslint-config-custom": "workspace:*", - "postcss": "^8.4.49", + "postcss": "^8.5.3", "tailwindcss": "3.4.6", "tsconfig": "workspace:*", "types": "workspace:*", - "typescript": "^5.7.2" + "typescript": "^5.8.2" } } diff --git a/apps/plugin/package.json b/apps/plugin/package.json index 12272dbc..4f69a1c1 100644 --- a/apps/plugin/package.json +++ b/apps/plugin/package.json @@ -10,32 +10,32 @@ "dev": "pnpm build:watch" }, "dependencies": { - "@figma/plugin-typings": "^1.105.0", + "@figma/plugin-typings": "^1.108.0", "backend": "workspace:*", "plugin-ui": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { - "@types/node": "^20.17.10", - "@types/react": "^18.3.17", + "@types/node": "^20.17.21", + "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@vitejs/plugin-react": "^4.3.4", - "@vitejs/plugin-react-swc": "^3.7.2", + "@vitejs/plugin-react-swc": "^3.8.0", "autoprefixer": "^10.4.20", "concurrently": "^8.2.2", "esbuild": "^0.23.1", "eslint-config-custom": "workspace:*", "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.16", - "postcss": "^8.4.49", + "eslint-plugin-react-refresh": "^0.4.19", + "postcss": "^8.5.3", "tailwindcss": "3.4.6", "tsconfig": "workspace:*", - "typescript": "^5.7.2", "types": "workspace:*", - "vite": "^5.4.11", + "typescript": "^5.8.2", + "vite": "^5.4.14", "vite-plugin-singlefile": "^2.1.0" } } diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 8a984412..093809bb 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -8,6 +8,7 @@ import { htmlMain, postSettingsChanged, } from "backend"; +import { nodesToJSON } from "backend/src/code"; import { retrieveGenericSolidUIColors } from "backend/src/common/retrieveUI/retrieveColors"; import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain"; import { htmlCodeGenTextStyles } from "backend/src/html/htmlMain"; @@ -30,6 +31,7 @@ export const defaultPluginSettings: PluginSettings = { customTailwindColors: false, customTailwindPrefix: "", embedImages: false, + embedVectors: false, }; // A helper type guard to ensure the key belongs to the PluginSettings type @@ -38,8 +40,13 @@ function isKeyOfPluginSettings(key: string): key is keyof PluginSettings { } const getUserSettings = async () => { + console.log("[DEBUG] getUserSettings - Starting to fetch user settings"); const possiblePluginSrcSettings = (await figma.clientStorage.getAsync("userPluginSettings")) ?? {}; + console.log( + "[DEBUG] getUserSettings - Raw settings from storage:", + possiblePluginSrcSettings, + ); const updatedPluginSrcSettings = { ...defaultPluginSettings, @@ -57,46 +64,72 @@ const getUserSettings = async () => { }; userPluginSettings = updatedPluginSrcSettings as PluginSettings; + console.log("[DEBUG] getUserSettings - Final settings:", userPluginSettings); + return userPluginSettings; }; const initSettings = async () => { + console.log("[DEBUG] initSettings - Initializing plugin settings"); await getUserSettings(); postSettingsChanged(userPluginSettings); + console.log("[DEBUG] initSettings - Calling safeRun with settings"); safeRun(userPluginSettings); }; // Used to prevent running from happening again. let isLoading = false; const safeRun = async (settings: PluginSettings) => { + console.log( + "[DEBUG] safeRun - Called with isLoading =", + isLoading, + "selection =", + figma.currentPage.selection, + ); if (isLoading === false) { try { isLoading = true; + console.log("[DEBUG] safeRun - Starting run execution"); await run(settings); + console.log("[DEBUG] safeRun - Run execution completed"); // hack to make it not immediately set to false when complete. (executes on next frame) setTimeout(() => { + console.log("[DEBUG] safeRun - Resetting isLoading to false"); isLoading = false; }, 1); } catch (e) { + console.log("[DEBUG] safeRun - Error caught in execution"); + isLoading = false; // Make sure to reset the flag on error if (e && typeof e === "object" && "message" in e) { const error = e as Error; console.log("error: ", error.stack); figma.ui.postMessage({ type: "error", error: error.message }); } } + } else { + console.log( + "[DEBUG] safeRun - Skipping execution because isLoading =", + isLoading, + ); } }; const standardMode = async () => { + console.log("[DEBUG] standardMode - Starting standard mode initialization"); figma.showUI(__html__, { width: 450, height: 700, themeColors: true }); await initSettings(); // Listen for selection changes figma.on("selectionchange", () => { + console.log( + "[DEBUG] selectionchange event - New selection:", + figma.currentPage.selection, + ); safeRun(userPluginSettings); }); // Listen for document changes figma.on("documentchange", () => { + console.log("[DEBUG] documentchange event triggered"); // Node: This was causing an infinite load when you try to export a background image from a group that contains children. // The reason for this is that the code will temporarily hide the children of the group in order to export a clean image // then restores the visibility of the children. This constitutes a document change so it's restarting the whole conversion. @@ -105,10 +138,11 @@ const standardMode = async () => { }); figma.ui.onmessage = (msg) => { - console.log("[node] figma.ui.onmessage", msg); + console.log("[DEBUG] figma.ui.onmessage", msg); if (msg.type === "pluginSettingWillChange") { const { key, value } = msg as SettingWillChangeMessage; + console.log(`[DEBUG] Setting changed: ${key} = ${value}`); (userPluginSettings as any)[key] = value; figma.clientStorage.setAsync("userPluginSettings", userPluginSettings); safeRun(userPluginSettings); @@ -117,13 +151,24 @@ const standardMode = async () => { }; const codegenMode = async () => { + console.log("[DEBUG] codegenMode - Starting codegen mode initialization"); // figma.showUI(__html__, { visible: false }); await getUserSettings(); figma.codegen.on( "generate", async ({ language, node }: CodegenEvent): Promise => { - const convertedSelection = convertIntoNodes([node], null); + console.log( + `[DEBUG] codegen.generate - Language: ${language}, Node:`, + node, + ); + + const nodeJson = await nodesToJSON([node]); + const convertedSelection = await convertIntoNodes(nodeJson, null); + console.log( + "[DEBUG] codegen.generate - Converted selection:", + convertedSelection, + ); switch (language) { case "html": @@ -243,11 +288,14 @@ const codegenMode = async () => { switch (figma.mode) { case "default": case "inspect": + console.log("[DEBUG] Starting plugin in", figma.mode, "mode"); standardMode(); break; case "codegen": + console.log("[DEBUG] Starting plugin in codegen mode"); codegenMode(); break; default: + console.log("[DEBUG] Unknown plugin mode:", figma.mode); break; } diff --git a/package.json b/package.json index 5257cd4a..41e66f7a 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "format": "prettier --write \"**/*.{ts,tsx,css,md}\"" }, "devDependencies": { - "eslint": "^9.17.0", + "eslint": "^9.21.0", "eslint-config-custom": "workspace:*", - "prettier": "^3.4.2", - "turbo": "^2.3.3", - "typescript": "^5.7.2" + "prettier": "^3.5.2", + "turbo": "^2.4.4", + "typescript": "^5.8.2" } } diff --git a/packages/backend/package.json b/packages/backend/package.json index d8aa0167..baab8739 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -13,19 +13,19 @@ "lint": "eslint \"src/**/*.ts*\"" }, "dependencies": { - "@figma/plugin-typings": "^1.105.0", + "@figma/plugin-typings": "^1.108.0", "js-base64": "^3.7.7", "react": "18.3.1", "react-dom": "18.3.1", "types": "workspace:*" }, "devDependencies": { - "@types/react": "^18.3.17", + "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", - "eslint": "^9.17.0", + "eslint": "^9.21.0", "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", - "tsup": "^8.3.5", - "typescript": "^5.7.2" + "tsup": "^8.4.0", + "typescript": "^5.8.2" } } diff --git a/packages/backend/src/altNodes/altConversion.ts b/packages/backend/src/altNodes/altConversion.ts index c74c1440..59b0db70 100644 --- a/packages/backend/src/altNodes/altConversion.ts +++ b/packages/backend/src/altNodes/altConversion.ts @@ -20,7 +20,9 @@ const canBeFlattened = isTypeOrGroupOfTypes([ export const convertNodeToAltNode = (parent: ParentNode | null) => - (node: SceneNode): SceneNode => { + async (node: SceneNode): Promise => { + console.log("node is", node); + (node as any).canBeFlattened = canBeFlattened(node); const type = node.type; switch (type) { // Standard nodes @@ -31,7 +33,7 @@ export const convertNodeToAltNode = case "POLYGON": case "VECTOR": case "BOOLEAN_OPERATION": - return cloneNode(node, parent); + return node; // Group nodes case "FRAME": @@ -39,31 +41,24 @@ export const convertNodeToAltNode = case "COMPONENT": case "COMPONENT_SET": // if the frame, instance etc. has no children, convert the frame to rectangle - if (node.children.length === 0) - return cloneAsRectangleNode(node, parent); - // goto SECTION - + if (node.children.length === 0) return cloneAsRectangleNode(node); case "GROUP": // if a Group is visible and has only one child, the Group should be ungrouped. if (type === "GROUP" && node.children.length === 1 && node.visible) return convertNodeToAltNode(parent)(node.children[0]); - // goto SECTION - case "SECTION": - const group = cloneNode(node, parent); - const groupChildren = convertNodesToAltNodes(node.children, group); - return assignChildren(groupChildren, group); + const groupChildren = await convertNodesToAltNodes(node.children, node); + return assignChildren(groupChildren, node); // Text Nodes case "TEXT": - globalTextStyleSegments[node.id] = extractStyledTextSegments(node); - return cloneNode(node, parent); + const textNode = (await figma.getNodeByIdAsync(node.id)) as TextNode; + globalTextStyleSegments[node.id] = extractStyledTextSegments(textNode); + return node; // Unsupported Nodes case "SLICE": - throw new Error( - `Sorry, Slices are not supported. Type:${node.type} id:${node.id}`, - ); + return null; default: throw new Error( `Sorry, an unsupported node type was selected. Type:${node.type} id:${node.id}`, @@ -71,58 +66,19 @@ export const convertNodeToAltNode = } }; -export const convertNodesToAltNodes = ( +export const convertNodesToAltNodes = async ( sceneNode: ReadonlyArray, parent: ParentNode | null, -): Array => - sceneNode.map(convertNodeToAltNode(parent)).filter(isNotEmpty); - -export const cloneNode = ( - node: T, - parent: ParentNode | null, -): T => { - // Create the cloned object with the correct prototype - const cloned = {} as T; - // Create a new object with only the desired descriptors (excluding 'parent' and 'children') - for (const prop in node) { - if ( - prop !== "parent" && - prop !== "children" && - prop !== "horizontalPadding" && - prop !== "verticalPadding" && - prop !== "mainComponent" && - prop !== "masterComponent" && - prop !== "variantProperties" && - prop !== "get_annotations" && - prop !== "componentPropertyDefinitions" && - prop !== "exposedInstances" && - prop !== "componentProperties" && - prop !== "componenPropertyReferences" && - prop !== "constrainProportions" - ) { - cloned[prop as keyof T] = node[prop as keyof T]; - } - } - assignParent(parent, cloned); - - const altNode = { - ...cloned, - originalNode: node, - canBeFlattened: canBeFlattened(node), - } as AltNode; - return altNode; -}; +): Promise> => + (await Promise.all(sceneNode.map(convertNodeToAltNode(parent)))).filter( + isNotEmpty, + ); // auto convert Frame to Rectangle when Frame has no Children -const cloneAsRectangleNode = ( - node: T, - parent: ParentNode | null, -): RectangleNode => { - const clonedNode = cloneNode(node, parent); - - assignRectangleType(clonedNode); +const cloneAsRectangleNode = (node: T): RectangleNode => { + assignRectangleType(node); - return clonedNode as unknown as RectangleNode; + return node as unknown as RectangleNode; }; const extractStyledTextSegments = (node: TextNode) => @@ -138,6 +94,11 @@ const extractStyledTextSegments = (node: TextNode) => "listOptions", "textCase", "textDecoration", + "textDecorationStyle", + "textDecorationOffset", + "textDecorationThickness", + "textDecorationColor", + "textDecorationSkipInk", "textStyleId", "fillStyleId", "openTypeFeatures", diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 1dd94cea..32589181 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -17,10 +17,156 @@ import { import { PluginSettings } from "types"; import { convertToCode } from "./common/retrieveUI/convertToCode"; +// Helper function to add parent references to all children in the node tree +const addParentReferences = (node: any) => { + if (node.children && node.children.length > 0) { + for (const child of node.children) { + // Add parent reference to the child + child.parent = node; + // Recursively process this child's children + addParentReferences(child); + } + } +}; + +// Define all property paths that might contain gradients +const GRADIENT_PROPERTIES = ["fills", "strokes", "effects"]; + +/** + * Recursively process node and its children to update with data not available in JSON + * @param node The node to process + * @param optimizeLayout Whether to extract and include inferredAutoLayout data + */ +const processNodeData = (node: any, optimizeLayout: boolean) => { + if (node.id) { + // Check if we need to fetch the Figma node at all + const hasGradient = GRADIENT_PROPERTIES.some((propName) => { + const property = node[propName]; + return ( + property && + Array.isArray(property) && + property.length > 0 && + property.some( + (item: any) => item.type && item.type.startsWith("GRADIENT_"), + ) + ); + }); + + // Only fetch the Figma node if we have gradients or optimizeLayout is enabled + if (hasGradient || optimizeLayout) { + try { + const figmaNode = figma.getNodeById(node.id); + if (figmaNode) { + // Handle gradients if needed + if (hasGradient) { + GRADIENT_PROPERTIES.forEach((propName) => { + const property = node[propName]; + if (property && Array.isArray(property) && property.length > 0) { + // We already know there's a gradient in at least one property + if ( + property.some( + (item: any) => + item.type && item.type.startsWith("GRADIENT_"), + ) && + propName in figmaNode + ) { + // Replace with the actual property that contains proper gradient transforms + node[propName] = JSON.parse( + JSON.stringify((figmaNode as any)[propName]), + ); + } + } + }); + } + + // Extract inferredAutoLayout if optimizeLayout is enabled + if (optimizeLayout && "inferredAutoLayout" in figmaNode) { + node.inferredAutoLayout = JSON.parse( + JSON.stringify((figmaNode as any).inferredAutoLayout), + ); + } + + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; + } + } catch (e) { + // Silently fail if there's an error accessing the Figma node + } + } else { + // Avoid calling getNodeById if we don't need to + if (node.rotation && node.rotation !== 0) { + const figmaNode = figma.getNodeById(node.id); + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; + } else { + // Use the absoluteRenderBounds if we don't need to fetch the Figma node. + node.width = node.absoluteRenderBounds.width; + node.height = node.absoluteRenderBounds.height; + node.x = node.absoluteRenderBounds.x; + node.y = node.absoluteRenderBounds.y; + } + } + + if (!node.LayoutMode) { + node.LayoutMode = "NONE"; + } + if (!node.layoutGrow) { + node.layoutGrow = 0; + } + if (!node.layoutSizingHorizontal) { + node.layoutSizingHorizontal = "FIXED"; + } + if (!node.layoutSizingVertical) { + node.layoutSizingVertical = "FIXED"; + } + } + + // Process children recursively + if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: any) => + processNodeData(child, optimizeLayout), + ); + } +}; + +/** + * Convert Figma nodes to JSON format with parent references added + * @param nodes The Figma nodes to convert to JSON + * @param optimizeLayout Whether to extract and include inferredAutoLayout data + * @returns JSON representation of the nodes with parent references + */ +export const nodesToJSON = async ( + nodes: ReadonlyArray, + optimizeLayout: boolean = false, +): Promise => { + const nodeJson = (await Promise.all( + nodes.map( + async (node) => + ( + (await node.exportAsync({ + format: "JSON_REST_V1", + })) as any + ).document, + ), + )) as SceneNode[]; + + // Process gradients and inferredAutoLayout in the JSON tree before adding parent references + nodeJson.forEach((node) => processNodeData(node, optimizeLayout)); + + // Add parent references to all children in the node tree + nodeJson.forEach((node) => addParentReferences(node)); + + return nodeJson; +}; + export const run = async (settings: PluginSettings) => { clearWarnings(); - const { framework } = settings; + const { framework, optimizeLayout } = settings; const selection = figma.currentPage.selection; if (selection.length > 1) { @@ -29,7 +175,15 @@ export const run = async (settings: PluginSettings) => { ); } - const convertedSelection = convertNodesToAltNodes(selection, null); + const nodeJson = await nodesToJSON(selection, optimizeLayout); + console.log("nodeJson", nodeJson); + + postConversionStart(); + // force postMessage to run right now. + await new Promise((resolve) => setTimeout(resolve, 30)); + + // Now we work directly with the JSON nodes + const convertedSelection = await convertNodesToAltNodes(nodeJson, null); // ignore when nothing was selected // If the selection was empty, the converted selection will also be empty. diff --git a/packages/backend/src/common/color.ts b/packages/backend/src/common/color.ts index 79f07d94..85d5e6b6 100644 --- a/packages/backend/src/common/color.ts +++ b/packages/backend/src/common/color.ts @@ -1,3 +1,7 @@ +import { retrieveTopFill } from "./retrieveFill"; +import { numberToFixedString } from "./numToAutoFixed"; + +// ---- Color Format Conversion ---- export const rgbTo6hex = (color: RGB | RGBA): string => { const hex = ((color.r * 255) | (1 << 8)).toString(16).slice(1) + @@ -19,6 +23,42 @@ export const rgbTo8hex = (color: RGB, alpha: number): string => { return hex; }; +/** + * Converts RGB values to CSS hex or rgba format + * @param color The RGB color object + * @param alpha The opacity value + * @returns A CSS color string + */ +export const rgbToCssColor = (color: RGB | RGBA, alpha: number = 1): string => { + // Special cases for common colors + if (color.r === 1 && color.g === 1 && color.b === 1 && alpha === 1) { + return "white"; + } + + if (color.r === 0 && color.g === 0 && color.b === 0 && alpha === 1) { + return "black"; + } + + // Return hex when possible (no transparency) + if (alpha === 1) { + const r = Math.round(color.r * 255); + const g = Math.round(color.g * 255); + const b = Math.round(color.b * 255); + + const toHex = (num: number): string => num.toString(16).padStart(2, "0"); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); + } + + // Use rgba for transparent colors + const r = numberToFixedString(color.r * 255); + const g = numberToFixedString(color.g * 255); + const b = numberToFixedString(color.b * 255); + const a = numberToFixedString(alpha); + + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +// ---- Gradient Transformation ---- export const gradientAngle = (fill: GradientPaint): number => { // Thanks Gleb and Liam for helping! const decomposed = decomposeRelativeTransform( @@ -28,6 +68,36 @@ export const gradientAngle = (fill: GradientPaint): number => { return (decomposed.rotation * 180) / Math.PI; }; + +// Calculate gradient angle for CSS (different coordinate system) +export const cssGradientAngle = (angle: number): number => { + // Normalize angle: if negative, add 360 to make it positive. + return angle < 0 ? angle + 360 : angle; +}; + +// Calculate gradient coordinates for a matrix transform +export const getGradientTransformCoordinates = ( + gradientTransform: number[][], +): { centerX: string; centerY: string; radiusX: string; radiusY: string } => { + const a = gradientTransform[0][0]; + const b = gradientTransform[0][1]; + const c = gradientTransform[1][0]; + const d = gradientTransform[1][1]; + const e = gradientTransform[0][2]; + const f = gradientTransform[1][2]; + + const scaleX = Math.sqrt(a ** 2 + b ** 2); + const scaleY = Math.sqrt(c ** 2 + d ** 2); + + const centerX = ((e * scaleX * 100) / (1 - scaleX)).toFixed(2); + const centerY = (((1 - f) * scaleY * 100) / (1 - scaleY)).toFixed(2); + + const radiusX = (scaleX * 100).toFixed(2); + const radiusY = (scaleY * 100).toFixed(2); + + return { centerX, centerY, radiusX, radiusY }; +}; + // from https://math.stackexchange.com/a/2888105 export const decomposeRelativeTransform = ( t1: [number, number, number], @@ -79,3 +149,34 @@ export const decomposeRelativeTransform = ( return result; }; + +// ---- Common color check helpers ---- + +/** + * Checks if color is black + */ +export const isBlack = (color: RGB, opacity: number = 1): boolean => + color.r === 0 && color.g === 0 && color.b === 0 && opacity === 1; + +/** + * Checks if color is white + */ +export const isWhite = (color: RGB, opacity: number = 1): boolean => + color.r === 1 && color.g === 1 && color.b === 1 && opacity === 1; + +/** + * Helper for calculating gradient stops in a consistent way across frameworks + */ +export const processGradientStops = ( + stops: ReadonlyArray, + opacity: number = 1, + colorFormatter: (color: RGB | RGBA, alpha: number) => string, +): string => { + return stops + .map((stop) => { + const color = colorFormatter(stop.color, stop.color.a * opacity); + const position = `${(stop.position * 100).toFixed(0)}%`; + return `${color} ${position}`; + }) + .join(", "); +}; diff --git a/packages/backend/src/common/nodeVisibility.ts b/packages/backend/src/common/nodeVisibility.ts index 8e3959d7..7e4eb6f4 100644 --- a/packages/backend/src/common/nodeVisibility.ts +++ b/packages/backend/src/common/nodeVisibility.ts @@ -1,4 +1,2 @@ -type VisibilityMixin = { visible: boolean }; -const isVisible = (node: VisibilityMixin) => node.visible; export const getVisibleNodes = (nodes: readonly SceneNode[]) => - nodes.filter(isVisible); + nodes.filter((d) => d.visible ?? true); diff --git a/packages/backend/src/flutter/builderImpl/flutterBorder.ts b/packages/backend/src/flutter/builderImpl/flutterBorder.ts index 8073f4ec..f3e22b1d 100644 --- a/packages/backend/src/flutter/builderImpl/flutterBorder.ts +++ b/packages/backend/src/flutter/builderImpl/flutterBorder.ts @@ -17,7 +17,7 @@ export const flutterBorder = (node: SceneNode): string => { } const color = skipDefaultProperty( - flutterColorFromFills(node.strokes), + flutterColorFromFills(node, "strokes"), "Colors.black", ); diff --git a/packages/backend/src/flutter/builderImpl/flutterColor.ts b/packages/backend/src/flutter/builderImpl/flutterColor.ts index ff3b1b0e..4fcf7dd2 100644 --- a/packages/backend/src/flutter/builderImpl/flutterColor.ts +++ b/packages/backend/src/flutter/builderImpl/flutterColor.ts @@ -10,10 +10,18 @@ import { getPlaceholderImage } from "../../common/images"; /** * Retrieve the SOLID color for Flutter when existent, otherwise "" + * @param node SceneNode containing the property to examine + * @param propertyPath Property path to extract fills from (e.g., 'fills', 'strokes') or direct fills array */ export const flutterColorFromFills = ( - fills: ReadonlyArray | PluginAPI["mixed"], + node: SceneNode, + propertyPath: string, ): string => { + let fills: ReadonlyArray | PluginAPI["mixed"]; + fills = node[propertyPath as keyof SceneNode] as + | ReadonlyArray + | PluginAPI["mixed"]; + const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { @@ -32,10 +40,18 @@ export const flutterColorFromFills = ( return ""; }; +/** + * Get box decoration properties for a Flutter node + */ export const flutterBoxDecorationColor = ( node: SceneNode, - fills: ReadonlyArray | PluginAPI["mixed"], + propertyPath: string, ): Record => { + let fills: ReadonlyArray | PluginAPI["mixed"]; + fills = node[propertyPath as keyof SceneNode] as + | ReadonlyArray + | PluginAPI["mixed"]; + const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { @@ -178,13 +194,13 @@ export const flutterColor = (color: RGB, opacity: number): string => { if (sum === 0) { return opacity === 1 ? "Colors.black" - : `Colors.black.withOpacity(${opacity})`; + : `Colors.black.withOpacity(${numberToFixedString(opacity)})`; } if (sum === 3) { return opacity === 1 ? "Colors.white" - : `Colors.white.withOpacity(${opacity})`; + : `Colors.white.withOpacity(${numberToFixedString(opacity)})`; } return `Color(0x${rgbTo8hex(color, opacity).toUpperCase()})`; diff --git a/packages/backend/src/flutter/flutterContainer.ts b/packages/backend/src/flutter/flutterContainer.ts index 50df4b0e..a82e3419 100644 --- a/packages/backend/src/flutter/flutterContainer.ts +++ b/packages/backend/src/flutter/flutterContainer.ts @@ -85,7 +85,7 @@ const getDecoration = (node: SceneNode): string => { } const propBoxShadow = flutterShadow(node); - const decorationBackground = flutterBoxDecorationColor(node, node.fills); + const decorationBackground = flutterBoxDecorationColor(node, "fills"); let shapeDecorationBorder = ""; if (node.type === "STAR") { @@ -139,7 +139,7 @@ const generateBorderSideCode = ( "BorderSide.strokeAlignInside", ), color: skipDefaultProperty( - flutterColorFromFills(node.strokes), + flutterColorFromFills(node, "strokes"), "Colors.black", ), }), diff --git a/packages/backend/src/flutter/flutterMain.ts b/packages/backend/src/flutter/flutterMain.ts index d7e8738d..a2bda1be 100644 --- a/packages/backend/src/flutter/flutterMain.ts +++ b/packages/backend/src/flutter/flutterMain.ts @@ -15,6 +15,7 @@ import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildr import { PluginSettings } from "types"; import { addWarning } from "../common/commonConversionWarnings"; import { getPlaceholderImage } from "../common/images"; +import { getVisibleNodes } from "../common/nodeVisibility"; let localSettings: PluginSettings; let previousExecutionCache: string[]; @@ -88,7 +89,7 @@ const flutterWidgetGenerator = ( let comp: string[] = []; // filter non visible nodes. This is necessary at this step because conversion already happened. - const visibleSceneNode = sceneNode.filter((d) => d.visible); + const visibleSceneNode = getVisibleNodes(sceneNode); const sceneLen = visibleSceneNode.length; visibleSceneNode.forEach((node, index) => { diff --git a/packages/backend/src/html/builderImpl/htmlBlend.ts b/packages/backend/src/html/builderImpl/htmlBlend.ts index 75e3b750..1a68039a 100644 --- a/packages/backend/src/html/builderImpl/htmlBlend.ts +++ b/packages/backend/src/html/builderImpl/htmlBlend.ts @@ -124,7 +124,9 @@ export const htmlRotation = (node: LayoutMixin, isJsx: boolean): string[] => { "parent" in node && node.parent ? (node.parent as LayoutMixin) : null; const parentRotation: number = parent && "rotation" in parent ? parent.rotation : 0; - const rotation: number = Math.round(parentRotation - node.rotation) ?? 0; + + const nodeRotation = node.rotation || 0; + const rotation = Math.round(parentRotation - nodeRotation) ?? 0; if ( roundToNearestHundreth(parentRotation) !== 0 && diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 4d1399fe..99f4989e 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -11,7 +11,7 @@ import { HTMLSettings, ExportableNode, } from "types"; -import { isSVGNode, renderAndAttachSVG } from "../altNodes/altNodeUtils"; +import { renderAndAttachSVG } from "../altNodes/altNodeUtils"; import { getVisibleNodes } from "../common/nodeVisibility"; import { exportNodeAsBase64PNG, @@ -71,11 +71,12 @@ export const generateHTMLPreview = async ( }; }; -// todo lint idea: replace BorderRadius.only(topleft: 8, topRight: 8) with BorderRadius.horizontal(8) const htmlWidgetGenerator = async ( sceneNode: ReadonlyArray, settings: HTMLSettings, ): Promise => { + console.log("htmlWidgetGenerator", sceneNode); + // filter non visible nodes. This is necessary at this step because conversion already happened. const promiseOfConvertedCode = getVisibleNodes(sceneNode).map( convertNode(settings), @@ -85,7 +86,9 @@ const htmlWidgetGenerator = async ( }; const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { - if (isSVGNode(node)) { + console.log("converting", node); + + if ((node as any).canBeFlattened) { const altNode = await renderAndAttachSVG(node); if (altNode.svg) return htmlWrapSVG(altNode, settings); } diff --git a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts index f669aae5..9ecb7a19 100644 --- a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts +++ b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts @@ -5,6 +5,10 @@ import { numberToFixedString } from "../../common/numToAutoFixed"; import { addWarning } from "../../common/commonConversionWarnings"; import { getPlaceholderImage } from "../../common/images"; +/** + * Retrieve the SwiftUI color for a Paint object + * @param fill The paint object to extract color from + */ export const swiftUISolidColor = (fill: Paint): string => { if (fill && fill.type === "SOLID") { return swiftuiColor(fill.color, fill.opacity ?? 1.0); @@ -22,9 +26,22 @@ export const swiftUISolidColor = (fill: Paint): string => { return ""; }; +/** + * Retrieve the SwiftUI solid color when existent, otherwise "" + * @param node SceneNode containing the property to examine + * @param propertyPath Property path to extract fills from (e.g., 'fills', 'strokes') or direct fills array + */ export const swiftuiSolidColor = ( - fills: ReadonlyArray | PluginAPI["mixed"], + node: SceneNode, + propertyPath: string | keyof SceneNode, ): string => { + let fills: ReadonlyArray | PluginAPI["mixed"]; + + // Property path string provided + fills = node[propertyPath as keyof SceneNode] as + | ReadonlyArray + | PluginAPI["mixed"]; + const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { @@ -47,14 +64,22 @@ export const swiftuiSolidColor = ( return ""; }; +/** + * Get SwiftUI background for a node + * @param node SceneNode containing the property to examine + * @param propertyPath Property path to extract fills from (e.g., 'fills', 'strokes') or direct fills array + */ export const swiftuiBackground = ( node: SceneNode, - fills: ReadonlyArray | PluginAPI["mixed"], + propertyPath: string, ): string => { + const fills = node[propertyPath as keyof SceneNode] as + | ReadonlyArray + | PluginAPI["mixed"]; + const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { - // opacity should only be null on set, not on get. But better be prevented. const opacity = fill.opacity ?? 1.0; return swiftuiColor(fill.color, opacity); } else if (fill?.type === "GRADIENT_LINEAR") { diff --git a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts index 2e629073..7ae8d73e 100644 --- a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts @@ -105,7 +105,7 @@ export class SwiftuiDefaultBuilder { shapeBackground(node: SceneNode): this { if ("fills" in node) { - const background = swiftuiBackground(node, node.fills); + const background = swiftuiBackground(node, "fills"); if (background) { this.pushModifier([`background`, background]); } diff --git a/packages/backend/src/swiftui/swiftuiMain.ts b/packages/backend/src/swiftui/swiftuiMain.ts index 3aab3d87..bf068794 100644 --- a/packages/backend/src/swiftui/swiftuiMain.ts +++ b/packages/backend/src/swiftui/swiftuiMain.ts @@ -8,6 +8,7 @@ import { SwiftuiDefaultBuilder } from "./swiftuiDefaultBuilder"; import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; import { PluginSettings } from "types"; import { addWarning } from "../common/commonConversionWarnings"; +import { getVisibleNodes } from "../common/nodeVisibility"; let localSettings: PluginSettings; let previousExecutionCache: string[]; @@ -66,7 +67,7 @@ const swiftuiWidgetGenerator = ( indentLevel: number, ): string => { // filter non visible nodes. This is necessary at this step because conversion already happened. - const visibleSceneNode = sceneNode.filter((d) => d.visible); + const visibleSceneNode = getVisibleNodes(sceneNode); let comp: string[] = []; visibleSceneNode.forEach((node, index) => { diff --git a/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts b/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts index 8078968f..19cfdfa0 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts @@ -47,13 +47,14 @@ const getFlex = ( export const tailwindAutoLayoutProps = ( node: SceneNode, autoLayout: InferredAutoLayoutResult, -): string => - Object.values({ - flexDirection: getFlexDirection(autoLayout), - justifyContent: getJustifyContent(autoLayout), - alignItems: getAlignItems(autoLayout), - gap: getGap(autoLayout), - flex: getFlex(node, autoLayout), - }) - .filter((value) => value !== "") - .join(" "); +): string => { + const classes = [ + getFlex(node, autoLayout), + getFlexDirection(autoLayout), + getJustifyContent(autoLayout), + getAlignItems(autoLayout), + getGap(autoLayout), + ].filter(Boolean); + + return classes.join(" "); +}; diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts index 7cb57c6d..8639702f 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts @@ -1,6 +1,5 @@ import { getCommonRadius } from "../../common/commonRadius"; import { commonStroke } from "../../common/commonStroke"; -import { numberToFixedString } from "../../common/numToAutoFixed"; import { nearestValue, pxToBorderRadius } from "../conversionTables"; /** diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 82f726ab..0954cfe6 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -1,4 +1,3 @@ -import { retrieveTopFill } from "../../common/retrieveFill"; import { gradientAngle } from "../../common/color"; import { getColorInfo, @@ -7,6 +6,7 @@ import { } from "../conversionTables"; import { TailwindColorType } from "types"; import { addWarning } from "../../common/commonConversionWarnings"; +import { retrieveTopFill } from "../../common/retrieveFill"; /** * Get a tailwind color value object @@ -76,10 +76,6 @@ export const tailwindColorFromFills = ( return ""; }; -/** - * https://tailwindcss.com/docs/box-shadow/ - * example: shadow - */ export const tailwindGradientFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"], ): string => { @@ -114,24 +110,19 @@ export const tailwindGradient = (fill: GradientPaint): string => { if (fill.gradientStops.length === 1) { const fromColor = tailwindSolidColor(fill.gradientStops[0]); - return `${direction} from-${fromColor}`; } else if (fill.gradientStops.length === 2) { const fromColor = tailwindSolidColor(fill.gradientStops[0]); const toColor = tailwindSolidColor(fill.gradientStops[1]); - return `${direction} from-${fromColor} to-${toColor}`; } else { const fromColor = tailwindSolidColor(fill.gradientStops[0]); - // middle (second color) const viaColor = tailwindSolidColor(fill.gradientStops[1]); - // last const toColor = tailwindSolidColor( fill.gradientStops[fill.gradientStops.length - 1], ); - return `${direction} from-${fromColor} via-${viaColor} to-${toColor}`; } }; diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index e271b63d..93c7e8f1 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -45,7 +45,7 @@ export class TailwindDefaultBuilder { return this.settings.showLayerNames ? this.node.name : ""; } get visible() { - return this.node.visible; + return this.node.visible ?? true; } get isJSX() { return this.settings.jsx; @@ -156,7 +156,6 @@ export class TailwindDefaultBuilder { paint: ReadonlyArray | PluginAPI["mixed"], kind: TailwindColorType, ): this { - // visible is true or undefinied (tests) if (this.visible) { let gradient = ""; if (kind === "bg") { diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 335b892a..b39e6225 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -1,103 +1,107 @@ import { retrieveTopFill } from "../common/retrieveFill"; import { indentString } from "../common/indentString"; +import { addWarning } from "../common/commonConversionWarnings"; +import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; +import { getVisibleNodes } from "../common/nodeVisibility"; +import { getPlaceholderImage } from "../common/images"; import { TailwindTextBuilder } from "./tailwindTextBuilder"; import { TailwindDefaultBuilder } from "./tailwindDefaultBuilder"; import { tailwindAutoLayoutProps } from "./builderImpl/tailwindAutoLayout"; -import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; -import { AltNode, PluginSettings, TailwindSettings } from "types"; -import { addWarning } from "../common/commonConversionWarnings"; import { renderAndAttachSVG } from "../altNodes/altNodeUtils"; -import { getVisibleNodes } from "../common/nodeVisibility"; -import { getPlaceholderImage } from "../common/images"; +import { AltNode, PluginSettings, TailwindSettings } from "types"; export let localTailwindSettings: PluginSettings; - -let previousExecutionCache: { style: string; text: string }[]; - -const selfClosingTags = ["img"]; +let previousExecutionCache: { + style: string; + text: string; + openTypeFeatures: Record; +}[] = []; +const SELF_CLOSING_TAGS = ["img"]; export const tailwindMain = async ( sceneNode: Array, settings: PluginSettings, -) => { +): Promise => { localTailwindSettings = settings; previousExecutionCache = []; let result = await tailwindWidgetGenerator(sceneNode, settings); - // remove the initial \n that is made in Container. - if (result.length > 0 && result.startsWith("\n")) { - result = result.slice(1, result.length); + // Remove the initial newline that is made in Container + if (result.startsWith("\n")) { + result = result.slice(1); } return result; }; -// TODO: lint idea: replace BorderRadius.only(topleft: 8, topRight: 8) with BorderRadius.horizontal(8) const tailwindWidgetGenerator = async ( sceneNode: ReadonlyArray, settings: TailwindSettings, ): Promise => { - // filter non visible nodes. This is necessary at this step because conversion already happened. - const promiseOfConvertedCode = getVisibleNodes(sceneNode).map( - convertNode(settings), - ); + const visibleNodes = getVisibleNodes(sceneNode); + const promiseOfConvertedCode = visibleNodes.map(convertNode(settings)); const code = (await Promise.all(promiseOfConvertedCode)).join(""); return code; }; -const convertNode = (settings: TailwindSettings) => async (node: SceneNode) => { - const altNode = await renderAndAttachSVG(node); - if (altNode.svg) return tailwindWrapSVG(altNode, settings); - - switch (node.type) { - case "RECTANGLE": - case "ELLIPSE": - return tailwindContainer(node, "", "", settings); - case "GROUP": - return tailwindGroup(node, settings); - case "FRAME": - case "COMPONENT": - case "INSTANCE": - case "COMPONENT_SET": - return tailwindFrame(node, settings); - case "TEXT": - return tailwindText(node, settings); - case "LINE": - return tailwindLine(node, settings); - case "SECTION": - return tailwindSection(node, settings); - case "VECTOR": - addWarning("VectorNodes are not supported in Tailwind"); - break; - default: - addWarning(`${node.type} nodes are not supported in Tailwind`); - } - return ""; -}; +const convertNode = + (settings: TailwindSettings) => + async (node: SceneNode): Promise => { + console.log("altNode", node); + const altNode = await renderAndAttachSVG(node); + if (altNode.svg) { + return tailwindWrapSVG(altNode, settings); + } + + switch (node.type) { + case "RECTANGLE": + case "ELLIPSE": + return tailwindContainer(node, "", "", settings); + case "GROUP": + return tailwindGroup(node, settings); + case "FRAME": + case "COMPONENT": + case "INSTANCE": + case "COMPONENT_SET": + return tailwindFrame(node, settings); + case "TEXT": + return tailwindText(node, settings); + case "LINE": + return tailwindLine(node, settings); + case "SECTION": + return tailwindSection(node, settings); + case "VECTOR": + addWarning("VectorNodes are not supported in Tailwind"); + break; + default: + addWarning(`${node.type} nodes are not supported in Tailwind`); + } + return ""; + }; const tailwindWrapSVG = ( node: AltNode, settings: TailwindSettings, ): string => { - if (node.svg === "") return ""; + if (!node.svg) return ""; + const builder = new TailwindDefaultBuilder(node, settings) .addData("svg-wrapper") .position(); - return `\n\n${node.svg ?? ""}`; + return `\n\n${node.svg}`; }; -const tailwindGroup = async (node: GroupNode, settings: TailwindSettings) => { - // ignore the view when size is zero or less - // while technically it shouldn't get less than 0, due to rounding errors, - // it can get to values like: -0.000004196293048153166 - // also ignore if there are no children inside, which makes no sense +const tailwindGroup = async ( + node: GroupNode, + settings: TailwindSettings, +): Promise => { + // Ignore the view when size is zero or less or if there are no children if (node.width < 0 || node.height <= 0 || node.children.length === 0) { return ""; } - // this needs to be called after CustomNode because widthHeight depends on it const builder = new TailwindDefaultBuilder(node, settings) .blend() .size() @@ -105,9 +109,7 @@ const tailwindGroup = async (node: GroupNode, settings: TailwindSettings) => { if (builder.attributes || builder.style) { const attr = builder.build(""); - const generator = await tailwindWidgetGenerator(node.children, settings); - return `\n${indentString(generator)}\n`; } @@ -118,7 +120,7 @@ export const tailwindText = ( node: TextNode, settings: TailwindSettings, ): string => { - let layoutBuilder = new TailwindTextBuilder(node, settings) + const layoutBuilder = new TailwindTextBuilder(node, settings) .commonPositionStyles() .textAlign(); @@ -127,19 +129,19 @@ export const tailwindText = ( let content = ""; if (styledHtml.length === 1) { - layoutBuilder.addAttributes(styledHtml[0].style); - content = styledHtml[0].text; - - const additionalTag = - styledHtml[0].openTypeFeatures.SUBS === true - ? "sub" - : styledHtml[0].openTypeFeatures.SUPS === true - ? "sup" - : ""; - - if (additionalTag) { - content = `<${additionalTag}>${content}`; - } + const segment = styledHtml[0]; + layoutBuilder.addAttributes(segment.style); + + const getFeatureTag = (features: Record): string => { + if (features.SUBS === true) return "sub"; + if (features.SUPS === true) return "sup"; + return ""; + }; + + const additionalTag = getFeatureTag(segment.openTypeFeatures); + content = additionalTag + ? `<${additionalTag}>${segment.text}` + : segment.text; } else { content = styledHtml .map((style) => { @@ -162,47 +164,34 @@ const tailwindFrame = async ( node: FrameNode | InstanceNode | ComponentNode | ComponentSetNode, settings: TailwindSettings, ): Promise => { - const childrenStr = await tailwindWidgetGenerator( - commonSortChildrenWhenInferredAutoLayout( - node, - localTailwindSettings.optimizeLayout, - ), - settings, + const sortedChildren = commonSortChildrenWhenInferredAutoLayout( + node, + localTailwindSettings.optimizeLayout, ); + const childrenStr = await tailwindWidgetGenerator(sortedChildren, settings); - // Add overflow-hidden class if clipsContent is true const clipsContentClass = node.clipsContent ? " overflow-hidden" : ""; + let layoutProps = ""; if (node.layoutMode !== "NONE") { - const rowColumn = tailwindAutoLayoutProps(node, node); - return tailwindContainer( - node, - childrenStr, - rowColumn + clipsContentClass, - settings, - ); - } else { - if ( - localTailwindSettings.optimizeLayout && - node.inferredAutoLayout !== null - ) { - const rowColumn = tailwindAutoLayoutProps(node, node.inferredAutoLayout); - return tailwindContainer( - node, - childrenStr, - rowColumn + clipsContentClass, - settings, - ); - } - - // node.layoutMode === "NONE" && node.children.length > 1 - // children needs to be absolute - return tailwindContainer(node, childrenStr, clipsContentClass, settings); + layoutProps = tailwindAutoLayoutProps(node, node); + } else if ( + localTailwindSettings.optimizeLayout && + node.inferredAutoLayout !== null + ) { + layoutProps = tailwindAutoLayoutProps(node, node.inferredAutoLayout); } + + return tailwindContainer( + node, + childrenStr, + layoutProps + clipsContentClass, + settings, + ); }; -// properties named propSomething always take care of "," -// sometimes a property might not exist, so it doesn't add "," +// Properties named propSomething always take care of "," +// Sometimes a property might not exist, so it doesn't add "," export const tailwindContainer = ( node: SceneNode & SceneNodeMixin & @@ -214,44 +203,46 @@ export const tailwindContainer = ( additionalAttr: string, settings: TailwindSettings, ): string => { - // ignore the view when size is zero or less - // while technically it shouldn't get less than 0, due to rounding errors, - // it can get to values like: -0.000004196293048153166 + // Ignore the view when size is zero or less if (node.width < 0 || node.height < 0) { return children; } - let builder = new TailwindDefaultBuilder(node, settings) + const builder = new TailwindDefaultBuilder(node, settings) .commonPositionStyles() .commonShapeStyles(); - if (builder.attributes || additionalAttr) { - const build = builder.build(additionalAttr); - - // image fill and no children -- let's emit an - let tag = "div"; - let src = ""; - if (retrieveTopFill(node.fills)?.type === "IMAGE") { - addWarning("Image fills are replaced with placeholders"); - const imageURL = getPlaceholderImage(node.width, node.height); - if (!("children" in node) || node.children.length === 0) { - tag = "img"; - src = ` src="${imageURL}"`; - } else { - builder.addAttributes(`bg-[url(${imageURL})]`); - } - } + if (!builder.attributes && !additionalAttr) { + return children; + } + + const build = builder.build(additionalAttr); - if (children) { - return `\n<${tag}${build}${src}>${indentString(children)}\n`; - } else if (selfClosingTags.includes(tag) || settings.jsx) { - return `\n<${tag}${build}${src} />`; + // Determine if we should use img tag + let tag = "div"; + let src = ""; + const topFill = retrieveTopFill(node.fills); + + if (topFill?.type === "IMAGE") { + addWarning("Image fills are replaced with placeholders"); + const imageURL = getPlaceholderImage(node.width, node.height); + + if (!("children" in node) || node.children.length === 0) { + tag = "img"; + src = ` src="${imageURL}"`; } else { - return `\n<${tag}${build}${src}>`; + builder.addAttributes(`bg-[url(${imageURL})]`); } } - return children; + // Generate appropriate HTML + if (children) { + return `\n<${tag}${build}${src}>${indentString(children)}\n`; + } else if (SELF_CLOSING_TAGS.includes(tag) || settings.jsx) { + return `\n<${tag}${build}${src} />`; + } else { + return `\n<${tag}${build}${src}>`; + } }; export const tailwindLine = ( @@ -275,21 +266,18 @@ export const tailwindSection = async ( .position() .customColor(node.fills, "bg"); - if (childrenStr) { - return `\n${indentString(childrenStr)}\n`; - } else { - return `\n`; - } + const build = builder.build(); + return childrenStr + ? `\n${indentString(childrenStr)}\n` + : `\n`; }; -export const tailwindCodeGenTextStyles = () => { - const result = previousExecutionCache - .map((style) => `// ${style.text}\n${style.style.split(" ").join("\n")}`) - .join("\n---\n"); - - if (!result) { +export const tailwindCodeGenTextStyles = (): string => { + if (previousExecutionCache.length === 0) { return "// No text styles in this selection"; } - return result; + return previousExecutionCache + .map((style) => `// ${style.text}\n${style.style.split(" ").join("\n")}`) + .join("\n---\n"); }; diff --git a/packages/eslint-config-custom/package.json b/packages/eslint-config-custom/package.json index 91fbbced..513f6bd8 100644 --- a/packages/eslint-config-custom/package.json +++ b/packages/eslint-config-custom/package.json @@ -4,9 +4,9 @@ "main": "index.js", "license": "MIT", "dependencies": { - "eslint-config-next": "^14.2.20", + "eslint-config-next": "^14.2.24", "eslint-config-prettier": "^9.1.0", - "eslint-config-turbo": "^2.3.3", + "eslint-config-turbo": "^2.4.4", "eslint-plugin-react": "7.35.0" }, "publishConfig": { diff --git a/packages/plugin-ui/package.json b/packages/plugin-ui/package.json index c3ebc43b..765ac372 100644 --- a/packages/plugin-ui/package.json +++ b/packages/plugin-ui/package.json @@ -10,7 +10,7 @@ "lint": "eslint \"src/**/*.ts*\"" }, "dependencies": { - "@types/react": "^18.3.17", + "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "15.5.13", "copy-to-clipboard": "^3.3.3", @@ -19,10 +19,10 @@ "tailwindcss": "3.4.6" }, "devDependencies": { - "eslint": "^9.17.0", + "eslint": "^9.21.0", "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", "types": "workspace:*", - "typescript": "^5.7.2" + "typescript": "^5.8.2" } } diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 946a96e6..24386f41 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -1,14 +1,6 @@ import { LocalCodegenPreferenceOptions, SelectPreferenceOptions } from "types"; export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ - { - itemType: "individual_select", - propertyName: "embedImages", - label: "Embed Images", - description: "Convert images to Base64 and embed them in the code.", - isDefault: false, - includedLanguages: ["HTML"], - }, { itemType: "individual_select", propertyName: "jsx", @@ -57,6 +49,22 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ isDefault: false, includedLanguages: ["Tailwind"], }, + { + itemType: "individual_select", + propertyName: "embedImages", + label: "Embed Images", + description: "Convert images to Base64 and embed them in the code.", + isDefault: false, + includedLanguages: ["HTML"], + }, + { + itemType: "individual_select", + propertyName: "embedVectors", + label: "Embed Vectors", + description: "Convert vectors in the code.", + isDefault: false, + includedLanguages: ["HTML"], + }, // Add your preferences data here ]; diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 2ae5413d..9d6eecc5 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -33,7 +33,7 @@ const CodePanel = (props: CodePanelProps) => { settings, onPreferenceChanged, } = props; - const isEmpty = code === ""; + const isCodeEmpty = code === ""; // State for custom prefix for Tailwind classes. // It is initially set from settings (if available) or an empty string. @@ -85,7 +85,7 @@ const CodePanel = (props: CodePanelProps) => {

Code

- {isEmpty === false && ( + {isCodeEmpty === false && ( - ))} +
+
+ {frameworks.map((tab) => ( + + ))} +
+
{ }} >
-
- {isEmpty === false && props.htmlPreview && ( - - )} - {warnings.length > 0 && ( -
-
-
- + {showAbout ? ( + + ) : ( +
+ {isEmpty === false && props.htmlPreview && ( + + )} + {warnings.length > 0 && ( +
+
+
+ +
+

Warnings:

-

Warnings:

+
    + {warnings.map((message: string, index) => ( +
  • + {message} +
  • + ))} +
-
    - {warnings.map((message: string) => ( -
  • - {message} -
  • - ))} -
-
- )} - - - {props.colors.length > 0 && ( - { - copy(value); - }} + )} + - )} - {props.gradients.length > 0 && ( - { - copy(value); - }} - /> - )} -
+ {props.colors.length > 0 && ( + { + copy(value); + }} + /> + )} + + {props.gradients.length > 0 && ( + { + copy(value); + }} + /> + )} +
+ )}
); diff --git a/packages/plugin-ui/src/components/About.tsx b/packages/plugin-ui/src/components/About.tsx new file mode 100644 index 00000000..6785bded --- /dev/null +++ b/packages/plugin-ui/src/components/About.tsx @@ -0,0 +1,67 @@ +import React from "react"; + +const About = () => { + return ( +
+
+

Figma to Code

+

+ Created by Bernardo Ferrari +

+
+ +
+

Privacy Policy

+

+ This plugin is completely private. It processes your design locally and + does not collect or transmit any of your data. +

+
+ +
+

Open Source

+

+ Figma to Code is an open-source project. You can view the source code + and contribute on GitHub. +

+ + View on GitHub + +
+ +
+

Contact

+

+ If you have any issues, feedback, or questions, please contact me: +

+ +
+
+ ); +}; + +export default About; diff --git a/packages/plugin-ui/tsconfig.json b/packages/plugin-ui/tsconfig.json index c1403a7e..e66b2206 100644 --- a/packages/plugin-ui/tsconfig.json +++ b/packages/plugin-ui/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "tsconfig/react-library.json", - "include": ["src"], + "include": [".turbo/src"], "exclude": ["dist", "build", "node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dc7c386..b1c9b179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: backend: specifier: workspace:* version: link:../../packages/backend + lucide-react: + specifier: ^0.477.0 + version: 0.477.0(react@18.3.1) plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui @@ -2257,6 +2260,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.477.0: + resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -5042,6 +5050,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.477.0(react@18.3.1): + dependencies: + react: 18.3.1 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} From be0c87ed6d8e059fd53943c1a79d0455c273cef2 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 12:37:33 -0300 Subject: [PATCH 005/134] Add info --- apps/debug/package.json | 8 +- apps/plugin/package.json | 8 +- apps/plugin/ui-src/main.tsx | 4 +- packages/backend/package.json | 8 +- packages/plugin-ui/package.json | 7 +- packages/plugin-ui/src/PluginUI.tsx | 9 +- packages/plugin-ui/src/components/About.tsx | 218 ++++++++++++++---- .../plugin-ui/src/components/CodePanel.tsx | 1 + .../plugin-ui/src/components/ColorsPanel.tsx | 1 + .../plugin-ui/src/components/ExpandIcon.tsx | 2 + .../src/components/GradientsPanel.tsx | 1 + packages/plugin-ui/src/components/Loading.tsx | 2 + packages/plugin-ui/src/components/Preview.tsx | 1 + .../src/components/SelectableToggle.tsx | 2 + .../plugin-ui/src/components/WarningIcon.tsx | 19 -- packages/types/package.json | 4 +- pnpm-lock.yaml | 150 ++++++------ 17 files changed, 276 insertions(+), 169 deletions(-) delete mode 100644 packages/plugin-ui/src/components/WarningIcon.tsx diff --git a/apps/debug/package.json b/apps/debug/package.json index 1efead3a..366db3d7 100644 --- a/apps/debug/package.json +++ b/apps/debug/package.json @@ -13,13 +13,13 @@ "backend": "workspace:*", "next": "^14.2.24", "plugin-ui": "workspace:*", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@types/node": "^20.17.21", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "autoprefixer": "^10.4.20", "eslint-config-custom": "workspace:*", "postcss": "^8.5.3", diff --git a/apps/plugin/package.json b/apps/plugin/package.json index 45b6602c..77e9f614 100644 --- a/apps/plugin/package.json +++ b/apps/plugin/package.json @@ -14,13 +14,13 @@ "backend": "workspace:*", "lucide-react": "^0.477.0", "plugin-ui": "workspace:*", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@types/node": "^20.17.21", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@vitejs/plugin-react": "^4.3.4", diff --git a/apps/plugin/ui-src/main.tsx b/apps/plugin/ui-src/main.tsx index faff9775..2a882473 100644 --- a/apps/plugin/ui-src/main.tsx +++ b/apps/plugin/ui-src/main.tsx @@ -1,5 +1,5 @@ -import ReactDOM from "react-dom"; +import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; -ReactDOM.render(, document.getElementById("root")); +ReactDOM.createRoot(document.getElementById("root")!).render(); diff --git a/packages/backend/package.json b/packages/backend/package.json index baab8739..3bd44a16 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,13 +15,13 @@ "dependencies": { "@figma/plugin-typings": "^1.108.0", "js-base64": "^3.7.7", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "19.0.0", + "react-dom": "19.0.0", "types": "workspace:*" }, "devDependencies": { - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "eslint": "^9.21.0", "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", diff --git a/packages/plugin-ui/package.json b/packages/plugin-ui/package.json index 765ac372..a8253394 100644 --- a/packages/plugin-ui/package.json +++ b/packages/plugin-ui/package.json @@ -10,11 +10,12 @@ "lint": "eslint \"src/**/*.ts*\"" }, "dependencies": { - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "@types/react-syntax-highlighter": "15.5.13", "copy-to-clipboard": "^3.3.3", - "react": "^18.3.1", + "lucide-react": "^0.477.0", + "react": "^19.0.0", "react-syntax-highlighter": "^15.6.1", "tailwindcss": "3.4.6" }, diff --git a/packages/plugin-ui/src/PluginUI.tsx b/packages/plugin-ui/src/PluginUI.tsx index 1a6db986..71a45f46 100644 --- a/packages/plugin-ui/src/PluginUI.tsx +++ b/packages/plugin-ui/src/PluginUI.tsx @@ -3,7 +3,6 @@ import Preview from "./components/Preview"; import GradientsPanel from "./components/GradientsPanel"; import ColorsPanel from "./components/ColorsPanel"; import CodePanel from "./components/CodePanel"; -import WarningIcon from "./components/WarningIcon"; import About from "./components/About"; import { Framework, @@ -19,6 +18,8 @@ import { } from "./codegenPreferenceOptions"; import Loading from "./components/Loading"; import { useState } from "react"; +import { InfoIcon, TriangleAlertIcon } from "lucide-react"; +import React from "react"; type PluginUIProps = { code: string; @@ -69,7 +70,7 @@ export const PluginUI = (props: PluginUIProps) => { ))}
{
- +

Warnings:

diff --git a/packages/plugin-ui/src/components/About.tsx b/packages/plugin-ui/src/components/About.tsx index 6785bded..0de731de 100644 --- a/packages/plugin-ui/src/components/About.tsx +++ b/packages/plugin-ui/src/components/About.tsx @@ -1,67 +1,189 @@ import React from "react"; +import { + Code, + Github, + Heart, + Lock, + Mail, + MessageCircle, + Star, + Zap, +} from "lucide-react"; const About = () => { return ( -
-
-

Figma to Code

-

- Created by Bernardo Ferrari -

+
+ {/* Header Section with Logo and Title */} +
+
+ +
+

Figma to Code

+
+ Created with + + by Bernardo Ferrari +
+
-
-

Privacy Policy

-

- This plugin is completely private. It processes your design locally and - does not collect or transmit any of your data. -

-
+ {/* Cards Section */} +
+ {/* Privacy Policy Card */} +
+
+
+ +
+

Privacy Policy

+
+

+ This plugin is completely private. All of your design data is + processed locally in your browser and never leaves your computer. No + analytics, no data collection, no tracking. +

+
-
-

Open Source

-

- Figma to Code is an open-source project. You can view the source code - and contribute on GitHub. -

- - View on GitHub - -
+ {/* Open Source Card */} +
+
+
+ +
+

Open Source

+
+

+ Figma to Code is completely open-source. Contributions, bug reports, + and feature requests are welcome! +

+ + + View on GitHub + +
- + + {/* Contact Card */} + +
+ + {/* Footer */} +
+

+ © {new Date().getFullYear()} Bernardo Ferrari. All rights reserved. +

); }; +function XLogo() { + return ( + + + + ); +} + export default About; diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 9d6eecc5..e0a4fa1f 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -9,6 +9,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coldarkDark as theme } from "react-syntax-highlighter/dist/esm/styles/prism"; import copy from "copy-to-clipboard"; import SelectableToggle from "./SelectableToggle"; +import React from "react"; interface CodePanelProps { code: string; diff --git a/packages/plugin-ui/src/components/ColorsPanel.tsx b/packages/plugin-ui/src/components/ColorsPanel.tsx index ed6463b3..dfd19d32 100644 --- a/packages/plugin-ui/src/components/ColorsPanel.tsx +++ b/packages/plugin-ui/src/components/ColorsPanel.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { useState } from "react"; import { SolidColorConversion } from "types"; diff --git a/packages/plugin-ui/src/components/ExpandIcon.tsx b/packages/plugin-ui/src/components/ExpandIcon.tsx index ae9f397d..0f7edc1a 100644 --- a/packages/plugin-ui/src/components/ExpandIcon.tsx +++ b/packages/plugin-ui/src/components/ExpandIcon.tsx @@ -1,3 +1,5 @@ +import React from "react"; + const ExpandIcon = (props: { size: number }) => ( (
void; isSelected?: boolean; diff --git a/packages/plugin-ui/src/components/WarningIcon.tsx b/packages/plugin-ui/src/components/WarningIcon.tsx deleted file mode 100644 index f6a5c1eb..00000000 --- a/packages/plugin-ui/src/components/WarningIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const WarningIcon = () => ( - - - - - -); -export default WarningIcon; diff --git a/packages/types/package.json b/packages/types/package.json index ccc54883..7bffd087 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -14,8 +14,8 @@ }, "dependencies": { "@figma/plugin-typings": "^1.108.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "tsconfig": "workspace:*" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1c9b179..7f9836b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,26 +31,26 @@ importers: version: link:../../packages/backend next: specifier: ^14.2.24 - version: 14.2.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0) plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) devDependencies: '@types/node': specifier: ^20.17.21 version: 20.17.21 '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.10 + version: 19.0.10 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.4 + version: 19.0.4(@types/react@19.0.10) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.3) @@ -83,26 +83,26 @@ importers: version: link:../../packages/backend lucide-react: specifier: ^0.477.0 - version: 0.477.0(react@18.3.1) + version: 0.477.0(react@19.0.0) plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) devDependencies: '@types/node': specifier: ^20.17.21 version: 20.17.21 '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.10 + version: 19.0.10 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.4 + version: 19.0.4(@types/react@19.0.10) '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) @@ -164,21 +164,21 @@ importers: specifier: ^3.7.7 version: 3.7.7 react: - specifier: 18.3.1 - version: 18.3.1 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: 18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) types: specifier: workspace:* version: link:../types devDependencies: '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.10 + version: 19.0.10 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.4 + version: 19.0.4(@types/react@19.0.10) eslint: specifier: ^9.21.0 version: 9.21.0(jiti@1.21.7) @@ -213,23 +213,26 @@ importers: packages/plugin-ui: dependencies: '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.10 + version: 19.0.10 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.4 + version: 19.0.4(@types/react@19.0.10) '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 + lucide-react: + specifier: ^0.477.0 + version: 0.477.0(react@19.0.0) react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-syntax-highlighter: specifier: ^15.6.1 - version: 15.6.1(react@18.3.1) + version: 15.6.1(react@19.0.0) tailwindcss: specifier: 3.4.6 version: 3.4.6 @@ -258,11 +261,11 @@ importers: specifier: ^1.108.0 version: 1.108.0 '@types/react': - specifier: ^18.3.18 - version: 18.3.18 + specifier: ^19.0.10 + version: 19.0.10 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.5(@types/react@18.3.18) + specifier: ^19.0.4 + version: 19.0.4(@types/react@19.0.10) tsconfig: specifier: workspace:* version: link:../tsconfig @@ -1170,19 +1173,16 @@ packages: '@types/node@20.17.21': resolution: {integrity: sha512-yw1WZ94lZpdZbpnaF+WRvlN/Sx2EZWe/YZVdK4mC4u02/ql6Ozen8qbRJhOtltOxCg97/kpijhGs5X6STwkvbg==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - - '@types/react-dom@18.3.5': - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + '@types/react-dom@19.0.4': + resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} peerDependencies: - '@types/react': ^18.0.0 + '@types/react': ^19.0.0 '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - '@types/react@18.3.18': - resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/react@19.0.10': + resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2531,10 +2531,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: ^18.3.1 + react: ^19.0.0 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2548,8 +2548,8 @@ packages: peerDependencies: react: '>= 0.14.0' - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -2628,8 +2628,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -3677,19 +3677,16 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/prop-types@15.7.14': {} - - '@types/react-dom@18.3.5(@types/react@18.3.18)': + '@types/react-dom@19.0.4(@types/react@19.0.10)': dependencies: - '@types/react': 18.3.18 + '@types/react': 19.0.10 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 18.3.18 + '@types/react': 19.0.10 - '@types/react@18.3.18': + '@types/react@19.0.10': dependencies: - '@types/prop-types': 15.7.14 csstype: 3.1.3 '@types/unist@2.0.11': {} @@ -5050,9 +5047,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.477.0(react@18.3.1): + lucide-react@0.477.0(react@19.0.0): dependencies: - react: 18.3.1 + react: 19.0.0 math-intrinsics@1.1.0: {} @@ -5087,7 +5084,7 @@ snapshots: natural-compare@1.4.0: {} - next@14.2.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 14.2.24 '@swc/helpers': 0.5.5 @@ -5095,9 +5092,9 @@ snapshots: caniuse-lite: 1.0.30001701 graceful-fs: 4.2.11 postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.1(react@19.0.0) optionalDependencies: '@next/swc-darwin-arm64': 14.2.24 '@next/swc-darwin-x64': 14.2.24 @@ -5297,29 +5294,26 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@18.3.1(react@18.3.1): + react-dom@19.0.0(react@19.0.0): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.0.0 + scheduler: 0.25.0 react-is@16.13.1: {} react-refresh@0.14.2: {} - react-syntax-highlighter@15.6.1(react@18.3.1): + react-syntax-highlighter@15.6.1(react@19.0.0): dependencies: '@babel/runtime': 7.26.9 highlight.js: 10.7.3 highlightjs-vue: 1.0.0 lowlight: 1.20.0 prismjs: 1.29.0 - react: 18.3.1 + react: 19.0.0 refractor: 3.6.0 - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.0.0: {} read-cache@1.0.0: dependencies: @@ -5433,9 +5427,7 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.25.0: {} semver@6.3.1: {} @@ -5591,10 +5583,10 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(react@19.0.0): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.0.0 sucrase@3.35.0: dependencies: From e1f863addc26f30c73c39517aaa5607780fc1ddf Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 13:23:39 -0300 Subject: [PATCH 006/134] Improve loading --- packages/plugin-ui/src/components/Loading.tsx | 62 ++++++++++++++----- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/plugin-ui/src/components/Loading.tsx b/packages/plugin-ui/src/components/Loading.tsx index e9d27553..c1ec5f1b 100644 --- a/packages/plugin-ui/src/components/Loading.tsx +++ b/packages/plugin-ui/src/components/Loading.tsx @@ -1,26 +1,54 @@ import React from "react"; +import { Code } from "lucide-react"; interface LoadingProps {} + const Loading = (_props: LoadingProps) => ( -
-
-

- Converting... -

-

- This can take a while if the selection has many images or paths +

+
+ {/* Logo animation */} +
+
+
+ +
+ {/* Loading spinner */} + + + + +
+ + {/* Text */} +

+ Converting Design +

+

+ Please wait while your design is being converted to code. This may take a moment for complex designs.

+ + {/* Progress bar */} +
+
+
-
); + export default Loading; From f42ef6fe676baa9d55a687e1740c6d151489c928 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 14:16:59 -0300 Subject: [PATCH 007/134] Improve preview and warning --- apps/plugin/package.json | 6 +- apps/plugin/tailwind.config.js | 6 +- .../src/common/commonFormatAttributes.ts | 4 +- .../src/common/lowercaseFirstLetter.ts | 7 + packages/plugin-ui/package.json | 5 +- packages/plugin-ui/src/PluginUI.tsx | 23 +- .../plugin-ui/src/components/CodePanel.tsx | 14 +- .../plugin-ui/src/components/CopyButton.tsx | 104 ++++++++ packages/plugin-ui/src/components/Preview.tsx | 181 +++++++++++--- .../src/components/SelectableToggle.tsx | 72 ++++-- .../src/components/WarningsPanel.tsx | 227 ++++++++++++++++++ packages/plugin-ui/src/lib/utils.ts | 6 + packages/plugin-ui/tailwind.config.js | 14 +- packages/plugin-ui/tsconfig.json | 2 +- pnpm-lock.yaml | 98 ++++++++ 15 files changed, 670 insertions(+), 99 deletions(-) create mode 100644 packages/backend/src/common/lowercaseFirstLetter.ts create mode 100644 packages/plugin-ui/src/components/CopyButton.tsx create mode 100644 packages/plugin-ui/src/components/WarningsPanel.tsx create mode 100644 packages/plugin-ui/src/lib/utils.ts diff --git a/apps/plugin/package.json b/apps/plugin/package.json index 77e9f614..641d287a 100644 --- a/apps/plugin/package.json +++ b/apps/plugin/package.json @@ -12,10 +12,14 @@ "dependencies": { "@figma/plugin-typings": "^1.108.0", "backend": "workspace:*", + "clsx": "^2.1.1", "lucide-react": "^0.477.0", + "motion": "^12.4.9", "plugin-ui": "workspace:*", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/node": "^20.17.21", diff --git a/apps/plugin/tailwind.config.js b/apps/plugin/tailwind.config.js index 77687bd7..0c74c637 100644 --- a/apps/plugin/tailwind.config.js +++ b/apps/plugin/tailwind.config.js @@ -6,9 +6,5 @@ module.exports = { "../../packages/plugin-ui/**/*.{js,ts,jsx,tsx}", ], darkMode: "class", - theme: { - extend: {}, - }, - variants: {}, - plugins: [], + }; diff --git a/packages/backend/src/common/commonFormatAttributes.ts b/packages/backend/src/common/commonFormatAttributes.ts index d157b7ac..8adb6392 100644 --- a/packages/backend/src/common/commonFormatAttributes.ts +++ b/packages/backend/src/common/commonFormatAttributes.ts @@ -1,4 +1,4 @@ -import { stringToClassName as stringToClassName } from "./numToAutoFixed"; +import { lowercaseFirstLetter } from "./lowercaseFirstLetter"; export const getClassLabel = (isJSX: boolean = false) => isJSX ? "className" : "class"; @@ -18,7 +18,7 @@ export const formatStyleAttribute = ( }; export const formatDataAttribute = (label: string, value?: string) => - ` data-${label}${value === undefined ? `` : `="${value}"`}`; + ` data-${lowercaseFirstLetter(label)}${value === undefined ? `` : `="${value}"`}`; export const formatClassAttribute = ( classes: string[], diff --git a/packages/backend/src/common/lowercaseFirstLetter.ts b/packages/backend/src/common/lowercaseFirstLetter.ts new file mode 100644 index 00000000..b7215dc3 --- /dev/null +++ b/packages/backend/src/common/lowercaseFirstLetter.ts @@ -0,0 +1,7 @@ +export function lowercaseFirstLetter(str: string): string { + if (!str || str.length === 0) { + return str; + } + + return str.charAt(0).toLowerCase() + str.slice(1); +} diff --git a/packages/plugin-ui/package.json b/packages/plugin-ui/package.json index a8253394..7c61b749 100644 --- a/packages/plugin-ui/package.json +++ b/packages/plugin-ui/package.json @@ -13,11 +13,14 @@ "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@types/react-syntax-highlighter": "15.5.13", + "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "lucide-react": "^0.477.0", "react": "^19.0.0", "react-syntax-highlighter": "^15.6.1", - "tailwindcss": "3.4.6" + "tailwind-merge": "^3.0.2", + "tailwindcss": "3.4.6", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "eslint": "^9.21.0", diff --git a/packages/plugin-ui/src/PluginUI.tsx b/packages/plugin-ui/src/PluginUI.tsx index 71a45f46..fede84a3 100644 --- a/packages/plugin-ui/src/PluginUI.tsx +++ b/packages/plugin-ui/src/PluginUI.tsx @@ -4,6 +4,7 @@ import GradientsPanel from "./components/GradientsPanel"; import ColorsPanel from "./components/ColorsPanel"; import CodePanel from "./components/CodePanel"; import About from "./components/About"; +import WarningsPanel from "./components/WarningsPanel"; import { Framework, HTMLPreview, @@ -18,7 +19,7 @@ import { } from "./codegenPreferenceOptions"; import Loading from "./components/Loading"; import { useState } from "react"; -import { InfoIcon, TriangleAlertIcon } from "lucide-react"; +import { InfoIcon } from "lucide-react"; import React from "react"; type PluginUIProps = { @@ -96,23 +97,9 @@ export const PluginUI = (props: PluginUIProps) => { {isEmpty === false && props.htmlPreview && ( )} - {warnings.length > 0 && ( -
-
-
- -
-

Warnings:

-
-
    - {warnings.map((message: string, index) => ( -
  • - {message} -
  • - ))} -
-
- )} + + {warnings.length > 0 && } + { Code

{isCodeEmpty === false && ( - + /> )}
diff --git a/packages/plugin-ui/src/components/CopyButton.tsx b/packages/plugin-ui/src/components/CopyButton.tsx new file mode 100644 index 00000000..7e2f0e9f --- /dev/null +++ b/packages/plugin-ui/src/components/CopyButton.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Copy, CheckCircle, Check } from "lucide-react"; +import copy from "copy-to-clipboard"; + +interface CopyButtonProps { + value: string; + className?: string; + showLabel?: boolean; + successDuration?: number; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export function CopyButton({ + value, + className, + showLabel = true, + successDuration = 750, + onMouseEnter, + onMouseLeave, +}: CopyButtonProps) { + const [isCopied, setIsCopied] = useState(false); + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => { + setIsCopied(false); + console.log("iscopied false"); + }, successDuration); + + return () => clearTimeout(timer); + } + }, [isCopied, successDuration]); + + const handleCopy = async () => { + try { + copy(value); + setIsCopied(true); + console.log("iscopied true"); + } catch (error) { + console.error("Failed to copy text: ", error); + } + }; + + return ( + + ); +} diff --git a/packages/plugin-ui/src/components/Preview.tsx b/packages/plugin-ui/src/components/Preview.tsx index 0b933235..b06bfb3c 100644 --- a/packages/plugin-ui/src/components/Preview.tsx +++ b/packages/plugin-ui/src/components/Preview.tsx @@ -1,52 +1,177 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { HTMLPreview } from "types"; +import { + Maximize2, + Minimize2, + MonitorSmartphone, + Smartphone, +} from "lucide-react"; const Preview: React.FC<{ htmlPreview: HTMLPreview; }> = (props) => { - const targetWidth = 240; - const targetHeight = 120; - const scaleFactor = Math.min( - targetWidth / props.htmlPreview.size.width, - targetHeight / props.htmlPreview.size.height, - ); + const [expanded, setExpanded] = useState(false); + const [viewMode, setViewMode] = useState<"desktop" | "mobile">("desktop"); + const [animationClass, setAnimationClass] = useState(""); + + // Define consistent dimensions regardless of mode + const containerWidth = expanded ? 320 : 240; + const containerHeight = expanded ? 180 : 120; + + // Calculate content dimensions based on view mode + const contentWidth = + viewMode === "desktop" ? containerWidth : Math.floor(containerWidth * 0.4); // Narrower for mobile + + // Adjust scale factor based on view mode + const scaleFactor = + viewMode === "desktop" + ? Math.min( + containerWidth / props.htmlPreview.size.width, + containerHeight / props.htmlPreview.size.height, + ) + : Math.min( + contentWidth / props.htmlPreview.size.width, + containerHeight / props.htmlPreview.size.height, + ); + + // Add animation when changing view mode + useEffect(() => { + setAnimationClass( + viewMode === "desktop" + ? "animate-slide-in-left" + : "animate-slide-in-right", + ); + const timer = setTimeout(() => setAnimationClass(""), 300); // Remove animation class after it completes + return () => clearTimeout(timer); + }, [viewMode]); + + // Add animation when changing size + useEffect(() => { + const timer = setTimeout(() => setAnimationClass("animate-scale-in"), 50); + return () => { + clearTimeout(timer); + setAnimationClass(""); + }; + }, [expanded]); return ( -
-
- Responsive Preview +
+ {/* Header with view mode controls */} +
+

+ + Preview +

+
+ {/* View Mode Toggle */} +
+ + +
+ + {/* Expand/Collapse Button */} + +
-
+ + {/* Preview container */} +
+ {/* Outer container with fixed dimensions */}
+ {/* Inner content positioned based on view mode */}
+ {/* Device frame - just a border for mobile, no status bar or home indicator */}
+ className={`w-full h-full flex justify-center items-center overflow-hidden bg-white dark:bg-black ${ + viewMode === "desktop" + ? "border border-neutral-300 dark:border-neutral-600 rounded shadow-sm" + : "border-2 border-neutral-400 dark:border-neutral-500 rounded-xl shadow-sm" + } transition-all duration-300 ease-in-out`} + > + {/* Content - no padding needed anymore */} +
+
+
+
+ + {/* Footer with size info */} +
+ + {props.htmlPreview.size.width}×{props.htmlPreview.size.height}px + +
+ {viewMode === "mobile" ? ( + + + Mobile view + + ) : ( + + + Desktop view + + )} +
+
); }; + export default Preview; diff --git a/packages/plugin-ui/src/components/SelectableToggle.tsx b/packages/plugin-ui/src/components/SelectableToggle.tsx index 40a35754..c9cba2fd 100644 --- a/packages/plugin-ui/src/components/SelectableToggle.tsx +++ b/packages/plugin-ui/src/components/SelectableToggle.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useState } from "react"; +import { Check, HelpCircle } from "lucide-react"; type SelectableToggleProps = { onSelect: (isSelected: boolean) => void; @@ -17,34 +18,61 @@ const SelectableToggle = ({ buttonClass, checkClass, }: SelectableToggleProps) => { + const [showTooltip, setShowTooltip] = useState(false); + const handleClick = () => { onSelect(!isSelected); }; return ( - + > +
+ {/* Checkbox circle with check mark for selected state */} +
+ {isSelected && ( + + )} +
+ + {title} + + {/* Help icon for description */} + {description && ( +
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + +
+ )} +
+ + + {/* Tooltip */} + {showTooltip && description && ( +
+ {description} +
+
+ )} +
); }; + export default SelectableToggle; diff --git a/packages/plugin-ui/src/components/WarningsPanel.tsx b/packages/plugin-ui/src/components/WarningsPanel.tsx new file mode 100644 index 00000000..5b7c4683 --- /dev/null +++ b/packages/plugin-ui/src/components/WarningsPanel.tsx @@ -0,0 +1,227 @@ +import React, { useState } from "react"; +import { + AlertTriangle, + ChevronDown, + ChevronUp, + XCircle, + AlertOctagon, + ExternalLink, + Info, +} from "lucide-react"; +import { Warning } from "types"; + +interface WarningsPanelProps { + warnings: Warning[]; +} + +// Helper function to categorize warnings by severity +const categorizeWarnings = (warnings: Warning[]) => { + const critical = warnings.filter( + (w) => + w.toString().toLowerCase().includes("error") || + w.toString().toLowerCase().includes("critical") || + w.toString().toLowerCase().includes("missing"), + ); + const standard = warnings.filter((w) => !critical.includes(w)); + + return { critical, standard }; +}; + +const WarningsPanel: React.FC = ({ warnings }) => { + const [isCollapsed, setIsCollapsed] = useState(false); + const [activeTab, setActiveTab] = useState<"all" | "critical" | "standard">( + "all", + ); + const { critical, standard } = categorizeWarnings(warnings); + + if (warnings.length === 0) return null; + + const displayedWarnings = + activeTab === "all" + ? warnings + : activeTab === "critical" + ? critical + : standard; + + return ( +
+ {/* Header - medium size */} +
setIsCollapsed(!isCollapsed)} + > +
+
+ +
+
+

+ {warnings.length} {warnings.length === 1 ? "Warning" : "Warnings"} +

+ {critical.length > 0 && ( + + {critical.length} critical + + )} +
+
+ +
+ + {/* Warning content - balanced size */} + {!isCollapsed && ( +
+ {/* Tabs - medium size */} + {critical.length > 0 && standard.length > 0 && ( +
+ + + +
+ )} + + {/* Warning list - balanced size */} +
+ {displayedWarnings.map((message, index) => { + const isCritical = critical.includes(message); + return ( +
+
+
+ {isCritical ? ( + + ) : ( + + )} +
+
+

+ {message.toString()} +

+ + {/* Suggested fix - balanced size */} + {isCritical && ( +
+ Tip: + {suggestFixForWarning(message.toString())} +
+ )} +
+ + {/* Action link - balanced size */} + {shouldShowActionButtons(message.toString()) && ( + + Info + + + )} +
+
+ ); + })} +
+ + {/* Help text - balanced size */} + {displayedWarnings.length > 0 && ( +
+ + + Addressing warnings can improve the quality of the generated + code. + +
+ )} +
+ )} +
+ ); +}; + +// Helper functions (these would be expanded with actual logic in your implementation) +const suggestFixForWarning = (warning: string): string => { + if (warning.toLowerCase().includes("missing")) { + return "Add the required properties to your component or select a parent element that includes all necessary children."; + } + if (warning.toLowerCase().includes("unsupported")) { + return "Consider using a different element type or simplifying the design for better conversion results."; + } + return "Check your design elements and ensure they follow the recommended structure for code conversion."; +}; + +const shouldShowActionButtons = (warning: string): boolean => { + // Example condition - you would customize this based on your specific warnings + return ( + warning.toLowerCase().includes("unsupported") || + warning.toLowerCase().includes("missing") + ); +}; + +const getDocsLinkForWarning = (warning: string): string => { + // Example URLs - in reality you would point to specific documentation pages + if (warning.toLowerCase().includes("unsupported")) { + return "https://github.com/bernaferrari/figma-to-code/wiki/Supported-Elements"; + } + return "https://github.com/bernaferrari/figma-to-code/wiki"; +}; + +export default WarningsPanel; diff --git a/packages/plugin-ui/src/lib/utils.ts b/packages/plugin-ui/src/lib/utils.ts new file mode 100644 index 00000000..365058ce --- /dev/null +++ b/packages/plugin-ui/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/plugin-ui/tailwind.config.js b/packages/plugin-ui/tailwind.config.js index 4006c8d3..1d108713 100644 --- a/packages/plugin-ui/tailwind.config.js +++ b/packages/plugin-ui/tailwind.config.js @@ -1,13 +1,5 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx}", - "./components/**/*.{js,ts,jsx,tsx}", - "../../packages/plugin-ui/**/*.{js,ts,jsx,tsx}", - ], - darkMode: "class", - theme: { - extend: {}, - }, - variants: {}, - plugins: [], + content: ['./src/**/*.{js,jsx,ts,tsx}'], + darkMode: 'media', }; diff --git a/packages/plugin-ui/tsconfig.json b/packages/plugin-ui/tsconfig.json index e66b2206..c1403a7e 100644 --- a/packages/plugin-ui/tsconfig.json +++ b/packages/plugin-ui/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "tsconfig/react-library.json", - "include": [".turbo/src"], + "include": ["src"], "exclude": ["dist", "build", "node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f9836b8..68c0431f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,9 +81,15 @@ importers: backend: specifier: workspace:* version: link:../../packages/backend + clsx: + specifier: ^2.1.1 + version: 2.1.1 lucide-react: specifier: ^0.477.0 version: 0.477.0(react@19.0.0) + motion: + specifier: ^12.4.9 + version: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui @@ -93,6 +99,12 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + tailwind-merge: + specifier: ^3.0.2 + version: 3.0.2 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.6) devDependencies: '@types/node': specifier: ^20.17.21 @@ -221,6 +233,9 @@ importers: '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 + clsx: + specifier: ^2.1.1 + version: 2.1.1 copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 @@ -233,9 +248,15 @@ importers: react-syntax-highlighter: specifier: ^15.6.1 version: 15.6.1(react@19.0.0) + tailwind-merge: + specifier: ^3.0.2 + version: 3.0.2 tailwindcss: specifier: 3.4.6 version: 3.4.6 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.6) devDependencies: eslint: specifier: ^9.21.0 @@ -1498,6 +1519,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1888,6 +1913,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@12.4.9: + resolution: {integrity: sha512-c+nDhfiNUwi8G4BrhrP2hjPsDHzIKRbUhDlcK7oC5kXY4QK1IrT/kuhY4BgK6h2ujDrZ8ocvFrG2X8+b1m/MkQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2291,6 +2330,26 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + motion-dom@12.4.5: + resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} + + motion-utils@12.0.0: + resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} + + motion@12.4.9: + resolution: {integrity: sha512-lOT2+X9b3yvDEC+pAClTzLSW/D5T/zZweO+UN1lMe86WtGFQIbHU/VjEhwGREW0QryG9KECB1uK3QJo8G3NGag==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2786,6 +2845,14 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@3.0.2: + resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tailwindcss@3.4.6: resolution: {integrity: sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==} engines: {node: '>=14.0.0'} @@ -4084,6 +4151,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4667,6 +4736,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + motion-dom: 12.4.5 + motion-utils: 12.0.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + fsevents@2.3.3: optional: true @@ -5072,6 +5150,20 @@ snapshots: minipass@7.1.2: {} + motion-dom@12.4.5: + dependencies: + motion-utils: 12.0.0 + + motion-utils@12.0.0: {} + + motion@12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + framer-motion: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + ms@2.1.3: {} mz@2.7.0: @@ -5608,6 +5700,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.0.2: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.6): + dependencies: + tailwindcss: 3.4.6 + tailwindcss@3.4.6: dependencies: '@alloc/quick-lru': 5.2.0 From 70c1d6946319c70350781fcfdf32e59ee5cab3a9 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 14:24:35 -0300 Subject: [PATCH 008/134] Fix variants --- packages/backend/src/html/htmlDefaultBuilder.ts | 7 +++---- packages/backend/src/tailwind/tailwindDefaultBuilder.ts | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 1e5eabe1..f8120c8e 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -336,10 +336,9 @@ export class HtmlDefaultBuilder { if ("variantProperties" in this.node && this.node.variantProperties) { Object.entries(this.node.variantProperties) - ?.map((prop) => { - this.addData(prop[0], prop[1]); - }) - .sort(); + ?.map((prop) => formatDataAttribute(prop[0], prop[1])) + .sort() + .forEach((d) => this.data.push(d)); } const dataAttributes = this.data.join(""); diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 9c7947d0..9946621f 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -261,12 +261,10 @@ export class TailwindDefaultBuilder { } if ("variantProperties" in this.node && this.node.variantProperties) { - console.log("entries", this.node.variantProperties); Object.entries(this.node.variantProperties) - ?.map((prop) => { - this.addData(prop[0], prop[1]); - }) - .sort(); + ?.map((prop) => formatDataAttribute(prop[0], prop[1])) + .sort() + .forEach((d) => this.data.push(d)); } const classLabel = getClassLabel(this.isJSX); From 268fb118c8fbac7aa6d20757aa6f67b187db8d38 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 14:57:45 -0300 Subject: [PATCH 009/134] Better empty state --- .../plugin-ui/src/components/CodePanel.tsx | 23 +++-- .../plugin-ui/src/components/EmptyState.tsx | 98 +++++++++++++++++++ 2 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-ui/src/components/EmptyState.tsx diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 0af0aefc..e07dc8ad 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -7,10 +7,10 @@ import { import { useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coldarkDark as theme } from "react-syntax-highlighter/dist/esm/styles/prism"; -import copy from "copy-to-clipboard"; import SelectableToggle from "./SelectableToggle"; import React from "react"; import { CopyButton } from "./CopyButton"; +import EmptyState from "./EmptyState"; interface CodePanelProps { code: string; @@ -25,7 +25,6 @@ interface CodePanelProps { } const CodePanel = (props: CodePanelProps) => { - const [isPressed, setIsPressed] = useState(false); const [syntaxHovered, setSyntaxHovered] = useState(false); const { code, @@ -66,13 +65,6 @@ const CodePanel = (props: CodePanelProps) => { ? applyPrefixToClasses(code, customPrefix) : code; - // Clipboard and hover handlers. - const handleButtonClick = () => { - setIsPressed(true); - setTimeout(() => setIsPressed(false), 250); - copy(prefixedCode); - }; - const handleButtonHover = () => setSyntaxHovered(true); const handleButtonLeave = () => setSyntaxHovered(false); @@ -177,15 +169,21 @@ const CodePanel = (props: CodePanelProps) => { )}
{isCodeEmpty ? ( -

No layer is selected. Please select a layer.

+ ) : ( {
); }; + export default CodePanel; diff --git a/packages/plugin-ui/src/components/EmptyState.tsx b/packages/plugin-ui/src/components/EmptyState.tsx new file mode 100644 index 00000000..586ead86 --- /dev/null +++ b/packages/plugin-ui/src/components/EmptyState.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { Code, MousePointer, Eye, Copy } from "lucide-react"; + +const EmptyState = () => { + return ( +
+ {/* Icon with "no code" symbol */} +
+
+ + + + + +
+
+ + {/* Title and hint */} +

+ No Layer Selected +

+

+ Select a layer from your Figma design to view the generated code. +

+ + {/* Completely redesigned steps section */} +
+
+ {/* Progress bar */} +
+ + {/* Steps with connecting line */} +
    + {/* Step 1 - Current */} +
  1. +
    +
    +
    + +
    +
    +
    +
    + Select +
    +

    + Choose a layer +

    +
    +
  2. + + {/* Step 2 */} +
  3. +
    + +
    +
    +
    + View +
    +

    + See the code +

    +
    +
  4. + + {/* Step 3 */} +
  5. +
    + +
    +
    +
    + Copy +
    +

    + Use anywhere +

    +
    +
  6. +
+
+
+
+ ); +}; + +export default EmptyState; From f313613e45117434cbea6debf6bfec76baec4b1e Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 15:32:09 -0300 Subject: [PATCH 010/134] Improve --- apps/debug/package.json | 2 +- packages/backend/src/code.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/debug/package.json b/apps/debug/package.json index 366db3d7..2a589dec 100644 --- a/apps/debug/package.json +++ b/apps/debug/package.json @@ -17,7 +17,7 @@ "react-dom": "^19.0.0" }, "devDependencies": { - "@types/node": "^20.17.21", + "@types/node": "^22.13.9", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "autoprefixer": "^10.4.20", diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 861c901d..8f971537 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -96,23 +96,29 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } } - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; + if ("width" in figmaNode) { + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; + } } } catch (e) { // Silently fail if there's an error accessing the Figma node } } else { // Avoid calling getNodeById if we don't need to - if (node.rotation && node.rotation !== 0) { + if ( + "rotation" in node && + node.rotation !== undefined && + node.rotation !== 0 + ) { const figmaNode = figma.getNodeById(node.id); node.width = (figmaNode as any).width; node.height = (figmaNode as any).height; node.x = (figmaNode as any).x; node.y = (figmaNode as any).y; - } else { + } else if (node.absoluteRenderBounds) { // Use the absoluteRenderBounds if we don't need to fetch the Figma node. node.width = node.absoluteRenderBounds.width; node.height = node.absoluteRenderBounds.height; From 8384584fc9b41ad925ab3c4f99fb342093129277 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 3 Mar 2025 16:10:56 -0300 Subject: [PATCH 011/134] Minor fixes --- packages/backend/src/code.ts | 4 +-- packages/backend/src/common/commonPosition.ts | 27 +++++++++---------- packages/backend/src/tailwind/tailwindMain.ts | 16 +++++------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 8f971537..5bf72b65 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -127,8 +127,8 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } } - if (!node.LayoutMode) { - node.LayoutMode = "NONE"; + if (!node.layoutMode) { + node.layoutMode = "NONE"; } if (!node.layoutGrow) { node.layoutGrow = 0; diff --git a/packages/backend/src/common/commonPosition.ts b/packages/backend/src/common/commonPosition.ts index acdb9007..ce97d203 100644 --- a/packages/backend/src/common/commonPosition.ts +++ b/packages/backend/src/common/commonPosition.ts @@ -122,22 +122,21 @@ export const commonIsAbsolutePosition = ( return false; } - if ("layoutAlign" in node) { - if (!node.parent || node.parent === undefined) { - return false; - } + if (!node.parent || node.parent === undefined) { + return false; + } - const parentLayoutIsNone = - "layoutMode" in node.parent && node.parent.layoutMode === "NONE"; - const hasNoLayoutMode = !("layoutMode" in node.parent); + const parentLayoutIsNone = + "layoutMode" in node.parent && node.parent.layoutMode === "NONE"; + const hasNoLayoutMode = !("layoutMode" in node.parent); - if ( - node.layoutPositioning === "ABSOLUTE" || - parentLayoutIsNone || - hasNoLayoutMode - ) { - return true; - } + if ( + ("layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE") || + parentLayoutIsNone || + hasNoLayoutMode + ) { + return true; } + return false; }; diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index b39e6225..aa1613c8 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -170,7 +170,7 @@ const tailwindFrame = async ( ); const childrenStr = await tailwindWidgetGenerator(sortedChildren, settings); - const clipsContentClass = node.clipsContent ? " overflow-hidden" : ""; + const clipsContentClass = node.clipsContent ? "overflow-hidden" : ""; let layoutProps = ""; if (node.layoutMode !== "NONE") { @@ -182,16 +182,14 @@ const tailwindFrame = async ( layoutProps = tailwindAutoLayoutProps(node, node.inferredAutoLayout); } - return tailwindContainer( - node, - childrenStr, - layoutProps + clipsContentClass, - settings, - ); + // Combine classes properly, ensuring no extra spaces + const combinedProps = [layoutProps, clipsContentClass] + .filter(Boolean) + .join(" "); + + return tailwindContainer(node, childrenStr, combinedProps, settings); }; -// Properties named propSomething always take care of "," -// Sometimes a property might not exist, so it doesn't add "," export const tailwindContainer = ( node: SceneNode & SceneNodeMixin & From 9078e4f6240a6e8203727d91409523446b9c8b96 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 4 Mar 2025 12:07:25 -0300 Subject: [PATCH 012/134] Improve about --- packages/backend/src/api_types.ts | 6899 +++++++++++++++++ .../src/tailwind/tailwindDefaultBuilder.ts | 47 +- packages/plugin-ui/src/components/About.tsx | 13 +- 3 files changed, 6936 insertions(+), 23 deletions(-) create mode 100644 packages/backend/src/api_types.ts diff --git a/packages/backend/src/api_types.ts b/packages/backend/src/api_types.ts new file mode 100644 index 00000000..b5657707 --- /dev/null +++ b/packages/backend/src/api_types.ts @@ -0,0 +1,6899 @@ +// Copied from https://github.com/figma/rest-api-spec/blob/main/dist/api_types.ts + +export type IsLayerTrait = { + /** + * A string uniquely identifying this node within the document. + */ + id: string + + /** + * The name given to the node by the user in the tool. + */ + name: string + + /** + * The type of the node + */ + type: string + + /** + * Whether or not the node is visible on the canvas. + */ + visible?: boolean + + /** + * If true, layer is locked and cannot be edited + */ + locked?: boolean + + /** + * Whether the layer is fixed while the parent is scrolling + * + * @deprecated + */ + isFixed?: boolean + + /** + * How layer should be treated when the frame is resized + */ + scrollBehavior: 'SCROLLS' | 'FIXED' | 'STICKY_SCROLLS' + + /** + * The rotation of the node, if not 0. + */ + rotation?: number + + /** + * A mapping of a layer's property to component property name of component properties attached to + * this node. The component property name can be used to look up more information on the + * corresponding component's or component set's componentPropertyDefinitions. + */ + componentPropertyReferences?: { [key: string]: string } + + /** + * Data written by plugins that is visible only to the plugin that wrote it. Requires the + * `pluginData` to include the ID of the plugin. + */ + pluginData?: unknown + + /** + * Data written by plugins that is visible to all plugins. Requires the `pluginData` parameter to + * include the string "shared". + */ + sharedPluginData?: unknown + + /** + * A mapping of field to the variables applied to this field. Most fields will only map to a single + * `VariableAlias`. However, for properties like `fills`, `strokes`, `size`, `componentProperties`, + * and `textRangeFills`, it is possible to have multiple variables bound to the field. + */ + boundVariables?: { + size?: { + x?: VariableAlias + + y?: VariableAlias + } + + individualStrokeWeights?: { + top?: VariableAlias + + bottom?: VariableAlias + + left?: VariableAlias + + right?: VariableAlias + } + + characters?: VariableAlias + + itemSpacing?: VariableAlias + + paddingLeft?: VariableAlias + + paddingRight?: VariableAlias + + paddingTop?: VariableAlias + + paddingBottom?: VariableAlias + + visible?: VariableAlias + + topLeftRadius?: VariableAlias + + topRightRadius?: VariableAlias + + bottomLeftRadius?: VariableAlias + + bottomRightRadius?: VariableAlias + + minWidth?: VariableAlias + + maxWidth?: VariableAlias + + minHeight?: VariableAlias + + maxHeight?: VariableAlias + + counterAxisSpacing?: VariableAlias + + opacity?: VariableAlias + + fontFamily?: VariableAlias[] + + fontSize?: VariableAlias[] + + fontStyle?: VariableAlias[] + + fontWeight?: VariableAlias[] + + letterSpacing?: VariableAlias[] + + lineHeight?: VariableAlias[] + + paragraphSpacing?: VariableAlias[] + + paragraphIndent?: VariableAlias[] + + fills?: VariableAlias[] + + strokes?: VariableAlias[] + + componentProperties?: { [key: string]: VariableAlias } + + textRangeFills?: VariableAlias[] + + effects?: VariableAlias[] + + layoutGrids?: VariableAlias[] + } + + /** + * A mapping of variable collection ID to mode ID representing the explicitly set modes for this + * node. + */ + explicitVariableModes?: { [key: string]: string } + } + + export type HasChildrenTrait = { + /** + * An array of nodes that are direct children of this node + */ + children: SubcanvasNode[] + } + + export type HasLayoutTrait = { + /** + * Bounding box of the node in absolute space coordinates. + */ + absoluteBoundingBox: Rectangle | null + + /** + * The actual bounds of a node accounting for drop shadows, thick strokes, and anything else that + * may fall outside the node's regular bounding box defined in `x`, `y`, `width`, and `height`. The + * `x` and `y` inside this property represent the absolute position of the node on the page. This + * value will be `null` if the node is invisible. + */ + absoluteRenderBounds: Rectangle | null + + /** + * Keep height and width constrained to same ratio. + */ + preserveRatio?: boolean + + /** + * Horizontal and vertical layout constraints for node. + */ + constraints?: LayoutConstraint + + /** + * The top two rows of a matrix that represents the 2D transform of this node relative to its + * parent. The bottom row of the matrix is implicitly always (0, 0, 1). Use to transform coordinates + * in geometry. Only present if `geometry=paths` is passed. + */ + relativeTransform?: Transform + + /** + * Width and height of element. This is different from the width and height of the bounding box in + * that the absolute bounding box represents the element after scaling and rotation. Only present if + * `geometry=paths` is passed. + */ + size?: Vector + + /** + * Determines if the layer should stretch along the parent's counter axis. This property is only + * provided for direct children of auto-layout frames. + * + * - `INHERIT` + * - `STRETCH` + * + * In previous versions of auto layout, determined how the layer is aligned inside an auto-layout + * frame. This property is only provided for direct children of auto-layout frames. + * + * - `MIN` + * - `CENTER` + * - `MAX` + * - `STRETCH` + * + * In horizontal auto-layout frames, "MIN" and "MAX" correspond to "TOP" and "BOTTOM". In vertical + * auto-layout frames, "MIN" and "MAX" correspond to "LEFT" and "RIGHT". + */ + layoutAlign?: 'INHERIT' | 'STRETCH' | 'MIN' | 'CENTER' | 'MAX' + + /** + * This property is applicable only for direct children of auto-layout frames, ignored otherwise. + * Determines whether a layer should stretch along the parent's primary axis. A `0` corresponds to a + * fixed size and `1` corresponds to stretch. + */ + layoutGrow?: 0 | 1 + + /** + * Determines whether a layer's size and position should be determined by auto-layout settings or + * manually adjustable. + */ + layoutPositioning?: 'AUTO' | 'ABSOLUTE' + + /** + * The minimum width of the frame. This property is only applicable for auto-layout frames or direct + * children of auto-layout frames. + */ + minWidth?: number + + /** + * The maximum width of the frame. This property is only applicable for auto-layout frames or direct + * children of auto-layout frames. + */ + maxWidth?: number + + /** + * The minimum height of the frame. This property is only applicable for auto-layout frames or + * direct children of auto-layout frames. + */ + minHeight?: number + + /** + * The maximum height of the frame. This property is only applicable for auto-layout frames or + * direct children of auto-layout frames. + */ + maxHeight?: number + + /** + * The horizontal sizing setting on this auto-layout frame or frame child. + * + * - `FIXED` + * - `HUG`: only valid on auto-layout frames and text nodes + * - `FILL`: only valid on auto-layout frame children + */ + layoutSizingHorizontal?: 'FIXED' | 'HUG' | 'FILL' + + /** + * The vertical sizing setting on this auto-layout frame or frame child. + * + * - `FIXED` + * - `HUG`: only valid on auto-layout frames and text nodes + * - `FILL`: only valid on auto-layout frame children + */ + layoutSizingVertical?: 'FIXED' | 'HUG' | 'FILL' + } + + export type HasFramePropertiesTrait = { + /** + * Whether or not this node clip content outside of its bounds + */ + clipsContent: boolean + + /** + * Background of the node. This is deprecated, as backgrounds for frames are now in the `fills` + * field. + * + * @deprecated + */ + background?: Paint[] + + /** + * Background color of the node. This is deprecated, as frames now support more than a solid color + * as a background. Please use the `fills` field instead. + * + * @deprecated + */ + backgroundColor?: RGBA + + /** + * An array of layout grids attached to this node (see layout grids section for more details). GROUP + * nodes do not have this attribute + */ + layoutGrids?: LayoutGrid[] + + /** + * Whether a node has primary axis scrolling, horizontal or vertical. + */ + overflowDirection?: + | 'HORIZONTAL_SCROLLING' + | 'VERTICAL_SCROLLING' + | 'HORIZONTAL_AND_VERTICAL_SCROLLING' + | 'NONE' + + /** + * Whether this layer uses auto-layout to position its children. + */ + layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL' + + /** + * Whether the primary axis has a fixed length (determined by the user) or an automatic length + * (determined by the layout engine). This property is only applicable for auto-layout frames. + */ + primaryAxisSizingMode?: 'FIXED' | 'AUTO' + + /** + * Whether the counter axis has a fixed length (determined by the user) or an automatic length + * (determined by the layout engine). This property is only applicable for auto-layout frames. + */ + counterAxisSizingMode?: 'FIXED' | 'AUTO' + + /** + * Determines how the auto-layout frame's children should be aligned in the primary axis direction. + * This property is only applicable for auto-layout frames. + */ + primaryAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN' + + /** + * Determines how the auto-layout frame's children should be aligned in the counter axis direction. + * This property is only applicable for auto-layout frames. + */ + counterAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'BASELINE' + + /** + * The padding between the left border of the frame and its children. This property is only + * applicable for auto-layout frames. + */ + paddingLeft?: number + + /** + * The padding between the right border of the frame and its children. This property is only + * applicable for auto-layout frames. + */ + paddingRight?: number + + /** + * The padding between the top border of the frame and its children. This property is only + * applicable for auto-layout frames. + */ + paddingTop?: number + + /** + * The padding between the bottom border of the frame and its children. This property is only + * applicable for auto-layout frames. + */ + paddingBottom?: number + + /** + * The distance between children of the frame. Can be negative. This property is only applicable for + * auto-layout frames. + */ + itemSpacing?: number + + /** + * Determines the canvas stacking order of layers in this frame. When true, the first layer will be + * draw on top. This property is only applicable for auto-layout frames. + */ + itemReverseZIndex?: boolean + + /** + * Determines whether strokes are included in layout calculations. When true, auto-layout frames + * behave like css "box-sizing: border-box". This property is only applicable for auto-layout + * frames. + */ + strokesIncludedInLayout?: boolean + + /** + * Whether this auto-layout frame has wrapping enabled. + */ + layoutWrap?: 'NO_WRAP' | 'WRAP' + + /** + * The distance between wrapped tracks of an auto-layout frame. This property is only applicable for + * auto-layout frames with `layoutWrap: "WRAP"` + */ + counterAxisSpacing?: number + + /** + * Determines how the auto-layout frame’s wrapped tracks should be aligned in the counter axis + * direction. This property is only applicable for auto-layout frames with `layoutWrap: "WRAP"`. + */ + counterAxisAlignContent?: 'AUTO' | 'SPACE_BETWEEN' + } + + export type HasBlendModeAndOpacityTrait = { + /** + * How this node blends with nodes behind it in the scene (see blend mode section for more details) + */ + blendMode: BlendMode + + /** + * Opacity of the node + */ + opacity?: number + } + + export type HasExportSettingsTrait = { + /** + * An array of export settings representing images to export from the node. + */ + exportSettings?: ExportSetting[] + } + + export type HasGeometryTrait = MinimalFillsTrait & + MinimalStrokesTrait & { + /** + * Map from ID to PaintOverride for looking up fill overrides. To see which regions are overriden, + * you must use the `geometry=paths` option. Each path returned may have an `overrideID` which maps + * to this table. + */ + fillOverrideTable?: { [key: string]: PaintOverride | null } + + /** + * Only specified if parameter `geometry=paths` is used. An array of paths representing the object + * fill. + */ + fillGeometry?: Path[] + + /** + * Only specified if parameter `geometry=paths` is used. An array of paths representing the object + * stroke. + */ + strokeGeometry?: Path[] + + /** + * A string enum describing the end caps of vector paths. + */ + strokeCap?: + | 'NONE' + | 'ROUND' + | 'SQUARE' + | 'LINE_ARROW' + | 'TRIANGLE_ARROW' + | 'DIAMOND_FILLED' + | 'CIRCLE_FILLED' + | 'TRIANGLE_FILLED' + | 'WASHI_TAPE_1' + | 'WASHI_TAPE_2' + | 'WASHI_TAPE_3' + | 'WASHI_TAPE_4' + | 'WASHI_TAPE_5' + | 'WASHI_TAPE_6' + + /** + * Only valid if `strokeJoin` is "MITER". The corner angle, in degrees, below which `strokeJoin` + * will be set to "BEVEL" to avoid super sharp corners. By default this is 28.96 degrees. + */ + strokeMiterAngle?: number + } + + export type MinimalFillsTrait = { + /** + * An array of fill paints applied to the node. + */ + fills: Paint[] + + /** + * A mapping of a StyleType to style ID (see Style) of styles present on this node. The style ID can + * be used to look up more information about the style in the top-level styles field. + */ + styles?: { [key: string]: string } + } + + export type MinimalStrokesTrait = { + /** + * An array of stroke paints applied to the node. + */ + strokes?: Paint[] + + /** + * The weight of strokes on the node. + */ + strokeWeight?: number + + /** + * Position of stroke relative to vector outline, as a string enum + * + * - `INSIDE`: stroke drawn inside the shape boundary + * - `OUTSIDE`: stroke drawn outside the shape boundary + * - `CENTER`: stroke drawn centered along the shape boundary + */ + strokeAlign?: 'INSIDE' | 'OUTSIDE' | 'CENTER' + + /** + * A string enum with value of "MITER", "BEVEL", or "ROUND", describing how corners in vector paths + * are rendered. + */ + strokeJoin?: 'MITER' | 'BEVEL' | 'ROUND' + + /** + * An array of floating point numbers describing the pattern of dash length and gap lengths that the + * vector stroke will use when drawn. + * + * For example a value of [1, 2] indicates that the stroke will be drawn with a dash of length 1 + * followed by a gap of length 2, repeated. + */ + strokeDashes?: number[] + } + + export type IndividualStrokesTrait = { + /** + * An object including the top, bottom, left, and right stroke weights. Only returned if individual + * stroke weights are used. + */ + individualStrokeWeights?: StrokeWeights + } + + export type CornerTrait = { + /** + * Radius of each corner if a single radius is set for all corners + */ + cornerRadius?: number + + /** + * A value that lets you control how "smooth" the corners are. Ranges from 0 to 1. 0 is the default + * and means that the corner is perfectly circular. A value of 0.6 means the corner matches the iOS + * 7 "squircle" icon shape. Other values produce various other curves. + */ + cornerSmoothing?: number + + /** + * Array of length 4 of the radius of each corner of the frame, starting in the top left and + * proceeding clockwise. + * + * Values are given in the order top-left, top-right, bottom-right, bottom-left. + */ + rectangleCornerRadii?: number[] + } + + export type HasEffectsTrait = { + /** + * An array of effects attached to this node (see effects section for more details) + */ + effects: Effect[] + } + + export type HasMaskTrait = { + /** + * Does this node mask sibling nodes in front of it? + */ + isMask?: boolean + + /** + * If this layer is a mask, this property describes the operation used to mask the layer's siblings. + * The value may be one of the following: + * + * - ALPHA: the mask node's alpha channel will be used to determine the opacity of each pixel in the + * masked result. + * - VECTOR: if the mask node has visible fill paints, every pixel inside the node's fill regions will + * be fully visible in the masked result. If the mask has visible stroke paints, every pixel + * inside the node's stroke regions will be fully visible in the masked result. + * - LUMINANCE: the luminance value of each pixel of the mask node will be used to determine the + * opacity of that pixel in the masked result. + */ + maskType?: 'ALPHA' | 'VECTOR' | 'LUMINANCE' + + /** + * True if maskType is VECTOR. This field is deprecated; use maskType instead. + * + * @deprecated + */ + isMaskOutline?: boolean + } + + export type ComponentPropertiesTrait = { + /** + * A mapping of name to `ComponentPropertyDefinition` for every component property on this + * component. Each property has a type, defaultValue, and other optional values. + */ + componentPropertyDefinitions?: { [key: string]: ComponentPropertyDefinition } + } + + export type TypePropertiesTrait = { + /** + * The raw characters in the text node. + */ + characters: string + + /** + * Style of text including font family and weight. + */ + style: TypeStyle + + /** + * The array corresponds to characters in the text box, where each element references the + * 'styleOverrideTable' to apply specific styles to each character. The array's length can be less + * than or equal to the number of characters due to the removal of trailing zeros. Elements with a + * value of 0 indicate characters that use the default type style. If the array is shorter than the + * total number of characters, the characters beyond the array's length also use the default style. + */ + characterStyleOverrides: number[] + + /** + * Internal property, preserved for backward compatibility. Avoid using this value. + */ + layoutVersion?: number + + /** + * Map from ID to TypeStyle for looking up style overrides. + */ + styleOverrideTable: { [key: string]: TypeStyle } + + /** + * An array with the same number of elements as lines in the text node, where lines are delimited by + * newline or paragraph separator characters. Each element in the array corresponds to the list type + * of a specific line. List types are represented as string enums with one of these possible + * values: + * + * - `NONE`: Not a list item. + * - `ORDERED`: Text is an ordered list (numbered). + * - `UNORDERED`: Text is an unordered list (bulleted). + */ + lineTypes: ('NONE' | 'ORDERED' | 'UNORDERED')[] + + /** + * An array with the same number of elements as lines in the text node, where lines are delimited by + * newline or paragraph separator characters. Each element in the array corresponds to the + * indentation level of a specific line. + */ + lineIndentations: number[] + } + + export type HasTextSublayerTrait = { + /** + * Text contained within a text box. + */ + characters: string + } + + export type TransitionSourceTrait = { + /** + * Node ID of node to transition to in prototyping + */ + transitionNodeID?: string + + /** + * The duration of the prototyping transition on this node (in milliseconds). This will override the + * default transition duration on the prototype, for this node. + */ + transitionDuration?: number + + /** + * The easing curve used in the prototyping transition on this node. + */ + transitionEasing?: EasingType + + interactions?: Interaction[] + } + + export type DevStatusTrait = { + /** + * Represents whether or not a node has a particular handoff (or dev) status applied to it. + */ + devStatus?: { + type: 'NONE' | 'READY_FOR_DEV' | 'COMPLETED' + + /** + * An optional field where the designer can add more information about the design and what has + * changed. + */ + description?: string + } + } + + export type AnnotationsTrait = object + + export type FrameTraits = IsLayerTrait & + HasBlendModeAndOpacityTrait & + HasChildrenTrait & + HasLayoutTrait & + HasFramePropertiesTrait & + CornerTrait & + HasGeometryTrait & + HasExportSettingsTrait & + HasEffectsTrait & + HasMaskTrait & + TransitionSourceTrait & + IndividualStrokesTrait & + DevStatusTrait & + AnnotationsTrait + + export type DefaultShapeTraits = IsLayerTrait & + HasBlendModeAndOpacityTrait & + HasLayoutTrait & + HasGeometryTrait & + HasExportSettingsTrait & + HasEffectsTrait & + HasMaskTrait & + TransitionSourceTrait + + export type CornerRadiusShapeTraits = DefaultShapeTraits & CornerTrait + + export type RectangularShapeTraits = DefaultShapeTraits & + CornerTrait & + IndividualStrokesTrait & + AnnotationsTrait + + export type Node = + | BooleanOperationNode + | ComponentNode + | ComponentSetNode + | ConnectorNode + | EllipseNode + | EmbedNode + | FrameNode + | GroupNode + | InstanceNode + | LineNode + | LinkUnfurlNode + | RectangleNode + | RegularPolygonNode + | SectionNode + | ShapeWithTextNode + | SliceNode + | StarNode + | StickyNode + | TableNode + | TableCellNode + | TextNode + | VectorNode + | WashiTapeNode + | WidgetNode + | DocumentNode + | CanvasNode + + export type DocumentNode = { + type: 'DOCUMENT' + + children: CanvasNode[] + } & IsLayerTrait + + export type CanvasNode = { + type: 'CANVAS' + + children: SubcanvasNode[] + + /** + * Background color of the canvas. + */ + backgroundColor: RGBA + + /** + * Node ID that corresponds to the start frame for prototypes. This is deprecated with the + * introduction of multiple flows. Please use the `flowStartingPoints` field. + * + * @deprecated + */ + prototypeStartNodeID: string | null + + /** + * An array of flow starting points sorted by its position in the prototype settings panel. + */ + flowStartingPoints: FlowStartingPoint[] + + /** + * The device used to view a prototype. + */ + prototypeDevice: PrototypeDevice + + measurements?: Measurement[] + } & IsLayerTrait & + HasExportSettingsTrait + + export type SubcanvasNode = + | BooleanOperationNode + | ComponentNode + | ComponentSetNode + | ConnectorNode + | EllipseNode + | EmbedNode + | FrameNode + | GroupNode + | InstanceNode + | LineNode + | LinkUnfurlNode + | RectangleNode + | RegularPolygonNode + | SectionNode + | ShapeWithTextNode + | SliceNode + | StarNode + | StickyNode + | TableNode + | TableCellNode + | TextNode + | VectorNode + | WashiTapeNode + | WidgetNode + + export type BooleanOperationNode = { + /** + * The type of this node, represented by the string literal "BOOLEAN_OPERATION" + */ + type: 'BOOLEAN_OPERATION' + + /** + * A string enum indicating the type of boolean operation applied. + */ + booleanOperation: 'UNION' | 'INTERSECT' | 'SUBTRACT' | 'EXCLUDE' + } & IsLayerTrait & + HasBlendModeAndOpacityTrait & + HasChildrenTrait & + HasLayoutTrait & + HasGeometryTrait & + HasExportSettingsTrait & + HasEffectsTrait & + HasMaskTrait & + TransitionSourceTrait + + export type SectionNode = { + /** + * The type of this node, represented by the string literal "SECTION" + */ + type: 'SECTION' + + /** + * Whether the contents of the section are visible + */ + sectionContentsHidden: boolean + } & IsLayerTrait & + HasGeometryTrait & + HasChildrenTrait & + HasLayoutTrait & + DevStatusTrait + + export type FrameNode = { + /** + * The type of this node, represented by the string literal "FRAME" + */ + type: 'FRAME' + } & FrameTraits + + export type GroupNode = { + /** + * The type of this node, represented by the string literal "GROUP" + */ + type: 'GROUP' + } & FrameTraits + + export type ComponentNode = { + /** + * The type of this node, represented by the string literal "COMPONENT" + */ + type: 'COMPONENT' + } & FrameTraits & + ComponentPropertiesTrait + + export type ComponentSetNode = { + /** + * The type of this node, represented by the string literal "COMPONENT_SET" + */ + type: 'COMPONENT_SET' + } & FrameTraits & + ComponentPropertiesTrait + + export type VectorNode = { + /** + * The type of this node, represented by the string literal "VECTOR" + */ + type: 'VECTOR' + } & CornerRadiusShapeTraits & + AnnotationsTrait + + export type StarNode = { + /** + * The type of this node, represented by the string literal "STAR" + */ + type: 'STAR' + } & CornerRadiusShapeTraits & + AnnotationsTrait + + export type LineNode = { + /** + * The type of this node, represented by the string literal "LINE" + */ + type: 'LINE' + } & DefaultShapeTraits & + AnnotationsTrait + + export type EllipseNode = { + /** + * The type of this node, represented by the string literal "ELLIPSE" + */ + type: 'ELLIPSE' + + arcData: ArcData + } & DefaultShapeTraits & + AnnotationsTrait + + export type RegularPolygonNode = { + /** + * The type of this node, represented by the string literal "REGULAR_POLYGON" + */ + type: 'REGULAR_POLYGON' + } & CornerRadiusShapeTraits & + AnnotationsTrait + + export type RectangleNode = { + /** + * The type of this node, represented by the string literal "RECTANGLE" + */ + type: 'RECTANGLE' + } & RectangularShapeTraits + + export type TextNode = { + /** + * The type of this node, represented by the string literal "TEXT" + */ + type: 'TEXT' + } & DefaultShapeTraits & + TypePropertiesTrait & + AnnotationsTrait + + export type TableNode = { + /** + * The type of this node, represented by the string literal "TABLE" + */ + type: 'TABLE' + } & IsLayerTrait & + HasChildrenTrait & + HasLayoutTrait & + MinimalStrokesTrait & + HasEffectsTrait & + HasBlendModeAndOpacityTrait & + HasExportSettingsTrait + + export type TableCellNode = { + /** + * The type of this node, represented by the string literal "TABLE_CELL" + */ + type: 'TABLE_CELL' + } & IsLayerTrait & + MinimalFillsTrait & + HasLayoutTrait & + HasTextSublayerTrait + + export type SliceNode = { + /** + * The type of this node, represented by the string literal "SLICE" + */ + type: 'SLICE' + } & IsLayerTrait + + export type InstanceNode = { + /** + * The type of this node, represented by the string literal "INSTANCE" + */ + type: 'INSTANCE' + + /** + * ID of component that this instance came from. + */ + componentId: string + + /** + * If true, this node has been marked as exposed to its containing component or component set. + */ + isExposedInstance?: boolean + + /** + * IDs of instances that have been exposed to this node's level. + */ + exposedInstances?: string[] + + /** + * A mapping of name to `ComponentProperty` for all component properties on this instance. Each + * property has a type, value, and other optional values. + */ + componentProperties?: { [key: string]: ComponentProperty } + + /** + * An array of all of the fields directly overridden on this instance. Inherited overrides are not + * included. + */ + overrides: Overrides[] + } & FrameTraits + + export type EmbedNode = { + /** + * The type of this node, represented by the string literal "EMBED" + */ + type: 'EMBED' + } & IsLayerTrait & + HasExportSettingsTrait + + export type LinkUnfurlNode = { + /** + * The type of this node, represented by the string literal "LINK_UNFURL" + */ + type: 'LINK_UNFURL' + } & IsLayerTrait & + HasExportSettingsTrait + + export type StickyNode = { + /** + * The type of this node, represented by the string literal "STICKY" + */ + type: 'STICKY' + + /** + * If true, author name is visible. + */ + authorVisible?: boolean + } & IsLayerTrait & + HasLayoutTrait & + HasBlendModeAndOpacityTrait & + MinimalFillsTrait & + HasMaskTrait & + HasEffectsTrait & + HasExportSettingsTrait & + HasTextSublayerTrait + + export type ShapeWithTextNode = { + /** + * The type of this node, represented by the string literal "SHAPE_WITH_TEXT" + */ + type: 'SHAPE_WITH_TEXT' + + /** + * Geometric shape type. Most shape types have the same name as their tooltip but there are a few + * exceptions. ENG_DATABASE: Cylinder, ENG_QUEUE: Horizontal cylinder, ENG_FILE: File, ENG_FOLDER: + * Folder. + */ + shapeType: ShapeType + } & IsLayerTrait & + HasLayoutTrait & + HasBlendModeAndOpacityTrait & + MinimalFillsTrait & + HasMaskTrait & + HasEffectsTrait & + HasExportSettingsTrait & + HasTextSublayerTrait & + CornerTrait & + MinimalStrokesTrait + + export type ConnectorNode = { + /** + * The type of this node, represented by the string literal "CONNECTOR" + */ + type: 'CONNECTOR' + + /** + * The starting point of the connector. + */ + connectorStart: ConnectorEndpoint + + /** + * The ending point of the connector. + */ + connectorEnd: ConnectorEndpoint + + /** + * A string enum describing the end cap of the start of the connector. + */ + connectorStartStrokeCap: + | 'NONE' + | 'LINE_ARROW' + | 'TRIANGLE_ARROW' + | 'DIAMOND_FILLED' + | 'CIRCLE_FILLED' + | 'TRIANGLE_FILLED' + + /** + * A string enum describing the end cap of the end of the connector. + */ + connectorEndStrokeCap: + | 'NONE' + | 'LINE_ARROW' + | 'TRIANGLE_ARROW' + | 'DIAMOND_FILLED' + | 'CIRCLE_FILLED' + | 'TRIANGLE_FILLED' + + /** + * Connector line type. + */ + connectorLineType: ConnectorLineType + + /** + * Connector text background. + */ + textBackground?: ConnectorTextBackground + } & IsLayerTrait & + HasLayoutTrait & + HasBlendModeAndOpacityTrait & + HasEffectsTrait & + HasExportSettingsTrait & + HasTextSublayerTrait & + MinimalStrokesTrait + + export type WashiTapeNode = { + /** + * The type of this node, represented by the string literal "WASHI_TAPE" + */ + type: 'WASHI_TAPE' + } & DefaultShapeTraits + + export type WidgetNode = { + /** + * The type of this node, represented by the string literal "WIDGET" + */ + type: 'WIDGET' + } & IsLayerTrait & + HasExportSettingsTrait & + HasChildrenTrait + + /** + * An RGB color + */ + export type RGB = { + /** + * Red channel value, between 0 and 1. + */ + r: number + + /** + * Green channel value, between 0 and 1. + */ + g: number + + /** + * Blue channel value, between 0 and 1. + */ + b: number + } + + /** + * An RGBA color + */ + export type RGBA = { + /** + * Red channel value, between 0 and 1. + */ + r: number + + /** + * Green channel value, between 0 and 1. + */ + g: number + + /** + * Blue channel value, between 0 and 1. + */ + b: number + + /** + * Alpha channel value, between 0 and 1. + */ + a: number + } + + /** + * A flow starting point used when launching a prototype to enter Presentation view. + */ + export type FlowStartingPoint = { + /** + * Unique identifier specifying the frame. + */ + nodeId: string + + /** + * Name of flow. + */ + name: string + } + + /** + * A width and a height. + */ + export type Size = { + /** + * The width of a size. + */ + width: number + + /** + * The height of a size. + */ + height: number + } + + /** + * The device used to view a prototype. + */ + export type PrototypeDevice = { + type: 'NONE' | 'PRESET' | 'CUSTOM' | 'PRESENTATION' + + size?: Size + + presetIdentifier?: string + + rotation: 'NONE' | 'CCW_90' + } + + /** + * Sizing constraint for exports. + */ + export type Constraint = { + /** + * Type of constraint to apply: + * + * - `SCALE`: Scale by `value`. + * - `WIDTH`: Scale proportionally and set width to `value`. + * - `HEIGHT`: Scale proportionally and set height to `value`. + */ + type: 'SCALE' | 'WIDTH' | 'HEIGHT' + + /** + * See type property for effect of this field. + */ + value: number + } + + /** + * An export setting. + */ + export type ExportSetting = { + suffix: string + + format: 'JPG' | 'PNG' | 'SVG' | 'PDF' + + constraint: Constraint + } + + /** + * This type is a string enum with the following possible values + * + * Normal blends: + * + * - `PASS_THROUGH` (only applicable to objects with children) + * - `NORMAL` + * + * Darken: + * + * - `DARKEN` + * - `MULTIPLY` + * - `LINEAR_BURN` + * - `COLOR_BURN` + * + * Lighten: + * + * - `LIGHTEN` + * - `SCREEN` + * - `LINEAR_DODGE` + * - `COLOR_DODGE` + * + * Contrast: + * + * - `OVERLAY` + * - `SOFT_LIGHT` + * - `HARD_LIGHT` + * + * Inversion: + * + * - `DIFFERENCE` + * - `EXCLUSION` + * + * Component: + * + * - `HUE` + * - `SATURATION` + * - `COLOR` + * - `LUMINOSITY` + */ + export type BlendMode = + | 'PASS_THROUGH' + | 'NORMAL' + | 'DARKEN' + | 'MULTIPLY' + | 'LINEAR_BURN' + | 'COLOR_BURN' + | 'LIGHTEN' + | 'SCREEN' + | 'LINEAR_DODGE' + | 'COLOR_DODGE' + | 'OVERLAY' + | 'SOFT_LIGHT' + | 'HARD_LIGHT' + | 'DIFFERENCE' + | 'EXCLUSION' + | 'HUE' + | 'SATURATION' + | 'COLOR' + | 'LUMINOSITY' + + /** + * A 2d vector. + */ + export type Vector = { + /** + * X coordinate of the vector. + */ + x: number + + /** + * Y coordinate of the vector. + */ + y: number + } + + /** + * A single color stop with its position along the gradient axis, color, and bound variables if any + */ + export type ColorStop = { + /** + * Value between 0 and 1 representing position along gradient axis. + */ + position: number + + /** + * Color attached to corresponding position. + */ + color: RGBA + + /** + * The variables bound to a particular gradient stop + */ + boundVariables?: { color?: VariableAlias } + } + + /** + * A transformation matrix is standard way in computer graphics to represent translation and + * rotation. These are the top two rows of a 3x3 matrix. The bottom row of the matrix is assumed to + * be [0, 0, 1]. This is known as an affine transform and is enough to represent translation, + * rotation, and skew. + * + * The identity transform is [[1, 0, 0], [0, 1, 0]]. + * + * A translation matrix will typically look like: + * + * ;[ + * [1, 0, tx], + * [0, 1, ty], + * ] + * + * And a rotation matrix will typically look like: + * + * ;[ + * [cos(angle), sin(angle), 0], + * [-sin(angle), cos(angle), 0], + * ] + * + * Another way to think about this transform is as three vectors: + * + * - The x axis (t[0][0], t[1][0]) + * - The y axis (t[0][1], t[1][1]) + * - The translation offset (t[0][2], t[1][2]) + * + * The most common usage of the Transform matrix is the `relativeTransform property`. This + * particular usage of the matrix has a few additional restrictions. The translation offset can take + * on any value but we do enforce that the axis vectors are unit vectors (i.e. have length 1). The + * axes are not required to be at 90° angles to each other. + */ + export type Transform = number[][] + + /** + * Image filters to apply to the node. + */ + export type ImageFilters = { + exposure?: number + + contrast?: number + + saturation?: number + + temperature?: number + + tint?: number + + highlights?: number + + shadows?: number + } + + export type BasePaint = { + /** + * Is the paint enabled? + */ + visible?: boolean + + /** + * Overall opacity of paint (colors within the paint can also have opacity values which would blend + * with this) + */ + opacity?: number + + /** + * How this node blends with nodes behind it in the scene + */ + blendMode: BlendMode + } + + export type SolidPaint = { + /** + * The string literal "SOLID" representing the paint's type. Always check the `type` before reading + * other properties. + */ + type: 'SOLID' + + /** + * Solid color of the paint + */ + color: RGBA + + /** + * The variables bound to a particular field on this paint + */ + boundVariables?: { color?: VariableAlias } + } & BasePaint + + export type GradientPaint = { + /** + * The string literal representing the paint's type. Always check the `type` before reading other + * properties. + */ + type: 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' + + /** + * This field contains three vectors, each of which are a position in normalized object space + * (normalized object space is if the top left corner of the bounding box of the object is (0, 0) + * and the bottom right is (1,1)). The first position corresponds to the start of the gradient + * (value 0 for the purposes of calculating gradient stops), the second position is the end of the + * gradient (value 1), and the third handle position determines the width of the gradient. + */ + gradientHandlePositions: Vector[] + + /** + * Positions of key points along the gradient axis with the colors anchored there. Colors along the + * gradient are interpolated smoothly between neighboring gradient stops. + */ + gradientStops: ColorStop[] + } & BasePaint + + export type ImagePaint = { + /** + * The string literal "IMAGE" representing the paint's type. Always check the `type` before reading + * other properties. + */ + type: 'IMAGE' + + /** + * Image scaling mode. + */ + scaleMode: 'FILL' | 'FIT' | 'TILE' | 'STRETCH' + + /** + * A reference to an image embedded in this node. To download the image using this reference, use + * the `GET file images` endpoint to retrieve the mapping from image references to image URLs. + */ + imageRef: string + + /** + * Affine transform applied to the image, only present if `scaleMode` is `STRETCH` + */ + imageTransform?: Transform + + /** + * Amount image is scaled by in tiling, only present if scaleMode is `TILE`. + */ + scalingFactor?: number + + /** + * Defines what image filters have been applied to this paint, if any. If this property is not + * defined, no filters have been applied. + */ + filters?: ImageFilters + + /** + * Image rotation, in degrees. + */ + rotation?: number + + /** + * A reference to an animated GIF embedded in this node. To download the image using this reference, + * use the `GET file images` endpoint to retrieve the mapping from image references to image URLs. + */ + gifRef?: string + } & BasePaint + + export type Paint = SolidPaint | GradientPaint | ImagePaint + + /** + * Layout constraint relative to containing Frame + */ + export type LayoutConstraint = { + /** + * Vertical constraint (relative to containing frame) as an enum: + * + * - `TOP`: Node is laid out relative to top of the containing frame + * - `BOTTOM`: Node is laid out relative to bottom of the containing frame + * - `CENTER`: Node is vertically centered relative to containing frame + * - `TOP_BOTTOM`: Both top and bottom of node are constrained relative to containing frame (node + * stretches with frame) + * - `SCALE`: Node scales vertically with containing frame + */ + vertical: 'TOP' | 'BOTTOM' | 'CENTER' | 'TOP_BOTTOM' | 'SCALE' + + /** + * Horizontal constraint (relative to containing frame) as an enum: + * + * - `LEFT`: Node is laid out relative to left of the containing frame + * - `RIGHT`: Node is laid out relative to right of the containing frame + * - `CENTER`: Node is horizontally centered relative to containing frame + * - `LEFT_RIGHT`: Both left and right of node are constrained relative to containing frame (node + * stretches with frame) + * - `SCALE`: Node scales horizontally with containing frame + */ + horizontal: 'LEFT' | 'RIGHT' | 'CENTER' | 'LEFT_RIGHT' | 'SCALE' + } + + /** + * A rectangle that expresses a bounding box in absolute coordinates. + */ + export type Rectangle = { + /** + * X coordinate of top left corner of the rectangle. + */ + x: number + + /** + * Y coordinate of top left corner of the rectangle. + */ + y: number + + /** + * Width of the rectangle. + */ + width: number + + /** + * Height of the rectangle. + */ + height: number + } + + /** + * Guides to align and place objects within a frames. + */ + export type LayoutGrid = { + /** + * Orientation of the grid as a string enum + * + * - `COLUMNS`: Vertical grid + * - `ROWS`: Horizontal grid + * - `GRID`: Square grid + */ + pattern: 'COLUMNS' | 'ROWS' | 'GRID' + + /** + * Width of column grid or height of row grid or square grid spacing. + */ + sectionSize: number + + /** + * Is the grid currently visible? + */ + visible: boolean + + /** + * Color of the grid + */ + color: RGBA + + /** + * Positioning of grid as a string enum + * + * - `MIN`: Grid starts at the left or top of the frame + * - `MAX`: Grid starts at the right or bottom of the frame + * - `STRETCH`: Grid is stretched to fit the frame + * - `CENTER`: Grid is center aligned + */ + alignment: 'MIN' | 'MAX' | 'STRETCH' | 'CENTER' + + /** + * Spacing in between columns and rows + */ + gutterSize: number + + /** + * Spacing before the first column or row + */ + offset: number + + /** + * Number of columns or rows + */ + count: number + + /** + * The variables bound to a particular field on this layout grid + */ + boundVariables?: { + gutterSize?: VariableAlias + + numSections?: VariableAlias + + sectionSize?: VariableAlias + + offset?: VariableAlias + } + } + + /** + * Base properties shared by all shadow effects + */ + export type BaseShadowEffect = { + /** + * The color of the shadow + */ + color: RGBA + + /** + * Blend mode of the shadow + */ + blendMode: BlendMode + + /** + * How far the shadow is projected in the x and y directions + */ + offset: Vector + + /** + * Radius of the blur effect (applies to shadows as well) + */ + radius: number + + /** + * The distance by which to expand (or contract) the shadow. + * + * For drop shadows, a positive `spread` value creates a shadow larger than the node, whereas a + * negative value creates a shadow smaller than the node. + * + * For inner shadows, a positive `spread` value contracts the shadow. Spread values are only + * accepted on rectangles and ellipses, or on frames, components, and instances with visible fill + * paints and `clipsContent` enabled. When left unspecified, the default value is 0. + */ + spread?: number + + /** + * Whether this shadow is visible. + */ + visible: boolean + + /** + * The variables bound to a particular field on this shadow effect + */ + boundVariables?: { + radius?: VariableAlias + + spread?: VariableAlias + + color?: VariableAlias + + offsetX?: VariableAlias + + offsetY?: VariableAlias + } + } + + export type DropShadowEffect = { + /** + * A string literal representing the effect's type. Always check the type before reading other + * properties. + */ + type: 'DROP_SHADOW' + + /** + * Whether to show the shadow behind translucent or transparent pixels + */ + showShadowBehindNode: boolean + } & BaseShadowEffect + + export type InnerShadowEffect = { + /** + * A string literal representing the effect's type. Always check the type before reading other + * properties. + */ + type?: 'INNER_SHADOW' + } & BaseShadowEffect + + /** + * A blur effect + */ + export type BlurEffect = { + /** + * A string literal representing the effect's type. Always check the type before reading other + * properties. + */ + type: 'LAYER_BLUR' | 'BACKGROUND_BLUR' + + /** + * Whether this blur is active. + */ + visible: boolean + + /** + * Radius of the blur effect + */ + radius: number + + /** + * The variables bound to a particular field on this blur effect + */ + boundVariables?: { radius?: VariableAlias } + } + + export type Effect = DropShadowEffect | InnerShadowEffect | BlurEffect + + /** + * A set of properties that can be applied to nodes and published. Styles for a property can be + * created in the corresponding property's panel while editing a file. + */ + export type Style = { + /** + * The key of the style + */ + key: string + + /** + * Name of the style + */ + name: string + + /** + * Description of the style + */ + description: string + + /** + * Whether this style is a remote style that doesn't live in this file + */ + remote: boolean + + styleType: StyleType + } + + /** + * This type is a string enum with the following possible values: + * + * - `EASE_IN`: Ease in with an animation curve similar to CSS ease-in. + * - `EASE_OUT`: Ease out with an animation curve similar to CSS ease-out. + * - `EASE_IN_AND_OUT`: Ease in and then out with an animation curve similar to CSS ease-in-out. + * - `LINEAR`: No easing, similar to CSS linear. + * - `EASE_IN_BACK`: Ease in with an animation curve that moves past the initial keyframe's value and + * then accelerates as it reaches the end. + * - `EASE_OUT_BACK`: Ease out with an animation curve that starts fast, then slows and goes past the + * ending keyframe's value. + * - `EASE_IN_AND_OUT_BACK`: Ease in and then out with an animation curve that overshoots the initial + * keyframe's value, then accelerates quickly before it slows and overshoots the ending keyframes + * value. + * - `CUSTOM_CUBIC_BEZIER`: User-defined cubic bezier curve. + * - `GENTLE`: Gentle animation similar to react-spring. + * - `QUICK`: Quick spring animation, great for toasts and notifications. + * - `BOUNCY`: Bouncy spring, for delightful animations like a heart bounce. + * - `SLOW`: Slow spring, useful as a steady, natural way to scale up fullscreen content. + * - `CUSTOM_SPRING`: User-defined spring animation. + */ + export type EasingType = + | 'EASE_IN' + | 'EASE_OUT' + | 'EASE_IN_AND_OUT' + | 'LINEAR' + | 'EASE_IN_BACK' + | 'EASE_OUT_BACK' + | 'EASE_IN_AND_OUT_BACK' + | 'CUSTOM_CUBIC_BEZIER' + | 'GENTLE' + | 'QUICK' + | 'BOUNCY' + | 'SLOW' + | 'CUSTOM_SPRING' + + /** + * Individual stroke weights + */ + export type StrokeWeights = { + /** + * The top stroke weight. + */ + top: number + + /** + * The right stroke weight. + */ + right: number + + /** + * The bottom stroke weight. + */ + bottom: number + + /** + * The left stroke weight. + */ + left: number + } + + /** + * Paint metadata to override default paints. + */ + export type PaintOverride = { + /** + * Paints applied to characters. + */ + fills?: Paint[] + + /** + * ID of style node, if any, that this inherits fill data from. + */ + inheritFillStyleId?: string + } + + /** + * Defines a single path + */ + export type Path = { + /** + * A series of path commands that encodes how to draw the path. + */ + path: string + + /** + * The winding rule for the path (same as in SVGs). This determines whether a given point in space + * is inside or outside the path. + */ + windingRule: 'NONZERO' | 'EVENODD' + + /** + * If there is a per-region fill, this refers to an ID in the `fillOverrideTable`. + */ + overrideID?: number + } + + /** + * Information about the arc properties of an ellipse. 0° is the x axis and increasing angles rotate + * clockwise. + */ + export type ArcData = { + /** + * Start of the sweep in radians. + */ + startingAngle: number + + /** + * End of the sweep in radians. + */ + endingAngle: number + + /** + * Inner radius value between 0 and 1 + */ + innerRadius: number + } + + /** + * A link to either a URL or another frame (node) in the document. + */ + export type Hyperlink = { + /** + * The type of hyperlink. Can be either `URL` or `NODE`. + */ + type: 'URL' | 'NODE' + + /** + * The URL that the hyperlink points to, if `type` is `URL`. + */ + url?: string + + /** + * The ID of the node that the hyperlink points to, if `type` is `NODE`. + */ + nodeID?: string + } + + /** + * Metadata for character formatting. + */ + export type TypeStyle = { + /** + * Font family of text (standard name). + */ + fontFamily?: string + + /** + * PostScript font name. + */ + fontPostScriptName?: string | null + + /** + * Describes visual weight or emphasis, such as Bold or Italic. + */ + fontStyle?: string + + /** + * Space between paragraphs in px, 0 if not present. + */ + paragraphSpacing?: number + + /** + * Paragraph indentation in px, 0 if not present. + */ + paragraphIndent?: number + + /** + * Space between list items in px, 0 if not present. + */ + listSpacing?: number + + /** + * Whether or not text is italicized. + */ + italic?: boolean + + /** + * Numeric font weight. + */ + fontWeight?: number + + /** + * Font size in px. + */ + fontSize?: number + + /** + * Text casing applied to the node, default is the original casing. + */ + textCase?: 'UPPER' | 'LOWER' | 'TITLE' | 'SMALL_CAPS' | 'SMALL_CAPS_FORCED' + + /** + * Text decoration applied to the node, default is none. + */ + textDecoration?: 'NONE' | 'STRIKETHROUGH' | 'UNDERLINE' + + /** + * Dimensions along which text will auto resize, default is that the text does not auto-resize. + * TRUNCATE means that the text will be shortened and trailing text will be replaced with "…" if the + * text contents is larger than the bounds. `TRUNCATE` as a return value is deprecated and will be + * removed in a future version. Read from `textTruncation` instead. + */ + textAutoResize?: 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT' | 'TRUNCATE' + + /** + * Whether this text node will truncate with an ellipsis when the text contents is larger than the + * text node. + */ + textTruncation?: 'DISABLED' | 'ENDING' + + /** + * When `textTruncation: "ENDING"` is set, `maxLines` determines how many lines a text node can grow + * to before it truncates. + */ + maxLines?: number + + /** + * Horizontal text alignment as string enum. + */ + textAlignHorizontal?: 'LEFT' | 'RIGHT' | 'CENTER' | 'JUSTIFIED' + + /** + * Vertical text alignment as string enum. + */ + textAlignVertical?: 'TOP' | 'CENTER' | 'BOTTOM' + + /** + * Space between characters in px. + */ + letterSpacing?: number + + /** + * An array of fill paints applied to the characters. + */ + fills?: Paint[] + + /** + * Link to a URL or frame. + */ + hyperlink?: Hyperlink + + /** + * A map of OpenType feature flags to 1 or 0, 1 if it is enabled and 0 if it is disabled. Note that + * some flags aren't reflected here. For example, SMCP (small caps) is still represented by the + * `textCase` field. + */ + opentypeFlags?: { [key: string]: number } + + /** + * Line height in px. + */ + lineHeightPx?: number + + /** + * Line height as a percentage of normal line height. This is deprecated; in a future version of the + * API only lineHeightPx and lineHeightPercentFontSize will be returned. + */ + lineHeightPercent?: number + + /** + * Line height as a percentage of the font size. Only returned when `lineHeightPercent` (deprecated) + * is not 100. + */ + lineHeightPercentFontSize?: number + + /** + * The unit of the line height value specified by the user. + */ + lineHeightUnit?: 'PIXELS' | 'FONT_SIZE_%' | 'INTRINSIC_%' + + /** + * The variables bound to a particular field on this style + */ + boundVariables?: { + fontFamily?: VariableAlias + + fontSize?: VariableAlias + + fontStyle?: VariableAlias + + fontWeight?: VariableAlias + + letterSpacing?: VariableAlias + + lineHeight?: VariableAlias + + paragraphSpacing?: VariableAlias + + paragraphIndent?: VariableAlias + } + + /** + * Whether or not this style has overrides over a text style. The possible fields to override are + * semanticWeight, semanticItalic, hyperlink, and textDecoration. If this is true, then those fields + * are overrides if present. + */ + isOverrideOverTextStyle?: boolean + + /** + * Indicates how the font weight was overridden when there is a text style override. + */ + semanticWeight?: 'BOLD' | 'NORMAL' + + /** + * Indicates how the font style was overridden when there is a text style override. + */ + semanticItalic?: 'ITALIC' | 'NORMAL' + } + + /** + * Component property type. + */ + export type ComponentPropertyType = 'BOOLEAN' | 'INSTANCE_SWAP' | 'TEXT' | 'VARIANT' + + /** + * Instance swap preferred value. + */ + export type InstanceSwapPreferredValue = { + /** + * Type of node for this preferred value. + */ + type: 'COMPONENT' | 'COMPONENT_SET' + + /** + * Key of this component or component set. + */ + key: string + } + + /** + * A property of a component. + */ + export type ComponentPropertyDefinition = { + /** + * Type of this component property. + */ + type: ComponentPropertyType + + /** + * Initial value of this property for instances. + */ + defaultValue: boolean | string + + /** + * All possible values for this property. Only exists on VARIANT properties. + */ + variantOptions?: string[] + + /** + * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`. + */ + preferredValues?: InstanceSwapPreferredValue[] + } + + /** + * A property of a component. + */ + export type ComponentProperty = { + /** + * Type of this component property. + */ + type: ComponentPropertyType + + /** + * Value of the property for this component instance. + */ + value: boolean | string + + /** + * Preferred values for this property. Only applicable if type is `INSTANCE_SWAP`. + */ + preferredValues?: InstanceSwapPreferredValue[] + + /** + * The variables bound to a particular field on this component property + */ + boundVariables?: { value?: VariableAlias } + } + + /** + * Fields directly overridden on an instance. Inherited overrides are not included. + */ + export type Overrides = { + /** + * A unique ID for a node. + */ + id: string + + /** + * An array of properties. + */ + overriddenFields: string[] + } + + /** + * Geometric shape type. + */ + export type ShapeType = + | 'SQUARE' + | 'ELLIPSE' + | 'ROUNDED_RECTANGLE' + | 'DIAMOND' + | 'TRIANGLE_UP' + | 'TRIANGLE_DOWN' + | 'PARALLELOGRAM_RIGHT' + | 'PARALLELOGRAM_LEFT' + | 'ENG_DATABASE' + | 'ENG_QUEUE' + | 'ENG_FILE' + | 'ENG_FOLDER' + | 'TRAPEZOID' + | 'PREDEFINED_PROCESS' + | 'SHIELD' + | 'DOCUMENT_SINGLE' + | 'DOCUMENT_MULTIPLE' + | 'MANUAL_INPUT' + | 'HEXAGON' + | 'CHEVRON' + | 'PENTAGON' + | 'OCTAGON' + | 'STAR' + | 'PLUS' + | 'ARROW_LEFT' + | 'ARROW_RIGHT' + | 'SUMMING_JUNCTION' + | 'OR' + | 'SPEECH_BUBBLE' + | 'INTERNAL_STORAGE' + + /** + * Stores canvas location for a connector start/end point. + */ + export type ConnectorEndpoint = + | { + /** + * Node ID that this endpoint attaches to. + */ + endpointNodeId?: string + + /** + * The position of the endpoint relative to the node. + */ + position?: Vector + } + | { + /** + * Node ID that this endpoint attaches to. + */ + endpointNodeId?: string + + /** + * The magnet type is a string enum. + */ + magnet?: 'AUTO' | 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'CENTER' + } + + /** + * Connector line type. + */ + export type ConnectorLineType = 'STRAIGHT' | 'ELBOWED' + + export type ConnectorTextBackground = CornerTrait & MinimalFillsTrait + + /** + * A description of a main component. Helps you identify which component instances are attached to. + */ + export type Component = { + /** + * The key of the component + */ + key: string + + /** + * Name of the component + */ + name: string + + /** + * The description of the component as entered in the editor + */ + description: string + + /** + * The ID of the component set if the component belongs to one + */ + componentSetId?: string + + /** + * An array of documentation links attached to this component + */ + documentationLinks: DocumentationLink[] + + /** + * Whether this component is a remote component that doesn't live in this file + */ + remote: boolean + } + + /** + * A description of a component set, which is a node containing a set of variants of a component. + */ + export type ComponentSet = { + /** + * The key of the component set + */ + key: string + + /** + * Name of the component set + */ + name: string + + /** + * The description of the component set as entered in the editor + */ + description: string + + /** + * An array of documentation links attached to this component set + */ + documentationLinks?: DocumentationLink[] + + /** + * Whether this component set is a remote component set that doesn't live in this file + */ + remote?: boolean + } + + /** + * Represents a link to documentation for a component or component set. + */ + export type DocumentationLink = { + /** + * Should be a valid URI (e.g. https://www.figma.com). + */ + uri: string + } + + /** + * Contains a variable alias + */ + export type VariableAlias = { + type: 'VARIABLE_ALIAS' + + /** + * The id of the variable that the current variable is aliased to. This variable can be a local or + * remote variable, and both can be retrieved via the GET /v1/files/:file_key/variables/local + * endpoint. + */ + id: string + } + + /** + * An interaction in the Figma viewer, containing a trigger and one or more actions. + */ + export type Interaction = { + /** + * The user event that initiates the interaction. + */ + trigger: Trigger | null + + /** + * The actions that are performed when the trigger is activated. + */ + actions?: Action[] + } + + /** + * The `"ON_HOVER"` and `"ON_PRESS"` trigger types revert the navigation when the trigger is + * finished (the result is temporary). `"MOUSE_ENTER"`, `"MOUSE_LEAVE"`, `"MOUSE_UP"` and + * `"MOUSE_DOWN"` are permanent, one-way navigation. The `delay` parameter requires the trigger to + * be held for a certain duration of time before the action occurs. Both `timeout` and `delay` + * values are in milliseconds. The `"ON_MEDIA_HIT"` and `"ON_MEDIA_END"` trigger types can only + * trigger from a video. They fire when a video reaches a certain time or ends. The `timestamp` + * value is in seconds. + */ + export type Trigger = + | { type: 'ON_CLICK' | 'ON_HOVER' | 'ON_PRESS' | 'ON_DRAG' } + | AfterTimeoutTrigger + | { + type: 'MOUSE_ENTER' | 'MOUSE_LEAVE' | 'MOUSE_UP' | 'MOUSE_DOWN' + + delay: number + + /** + * Whether this is a [deprecated + * version](https://help.figma.com/hc/en-us/articles/360040035834-Prototype-triggers#h_01HHN04REHJNP168R26P1CMP0A) + * of the trigger that was left unchanged for backwards compatibility. If not present, the trigger + * is the latest version. + */ + deprecatedVersion?: boolean + } + | OnKeyDownTrigger + | OnMediaHitTrigger + | { type: 'ON_MEDIA_END' } + + export type AfterTimeoutTrigger = { + type: 'AFTER_TIMEOUT' + + timeout: number + } + + export type OnKeyDownTrigger = { + type: 'ON_KEY_DOWN' + + device: 'KEYBOARD' | 'XBOX_ONE' | 'PS4' | 'SWITCH_PRO' | 'UNKNOWN_CONTROLLER' + + keyCodes: number[] + } + + export type OnMediaHitTrigger = { + type: 'ON_MEDIA_HIT' + + mediaHitTime: number + } + + /** + * An action that is performed when a trigger is activated. + */ + export type Action = + | { type: 'BACK' | 'CLOSE' } + | OpenURLAction + | UpdateMediaRuntimeAction + | SetVariableAction + | SetVariableModeAction + | ConditionalAction + | NodeAction + + /** + * An action that opens a URL. + */ + export type OpenURLAction = { + type: 'URL' + + url: string + } + + /** + * An action that affects a video node in the Figma viewer. For example, to play, pause, or skip. + */ + export type UpdateMediaRuntimeAction = + | { + type: 'UPDATE_MEDIA_RUNTIME' + + destinationId: string | null + + mediaAction: 'PLAY' | 'PAUSE' | 'TOGGLE_PLAY_PAUSE' | 'MUTE' | 'UNMUTE' | 'TOGGLE_MUTE_UNMUTE' + } + | { + type: 'UPDATE_MEDIA_RUNTIME' + + destinationId?: string | null + + mediaAction: 'SKIP_FORWARD' | 'SKIP_BACKWARD' + + amountToSkip: number + } + | { + type: 'UPDATE_MEDIA_RUNTIME' + + destinationId?: string | null + + mediaAction: 'SKIP_TO' + + newTimestamp: number + } + + /** + * An action that navigates to a specific node in the Figma viewer. + */ + export type NodeAction = { + type: 'NODE' + + destinationId: string | null + + navigation: Navigation + + transition: Transition | null + + /** + * Whether the scroll offsets of any scrollable elements in the current screen or overlay are + * preserved when navigating to the destination. This is applicable only if the layout of both the + * current frame and its destination are the same. + */ + preserveScrollPosition?: boolean + + /** + * Applicable only when `navigation` is `"OVERLAY"` and the destination is a frame with + * `overlayPosition` equal to `"MANUAL"`. This value represents the offset by which the overlay is + * opened relative to this node. + */ + overlayRelativePosition?: Vector + + /** + * When true, all videos within the destination frame will reset their memorized playback position + * to 00:00 before starting to play. + */ + resetVideoPosition?: boolean + + /** + * Whether the scroll offsets of any scrollable elements in the current screen or overlay reset when + * navigating to the destination. This is applicable only if the layout of both the current frame + * and its destination are the same. + */ + resetScrollPosition?: boolean + + /** + * Whether the state of any interactive components in the current screen or overlay reset when + * navigating to the destination. This is applicable if there are interactive components in the + * destination frame. + */ + resetInteractiveComponents?: boolean + } + + /** + * The method of navigation. The possible values are: + * + * - `"NAVIGATE"`: Replaces the current screen with the destination, also closing all overlays. + * - `"OVERLAY"`: Opens the destination as an overlay on the current screen. + * - `"SWAP"`: On an overlay, replaces the current (topmost) overlay with the destination. On a + * top-level frame, behaves the same as `"NAVIGATE"` except that no entry is added to the + * navigation history. + * - `"SCROLL_TO"`: Scrolls to the destination on the current screen. + * - `"CHANGE_TO"`: Changes the closest ancestor instance of source node to the specified variant. + */ + export type Navigation = 'NAVIGATE' | 'SWAP' | 'OVERLAY' | 'SCROLL_TO' | 'CHANGE_TO' + + export type Transition = SimpleTransition | DirectionalTransition + + /** + * Describes an animation used when navigating in a prototype. + */ + export type SimpleTransition = { + type: 'DISSOLVE' | 'SMART_ANIMATE' | 'SCROLL_ANIMATE' + + /** + * The duration of the transition in milliseconds. + */ + duration: number + + /** + * The easing curve of the transition. + */ + easing: Easing + } + + /** + * Describes an animation used when navigating in a prototype. + */ + export type DirectionalTransition = { + type: 'MOVE_IN' | 'MOVE_OUT' | 'PUSH' | 'SLIDE_IN' | 'SLIDE_OUT' + + direction: 'LEFT' | 'RIGHT' | 'TOP' | 'BOTTOM' + + /** + * The duration of the transition in milliseconds. + */ + duration: number + + /** + * The easing curve of the transition. + */ + easing: Easing + + /** + * When the transition `type` is `"SMART_ANIMATE"` or when `matchLayers` is `true`, then the + * transition will be performed using smart animate, which attempts to match corresponding layers an + * interpolate other properties during the animation. + */ + matchLayers?: boolean + } + + /** + * Describes an easing curve. + */ + export type Easing = { + /** + * The type of easing curve. + */ + type: EasingType + + /** + * A cubic bezier curve that defines the easing. + */ + easingFunctionCubicBezier?: { + /** + * The x component of the first control point. + */ + x1: number + + /** + * The y component of the first control point. + */ + y1: number + + /** + * The x component of the second control point. + */ + x2: number + + /** + * The y component of the second control point. + */ + y2: number + } + + /** + * A spring function that defines the easing. + */ + easingFunctionSpring?: { + mass: number + + stiffness: number + + damping: number + } + } + + /** + * Sets a variable to a specific value. + */ + export type SetVariableAction = { + type: 'SET_VARIABLE' + + variableId: string | null + + variableValue?: VariableData + } + + /** + * Sets a variable to a specific mode. + */ + export type SetVariableModeAction = { + type: 'SET_VARIABLE_MODE' + + variableCollectionId?: string | null + + variableModeId?: string | null + } + + /** + * Checks if a condition is met before performing certain actions by using an if/else conditional + * statement. + */ + export type ConditionalAction = { + type: 'CONDITIONAL' + + conditionalBlocks: ConditionalBlock[] + } + + /** + * A value to set a variable to during prototyping. + */ + export type VariableData = { + type?: VariableDataType + + resolvedType?: VariableResolvedDataType + + value?: boolean | number | string | RGB | RGBA | VariableAlias | Expression + } + + /** + * Defines the types of data a VariableData object can hold + */ + export type VariableDataType = + | 'BOOLEAN' + | 'FLOAT' + | 'STRING' + | 'COLOR' + | 'VARIABLE_ALIAS' + | 'EXPRESSION' + + /** + * Defines the types of data a VariableData object can eventually equal + */ + export type VariableResolvedDataType = 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR' + + /** + * Defines the [Expression](https://help.figma.com/hc/en-us/articles/15253194385943) object, which + * contains a list of `VariableData` objects strung together by operators (`ExpressionFunction`). + */ + export type Expression = { + expressionFunction: ExpressionFunction + + expressionArguments: VariableData[] + } + + /** + * Defines the list of operators available to use in an Expression. + */ + export type ExpressionFunction = + | 'ADDITION' + | 'SUBTRACTION' + | 'MULTIPLICATION' + | 'DIVISION' + | 'EQUALS' + | 'NOT_EQUAL' + | 'LESS_THAN' + | 'LESS_THAN_OR_EQUAL' + | 'GREATER_THAN' + | 'GREATER_THAN_OR_EQUAL' + | 'AND' + | 'OR' + | 'VAR_MODE_LOOKUP' + | 'NEGATE' + | 'NOT' + + /** + * Either the if or else conditional blocks. The if block contains a condition to check. If that + * condition is met then it will run those list of actions, else it will run the actions in the else + * block. + */ + export type ConditionalBlock = { + condition?: VariableData + + actions: Action[] + } + + /** + * A pinned distance between two nodes in Dev Mode + */ + export type Measurement = { + id: string + + start: MeasurementStartEnd + + end: MeasurementStartEnd + + offset: MeasurementOffsetInner | MeasurementOffsetOuter + + /** + * When manually overridden, the displayed value of the measurement + */ + freeText?: string + } + + /** + * The node and side a measurement is pinned to + */ + export type MeasurementStartEnd = { + nodeId: string + + side: 'TOP' | 'RIGHT' | 'BOTTOM' | 'LEFT' + } + + /** + * Measurement offset relative to the inside of the start node + */ + export type MeasurementOffsetInner = { + type: 'INNER' + + relative: number + } + + /** + * Measurement offset relative to the outside of the start node + */ + export type MeasurementOffsetOuter = { + type: 'OUTER' + + fixed: number + } + + /** + * Position of a comment relative to the frame to which it is attached. + */ + export type FrameOffset = { + /** + * Unique id specifying the frame. + */ + node_id: string + + /** + * 2D vector offset within the frame from the top-left corner. + */ + node_offset: Vector + } + + /** + * Position of a region comment on the canvas. + */ + export type Region = { + /** + * X coordinate of the position. + */ + x: number + + /** + * Y coordinate of the position. + */ + y: number + + /** + * The height of the comment region. Must be greater than 0. + */ + region_height: number + + /** + * The width of the comment region. Must be greater than 0. + */ + region_width: number + + /** + * The corner of the comment region to pin to the node's corner as a string enum. + */ + comment_pin_corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' + } + + /** + * Position of a region comment relative to the frame to which it is attached. + */ + export type FrameOffsetRegion = { + /** + * Unique id specifying the frame. + */ + node_id: string + + /** + * 2D vector offset within the frame from the top-left corner. + */ + node_offset: Vector + + /** + * The height of the comment region. Must be greater than 0. + */ + region_height: number + + /** + * The width of the comment region. Must be greater than 0. + */ + region_width: number + + /** + * The corner of the comment region to pin to the node's corner as a string enum. + */ + comment_pin_corner?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' + } + + /** + * A comment or reply left by a user. + */ + export type Comment = { + /** + * Unique identifier for comment. + */ + id: string + + /** + * Positioning information of the comment. Includes information on the location of the comment pin, + * which is either the absolute coordinates on the canvas or a relative offset within a frame. If + * the comment is a region, it will also contain the region height, width, and position of the + * anchor in regards to the region. + */ + client_meta: Vector | FrameOffset | Region | FrameOffsetRegion + + /** + * The file in which the comment lives + */ + file_key: string + + /** + * If present, the id of the comment to which this is the reply + */ + parent_id?: string + + /** + * The user who left the comment + */ + user: User + + /** + * The UTC ISO 8601 time at which the comment was left + */ + created_at: string + + /** + * If set, the UTC ISO 8601 time the comment was resolved + */ + resolved_at?: string | null + + /** + * The content of the comment + */ + message: string + + /** + * Only set for top level comments. The number displayed with the comment in the UI + */ + order_id: string | null + + /** + * An array of reactions to the comment + */ + reactions: Reaction[] + } + + /** + * A reaction left by a user. + */ + export type Reaction = { + /** + * The user who left the reaction. + */ + user: User + + emoji: Emoji + + /** + * The UTC ISO 8601 time at which the reaction was left. + */ + created_at: string + } + + /** + * The emoji type of reaction as shortcode (e.g. `:heart:`, `:+1::skin-tone-2:`). The list of + * accepted emoji shortcodes can be found in [this + * file](https://raw.githubusercontent.com/missive/emoji-mart/main/packages/emoji-mart-data/sets/14/native.json) + * under the top-level emojis and aliases fields, with optional skin tone modifiers when + * applicable. + */ + export type Emoji = string + + /** + * A description of a user. + */ + export type User = { + /** + * Unique stable id of the user. + */ + id: string + + /** + * Name of the user. + */ + handle: string + + /** + * URL link to the user's profile image. + */ + img_url: string + } + + /** + * Data on the frame a component resides in. + */ + export type FrameInfo = { + /** + * The ID of the frame node within the file. + */ + nodeId?: string + + /** + * The name of the frame node. + */ + name?: string + + /** + * The background color of the frame node. + */ + backgroundColor?: string + + /** + * The ID of the page containing the frame node. + */ + pageId: string + + /** + * The name of the page containing the frame node. + */ + pageName: string + } + + /** + * An arrangement of published UI elements that can be instantiated across figma files. + */ + export type PublishedComponent = { + /** + * The unique identifier for the component. + */ + key: string + + /** + * The unique identifier of the Figma file that contains the component. + */ + file_key: string + + /** + * The unique identifier of the component node within the Figma file. + */ + node_id: string + + /** + * A URL to a thumbnail image of the component. + */ + thumbnail_url?: string + + /** + * The name of the component. + */ + name: string + + /** + * The description of the component as entered by the publisher. + */ + description: string + + /** + * The UTC ISO 8601 time when the component was created. + */ + created_at: string + + /** + * The UTC ISO 8601 time when the component was last updated. + */ + updated_at: string + + /** + * The user who last updated the component. + */ + user: User + + /** + * The containing frame of the component. + */ + containing_frame?: FrameInfo + } + + /** + * A node containing a set of variants of a component. + */ + export type PublishedComponentSet = { + /** + * The unique identifier for the component set. + */ + key: string + + /** + * The unique identifier of the Figma file that contains the component set. + */ + file_key: string + + /** + * The unique identifier of the component set node within the Figma file. + */ + node_id: string + + /** + * A URL to a thumbnail image of the component set. + */ + thumbnail_url?: string + + /** + * The name of the component set. + */ + name: string + + /** + * The description of the component set as entered by the publisher. + */ + description: string + + /** + * The UTC ISO 8601 time when the component set was created. + */ + created_at: string + + /** + * The UTC ISO 8601 time when the component set was last updated. + */ + updated_at: string + + /** + * The user who last updated the component set. + */ + user: User + + /** + * The containing frame of the component set. + */ + containing_frame?: FrameInfo + } + + /** + * The type of style + */ + export type StyleType = 'FILL' | 'TEXT' | 'EFFECT' | 'GRID' + + /** + * A set of published properties that can be applied to nodes. + */ + export type PublishedStyle = { + /** + * The unique identifier for the style + */ + key: string + + /** + * The unique identifier of the Figma file that contains the style. + */ + file_key: string + + /** + * ID of the style node within the figma file + */ + node_id: string + + style_type: StyleType + + /** + * A URL to a thumbnail image of the style. + */ + thumbnail_url?: string + + /** + * The name of the style. + */ + name: string + + /** + * The description of the style as entered by the publisher. + */ + description: string + + /** + * The UTC ISO 8601 time when the style was created. + */ + created_at: string + + /** + * The UTC ISO 8601 time when the style was last updated. + */ + updated_at: string + + /** + * The user who last updated the style. + */ + user: User + + /** + * A user specified order number by which the style can be sorted. + */ + sort_position: string + } + + /** + * A Project can be identified by both the Project name, and the Project ID. + */ + export type Project = { + /** + * The ID of the project. + */ + id: string + + /** + * The name of the project. + */ + name: string + } + + /** + * A version of a file + */ + export type Version = { + /** + * Unique identifier for version + */ + id: string + + /** + * The UTC ISO 8601 time at which the version was created + */ + created_at: string + + /** + * The label given to the version in the editor + */ + label: string | null + + /** + * The description of the version as entered in the editor + */ + description: string | null + + /** + * The user that created the version + */ + user: User + + /** + * A URL to a thumbnail image of the file version. + */ + thumbnail_url?: string + } + + /** + * A description of an HTTP webhook (from Figma back to your application) + */ + export type WebhookV2 = { + /** + * The ID of the webhook + */ + id: string + + /** + * The event this webhook triggers on + */ + event_type: WebhookV2Event + + /** + * The team id you are subscribed to for updates + */ + team_id: string + + /** + * The current status of the webhook + */ + status: WebhookV2Status + + /** + * The client ID of the OAuth application that registered this webhook, if any + */ + client_id: string | null + + /** + * The passcode that will be passed back to the webhook endpoint + */ + passcode: string + + /** + * The endpoint that will be hit when the webhook is triggered + */ + endpoint: string + + /** + * Optional user-provided description or name for the webhook. This is provided to help make + * maintaining a number of webhooks more convenient. Max length 140 characters. + */ + description: string | null + } + + /** + * An enum representing the possible events that a webhook can subscribe to + */ + export type WebhookV2Event = + | 'PING' + | 'FILE_UPDATE' + | 'FILE_VERSION_UPDATE' + | 'FILE_DELETE' + | 'LIBRARY_PUBLISH' + | 'FILE_COMMENT' + + /** + * An enum representing the possible statuses you can set a webhook to: + * + * - `ACTIVE`: The webhook is healthy and receive all events + * - `PAUSED`: The webhook is paused and will not receive any events + */ + export type WebhookV2Status = 'ACTIVE' | 'PAUSED' + + /** + * Information regarding the most recent interactions sent to a webhook endpoint + */ + export type WebhookV2Request = { + /** + * The ID of the webhook the requests were sent to + */ + webhook_id: string + + request_info: WebhookV2RequestInfo + + response_info: WebhookV2ResponseInfo + + /** + * Error message for this request. NULL if no error occurred + */ + error_msg: string | null + } + + /** + * Information regarding the request sent to a webhook endpoint + */ + export type WebhookV2RequestInfo = { + /** + * The ID of the webhook + */ + id: string + + /** + * The actual endpoint the request was sent to + */ + endpoint: string + + /** + * The contents of the request that was sent to the endpoint + */ + payload: object + + /** + * UTC ISO 8601 timestamp of when the request was sent + */ + sent_at: string + } + + /** + * Information regarding the reply sent back from a webhook endpoint + */ + export type WebhookV2ResponseInfo = object | null + + /** + * An object representing the library item information in the payload of the `LIBRARY_PUBLISH` event + */ + export type LibraryItemData = { + /** + * Unique identifier for the library item + */ + key: string + + /** + * Name of the library item + */ + name: string + } + + /** + * An object representing a fragment of a comment left by a user, used in the payload of the + * `FILE_COMMENT` event. Note only ONE of the fields below will be set + */ + export type CommentFragment = { + /** + * Comment text that is set if a fragment is text based + */ + text?: string + + /** + * User id that is set if a fragment refers to a user mention + */ + mention?: string + } + + export type WebhookBasePayload = { + /** + * The passcode specified when the webhook was created, should match what was initially provided + */ + passcode: string + + /** + * UTC ISO 8601 timestamp of when the event was triggered. + */ + timestamp: string + + /** + * The id of the webhook that caused the callback + */ + webhook_id: string + } + + export type WebhookPingPayload = WebhookBasePayload & { event_type: 'PING' } + + export type WebhookFileUpdatePayload = WebhookBasePayload & { + event_type: 'FILE_UPDATE' + + /** + * The key of the file that was updated + */ + file_key: string + + /** + * The name of the file that was updated + */ + file_name: string + } + + export type WebhookFileDeletePayload = WebhookBasePayload & { + event_type: 'FILE_DELETE' + + /** + * The key of the file that was deleted + */ + file_key: string + + /** + * The name of the file that was deleted + */ + file_name: string + + /** + * The user that deleted the file and triggered this event + */ + triggered_by: User + } + + export type WebhookFileVersionUpdatePayload = WebhookBasePayload & { + event_type: 'FILE_VERSION_UPDATE' + + /** + * UTC ISO 8601 timestamp of when the version was created + */ + created_at: string + + /** + * Description of the version in the version history + */ + description?: string + + /** + * The key of the file that was updated + */ + file_key: string + + /** + * The name of the file that was updated + */ + file_name: string + + /** + * The user that created the named version and triggered this event + */ + triggered_by: User + + /** + * ID of the published version + */ + version_id: string + } + + export type WebhookLibraryPublishPayload = WebhookBasePayload & { + event_type: 'LIBRARY_PUBLISH' + + /** + * Components that were created by the library publish + */ + created_components: LibraryItemData[] + + /** + * Styles that were created by the library publish + */ + created_styles: LibraryItemData[] + + /** + * Variables that were created by the library publish + */ + created_variables: LibraryItemData[] + + /** + * Components that were modified by the library publish + */ + modified_components: LibraryItemData[] + + /** + * Styles that were modified by the library publish + */ + modified_styles: LibraryItemData[] + + /** + * Variables that were modified by the library publish + */ + modified_variables: LibraryItemData[] + + /** + * Components that were deleted by the library publish + */ + deleted_components: LibraryItemData[] + + /** + * Styles that were deleted by the library publish + */ + deleted_styles: LibraryItemData[] + + /** + * Variables that were deleted by the library publish + */ + deleted_variables: LibraryItemData[] + + /** + * Description of the library publish + */ + description?: string + + /** + * The key of the file that was published + */ + file_key: string + + /** + * The name of the file that was published + */ + file_name: string + + /** + * The library item that was published + */ + library_item: LibraryItemData + + /** + * The user that published the library and triggered this event + */ + triggered_by: User + } + + export type WebhookFileCommentPayload = WebhookBasePayload & { + event_type: 'FILE_COMMENT' + + /** + * Contents of the comment itself + */ + comment: CommentFragment[] + + /** + * Unique identifier for comment + */ + comment_id: string + + /** + * The UTC ISO 8601 time at which the comment was left + */ + created_at: string + + /** + * The key of the file that was commented on + */ + file_key: string + + /** + * The name of the file that was commented on + */ + file_name: string + + /** + * Users that were mentioned in the comment + */ + mentions?: User[] + + /** + * The user that made the comment and triggered this event + */ + triggered_by: User + } + + /** + * A Figma user + */ + export type ActivityLogUserEntity = { + /** + * The type of entity. + */ + type: 'user' + + /** + * Unique stable id of the user. + */ + id: string + + /** + * Name of the user. + */ + name: string + + /** + * Email associated with the user's account. + */ + email: string + } + + /** + * A Figma Design or FigJam file + */ + export type ActivityLogFileEntity = { + /** + * The type of entity. + */ + type: 'file' + + /** + * Unique identifier of the file. + */ + key: string + + /** + * Name of the file. + */ + name: string + + /** + * Indicates if the object is a file on Figma Design or FigJam. + */ + editor_type: 'figma' | 'figjam' + + /** + * Access policy for users who have the link to the file. + */ + link_access: 'view' | 'edit' | 'org_view' | 'org_edit' | 'inherit' + + /** + * Access policy for users who have the link to the file's prototype. + */ + proto_link_access: 'view' | 'org_view' | 'inherit' + } + + /** + * A file branch that diverges from and can be merged back into the main file + */ + export type ActivityLogFileRepoEntity = { + /** + * The type of entity. + */ + type: 'file_repo' + + /** + * Unique identifier of the file branch. + */ + id: string + + /** + * Name of the file. + */ + name: string + + /** + * Key of the main file. + */ + main_file_key: string + } + + /** + * A project that a collection of Figma files are grouped under + */ + export type ActivityLogProjectEntity = { + /** + * The type of entity. + */ + type: 'project' + + /** + * Unique identifier of the project. + */ + id: string + + /** + * Name of the project. + */ + name: string + } + + /** + * A Figma team that contains multiple users and projects + */ + export type ActivityLogTeamEntity = { + /** + * The type of entity. + */ + type: 'team' + + /** + * Unique identifier of the team. + */ + id: string + + /** + * Name of the team. + */ + name: string + } + + /** + * Part of the organizational hierarchy of managing files and users within Figma, only available on + * the Enterprise Plan + */ + export type ActivityLogWorkspaceEntity = { + /** + * The type of entity. + */ + type: 'workspace' + + /** + * Unique identifier of the workspace. + */ + id: string + + /** + * Name of the workspace. + */ + name: string + } + + /** + * A Figma organization + */ + export type ActivityLogOrgEntity = { + /** + * The type of entity. + */ + type: 'org' + + /** + * Unique identifier of the organization. + */ + id: string + + /** + * Name of the organization. + */ + name: string + } + + /** + * A Figma plugin + */ + export type ActivityLogPluginEntity = { + /** + * The type of entity. + */ + type: 'plugin' + + /** + * Unique identifier of the plugin. + */ + id: string + + /** + * Name of the plugin. + */ + name: string + + /** + * Indicates if the object is a plugin is available on Figma Design or FigJam. + */ + editor_type: 'figma' | 'figjam' + } + + /** + * A Figma widget + */ + export type ActivityLogWidgetEntity = { + /** + * The type of entity. + */ + type: 'widget' + + /** + * Unique identifier of the widget. + */ + id: string + + /** + * Name of the widget. + */ + name: string + + /** + * Indicates if the object is a widget available on Figma Design or FigJam. + */ + editor_type: 'figma' | 'figjam' + } + + /** + * An event returned by the Activity Logs API. + */ + export type ActivityLog = { + /** + * The ID of the event. + */ + id: string + + /** + * The timestamp of the event in seconds since the Unix epoch. + */ + timestamp: number + + /** + * The user who performed the action. + */ + actor: object | null + + /** + * The task or activity the actor performed. + */ + action: { + /** + * The type of the action. + */ + type: string + + /** + * Metadata of the action. Each action type supports its own metadata attributes. + */ + details: object | null + } + + /** + * The resource the actor took the action on. It can be a user, file, project or other resource + * types. + */ + entity: + | ActivityLogUserEntity + | ActivityLogFileEntity + | ActivityLogFileRepoEntity + | ActivityLogProjectEntity + | ActivityLogTeamEntity + | ActivityLogWorkspaceEntity + | ActivityLogOrgEntity + | ActivityLogPluginEntity + | ActivityLogWidgetEntity + + /** + * Contextual information about the event. + */ + context: { + /** + * The third-party application that triggered the event, if applicable. + */ + client_name: string | null + + /** + * The IP address from of the client that sent the event request. + */ + ip_address: string + + /** + * If Figma's Support team triggered the event. This is either true or false. + */ + is_figma_support_team_action: boolean + + /** + * The id of the organization where the event took place. + */ + org_id: string + + /** + * The id of the team where the event took place -- if this took place in a specific team. + */ + team_id: string | null + } + } + + /** + * An object describing the user's payment status. + */ + export type PaymentStatus = { + /** + * The current payment status of the user on the resource, as a string enum: + * + * - `UNPAID`: user has not paid for the resource + * - `PAID`: user has an active purchase on the resource + * - `TRIAL`: user is in the trial period for a subscription resource + */ + type?: 'UNPAID' | 'PAID' | 'TRIAL' + } + + /** + * An object describing a user's payment information for a plugin, widget, or Community file. + */ + export type PaymentInformation = { + /** + * The ID of the user whose payment information was queried. Can be used to verify the validity of a + * response. + */ + user_id: string + + /** + * The ID of the plugin, widget, or Community file that was queried. Can be used to verify the + * validity of a response. + */ + resource_id: string + + /** + * The type of the resource. + */ + resource_type: 'PLUGIN' | 'WIDGET' | 'COMMUNITY_FILE' + + payment_status: PaymentStatus + + /** + * The UTC ISO 8601 timestamp indicating when the user purchased the resource. No value is given if + * the user has never purchased the resource. + * + * Note that a value will still be returned if the user had purchased the resource, but no longer + * has active access to it (e.g. purchase refunded, subscription ended). + */ + date_of_purchase?: string + } + + /** + * Scopes allow a variable to be shown or hidden in the variable picker for various fields. This + * declutters the Figma UI if you have a large number of variables. Variable scopes are currently + * supported on `FLOAT`, `STRING`, and `COLOR` variables. + * + * `ALL_SCOPES` is a special scope that means that the variable will be shown in the variable picker + * for all variable fields. If `ALL_SCOPES` is set, no additional scopes can be set. + * + * `ALL_FILLS` is a special scope that means that the variable will be shown in the variable picker + * for all fill fields. If `ALL_FILLS` is set, no additional fill scopes can be set. + * + * Valid scopes for `FLOAT` variables: + * + * - `ALL_SCOPES` + * - `TEXT_CONTENT` + * - `WIDTH_HEIGHT` + * - `GAP` + * - `STROKE_FLOAT` + * - `EFFECT_FLOAT` + * - `OPACITY` + * - `FONT_WEIGHT` + * - `FONT_SIZE` + * - `LINE_HEIGHT` + * - `LETTER_SPACING` + * - `PARAGRAPH_SPACING` + * - `PARAGRAPH_INDENT` + * + * Valid scopes for `STRING` variables: + * + * - `ALL_SCOPES` + * - `TEXT_CONTENT` + * - `FONT_FAMILY` + * - `FONT_STYLE` + * + * Valid scopes for `COLOR` variables: + * + * - `ALL_SCOPES` + * - `ALL_FILLS` + * - `FRAME_FILL` + * - `SHAPE_FILL` + * - `TEXT_FILL` + * - `STROKE_COLOR` + * - `EFFECT_COLOR` + */ + export type VariableScope = + | 'ALL_SCOPES' + | 'TEXT_CONTENT' + | 'CORNER_RADIUS' + | 'WIDTH_HEIGHT' + | 'GAP' + | 'ALL_FILLS' + | 'FRAME_FILL' + | 'SHAPE_FILL' + | 'TEXT_FILL' + | 'STROKE_COLOR' + | 'STROKE_FLOAT' + | 'EFFECT_FLOAT' + | 'EFFECT_COLOR' + | 'OPACITY' + | 'FONT_FAMILY' + | 'FONT_STYLE' + | 'FONT_WEIGHT' + | 'FONT_SIZE' + | 'LINE_HEIGHT' + | 'LETTER_SPACING' + | 'PARAGRAPH_SPACING' + | 'PARAGRAPH_INDENT' + + /** + * An object containing platform-specific code syntax definitions for a variable. All platforms are + * optional. + */ + export type VariableCodeSyntax = { + WEB?: string + + ANDROID?: string + + iOS?: string + } + + /** + * A grouping of related Variable objects each with the same modes. + */ + export type LocalVariableCollection = { + /** + * The unique identifier of this variable collection. + */ + id: string + + /** + * The name of this variable collection. + */ + name: string + + /** + * The key of this variable collection. + */ + key: string + + /** + * The modes of this variable collection. + */ + modes: { + /** + * The unique identifier of this mode. + */ + modeId: string + + /** + * The name of this mode. + */ + name: string + }[] + + /** + * The id of the default mode. + */ + defaultModeId: string + + /** + * Whether this variable collection is remote. + */ + remote: boolean + + /** + * Whether this variable collection is hidden when publishing the current file as a library. + */ + hiddenFromPublishing: boolean + + /** + * The ids of the variables in the collection. Note that the order of these variables is roughly the + * same as what is shown in Figma Design, however it does not account for groups. As a result, the + * order of these variables may not exactly reflect the exact ordering and grouping shown in the + * authoring UI. + */ + variableIds: string[] + } + + /** + * A Variable is a single design token that defines values for each of the modes in its + * VariableCollection. These values can be applied to various kinds of design properties. + */ + export type LocalVariable = { + /** + * The unique identifier of this variable. + */ + id: string + + /** + * The name of this variable. + */ + name: string + + /** + * The key of this variable. + */ + key: string + + /** + * The id of the variable collection that contains this variable. + */ + variableCollectionId: string + + /** + * The resolved type of the variable. + */ + resolvedType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR' + + /** + * The values for each mode of this variable. + */ + valuesByMode: { [key: string]: boolean | number | string | RGBA | VariableAlias } + + /** + * Whether this variable is remote. + */ + remote: boolean + + /** + * The description of this variable. + */ + description: string + + /** + * Whether this variable is hidden when publishing the current file as a library. + * + * If the parent `VariableCollection` is marked as `hiddenFromPublishing`, then this variable will + * also be hidden from publishing via the UI. `hiddenFromPublishing` is independently toggled for a + * variable and collection. However, both must be true for a given variable to be publishable. + */ + hiddenFromPublishing: boolean + + /** + * An array of scopes in the UI where this variable is shown. Setting this property will show/hide + * this variable in the variable picker UI for different fields. + * + * Setting scopes for a variable does not prevent that variable from being bound in other scopes + * (for example, via the Plugin API). This only limits the variables that are shown in pickers + * within the Figma UI. + */ + scopes: VariableScope[] + + codeSyntax: VariableCodeSyntax + + /** + * Indicates that the variable was deleted in the editor, but the document may still contain + * references to the variable. References to the variable may exist through bound values or variable + * aliases. + */ + deletedButReferenced?: boolean + } + + /** + * A grouping of related Variable objects each with the same modes. + */ + export type PublishedVariableCollection = { + /** + * The unique identifier of this variable collection. + */ + id: string + + /** + * The ID of the variable collection that is used by subscribing files. This ID changes every time + * the variable collection is modified and published. + */ + subscribed_id: string + + /** + * The name of this variable collection. + */ + name: string + + /** + * The key of this variable collection. + */ + key: string + + /** + * The UTC ISO 8601 time at which the variable collection was last updated. + * + * This timestamp will change any time a variable in the collection is changed. + */ + updatedAt: string + } + + /** + * A Variable is a single design token that defines values for each of the modes in its + * VariableCollection. These values can be applied to various kinds of design properties. + */ + export type PublishedVariable = { + /** + * The unique identifier of this variable. + */ + id: string + + /** + * The ID of the variable that is used by subscribing files. This ID changes every time the variable + * is modified and published. + */ + subscribed_id: string + + /** + * The name of this variable. + */ + name: string + + /** + * The key of this variable. + */ + key: string + + /** + * The id of the variable collection that contains this variable. + */ + variableCollectionId: string + + /** + * The resolved type of the variable. + */ + resolvedDataType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR' + + /** + * The UTC ISO 8601 time at which the variable was last updated. + */ + updatedAt: string + } + + /** + * An object that contains details about creating a `VariableCollection`. + */ + export type VariableCollectionCreate = { + /** + * The action to perform for the variable collection. + */ + action: 'CREATE' + + /** + * A temporary id for this variable collection. + */ + id?: string + + /** + * The name of this variable collection. + */ + name: string + + /** + * The initial mode refers to the mode that is created by default. You can set a temporary id here, + * in order to reference this mode later in this request. + */ + initialModeId?: string + + /** + * Whether this variable collection is hidden when publishing the current file as a library. + */ + hiddenFromPublishing?: boolean + } + + /** + * An object that contains details about updating a `VariableCollection`. + */ + export type VariableCollectionUpdate = { + /** + * The action to perform for the variable collection. + */ + action: 'UPDATE' + + /** + * The id of the variable collection to update. + */ + id: string + + /** + * The name of this variable collection. + */ + name?: string + + /** + * Whether this variable collection is hidden when publishing the current file as a library. + */ + hiddenFromPublishing?: boolean + } + + /** + * An object that contains details about deleting a `VariableCollection`. + */ + export type VariableCollectionDelete = { + /** + * The action to perform for the variable collection. + */ + action: 'DELETE' + + /** + * The id of the variable collection to delete. + */ + id: string + } + + export type VariableCollectionChange = + | VariableCollectionCreate + | VariableCollectionUpdate + | VariableCollectionDelete + + /** + * An object that contains details about creating a `VariableMode`. + */ + export type VariableModeCreate = { + /** + * The action to perform for the variable mode. + */ + action: 'CREATE' + + /** + * A temporary id for this variable mode. + */ + id?: string + + /** + * The name of this variable mode. + */ + name: string + + /** + * The variable collection that will contain the mode. You can use the temporary id of a variable + * collection. + */ + variableCollectionId: string + } + + /** + * An object that contains details about updating a `VariableMode`. + */ + export type VariableModeUpdate = { + /** + * The action to perform for the variable mode. + */ + action: 'UPDATE' + + /** + * The id of the variable mode to update. + */ + id: string + + /** + * The name of this variable mode. + */ + name?: string + + /** + * The variable collection that contains the mode. + */ + variableCollectionId: string + } + + /** + * An object that contains details about deleting a `VariableMode`. + */ + export type VariableModeDelete = { + /** + * The action to perform for the variable mode. + */ + action: 'DELETE' + + /** + * The id of the variable mode to delete. + */ + id: string + } + + export type VariableModeChange = VariableModeCreate | VariableModeUpdate | VariableModeDelete + + /** + * An object that contains details about creating a `Variable`. + */ + export type VariableCreate = { + /** + * The action to perform for the variable. + */ + action: 'CREATE' + + /** + * A temporary id for this variable. + */ + id?: string + + /** + * The name of this variable. + */ + name: string + + /** + * The variable collection that will contain the variable. You can use the temporary id of a + * variable collection. + */ + variableCollectionId: string + + /** + * The resolved type of the variable. + */ + resolvedType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR' + + /** + * The description of this variable. + */ + description?: string + + /** + * Whether this variable is hidden when publishing the current file as a library. + */ + hiddenFromPublishing?: boolean + + /** + * An array of scopes in the UI where this variable is shown. Setting this property will show/hide + * this variable in the variable picker UI for different fields. + */ + scopes?: VariableScope[] + + codeSyntax?: VariableCodeSyntax + } + + /** + * An object that contains details about updating a `Variable`. + */ + export type VariableUpdate = { + /** + * The action to perform for the variable. + */ + action: 'UPDATE' + + /** + * The id of the variable to update. + */ + id: string + + /** + * The name of this variable. + */ + name?: string + + /** + * The description of this variable. + */ + description?: string + + /** + * Whether this variable is hidden when publishing the current file as a library. + */ + hiddenFromPublishing?: boolean + + /** + * An array of scopes in the UI where this variable is shown. Setting this property will show/hide + * this variable in the variable picker UI for different fields. + */ + scopes?: VariableScope[] + + codeSyntax?: VariableCodeSyntax + } + + /** + * An object that contains details about deleting a `Variable`. + */ + export type VariableDelete = { + /** + * The action to perform for the variable. + */ + action: 'DELETE' + + /** + * The id of the variable to delete. + */ + id: string + } + + export type VariableChange = VariableCreate | VariableUpdate | VariableDelete + + /** + * An object that represents a value for a given mode of a variable. All properties are required. + */ + export type VariableModeValue = { + /** + * The target variable. You can use the temporary id of a variable. + */ + variableId: string + + /** + * Must correspond to a mode in the variable collection that contains the target variable. + */ + modeId: string + + value: VariableValue + } + + /** + * The value for the variable. The value must match the variable's type. If setting to a variable + * alias, the alias must resolve to this type. + */ + export type VariableValue = boolean | number | string | RGB | RGBA | VariableAlias + + /** + * A dev resource in a file + */ + export type DevResource = { + /** + * Unique identifier of the dev resource + */ + id: string + + /** + * The name of the dev resource. + */ + name: string + + /** + * The URL of the dev resource. + */ + url: string + + /** + * The file key where the dev resource belongs. + */ + file_key: string + + /** + * The target node to attach the dev resource to. + */ + node_id: string + } + + /** + * Library analytics component actions data broken down by asset. + */ + export type LibraryAnalyticsComponentActionsByAsset = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * Unique, stable id of the component. + */ + component_key: string + + /** + * Name of the component. + */ + component_name: string + + /** + * Unique, stable id of the component set that this component belongs to. + */ + component_set_key?: string + + /** + * Name of the component set that this component belongs to. + */ + component_set_name?: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics action data broken down by team. + */ + export type LibraryAnalyticsComponentActionsByTeam = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * The name of the team using the library. + */ + team_name: string + + /** + * The name of the workspace that the team belongs to. + */ + workspace_name?: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics component usage data broken down by component. + */ + export type LibraryAnalyticsComponentUsagesByAsset = { + /** + * Unique, stable id of the component. + */ + component_key: string + + /** + * Name of the component. + */ + component_name: string + + /** + * Unique, stable id of the component set that this component belongs to. + */ + component_set_key?: string + + /** + * Name of the component set that this component belongs to. + */ + component_set_name?: string + + /** + * The number of instances of the component within the organization. + */ + usages: number + + /** + * The number of teams using the component within the organization. + */ + teams_using: number + + /** + * The number of files using the component within the organization. + */ + files_using: number + } + + /** + * Library analytics component usage data broken down by file. + */ + export type LibraryAnalyticsComponentUsagesByFile = { + /** + * The name of the file using the library. + */ + file_name: string + + /** + * The name of the team the file belongs to. + */ + team_name: string + + /** + * The name of the workspace that the file belongs to. + */ + workspace_name?: string + + /** + * The number of component instances from the library used within the file. + */ + usages: number + } + + /** + * Library analytics style actions data broken down by asset. + */ + export type LibraryAnalyticsStyleActionsByAsset = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * Unique, stable id of the style. + */ + style_key: string + + /** + * The name of the style. + */ + style_name: string + + /** + * The type of the style. + */ + style_type: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics style action data broken down by team. + */ + export type LibraryAnalyticsStyleActionsByTeam = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * The name of the team using the library. + */ + team_name: string + + /** + * The name of the workspace that the team belongs to. + */ + workspace_name?: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics style usage data broken down by component. + */ + export type LibraryAnalyticsStyleUsagesByAsset = { + /** + * Unique, stable id of the style. + */ + style_key: string + + /** + * The name of the style. + */ + style_name: string + + /** + * The type of the style. + */ + style_type: string + + /** + * The number of usages of the style within the organization. + */ + usages: number + + /** + * The number of teams using the style within the organization. + */ + teams_using: number + + /** + * The number of files using the style within the organization. + */ + files_using: number + } + + /** + * Library analytics style usage data broken down by file. + */ + export type LibraryAnalyticsStyleUsagesByFile = { + /** + * The name of the file using the library. + */ + file_name: string + + /** + * The name of the team the file belongs to. + */ + team_name: string + + /** + * The name of the workspace that the file belongs to. + */ + workspace_name?: string + + /** + * The number of times styles from this library are used within the file. + */ + usages: number + } + + /** + * Library analytics variable actions data broken down by asset. + */ + export type LibraryAnalyticsVariableActionsByAsset = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * Unique, stable id of the variable. + */ + variable_key: string + + /** + * The name of the variable. + */ + variable_name: string + + /** + * The type of the variable. + */ + variable_type: string + + /** + * Unique, stable id of the collection the variable belongs to. + */ + collection_key: string + + /** + * The name of the collection the variable belongs to. + */ + collection_name: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics variable action data broken down by team. + */ + export type LibraryAnalyticsVariableActionsByTeam = { + /** + * The date in ISO 8601 format. e.g. 2023-12-13 + */ + week: string + + /** + * The name of the team using the library. + */ + team_name: string + + /** + * The name of the workspace that the team belongs to. + */ + workspace_name?: string + + /** + * The number of detach events for this period. + */ + detachments: number + + /** + * The number of insertion events for this period. + */ + insertions: number + } + + /** + * Library analytics variable usage data broken down by component. + */ + export type LibraryAnalyticsVariableUsagesByAsset = { + /** + * Unique, stable id of the variable. + */ + variable_key: string + + /** + * The name of the variable. + */ + variable_name: string + + /** + * The type of the variable. + */ + variable_type: string + + /** + * Unique, stable id of the collection the variable belongs to. + */ + collection_key: string + + /** + * The name of the collection the variable belongs to. + */ + collection_name: string + + /** + * The number of usages of the variable within the organization. + */ + usages: number + + /** + * The number of teams using the variable within the organization. + */ + teams_using: number + + /** + * The number of files using the variable within the organization. + */ + files_using: number + } + + /** + * Library analytics variable usage data broken down by file. + */ + export type LibraryAnalyticsVariableUsagesByFile = { + /** + * The name of the file using the library. + */ + file_name: string + + /** + * The name of the team the file belongs to. + */ + team_name: string + + /** + * The name of the workspace that the file belongs to. + */ + workspace_name?: string + + /** + * The number of times variables from this library are used within the file. + */ + usages: number + } + + /** + * If pagination is needed due to the length of the response, identifies the next and previous + * pages. + */ + export type ResponsePagination = { + /** + * A URL that calls the previous page of the response. + */ + prev_page?: string + + /** + * A URL that calls the next page of the response. + */ + next_page?: string + } + + /** + * Pagination cursor + */ + export type ResponseCursor = { + before?: number + + after?: number + } + + /** + * A response indicating an error occurred. + */ + export type ErrorResponsePayloadWithErrMessage = { + /** + * Status code + */ + status: number + + /** + * A string describing the error + */ + err: string + } + + /** + * A response indicating an error occurred. + */ + export type ErrorResponsePayloadWithErrorBoolean = { + /** + * For erroneous requests, this value is always `true`. + */ + error: true + + /** + * Status code + */ + status: number + + /** + * A string describing the error + */ + message: string + } + + /** + * Response from the GET /v1/files/{file_key} endpoint. + */ + export type GetFileResponse = { + /** + * The name of the file as it appears in the editor. + */ + name: string + + /** + * The role of the user making the API request in relation to the file. + */ + role: 'owner' | 'editor' | 'viewer' + + /** + * The UTC ISO 8601 time at which the file was last modified. + */ + lastModified: string + + /** + * The type of editor associated with this file. + */ + editorType: 'figma' | 'figjam' + + /** + * A URL to a thumbnail image of the file. + */ + thumbnailUrl?: string + + /** + * The version number of the file. This number is incremented when a file is modified and can be + * used to check if the file has changed between requests. + */ + version: string + + document: DocumentNode + + /** + * A mapping from component IDs to component metadata. + */ + components: { [key: string]: Component } + + /** + * A mapping from component set IDs to component set metadata. + */ + componentSets: { [key: string]: ComponentSet } + + /** + * The version of the file schema that this file uses. + */ + schemaVersion: number + + /** + * A mapping from style IDs to style metadata. + */ + styles: { [key: string]: Style } + + /** + * The share permission level of the file link. + */ + linkAccess?: string + + /** + * The key of the main file for this file. If present, this file is a component or component set. + */ + mainFileKey?: string + + /** + * A list of branches for this file. + */ + branches?: { + /** + * The key of the branch. + */ + key: string + + /** + * The name of the branch. + */ + name: string + + /** + * A URL to a thumbnail image of the branch. + */ + thumbnail_url: string + + /** + * The UTC ISO 8601 time at which the branch was last modified. + */ + last_modified: string + }[] + } + + /** + * Response from the GET /v1/files/{file_key}/nodes endpoint. + */ + export type GetFileNodesResponse = { + /** + * The name of the file as it appears in the editor. + */ + name: string + + /** + * The role of the user making the API request in relation to the file. + */ + role: 'owner' | 'editor' | 'viewer' + + /** + * The UTC ISO 8601 time at which the file was last modified. + */ + lastModified: string + + /** + * The type of editor associated with this file. + */ + editorType: 'figma' | 'figjam' + + /** + * A URL to a thumbnail image of the file. + */ + thumbnailUrl: string + + /** + * The version number of the file. This number is incremented when a file is modified and can be + * used to check if the file has changed between requests. + */ + version: string + + /** + * A mapping from node IDs to node metadata. + */ + nodes: { + [key: string]: { + document: Node + + /** + * A mapping from component IDs to component metadata. + */ + components: { [key: string]: Component } + + /** + * A mapping from component set IDs to component set metadata. + */ + componentSets: { [key: string]: ComponentSet } + + /** + * The version of the file schema that this file uses. + */ + schemaVersion: number + + /** + * A mapping from style IDs to style metadata. + */ + styles: { [key: string]: Style } + } + } + } + + /** + * Response from the GET /v1/images/{file_key} endpoint. + */ + export type GetImagesResponse = { + /** + * For successful requests, this value is always `null`. + */ + err: null + + /** + * A map from node IDs to URLs of the rendered images. + */ + images: { [key: string]: string | null } + } + + /** + * Response from the GET /v1/files/{file_key}/images endpoint. + */ + export type GetImageFillsResponse = { + /** + * For successful requests, this value is always `false`. + */ + error: false + + /** + * Status code + */ + status: 200 + + meta: { + /** + * A map of image references to URLs of the image fills. + */ + images: { [key: string]: string } + } + } + + /** + * Response from the GET /v1/teams/{team_id}/projects endpoint. + */ + export type GetTeamProjectsResponse = { + /** + * The team's name. + */ + name: string + + /** + * An array of projects. + */ + projects: Project[] + } + + /** + * Response from the GET /v1/projects/{project_id}/files endpoint. + */ + export type GetProjectFilesResponse = { + /** + * The project's name. + */ + name: string + + /** + * An array of files. + */ + files: { + /** + * The file's key. + */ + key: string + + /** + * The file's name. + */ + name: string + + /** + * The file's thumbnail URL. + */ + thumbnail_url?: string + + /** + * The UTC ISO 8601 time at which the file was last modified. + */ + last_modified: string + }[] + } + + /** + * Response from the GET /v1/files/{file_key}/versions endpoint. + */ + export type GetFileVersionsResponse = { + /** + * An array of versions. + */ + versions: Version[] + + pagination: ResponsePagination + } + + /** + * Response from the GET /v1/files/{file_key}/comments endpoint. + */ + export type GetCommentsResponse = { + /** + * An array of comments. + */ + comments: Comment[] + } + + /** + * Response from the POST /v1/files/{file_key}/comments endpoint. + */ + export type PostCommentResponse = Comment + + /** + * Response from the DELETE /v1/files/{file_key}/comments/{comment_id} endpoint. + */ + export type DeleteCommentResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + } + + /** + * Response from the GET /v1/files/{file_key}/comments/{comment_id}/reactions endpoint. + */ + export type GetCommentReactionsResponse = { + /** + * An array of reactions. + */ + reactions: Reaction[] + + pagination: ResponsePagination + } + + /** + * Response from the POST /v1/files/{file_key}/comments/{comment_id}/reactions endpoint. + */ + export type PostCommentReactionResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + } + + /** + * Response from the DELETE /v1/files/{file_key}/comments/{comment_id}/reactions endpoint. + */ + export type DeleteCommentReactionResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + } + + /** + * Response from the GET /v1/me endpoint. + */ + export type GetMeResponse = User & { + /** + * Email associated with the user's account. This property is only present on the /v1/me endpoint. + */ + email: string + } + + /** + * Response from the GET /v1/teams/{team_id}/components endpoint. + */ + export type GetTeamComponentsResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + components: PublishedComponent[] + + cursor?: ResponseCursor + } + } + + /** + * Response from the GET /v1/files/{file_key}/components endpoint. + */ + export type GetFileComponentsResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { components: PublishedComponent[] } + } + + /** + * Response from the GET /v1/components/{key} endpoint. + */ + export type GetComponentResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: PublishedComponent + } + + /** + * Response from the GET /v1/teams/{team_id}/component_sets endpoint. + */ + export type GetTeamComponentSetsResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + component_sets: PublishedComponentSet[] + + cursor?: ResponseCursor + } + } + + /** + * Response from the GET /v1/files/{file_key}/component_sets endpoint. + */ + export type GetFileComponentSetsResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { component_sets: PublishedComponentSet[] } + } + + /** + * Response from the GET /v1/component_sets/{key} endpoint. + */ + export type GetComponentSetResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: PublishedComponentSet + } + + /** + * Response from the GET /v1/teams/{team_id}/styles endpoint. + */ + export type GetTeamStylesResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + styles: PublishedStyle[] + + cursor?: ResponseCursor + } + } + + /** + * Response from the GET /v1/files/{file_key}/styles endpoint. + */ + export type GetFileStylesResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { styles: PublishedStyle[] } + } + + /** + * Response from the GET /v1/styles/{key} endpoint. + */ + export type GetStyleResponse = { + /** + * The status of the request. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: PublishedStyle + } + + /** + * Response from the POST /v2/webhooks endpoint. + */ + export type PostWebhookResponse = WebhookV2 + + /** + * Response from the GET /v2/webhooks/{webhook_id} endpoint. + */ + export type GetWebhookResponse = WebhookV2 + + /** + * Response from the PUT /v2/webhooks/{webhook_id} endpoint. + */ + export type PutWebhookResponse = WebhookV2 + + /** + * Response from the DELETE /v2/webhooks/{webhook_id} endpoint. + */ + export type DeleteWebhookResponse = WebhookV2 + + /** + * Response from the GET /v2/teams/{team_id}/webhooks endpoint. + */ + export type GetTeamWebhooksResponse = { + /** + * An array of webhooks. + */ + webhooks: WebhookV2[] + } + + /** + * Response from the GET /v2/webhooks/{webhook_id}/requests endpoint. + */ + export type GetWebhookRequestsResponse = { + /** + * An array of webhook requests. + */ + requests: WebhookV2Request[] + } + + /** + * Response from the GET /v1/activity_logs endpoint. + */ + export type GetActivityLogsResponse = { + /** + * The response status code. + */ + status?: 200 + + /** + * For successful requests, this value is always `false`. + */ + error?: false + + meta?: { + /** + * An array of activity logs sorted by timestamp in ascending order by default. + */ + activity_logs?: ActivityLog[] + + /** + * Encodes the last event (the most recent event) + */ + cursor?: string + + /** + * Whether there is a next page of events + */ + next_page?: boolean + } + } + + /** + * Response from the GET /v1/payments endpoint. + */ + export type GetPaymentsResponse = { + /** + * The response status code. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: PaymentInformation + } + + /** + * Response from the GET /v1/files/{file_key}/variables/local endpoint. + */ + export type GetLocalVariablesResponse = { + /** + * The response status code. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + /** + * A map of variable ids to variables + */ + variables: { [key: string]: LocalVariable } + + /** + * A map of variable collection ids to variable collections + */ + variableCollections: { [key: string]: LocalVariableCollection } + } + } + + /** + * Response from the GET /v1/files/{file_key}/variables/published endpoint. + */ + export type GetPublishedVariablesResponse = { + /** + * The response status code. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + /** + * A map of variable ids to variables + */ + variables: { [key: string]: PublishedVariable } + + /** + * A map of variable collection ids to variable collections + */ + variableCollections: { [key: string]: PublishedVariableCollection } + } + } + + /** + * Response from the POST /v1/files/{file_key}/variables endpoint. + */ + export type PostVariablesResponse = { + /** + * The response status code. + */ + status: 200 + + /** + * For successful requests, this value is always `false`. + */ + error: false + + meta: { + /** + * A map of temporary ids in the request to the real ids of the newly created objects + */ + tempIdToRealId: { [key: string]: string } + } + } + + /** + * Response from the GET /v1/files/{file_key}/dev_resources endpoint. + */ + export type GetDevResourcesResponse = { + /** + * An array of dev resources. + */ + dev_resources: DevResource[] + } + + /** + * Response from the POST /v1/dev_resources endpoint. + */ + export type PostDevResourcesResponse = { + /** + * An array of links created. + */ + links_created: DevResource[] + + /** + * An array of errors. + */ + errors?: { + /** + * The file key. + */ + file_key?: string | null + + /** + * The node id. + */ + node_id?: string | null + + /** + * The error message. + */ + error: string + }[] + } + + /** + * Response from the PUT /v1/dev_resources endpoint. + */ + export type PutDevResourcesResponse = { + /** + * An array of links updated. + */ + links_updated?: DevResource[] + + /** + * An array of errors. + */ + errors?: { + /** + * The id of the dev resource. + */ + id?: string + + /** + * The error message. + */ + error: string + }[] + } + + /** + * Response from the DELETE /v1/files/{file_key}/dev_resources/{dev_resource_id} endpoint. + */ + export type DeleteDevResourceResponse = void + + /** + * Response from the GET /v1/analytics/libraries/{file_key}/component/actions. + */ + export type GetLibraryAnalyticsComponentActionsResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsComponentActionsByAsset[] | LibraryAnalyticsComponentActionsByTeam[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Response from the PUT /v1/analytics/libraries/{file_key}/component/usages. + */ + export type GetLibraryAnalyticsComponentUsagesResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsComponentUsagesByAsset[] | LibraryAnalyticsComponentUsagesByFile[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Response from the GET /v1/analytics/libraries/{file_key}/style/actions. + */ + export type GetLibraryAnalyticsStyleActionsResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsStyleActionsByAsset[] | LibraryAnalyticsStyleActionsByTeam[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Response from the PUT /v1/analytics/libraries/{file_key}/style/usages. + */ + export type GetLibraryAnalyticsStyleUsagesResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsStyleUsagesByAsset[] | LibraryAnalyticsStyleUsagesByFile[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Response from the GET /v1/analytics/libraries/{file_key}/variable/actions. + */ + export type GetLibraryAnalyticsVariableActionsResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsVariableActionsByAsset[] | LibraryAnalyticsVariableActionsByTeam[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Response from the PUT /v1/analytics/libraries/{file_key}/variable/usages. + */ + export type GetLibraryAnalyticsVariableUsagesResponse = { + /** + * An array of analytics data. + */ + rows: LibraryAnalyticsVariableUsagesByAsset[] | LibraryAnalyticsVariableUsagesByFile[] + + /** + * Whether there is a next page of data that can be fetched. + */ + next_page: boolean + + /** + * The cursor to use to fetch the next page of data. Not present if next_page is false. + */ + cursor?: string + } + + /** + * Bad request. Parameters are invalid or malformed. Please check the input formats. This error can + * also happen if the requested resources are too large to complete the request, which results in a + * timeout. Please reduce the number and size of objects requested. + */ + export type BadRequestErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & { + /** + * Status code + */ + status: 400 + } + + /** + * Bad request. Parameters are invalid or malformed. Please check the input formats. This error can + * also happen if the requested resources are too large to complete the request, which results in a + * timeout. Please reduce the number and size of objects requested. + */ + export type BadRequestErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 400 + } + + /** + * Token is missing or incorrect. + */ + export type UnauthorizedErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 401 + } + + /** + * The request was valid, but the server is refusing action. The user might not have the necessary + * permissions for a resource, or may need an account of some sort. + */ + export type ForbiddenErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & { + /** + * Status code + */ + status: 403 + } + + /** + * The request was valid, but the server is refusing action. The user might not have the necessary + * permissions for a resource, or may need an account of some sort. + */ + export type ForbiddenErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 403 + } + + /** + * The requested file or resource was not found. + */ + export type NotFoundErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & { + /** + * Status code + */ + status: 404 + } + + /** + * The requested file or resource was not found. + */ + export type NotFoundErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 404 + } + + /** + * In some cases API requests may be throttled or rate limited. Please wait a while before + * attempting the request again (typically a minute). + */ + export type TooManyRequestsErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & { + /** + * Status code + */ + status: 429 + } + + /** + * In some cases API requests may be throttled or rate limited. Please wait a while before + * attempting the request again (typically a minute). + */ + export type TooManyRequestsErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 429 + } + + /** + * An internal server error occurred. + */ + export type InternalServerErrorResponseWithErrMessage = ErrorResponsePayloadWithErrMessage & { + /** + * Status code + */ + status: 500 + } + + /** + * An internal server error occurred. + */ + export type InternalServerErrorResponseWithErrorBoolean = ErrorResponsePayloadWithErrorBoolean & { + /** + * Status code + */ + status: 500 + } + + /** + * Path parameters for GET /v1/files/{file_key} + */ + export type GetFilePathParams = { + /** + * File to export JSON from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/files/{file_key} + */ + export type GetFileQueryParams = { + /** + * A specific version ID to get. Omitting this will get the current version of the file. + */ + version?: string + /** + * Comma separated list of nodes that you care about in the document. If specified, only a subset of + * the document will be returned corresponding to the nodes listed, their children, and everything + * between the root node and the listed nodes. + * + * Note: There may be other nodes included in the returned JSON that are outside the ancestor chains + * of the desired nodes. The response may also include dependencies of anything in the nodes' + * subtrees. For example, if a node subtree contains an instance of a local component that lives + * elsewhere in that file, that component and its ancestor chain will also be included. + * + * For historical reasons, top-level canvas nodes are always returned, regardless of whether they + * are listed in the `ids` parameter. This quirk may be removed in a future version of the API. + */ + ids?: string + /** + * Positive integer representing how deep into the document tree to traverse. For example, setting + * this to 1 returns only Pages, setting it to 2 returns Pages and all top level objects on each + * page. Not setting this parameter returns all nodes. + */ + depth?: number + /** + * Set to "paths" to export vector data. + */ + geometry?: string + /** + * A comma separated list of plugin IDs and/or the string "shared". Any data present in the document + * written by those plugins will be included in the result in the `pluginData` and + * `sharedPluginData` properties. + */ + plugin_data?: string + /** + * Returns branch metadata for the requested file. If the file is a branch, the main file's key will + * be included in the returned response. If the file has branches, their metadata will be included + * in the returned response. Default: false. + */ + branch_data?: boolean + } + + /** + * Path parameters for GET /v1/files/{file_key}/nodes + */ + export type GetFileNodesPathParams = { + /** + * File to export JSON from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/files/{file_key}/nodes + */ + export type GetFileNodesQueryParams = { + /** + * A comma separated list of node IDs to retrieve and convert. + */ + ids: string + /** + * A specific version ID to get. Omitting this will get the current version of the file. + */ + version?: string + /** + * Positive integer representing how deep into the node tree to traverse. For example, setting this + * to 1 will return only the children directly underneath the desired nodes. Not setting this + * parameter returns all nodes. + * + * Note: this parameter behaves differently from the same parameter in the `GET /v1/files/:key` + * endpoint. In this endpoint, the depth will be counted starting from the desired node rather than + * the document root node. + */ + depth?: number + /** + * Set to "paths" to export vector data. + */ + geometry?: string + /** + * A comma separated list of plugin IDs and/or the string "shared". Any data present in the document + * written by those plugins will be included in the result in the `pluginData` and + * `sharedPluginData` properties. + */ + plugin_data?: string + } + + /** + * Path parameters for GET /v1/images/{file_key} + */ + export type GetImagesPathParams = { + /** + * File to export images from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/images/{file_key} + */ + export type GetImagesQueryParams = { + /** + * A comma separated list of node IDs to render. + */ + ids: string + /** + * A specific version ID to get. Omitting this will get the current version of the file. + */ + version?: string + /** + * A number between 0.01 and 4, the image scaling factor. + */ + scale?: number + /** + * A string enum for the image output format. + */ + format?: 'jpg' | 'png' | 'svg' | 'pdf' + /** + * Whether text elements are rendered as outlines (vector paths) or as `` elements in SVGs. + * + * Rendering text elements as outlines guarantees that the text looks exactly the same in the SVG as + * it does in the browser/inside Figma. + * + * Exporting as `` allows text to be selectable inside SVGs and generally makes the SVG easier + * to read. However, this relies on the browser's rendering engine which can vary between browsers + * and/or operating systems. As such, visual accuracy is not guaranteed as the result could look + * different than in Figma. + */ + svg_outline_text?: boolean + /** + * Whether to include id attributes for all SVG elements. Adds the layer name to the `id` attribute + * of an svg element. + */ + svg_include_id?: boolean + /** + * Whether to include node id attributes for all SVG elements. Adds the node id to a `data-node-id` + * attribute of an svg element. + */ + svg_include_node_id?: boolean + /** + * Whether to simplify inside/outside strokes and use stroke attribute if possible instead of + * ``. + */ + svg_simplify_stroke?: boolean + /** + * Whether content that overlaps the node should be excluded from rendering. Passing false (i.e., + * rendering overlaps) may increase processing time, since more of the document must be included in + * rendering. + */ + contents_only?: boolean + /** + * Use the full dimensions of the node regardless of whether or not it is cropped or the space + * around it is empty. Use this to export text nodes without cropping. + */ + use_absolute_bounds?: boolean + } + + /** + * Path parameters for GET /v1/files/{file_key}/images + */ + export type GetImageFillsPathParams = { + /** + * File to get image URLs from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Path parameters for GET /v1/teams/{team_id}/projects + */ + export type GetTeamProjectsPathParams = { + /** + * ID of the team to list projects from + */ + team_id: string + } + + /** + * Path parameters for GET /v1/projects/{project_id}/files + */ + export type GetProjectFilesPathParams = { + /** + * ID of the project to list files from + */ + project_id: string + } + + /** + * Query parameters for GET /v1/projects/{project_id}/files + */ + export type GetProjectFilesQueryParams = { + /** + * Returns branch metadata in the response for each main file with a branch inside the project. + */ + branch_data?: boolean + } + + /** + * Path parameters for GET /v1/files/{file_key}/versions + */ + export type GetFileVersionsPathParams = { + /** + * File to get version history from. This can be a file key or branch key. Use `GET /v1/files/:key` + * with the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/files/{file_key}/versions + */ + export type GetFileVersionsQueryParams = { + /** + * The number of items returned in a page of the response. If not included, `page_size` is `30`. + */ + page_size?: number + /** + * A version ID for one of the versions in the history. Gets versions before this ID. Used for + * paginating. If the response is not paginated, this link returns the same data in the current + * response. + */ + before?: number + /** + * A version ID for one of the versions in the history. Gets versions after this ID. Used for + * paginating. If the response is not paginated, this property is not included. + */ + after?: number + } + + /** + * Path parameters for GET /v1/files/{file_key}/comments + */ + export type GetCommentsPathParams = { + /** + * File to get comments from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/files/{file_key}/comments + */ + export type GetCommentsQueryParams = { + /** + * If enabled, will return comments as their markdown equivalents when applicable. + */ + as_md?: boolean + } + + /** + * Path parameters for POST /v1/files/{file_key}/comments + */ + export type PostCommentPathParams = { + /** + * File to add comments in. This can be a file key or branch key. Use `GET /v1/files/:key` with the + * `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Request body parameters for POST /v1/files/{file_key}/comments + */ + export type PostCommentRequestBody = { + /** + * The text contents of the comment to post. + */ + message: string + + /** + * The ID of the comment to reply to, if any. This must be a root comment. You cannot reply to other + * replies (a comment that has a parent_id). + */ + comment_id?: string + + /** + * The position where to place the comment. + */ + client_meta?: Vector | FrameOffset | Region | FrameOffsetRegion + } + + /** + * Path parameters for DELETE /v1/files/{file_key}/comments/{comment_id} + */ + export type DeleteCommentPathParams = { + /** + * File to delete comment from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + /** + * Comment id of comment to delete + */ + comment_id: string + } + + /** + * Path parameters for DELETE /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type DeleteCommentReactionPathParams = { + /** + * File to delete comment reaction from. This can be a file key or branch key. Use `GET + * /v1/files/:key` with the `branch_data` query param to get the branch key. + */ + file_key: string + /** + * ID of comment to delete reaction from. + */ + comment_id: string + } + + /** + * Query parameters for DELETE /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type DeleteCommentReactionQueryParams = { emoji: Emoji } + + /** + * Path parameters for GET /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type GetCommentReactionsPathParams = { + /** + * File to get comment containing reactions from. This can be a file key or branch key. Use `GET + * /v1/files/:key` with the `branch_data` query param to get the branch key. + */ + file_key: string + /** + * ID of comment to get reactions from. + */ + comment_id: string + } + + /** + * Query parameters for GET /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type GetCommentReactionsQueryParams = { + /** + * Cursor for pagination, retrieved from the response of the previous call. + */ + cursor?: string + } + + /** + * Path parameters for POST /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type PostCommentReactionPathParams = { + /** + * File to post comment reactions to. This can be a file key or branch key. Use `GET + * /v1/files/:key` with the `branch_data` query param to get the branch key. + */ + file_key: string + /** + * ID of comment to react to. + */ + comment_id: string + } + + /** + * Request body parameters for POST /v1/files/{file_key}/comments/{comment_id}/reactions + */ + export type PostCommentReactionRequestBody = { emoji: Emoji } + + /** + * Path parameters for GET /v1/teams/{team_id}/components + */ + export type GetTeamComponentsPathParams = { + /** + * Id of the team to list components from. + */ + team_id: string + } + + /** + * Query parameters for GET /v1/teams/{team_id}/components + */ + export type GetTeamComponentsQueryParams = { + /** + * Number of items to return in a paged list of results. Defaults to 30. + */ + page_size?: number + /** + * Cursor indicating which id after which to start retrieving components for. Exclusive with before. + * The cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + after?: number + /** + * Cursor indicating which id before which to start retrieving components for. Exclusive with after. + * The cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + before?: number + } + + /** + * Path parameters for GET /v1/files/{file_key}/components + */ + export type GetFileComponentsPathParams = { + /** + * File to list components from. This must be a main file key, not a branch key, as it is not + * possible to publish from branches. + */ + file_key: string + } + + /** + * Path parameters for GET /v1/components/{key} + */ + export type GetComponentPathParams = { + /** + * The unique identifier of the component. + */ + key: string + } + + /** + * Path parameters for GET /v1/teams/{team_id}/component_sets + */ + export type GetTeamComponentSetsPathParams = { + /** + * Id of the team to list component sets from. + */ + team_id: string + } + + /** + * Query parameters for GET /v1/teams/{team_id}/component_sets + */ + export type GetTeamComponentSetsQueryParams = { + /** + * Number of items to return in a paged list of results. Defaults to 30. + */ + page_size?: number + /** + * Cursor indicating which id after which to start retrieving component sets for. Exclusive with + * before. The cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + after?: number + /** + * Cursor indicating which id before which to start retrieving component sets for. Exclusive with + * after. The cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + before?: number + } + + /** + * Path parameters for GET /v1/files/{file_key}/component_sets + */ + export type GetFileComponentSetsPathParams = { + /** + * File to list component sets from. This must be a main file key, not a branch key, as it is not + * possible to publish from branches. + */ + file_key: string + } + + /** + * Path parameters for GET /v1/component_sets/{key} + */ + export type GetComponentSetPathParams = { + /** + * The unique identifier of the component set. + */ + key: string + } + + /** + * Path parameters for GET /v1/teams/{team_id}/styles + */ + export type GetTeamStylesPathParams = { + /** + * Id of the team to list styles from. + */ + team_id: string + } + + /** + * Query parameters for GET /v1/teams/{team_id}/styles + */ + export type GetTeamStylesQueryParams = { + /** + * Number of items to return in a paged list of results. Defaults to 30. + */ + page_size?: number + /** + * Cursor indicating which id after which to start retrieving styles for. Exclusive with before. The + * cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + after?: number + /** + * Cursor indicating which id before which to start retrieving styles for. Exclusive with after. The + * cursor value is an internally tracked integer that doesn't correspond to any Ids. + */ + before?: number + } + + /** + * Path parameters for GET /v1/files/{file_key}/styles + */ + export type GetFileStylesPathParams = { + /** + * File to list styles from. This must be a main file key, not a branch key, as it is not possible + * to publish from branches. + */ + file_key: string + } + + /** + * Path parameters for GET /v1/styles/{key} + */ + export type GetStylePathParams = { + /** + * The unique identifier of the style. + */ + key: string + } + + /** + * Request body parameters for POST /v2/webhooks + */ + export type PostWebhookRequestBody = { + event_type: WebhookV2Event + + /** + * Team id to receive updates about + */ + team_id: string + + /** + * The HTTP endpoint that will receive a POST request when the event triggers. Max length 2048 + * characters. + */ + endpoint: string + + /** + * String that will be passed back to your webhook endpoint to verify that it is being called by + * Figma. Max length 100 characters. + */ + passcode: string + + /** + * State of the webhook, including any error state it may be in + */ + status?: WebhookV2Status + + /** + * User provided description or name for the webhook. Max length 150 characters. + */ + description?: string + } + + /** + * Path parameters for DELETE /v2/webhooks/{webhook_id} + */ + export type DeleteWebhookPathParams = { + /** + * ID of webhook to delete + */ + webhook_id: string + } + + /** + * Path parameters for GET /v2/webhooks/{webhook_id} + */ + export type GetWebhookPathParams = { + /** + * ID of webhook to get + */ + webhook_id: string + } + + /** + * Path parameters for PUT /v2/webhooks/{webhook_id} + */ + export type PutWebhookPathParams = { + /** + * ID of webhook to update + */ + webhook_id: string + } + + /** + * Request body parameters for PUT /v2/webhooks/{webhook_id} + */ + export type PutWebhookRequestBody = { + event_type: WebhookV2Event + + /** + * The HTTP endpoint that will receive a POST request when the event triggers. Max length 2048 + * characters. + */ + endpoint: string + + /** + * String that will be passed back to your webhook endpoint to verify that it is being called by + * Figma. Max length 100 characters. + */ + passcode: string + + /** + * State of the webhook, including any error state it may be in + */ + status?: WebhookV2Status + + /** + * User provided description or name for the webhook. Max length 150 characters. + */ + description?: string + } + + /** + * Path parameters for GET /v2/teams/{team_id}/webhooks + */ + export type GetTeamWebhooksPathParams = { + /** + * ID of team to get webhooks for + */ + team_id: string + } + + /** + * Path parameters for GET /v2/webhooks/{webhook_id}/requests + */ + export type GetWebhookRequestsPathParams = { + /** + * The id of the webhook subscription you want to see events from + */ + webhook_id: string + } + + /** + * Query parameters for GET /v1/activity_logs + */ + export type GetActivityLogsQueryParams = { + /** + * Event type(s) to include in the response. Can have multiple values separated by comma. All + * events are returned by default. + */ + events?: string + /** + * Unix timestamp of the least recent event to include. This param defaults to one year ago if + * unspecified. Events prior to one year ago are not available. + */ + start_time?: number + /** + * Unix timestamp of the most recent event to include. This param defaults to the current timestamp + * if unspecified. + */ + end_time?: number + /** + * Maximum number of events to return. This param defaults to 1000 if unspecified. + */ + limit?: number + /** + * Event order by timestamp. This param can be either "asc" (default) or "desc". + */ + order?: 'asc' | 'desc' + } + + /** + * Query parameters for GET /v1/payments + */ + export type GetPaymentsQueryParams = { + /** + * Short-lived token returned from "getPluginPaymentTokenAsync" in the plugin payments API and used + * to authenticate to this endpoint. Read more about generating this token through "Calling the + * Payments REST API from a plugin or widget" below. + */ + plugin_payment_token?: string + /** + * The ID of the user to query payment information about. You can get the user ID by having the user + * OAuth2 to the Figma REST API. + */ + user_id?: number + /** + * The ID of the Community file to query a user's payment information on. You can get the Community + * file ID from the file's Community page (look for the number after "file/" in the URL). Provide + * exactly one of "community_file_id", "plugin_id", or "widget_id". + */ + community_file_id?: number + /** + * The ID of the plugin to query a user's payment information on. You can get the plugin ID from the + * plugin's manifest, or from the plugin's Community page (look for the number after "plugin/" in + * the URL). Provide exactly one of "community_file_id", "plugin_id", or "widget_id". + */ + plugin_id?: number + /** + * The ID of the widget to query a user's payment information on. You can get the widget ID from the + * widget's manifest, or from the widget's Community page (look for the number after "widget/" in + * the URL). Provide exactly one of "community_file_id", "plugin_id", or "widget_id". + */ + widget_id?: number + } + + /** + * Path parameters for GET /v1/files/{file_key}/variables/local + */ + export type GetLocalVariablesPathParams = { + /** + * File to get variables from. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Path parameters for GET /v1/files/{file_key}/variables/published + */ + export type GetPublishedVariablesPathParams = { + /** + * File to get variables from. This must be a main file key, not a branch key, as it is not + * possible to publish from branches. + */ + file_key: string + } + + /** + * Path parameters for POST /v1/files/{file_key}/variables + */ + export type PostVariablesPathParams = { + /** + * File to modify variables in. This can be a file key or branch key. Use `GET /v1/files/:key` with + * the `branch_data` query param to get the branch key. + */ + file_key: string + } + + /** + * Request body parameters for POST /v1/files/{file_key}/variables + */ + export type PostVariablesRequestBody = { + /** + * For creating, updating, and deleting variable collections. + */ + variableCollections?: VariableCollectionChange[] + + /** + * For creating, updating, and deleting modes within variable collections. + */ + variableModes?: VariableModeChange[] + + /** + * For creating, updating, and deleting variables. + */ + variables?: VariableChange[] + + /** + * For setting a specific value, given a variable and a mode. + */ + variableModeValues?: VariableModeValue[] + } + + /** + * Path parameters for GET /v1/files/{file_key}/dev_resources + */ + export type GetDevResourcesPathParams = { + /** + * The file to get the dev resources from. This must be a main file key, not a branch key. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/files/{file_key}/dev_resources + */ + export type GetDevResourcesQueryParams = { + /** + * Comma separated list of nodes that you care about in the document. If specified, only dev + * resources attached to these nodes will be returned. If not specified, all dev resources in the + * file will be returned. + */ + node_ids?: string + } + + /** + * Request body parameters for POST /v1/dev_resources + */ + export type PostDevResourcesRequestBody = { + /** + * An array of dev resources. + */ + dev_resources: { + /** + * The name of the dev resource. + */ + name: string + + /** + * The URL of the dev resource. + */ + url: string + + /** + * The file key where the dev resource belongs. + */ + file_key: string + + /** + * The target node to attach the dev resource to. + */ + node_id: string + }[] + } + + /** + * Request body parameters for PUT /v1/dev_resources + */ + export type PutDevResourcesRequestBody = { + /** + * An array of dev resources. + */ + dev_resources: { + /** + * Unique identifier of the dev resource + */ + id: string + + /** + * The name of the dev resource. + */ + name?: string + + /** + * The URL of the dev resource. + */ + url?: string + }[] + } + + /** + * Path parameters for DELETE /v1/files/{file_key}/dev_resources/{dev_resource_id} + */ + export type DeleteDevResourcePathParams = { + /** + * The file to delete the dev resource from. This must be a main file key, not a branch key. + */ + file_key: string + /** + * The id of the dev resource to delete. + */ + dev_resource_id: string + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/component/actions + */ + export type GetLibraryAnalyticsComponentActionsPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/component/actions + */ + export type GetLibraryAnalyticsComponentActionsQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'component' | 'team' + /** + * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the + * nearest start of a week. Defaults to one year prior. + */ + start_date?: string + /** + * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the + * nearest end of a week. Defaults to the latest computed week. + */ + end_date?: string + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/component/usages + */ + export type GetLibraryAnalyticsComponentUsagesPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/component/usages + */ + export type GetLibraryAnalyticsComponentUsagesQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'component' | 'file' + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/style/actions + */ + export type GetLibraryAnalyticsStyleActionsPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/style/actions + */ + export type GetLibraryAnalyticsStyleActionsQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'style' | 'team' + /** + * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the + * nearest start of a week. Defaults to one year prior. + */ + start_date?: string + /** + * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the + * nearest end of a week. Defaults to the latest computed week. + */ + end_date?: string + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/style/usages + */ + export type GetLibraryAnalyticsStyleUsagesPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/style/usages + */ + export type GetLibraryAnalyticsStyleUsagesQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'style' | 'file' + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/variable/actions + */ + export type GetLibraryAnalyticsVariableActionsPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/variable/actions + */ + export type GetLibraryAnalyticsVariableActionsQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'variable' | 'team' + /** + * ISO 8601 date string (YYYY-MM-DD) of the earliest week to include. Dates are rounded back to the + * nearest start of a week. Defaults to one year prior. + */ + start_date?: string + /** + * ISO 8601 date string (YYYY-MM-DD) of the latest week to include. Dates are rounded forward to the + * nearest end of a week. Defaults to the latest computed week. + */ + end_date?: string + } + + /** + * Path parameters for GET /v1/analytics/libraries/{file_key}/variable/usages + */ + export type GetLibraryAnalyticsVariableUsagesPathParams = { + /** + * File key of the library to fetch analytics data for. + */ + file_key: string + } + + /** + * Query parameters for GET /v1/analytics/libraries/{file_key}/variable/usages + */ + export type GetLibraryAnalyticsVariableUsagesQueryParams = { + /** + * Cursor indicating what page of data to fetch. Obtained from prior API call. + */ + cursor?: string + /** + * A dimension to group returned analytics data by. + */ + group_by: 'variable' | 'file' + } \ No newline at end of file diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 9946621f..f1b616ad 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -30,7 +30,7 @@ import { } from "../common/commonFormatAttributes"; import { TailwindColorType, TailwindSettings } from "types"; -const isNotEmpty = (s: string) => s !== ""; +const isNotEmpty = (s: string) => s !== "" && s !== null && s !== undefined; const dropEmptyStrings = (strings: string[]) => strings.filter(isNotEmpty); export class TailwindDefaultBuilder { @@ -63,10 +63,15 @@ export class TailwindDefaultBuilder { } addAttributes = (...newStyles: string[]) => { - this.attributes.push(...dropEmptyStrings(newStyles)); + // Filter out empty strings and trim any extra spaces + const cleanedStyles = dropEmptyStrings(newStyles).map((s) => s.trim()); + this.attributes.push(...cleanedStyles); }; + prependAttributes = (...newStyles: string[]) => { - this.attributes.unshift(...dropEmptyStrings(newStyles)); + // Filter out empty strings and trim any extra spaces + const cleanedStyles = dropEmptyStrings(newStyles).map((s) => s.trim()); + this.attributes.unshift(...cleanedStyles); }; blend(): this { @@ -184,21 +189,21 @@ export class TailwindDefaultBuilder { const { node, optimizeLayout } = this; const { width, height } = tailwindSizePartial(node, optimizeLayout); - if (node.type === "TEXT") { - switch (node.textAutoResize) { - case "WIDTH_AND_HEIGHT": - break; - case "HEIGHT": - this.addAttributes(width); - break; - case "NONE": - case "TRUNCATE": - this.addAttributes(width, height); - break; - } - } else { - this.addAttributes(width, height); - } + // if (node.type === "TEXT") { + // switch (node.textAutoResize) { + // case "WIDTH_AND_HEIGHT": + // break; + // case "HEIGHT": + // this.addAttributes(width); + // break; + // case "NONE": + // case "TRUNCATE": + // this.addAttributes(width, height); + // break; + // } + // } else { + this.addAttributes(width, height); + // } return this; } @@ -251,7 +256,9 @@ export class TailwindDefaultBuilder { } build(additionalAttr = ""): string { - this.addAttributes(additionalAttr); + if (additionalAttr) { + this.addAttributes(additionalAttr); + } if (this.name !== "") { this.prependAttributes(stringToClassName(this.name)); @@ -270,7 +277,7 @@ export class TailwindDefaultBuilder { const classLabel = getClassLabel(this.isJSX); const classNames = this.attributes.length > 0 - ? ` ${classLabel}="${this.attributes.join(" ")}"` + ? ` ${classLabel}="${this.attributes.filter(Boolean).join(" ")}"` : ""; const styles = this.style.length > 0 ? ` style="${this.style}"` : ""; const dataAttributes = this.data.join(""); diff --git a/packages/plugin-ui/src/components/About.tsx b/packages/plugin-ui/src/components/About.tsx index 0de731de..1a3f01f3 100644 --- a/packages/plugin-ui/src/components/About.tsx +++ b/packages/plugin-ui/src/components/About.tsx @@ -1,5 +1,6 @@ import React from "react"; import { + ArrowRightIcon, Code, Github, Heart, @@ -108,17 +109,23 @@ const About = () => {
  • -
    +
    + +
    Convert Figma designs to HTML, Tailwind, Flutter, and SwiftUI
  • -
    +
    + +
    Extract colors and gradients from your designs
  • -
    +
    + +
    Get responsive code that matches your design
From 0aedab5a39d1e5b13a9b5aec077eb8bd784e85fe Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 4 Mar 2025 12:37:45 -0300 Subject: [PATCH 013/134] All-new text segments --- .../backend/src/altNodes/altConversion.ts | 33 +--------- packages/backend/src/code.ts | 60 ++++++++++++------- .../src/flutter/builderImpl/flutterColor.ts | 10 ++++ .../backend/src/flutter/flutterTextBuilder.ts | 23 ++++--- packages/backend/src/html/htmlMain.ts | 2 +- packages/backend/src/html/htmlTextBuilder.ts | 18 +++--- .../src/swiftui/builderImpl/swiftuiColor.ts | 38 ++++-------- .../src/swiftui/swiftuiDefaultBuilder.ts | 4 +- .../backend/src/swiftui/swiftuiTextBuilder.ts | 17 +++--- .../src/tailwind/tailwindDefaultBuilder.ts | 31 +++++----- packages/backend/src/tailwind/tailwindMain.ts | 2 +- .../src/tailwind/tailwindTextBuilder.ts | 12 ++-- 12 files changed, 117 insertions(+), 133 deletions(-) diff --git a/packages/backend/src/altNodes/altConversion.ts b/packages/backend/src/altNodes/altConversion.ts index 59b0db70..226a16d8 100644 --- a/packages/backend/src/altNodes/altConversion.ts +++ b/packages/backend/src/altNodes/altConversion.ts @@ -1,15 +1,11 @@ -import { StyledTextSegmentSubset, ParentNode, AltNode } from "types"; +import { ParentNode } from "types"; import { - assignParent, isNotEmpty, assignRectangleType, assignChildren, isTypeOrGroupOfTypes, } from "./altNodeUtils"; -export let globalTextStyleSegments: Record = - {}; - // List of types that can be flattened into SVG const canBeFlattened = isTypeOrGroupOfTypes([ "VECTOR", @@ -49,13 +45,9 @@ export const convertNodeToAltNode = case "SECTION": const groupChildren = await convertNodesToAltNodes(node.children, node); return assignChildren(groupChildren, node); - // Text Nodes case "TEXT": - const textNode = (await figma.getNodeByIdAsync(node.id)) as TextNode; - globalTextStyleSegments[node.id] = extractStyledTextSegments(textNode); return node; - // Unsupported Nodes case "SLICE": return null; @@ -80,26 +72,3 @@ const cloneAsRectangleNode = (node: T): RectangleNode => { return node as unknown as RectangleNode; }; - -const extractStyledTextSegments = (node: TextNode) => - node.getStyledTextSegments([ - "fontName", - "fills", - "fontSize", - "fontWeight", - "hyperlink", - "indentation", - "letterSpacing", - "lineHeight", - "listOptions", - "textCase", - "textDecoration", - "textDecorationStyle", - "textDecorationOffset", - "textDecorationThickness", - "textDecorationColor", - "textDecorationSkipInk", - "textStyleId", - "fillStyleId", - "openTypeFeatures", - ]); diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 5bf72b65..93ad03c2 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -52,11 +52,13 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { ); }); - // Check if node is an instance to extract component metadata - const isInstance = node.type === "INSTANCE"; - // Only fetch the Figma node if we have gradients, optimizeLayout is enabled, or it's an instance - if (hasGradient || optimizeLayout || isInstance) { + if ( + hasGradient || + optimizeLayout || + node.type === "INSTANCE" || + node.type === "TEXT" + ) { try { const figmaNode = figma.getNodeById(node.id); if (figmaNode) { @@ -82,6 +84,31 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { }); } + if (figmaNode.type === "TEXT") { + node.styledTextSegments = figmaNode.getStyledTextSegments([ + "fontName", + "fills", + "fontSize", + "fontWeight", + "hyperlink", + "indentation", + "letterSpacing", + "lineHeight", + "listOptions", + "textCase", + "textDecoration", + "textDecorationStyle", + "textDecorationOffset", + "textDecorationThickness", + "textDecorationColor", + "textDecorationSkipInk", + "textStyleId", + "fillStyleId", + "openTypeFeatures", + ]); + Object.assign(node, node.style); + } + // Extract inferredAutoLayout if optimizeLayout is enabled if (optimizeLayout && "inferredAutoLayout" in figmaNode) { node.inferredAutoLayout = JSON.parse( @@ -90,7 +117,7 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } // Extract component metadata from instances - if (isInstance && figmaNode.type === "INSTANCE") { + if (figmaNode.type === "INSTANCE") { if (figmaNode.variantProperties) { node.variantProperties = figmaNode.variantProperties; } @@ -107,24 +134,11 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { // Silently fail if there's an error accessing the Figma node } } else { - // Avoid calling getNodeById if we don't need to - if ( - "rotation" in node && - node.rotation !== undefined && - node.rotation !== 0 - ) { - const figmaNode = figma.getNodeById(node.id); - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; - } else if (node.absoluteRenderBounds) { - // Use the absoluteRenderBounds if we don't need to fetch the Figma node. - node.width = node.absoluteRenderBounds.width; - node.height = node.absoluteRenderBounds.height; - node.x = node.absoluteRenderBounds.x; - node.y = node.absoluteRenderBounds.y; - } + const figmaNode = figma.getNodeById(node.id); + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; } if (!node.layoutMode) { diff --git a/packages/backend/src/flutter/builderImpl/flutterColor.ts b/packages/backend/src/flutter/builderImpl/flutterColor.ts index 4fcf7dd2..c04b2078 100644 --- a/packages/backend/src/flutter/builderImpl/flutterColor.ts +++ b/packages/backend/src/flutter/builderImpl/flutterColor.ts @@ -22,6 +22,16 @@ export const flutterColorFromFills = ( | ReadonlyArray | PluginAPI["mixed"]; + return flutterColorFromDirectFills(fills); +}; + +/** + * Retrieve the SOLID color for Flutter directly from fills when existent, otherwise "" + * @param fills The fills array to process + */ +export const flutterColorFromDirectFills = ( + fills: ReadonlyArray | PluginAPI["mixed"], +): string => { const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { diff --git a/packages/backend/src/flutter/flutterTextBuilder.ts b/packages/backend/src/flutter/flutterTextBuilder.ts index bda01b99..1239e3b8 100644 --- a/packages/backend/src/flutter/flutterTextBuilder.ts +++ b/packages/backend/src/flutter/flutterTextBuilder.ts @@ -4,13 +4,16 @@ import { numberToFixedString, } from "./../common/numToAutoFixed"; import { FlutterDefaultBuilder } from "./flutterDefaultBuilder"; -import { flutterColorFromFills } from "./builderImpl/flutterColor"; +import { + flutterColorFromDirectFills, + flutterColorFromFills, +} from "./builderImpl/flutterColor"; import { flutterSize } from "./builderImpl/flutterSize"; -import { globalTextStyleSegments } from "../altNodes/altConversion"; import { commonLetterSpacing, commonLineHeight, } from "../common/commonTextHeightSpacing"; +import { StyledTextSegmentSubset } from "types/src/types"; export class FlutterTextBuilder extends FlutterDefaultBuilder { node?: TextNode; @@ -35,7 +38,7 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder { alignHorizontal !== "left" ? `TextAlign.${alignHorizontal}` : "", }; - const segments = this.getTextSegments(node.id); + const segments = this.getTextSegments(node); if (segments.length === 1) { this.child = generateWidgetCode( "Text", @@ -61,18 +64,19 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder { return this; } - getTextSegments(id: string): { + getTextSegments(node: TextNode): { style: string; text: string; openTypeFeatures: { [key: string]: boolean }; }[] { - const segments = globalTextStyleSegments[id]; + const segments = (node as any) + .styledTextSegments as StyledTextSegmentSubset[]; if (!segments) { return []; } return segments.map((segment) => { - const color = flutterColorFromFills(segment.fills); + const color = flutterColorFromDirectFills(segment.fills); const fontSize = `${numberToFixedString(segment.fontSize)}`; const fontStyle = this.fontStyle(segment.fontName); @@ -188,8 +192,7 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder { if (this.node && (this.node as TextNode).effects) { const effects = (this.node as TextNode).effects; const dropShadow = effects.find( - (effect) => - effect.type === "DROP_SHADOW" && effect.visible !== false, + (effect) => effect.type === "DROP_SHADOW" && effect.visible !== false, ); if (dropShadow) { const ds = dropShadow as DropShadowEffect; @@ -255,7 +258,9 @@ export const wrapTextWithLayerBlur = ( if (node.effects) { const blurEffect = node.effects.find( (effect) => - effect.type === "LAYER_BLUR" && effect.visible !== false && effect.radius > 0, + effect.type === "LAYER_BLUR" && + effect.visible !== false && + effect.radius > 0, ); if (blurEffect) { return generateWidgetCode("ImageFiltered", { diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 99f4989e..d12bd73e 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -163,7 +163,7 @@ const htmlText = (node: TextNode, settings: HTMLSettings): string => { .commonPositionStyles() .textAlign(); - const styledHtml = layoutBuilder.getTextSegments(node.id); + const styledHtml = layoutBuilder.getTextSegments(node); previousExecutionCache.push(...styledHtml); let content = ""; diff --git a/packages/backend/src/html/htmlTextBuilder.ts b/packages/backend/src/html/htmlTextBuilder.ts index 30ab8ca5..d18e08fe 100644 --- a/packages/backend/src/html/htmlTextBuilder.ts +++ b/packages/backend/src/html/htmlTextBuilder.ts @@ -1,24 +1,24 @@ import { formatMultipleJSX, formatWithJSX } from "../common/parseJSX"; import { HtmlDefaultBuilder } from "./htmlDefaultBuilder"; -import { globalTextStyleSegments } from "../altNodes/altConversion"; import { htmlColorFromFills } from "./builderImpl/htmlColor"; import { commonLetterSpacing, commonLineHeight, } from "../common/commonTextHeightSpacing"; -import { HTMLSettings } from "types"; +import { HTMLSettings, StyledTextSegmentSubset } from "types"; export class HtmlTextBuilder extends HtmlDefaultBuilder { constructor(node: TextNode, settings: HTMLSettings) { super(node, settings); } - getTextSegments(id: string): { + getTextSegments(node: TextNode): { style: string; text: string; openTypeFeatures: { [key: string]: boolean }; }[] { - const segments = globalTextStyleSegments[id]; + const segments = (node as any) + .styledTextSegments as StyledTextSegmentSubset[]; if (!segments) { return []; } @@ -48,13 +48,13 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { "line-height": this.lineHeight(segment.lineHeight, segment.fontSize), "letter-spacing": this.letterSpacing( segment.letterSpacing, - segment.fontSize + segment.fontSize, ), // "text-indent": segment.indentation, "word-wrap": "break-word", ...additionalStyles, }, - this.isJSX + this.isJSX, ); const charsWithLineBreak = segment.characters.split("\n").join("
"); @@ -163,7 +163,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { (effect) => effect.type === "LAYER_BLUR" && effect.visible !== false && - effect.radius > 0 + effect.radius > 0, ); if (blurEffect && blurEffect.radius) { return `blur(${blurEffect.radius}px)`; @@ -179,7 +179,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { if (this.node && (this.node as TextNode).effects) { const effects = (this.node as TextNode).effects; const dropShadow = effects.find( - (effect) => effect.type === "DROP_SHADOW" && effect.visible !== false + (effect) => effect.type === "DROP_SHADOW" && effect.visible !== false, ); if (dropShadow) { const ds = dropShadow as DropShadowEffect; // Type narrow the effect. @@ -191,7 +191,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { const b = Math.round(ds.color.b * 255); const a = ds.color.a; return `${offsetX}px ${offsetY}px ${blurRadius}px rgba(${r}, ${g}, ${b}, ${a.toFixed( - 2 + 2, )})`; } } diff --git a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts index 9ecb7a19..39b6c851 100644 --- a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts +++ b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts @@ -42,6 +42,16 @@ export const swiftuiSolidColor = ( | ReadonlyArray | PluginAPI["mixed"]; + return swiftuiSolidColorFromDirectFills(fills); +}; + +/** + * Retrieve the SwiftUI solid color directly from fills when existent, otherwise "" + * @param fills The fills array to process + */ +export const swiftuiSolidColorFromDirectFills = ( + fills: ReadonlyArray | PluginAPI["mixed"], +): string => { const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { @@ -64,34 +74,6 @@ export const swiftuiSolidColor = ( return ""; }; -/** - * Get SwiftUI background for a node - * @param node SceneNode containing the property to examine - * @param propertyPath Property path to extract fills from (e.g., 'fills', 'strokes') or direct fills array - */ -export const swiftuiBackground = ( - node: SceneNode, - propertyPath: string, -): string => { - const fills = node[propertyPath as keyof SceneNode] as - | ReadonlyArray - | PluginAPI["mixed"]; - - const fill = retrieveTopFill(fills); - - if (fill && fill.type === "SOLID") { - const opacity = fill.opacity ?? 1.0; - return swiftuiColor(fill.color, opacity); - } else if (fill?.type === "GRADIENT_LINEAR") { - return swiftuiGradient(fill); - } else if (fill?.type === "IMAGE") { - addWarning("Image fills are replaced with placeholders"); - return `AsyncImage(url: URL(string: "${getPlaceholderImage(node.width, node.height)}"))`; - } - - return ""; -}; - export const swiftuiGradient = (fill: GradientPaint): string => { const direction = gradientDirection(gradientAngle(fill)); diff --git a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts index 7ae8d73e..3648877f 100644 --- a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts @@ -4,7 +4,6 @@ import { swiftuiBorder, swiftuiCornerRadius, } from "./builderImpl/swiftuiBorder"; -import { swiftuiBackground } from "./builderImpl/swiftuiColor"; import { swiftuiPadding } from "./builderImpl/swiftuiPadding"; import { swiftuiSize } from "./builderImpl/swiftuiSize"; @@ -20,6 +19,7 @@ import { } from "../common/commonPosition"; import { SwiftUIElement } from "./builderImpl/swiftuiParser"; import { SwiftUIModifier } from "types"; +import { swiftuiSolidColor } from "./builderImpl/swiftuiColor"; export class SwiftuiDefaultBuilder { element: SwiftUIElement; @@ -105,7 +105,7 @@ export class SwiftuiDefaultBuilder { shapeBackground(node: SceneNode): this { if ("fills" in node) { - const background = swiftuiBackground(node, "fills"); + const background = swiftuiSolidColor(node, "fills"); if (background) { this.pushModifier([`background`, background]); } diff --git a/packages/backend/src/swiftui/swiftuiTextBuilder.ts b/packages/backend/src/swiftui/swiftuiTextBuilder.ts index e1438f44..56307bd3 100644 --- a/packages/backend/src/swiftui/swiftuiTextBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiTextBuilder.ts @@ -6,10 +6,10 @@ import { import { SwiftuiDefaultBuilder } from "./swiftuiDefaultBuilder"; import { swiftuiWeightMatcher } from "./builderImpl/swiftuiTextWeight"; import { swiftuiSize } from "./builderImpl/swiftuiSize"; -import { globalTextStyleSegments } from "../altNodes/altConversion"; import { SwiftUIElement } from "./builderImpl/swiftuiParser"; import { parseTextAsCode } from "../flutter/flutterTextBuilder"; -import { swiftuiSolidColor } from "./builderImpl/swiftuiColor"; +import { swiftuiSolidColorFromDirectFills } from "./builderImpl/swiftuiColor"; +import { StyledTextSegmentSubset } from "types"; export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { node?: TextNode; @@ -42,7 +42,7 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { } textColor(fills: Paint[]): string { - const fillColor = swiftuiSolidColor(fills); + const fillColor = swiftuiSolidColorFromDirectFills(fills); if (fillColor) { return fillColor; } @@ -92,7 +92,7 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { // alignHorizontal !== "left" ? `TextAlign.${alignHorizontal}` : "", // }; - const segments = this.getTextSegments(node.id, node.characters); + const segments = this.getTextSegments(node, node.characters); if (segments) { this.element = segments; } else { @@ -102,8 +102,9 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { return this; } - getTextSegments(id: string, characters: string): SwiftUIElement | null { - const segments = globalTextStyleSegments[id]; + getTextSegments(node: TextNode, characters: string): SwiftUIElement | null { + const segments = (node as any) + .styledTextSegments as StyledTextSegmentSubset[]; if (!segments) { return null; } @@ -261,9 +262,7 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { const blurRadius = Math.round(ds.radius); return `.shadow(color: Color(red: ${ds.color.r.toFixed( 2, - )}, green: ${ds.color.g.toFixed( - 2, - )}, blue: ${ds.color.b.toFixed( + )}, green: ${ds.color.g.toFixed(2)}, blue: ${ds.color.b.toFixed( 2, )}, opacity: ${ds.color.a.toFixed( 2, diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index f1b616ad..d40c9e6d 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -125,6 +125,7 @@ export class TailwindDefaultBuilder { if (commonIsAbsolutePosition(node, optimizeLayout)) { const { x, y } = getCommonPositionValue(node); + console.log("x", x, y); const parsedX = numberToFixedString(x); const parsedY = numberToFixedString(y); @@ -189,21 +190,21 @@ export class TailwindDefaultBuilder { const { node, optimizeLayout } = this; const { width, height } = tailwindSizePartial(node, optimizeLayout); - // if (node.type === "TEXT") { - // switch (node.textAutoResize) { - // case "WIDTH_AND_HEIGHT": - // break; - // case "HEIGHT": - // this.addAttributes(width); - // break; - // case "NONE": - // case "TRUNCATE": - // this.addAttributes(width, height); - // break; - // } - // } else { - this.addAttributes(width, height); - // } + if (node.type === "TEXT") { + switch (node.textAutoResize) { + case "WIDTH_AND_HEIGHT": + break; + case "HEIGHT": + this.addAttributes(width); + break; + case "NONE": + case "TRUNCATE": + this.addAttributes(width, height); + break; + } + } else { + this.addAttributes(width, height); + } return this; } diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index aa1613c8..6074aef9 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -124,7 +124,7 @@ export const tailwindText = ( .commonPositionStyles() .textAlign(); - const styledHtml = layoutBuilder.getTextSegments(node.id); + const styledHtml = layoutBuilder.getTextSegments(node); previousExecutionCache.push(...styledHtml); let content = ""; diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index ca3518e2..de5e7e4c 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -1,4 +1,3 @@ -import { globalTextStyleSegments } from "../altNodes/altConversion"; import { commonLetterSpacing, commonLineHeight, @@ -12,14 +11,17 @@ import { } from "./conversionTables"; import { TailwindDefaultBuilder } from "./tailwindDefaultBuilder"; import { config } from "./tailwindConfig"; +import { StyledTextSegmentSubset } from "types"; export class TailwindTextBuilder extends TailwindDefaultBuilder { - getTextSegments(id: string): { + getTextSegments(node: TextNode): { style: string; text: string; openTypeFeatures: { [key: string]: boolean }; }[] { - const segments = globalTextStyleSegments[id]; + const segments = (node as any) + .styledTextSegments as StyledTextSegmentSubset[]; + if (!segments) { return []; } @@ -53,9 +55,11 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { blurStyle, shadowStyle, ] - .filter((d) => d !== "") + .filter(Boolean) .join(" "); + console.log("styleClasses", styleClasses, segment); + const charsWithLineBreak = segment.characters.split("\n").join("
"); return { style: styleClasses, From 496566832224f0c6d53407d0f9ac207e545d117a Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 4 Mar 2025 13:03:56 -0300 Subject: [PATCH 014/134] try to improve color --- .../plugin-ui/src/components/ColorsPanel.tsx | 18 +++++++++++++----- .../src/components/GradientsPanel.tsx | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/plugin-ui/src/components/ColorsPanel.tsx b/packages/plugin-ui/src/components/ColorsPanel.tsx index dfd19d32..32ed866c 100644 --- a/packages/plugin-ui/src/components/ColorsPanel.tsx +++ b/packages/plugin-ui/src/components/ColorsPanel.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useState } from "react"; import { SolidColorConversion } from "types"; @@ -15,10 +14,19 @@ const ColorsPanel = (props: { }; return ( -
-

- Colors -

+
+
+
+

+ {/*
*/} + Color Palette +

+ + {props.colors.length} color{props.colors.length > 1 ? "s" : ""} + +
+
+
{props.colors.map((color, idx) => ( + )} + + {(expanded || alwaysExpanded) && ( +
+ {/* Render preference toggles if any */} + {settings.length > 0 && ( +
+ {settings.map((preference) => ( + { + onPreferenceChanged?.(preference.propertyName, value); + }} + buttonClass="bg-green-100 dark:bg-black dark:ring-green-800 ring-green-500" + checkClass="bg-green-400 dark:bg-black dark:bg-green-500 dark:border-green-500 ring-green-300 border-green-400" + /> + ))} +
+ )} + {children} +
+ )} +
+ ); +}; + +export default SettingsGroup; diff --git a/packages/plugin-ui/src/components/WarningsPanel.tsx b/packages/plugin-ui/src/components/WarningsPanel.tsx index 5b7c4683..0ca36ff5 100644 --- a/packages/plugin-ui/src/components/WarningsPanel.tsx +++ b/packages/plugin-ui/src/components/WarningsPanel.tsx @@ -183,8 +183,8 @@ const WarningsPanel: React.FC = ({ warnings }) => { {/* Help text - balanced size */} {displayedWarnings.length > 0 && ( -
- +
+ {/* */} Addressing warnings can improve the quality of the generated code. From f3d178e9e9bd2a70536a829a94e9fe73d57c01c8 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 5 Mar 2025 03:14:26 -0300 Subject: [PATCH 024/134] All new settings part 2 --- .../plugin-ui/src/components/CodePanel.tsx | 49 ++--- .../src/components/CustomPrefixInput.tsx | 177 ++++++++++++++++++ .../src/components/SettingsGroup.tsx | 21 ++- 3 files changed, 209 insertions(+), 38 deletions(-) create mode 100644 packages/plugin-ui/src/components/CustomPrefixInput.tsx diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 298efa8a..26f139b3 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -4,13 +4,14 @@ import { PluginSettings, SelectPreferenceOptions, } from "types"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useEffect } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coldarkDark as theme } from "react-syntax-highlighter/dist/esm/styles/prism"; import SelectableToggle from "./SelectableToggle"; import { CopyButton } from "./CopyButton"; import EmptyState from "./EmptyState"; import SettingsGroup from "./SettingsGroup"; +import CustomPrefixInput from "./CustomPrefixInput"; interface CodePanelProps { code: string; @@ -36,12 +37,6 @@ const CodePanel = (props: CodePanelProps) => { } = props; const isCodeEmpty = code === ""; - // State for custom prefix for Tailwind classes. - // It is initially set from settings (if available) or an empty string. - const [customPrefix, setCustomPrefix] = useState( - settings?.customTailwindPrefix || "", - ); - // Helper function to add the prefix before every class (or className) in the code. // It finds every occurrence of class="..." or className="..." and, for each class, // prepends the custom prefix. @@ -61,8 +56,9 @@ const CodePanel = (props: CodePanelProps) => { // If the selected framework is Tailwind and a prefix is provided then transform the code. const prefixedCode = - selectedFramework === "Tailwind" && customPrefix.trim() !== "" - ? applyPrefixToClasses(code, customPrefix) + selectedFramework === "Tailwind" && + settings?.customTailwindPrefix?.trim() !== "" + ? applyPrefixToClasses(code, settings.customTailwindPrefix) : code; const handleButtonHover = () => setSyntaxHovered(true); @@ -107,25 +103,10 @@ const CodePanel = (props: CodePanelProps) => { }; }, [preferenceOptions, selectPreferenceOptions, selectedFramework]); - // Create the custom prefix input component - const CustomPrefixInput = () => ( -
- - { - const newVal = e.target.value; - setCustomPrefix(newVal); - onPreferenceChanged("customTailwindPrefix", newVal); - }} - placeholder="e.g., tw-" - className="p-1.5 px-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-neutral-800 text-sm w-full max-w-xs" - /> -
- ); + // Handle custom prefix change + const handleCustomPrefixChange = (newValue: string) => { + onPreferenceChanged("customTailwindPrefix", newValue); + }; return (
@@ -146,7 +127,7 @@ const CodePanel = (props: CodePanelProps) => {
{/* Essential settings always shown */} { /> {/* Styling preferences with custom prefix for Tailwind */} - {stylingPreferences.length > 0 && ( + {(stylingPreferences.length > 0 || + selectedFramework === "Tailwind") && ( - {selectedFramework === "Tailwind" && } + {selectedFramework === "Tailwind" && ( + + )} )} diff --git a/packages/plugin-ui/src/components/CustomPrefixInput.tsx b/packages/plugin-ui/src/components/CustomPrefixInput.tsx new file mode 100644 index 00000000..2d6bd669 --- /dev/null +++ b/packages/plugin-ui/src/components/CustomPrefixInput.tsx @@ -0,0 +1,177 @@ +import React, { useState, useRef, useEffect } from "react"; +import { HelpCircle, Check } from "lucide-react"; + +interface CustomPrefixInputProps { + initialValue: string; + onValueChange: (value: string) => void; +} + +const CustomPrefixInput = React.memo(({ initialValue, onValueChange }: CustomPrefixInputProps) => { + // Use internal state to manage the input value + const [inputValue, setInputValue] = useState(initialValue); + const [isFocused, setIsFocused] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const inputRef = useRef(null); + + // Update internal state when initialValue changes (from parent) + useEffect(() => { + setInputValue(initialValue); + setHasChanges(false); + }, [initialValue]); + + const examples = ["flex"]; + const hasInvalidChars = /\s/.test(inputValue); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + setHasChanges(newValue !== initialValue); + }; + + const applyChanges = () => { + if (hasInvalidChars) return; + + onValueChange(inputValue); + setHasChanges(false); + + // Show success indicator briefly + setShowSuccess(true); + setTimeout(() => setShowSuccess(false), 1500); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + applyChanges(); + inputRef.current?.blur(); + } + }; + + return ( +
+
+ + +
+ +
+ Add a prefix to all generated Tailwind classes. +
+ Useful for avoiding conflicts with existing CSS. +
+
+
+ + {showSuccess && ( + + Applied + + )} +
+ +
+
+ setIsFocused(true)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + placeholder="e.g., tw-" + className={`p-1.5 px-2.5 border rounded-md text-sm w-full transition-all focus:outline-none ${ + hasInvalidChars + ? "border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20" + : isFocused + ? "border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800" + : "border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500" + }`} + /> + + {hasInvalidChars && ( +

+ Prefix cannot contain spaces +

+ )} +
+ + {hasChanges && ( + + )} +
+ + {inputValue && !hasInvalidChars && ( +
+

+ Preview{hasChanges ? " (not applied yet)" : ""}: +

+
+ {examples.map((example) => ( +
+
+ + {inputValue} + + + {example} + +
+ + → + +
+ {example} +
+
+ ))} +
+ + {hasChanges && ( +

+ Press Enter or click Done to apply changes +

+ )} +
+ )} +
+ ); +}); + +CustomPrefixInput.displayName = "CustomPrefixInput"; + +// Add a keyframe for fade-in-out animation +if (typeof document !== 'undefined') { + const style = document.createElement('style'); + style.innerHTML = ` + @keyframes fadeInOut { + 0% { opacity: 0; } + 20% { opacity: 1; } + 80% { opacity: 1; } + 100% { opacity: 0; } + } + .animate-fade-in-out { + animation: fadeInOut 1.5s ease-in-out; + } + `; + document.head.appendChild(style); +} + +export default CustomPrefixInput; diff --git a/packages/plugin-ui/src/components/SettingsGroup.tsx b/packages/plugin-ui/src/components/SettingsGroup.tsx index 69854309..8115d31d 100644 --- a/packages/plugin-ui/src/components/SettingsGroup.tsx +++ b/packages/plugin-ui/src/components/SettingsGroup.tsx @@ -32,24 +32,31 @@ const SettingsGroup: React.FC = ({ } return ( -
- {!alwaysExpanded && ( +
+ {alwaysExpanded ? ( +
+ + {title} + +
+ ) : ( )} {(expanded || alwaysExpanded) && ( -
+
{/* Render preference toggles if any */} {settings.length > 0 && (
From 83420b8a13ba080b4750190c1f81d160621588aa Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 5 Mar 2025 03:16:59 -0300 Subject: [PATCH 025/134] Fix lint --- packages/plugin-ui/src/components/CodePanel.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 26f139b3..6b2db900 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -40,7 +40,14 @@ const CodePanel = (props: CodePanelProps) => { // Helper function to add the prefix before every class (or className) in the code. // It finds every occurrence of class="..." or className="..." and, for each class, // prepends the custom prefix. - const applyPrefixToClasses = (codeString: string, prefix: string) => { + const applyPrefixToClasses = ( + codeString: string, + prefix: string | undefined, + ) => { + if (!prefix) { + return codeString; + } + return codeString.replace( /(class(?:Name)?)="([^"]*)"/g, (match, attr, classes) => { @@ -58,7 +65,7 @@ const CodePanel = (props: CodePanelProps) => { const prefixedCode = selectedFramework === "Tailwind" && settings?.customTailwindPrefix?.trim() !== "" - ? applyPrefixToClasses(code, settings.customTailwindPrefix) + ? applyPrefixToClasses(code, settings?.customTailwindPrefix) : code; const handleButtonHover = () => setSyntaxHovered(true); From 456feb941cd38a47a843fb4c0d06a81f11d80100 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 5 Mar 2025 15:07:53 -0300 Subject: [PATCH 026/134] Improve rounding mechanism --- packages/backend/src/altNodes/altNodeUtils.ts | 1 + packages/backend/src/html/htmlMain.ts | 12 +++++-- .../backend/src/tailwind/conversionTables.ts | 33 +++++++++++++++++-- packages/backend/src/tailwind/tailwindMain.ts | 14 ++++++-- .../plugin-ui/src/codegenPreferenceOptions.ts | 5 +-- .../plugin-ui/src/components/CodePanel.tsx | 18 ++-------- 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/packages/backend/src/altNodes/altNodeUtils.ts b/packages/backend/src/altNodes/altNodeUtils.ts index 738db589..5ede70ca 100644 --- a/packages/backend/src/altNodes/altNodeUtils.ts +++ b/packages/backend/src/altNodes/altNodeUtils.ts @@ -61,6 +61,7 @@ export const renderAndAttachSVG = async (node: any) => { // console.log(altNode); if (node.canBeFlattened) { console.log("altNode is", node); + if (node.svg) { // console.log(`SVG already rendered for ${nodeName}`); return node; diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 9bbb3168..40788221 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -88,9 +88,11 @@ const htmlWidgetGenerator = async ( const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { console.log("converting", node); - if ((node as any).canBeFlattened) { + if (settings.embedVectors && (node as any).canBeFlattened) { const altNode = await renderAndAttachSVG(node); - if (altNode.svg) return htmlWrapSVG(altNode, settings); + if (altNode.svg) { + return htmlWrapSVG(altNode, settings); + } } switch (node.type) { @@ -111,6 +113,10 @@ const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { case "LINE": return htmlLine(node, settings); case "VECTOR": + addWarning( + "VectorNodes are not supported in HTML. They can be converted via Embed Vector setting", + ); + return node; throw new Error( "Normally vector type nodes are converted to SVG so this code point should be unreachable.", ); @@ -273,7 +279,7 @@ const htmlContainer = async ( ) { imgUrl = (await exportNodeAsBase64PNG(altNode, hasChildren)) ?? ""; } else { - addWarning("Some images were exported as placeholder URLs"); + // addWarning("Some images were exported as placeholder URLs"); imgUrl = getPlaceholderImage(node.width, node.height); } diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index 457e073c..884fd4e0 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -10,6 +10,22 @@ export const nearestValue = (goal: number, array: Array): number => { }); }; +// New function to get nearest value only if it's within acceptable threshold +export const nearestValueWithThreshold = ( + goal: number, + array: Array, + thresholdPercent: number = 15, +): number | null => { + const nearest = nearestValue(goal, array); + const diff = Math.abs(nearest - goal); + const percentDiff = (diff / goal) * 100; + + if (percentDiff <= thresholdPercent) { + return nearest; + } + return null; +}; + export const exactValue = ( goal: number, array: Array, @@ -36,12 +52,18 @@ const pxToRemToTailwind = ( conversionMap: Record, ): string => { const keys = Object.keys(conversionMap).map((d) => +d); - const convertedValue = exactValue(value / 16, keys); + const remValue = value / 16; + const convertedValue = exactValue(remValue, keys); if (convertedValue) { return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { - return conversionMap[nearestValue(value / 16, keys)]; + // Only round if the nearest value is within acceptable threshold + const thresholdValue = nearestValueWithThreshold(remValue, keys, 15); + + if (thresholdValue !== null) { + return conversionMap[thresholdValue]; + } } return `[${numberToFixedString(value)}px]`; @@ -57,7 +79,12 @@ const pxToTailwind = ( if (convertedValue) { return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { - return conversionMap[nearestValue(value, keys)]; + // Only round if the nearest value is within acceptable threshold + const thresholdValue = nearestValueWithThreshold(value, keys, 15); + + if (thresholdValue !== null) { + return conversionMap[thresholdValue]; + } } return `[${numberToFixedString(value)}px]`; diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index c0138e06..57d763ab 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -11,6 +11,12 @@ import { renderAndAttachSVG } from "../altNodes/altNodeUtils"; import { AltNode, PluginSettings, TailwindSettings } from "types"; export let localTailwindSettings: PluginSettings; +localTailwindSettings = { + // ...existing settings... + roundTailwindValues: true, + roundingThreshold: 15, // Maximum % difference allowed for rounding (e.g., 15%) + // ...existing settings... +}; let previousExecutionCache: { style: string; text: string; @@ -49,9 +55,11 @@ const convertNode = (settings: TailwindSettings) => async (node: SceneNode): Promise => { console.log("altNode", node); - const altNode = await renderAndAttachSVG(node); - if (altNode.svg) { - return tailwindWrapSVG(altNode, settings); + if (settings.embedVectors && (node as any).canBeFlattened) { + const altNode = await renderAndAttachSVG(node); + if (altNode.svg) { + return tailwindWrapSVG(altNode, settings); + } } switch (node.type) { diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 77619c3e..25972488 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -29,7 +29,8 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "roundTailwindValues", label: "Round values", - description: "Round pixel values to nearest Tailwind sizes", + description: + "Round pixel values to nearest Tailwind sizes (within a 15% range)", isDefault: false, includedLanguages: ["Tailwind"], }, @@ -63,7 +64,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ label: "Embed Vectors", description: "Convert vectors in the code.", isDefault: false, - includedLanguages: ["HTML"], + includedLanguages: ["HTML", "Tailwind"], }, // Add your preferences data here ]; diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 6b2db900..99bad9e7 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -75,7 +75,6 @@ const CodePanel = (props: CodePanelProps) => { const { essentialPreferences, stylingPreferences, - advancedPreferences, selectableSettingsFiltered, } = useMemo(() => { // Get preferences for the current framework @@ -90,8 +89,9 @@ const CodePanel = (props: CodePanelProps) => { "roundTailwindColors", "customTailwindColors", "showLayerNames", + "embedImages", + "embedVectors", ]; - const advancedPropertyNames = ["embedImages", "embedVectors"]; // Group preferences by category return { @@ -101,9 +101,7 @@ const CodePanel = (props: CodePanelProps) => { stylingPreferences: frameworkPreferences.filter((p) => stylingPropertyNames.includes(p.propertyName), ), - advancedPreferences: frameworkPreferences.filter((p) => - advancedPropertyNames.includes(p.propertyName), - ), + selectableSettingsFiltered: selectPreferenceOptions.filter((p) => p.includedLanguages?.includes(selectedFramework), ), @@ -159,16 +157,6 @@ const CodePanel = (props: CodePanelProps) => { )} - {/* Advanced settings */} - {advancedPreferences.length > 0 && ( - - )} - {/* Framework-specific options */} {selectableSettingsFiltered.length > 0 && (
From 1b2a41d7585ac683bd78accf914dd1508164d158 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 5 Mar 2025 20:49:05 -0300 Subject: [PATCH 027/134] Trim layer, improve warning, improve vector --- apps/plugin/plugin-src/code.ts | 2 +- .../backend/src/html/builderImpl/htmlColor.ts | 4 +- .../backend/src/html/htmlDefaultBuilder.ts | 4 +- packages/backend/src/html/htmlMain.ts | 38 ++++++++++--------- .../src/tailwind/builderImpl/tailwindColor.ts | 2 +- .../backend/src/tailwind/conversionTables.ts | 2 +- .../src/tailwind/tailwindDefaultBuilder.ts | 2 +- packages/backend/src/tailwind/tailwindMain.ts | 17 ++++----- .../plugin-ui/src/codegenPreferenceOptions.ts | 23 +++++------ .../plugin-ui/src/components/CodePanel.tsx | 2 +- packages/types/src/types.ts | 4 +- 11 files changed, 51 insertions(+), 49 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 093809bb..07978a0a 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -28,7 +28,7 @@ export const defaultPluginSettings: PluginSettings = { swiftUIGenerationMode: "snippet", roundTailwindValues: false, roundTailwindColors: false, - customTailwindColors: false, + useColorVariables: false, customTailwindPrefix: "", embedImages: false, embedVectors: false, diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 336c2b27..3defdc16 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -55,7 +55,7 @@ export const htmlColorFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"] | undefined, settings: HTMLSettings, ): string => { - const useCustomColors = settings.customTailwindColors === true; + const useCustomColors = settings.useColorVariables === true; const fill = retrieveTopFill(fills); if (fill) { const { color, opacity, boundVariable } = getColorAndVariable(fill); @@ -133,7 +133,7 @@ export const htmlGradientFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"], settings: HTMLSettings, ): string => { - const useCustomColors = settings.customTailwindColors === true; + const useCustomColors = settings.useColorVariables === true; const fill = retrieveTopFill(fills); if (!fill) return ""; switch (fill.type) { diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 40204be5..d5cfda84 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -331,8 +331,8 @@ export class HtmlDefaultBuilder { let classAttribute = ""; if (this.name) { - this.addData("layer", this.name); - const layerNameClass = stringToClassName(this.name); + this.addData("layer", this.name.trim()); + const layerNameClass = stringToClassName(this.name.trim()); classAttribute = formatClassAttribute( layerNameClass === "" ? [] : [layerNameClass], this.isJSX, diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 40788221..b1d6389f 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -49,18 +49,19 @@ export const generateHTMLPreview = async ( settings: PluginSettings, code?: string, ): Promise => { - const htmlCodeAlreadyGenerated = - settings.framework === "HTML" && settings.jsx === false && code; - const htmlCode = htmlCodeAlreadyGenerated - ? code - : await htmlMain( - nodes, - { - ...settings, - jsx: false, - }, - true, - ); + // const htmlCodeAlreadyGenerated = + // settings.framework === "HTML" && settings.jsx === false && code; + const htmlCode = + // htmlCodeAlreadyGenerated + // ? code : + await htmlMain( + nodes, + { + ...settings, + jsx: false, + }, + true, + ); return { size: { @@ -113,14 +114,15 @@ const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { case "LINE": return htmlLine(node, settings); case "VECTOR": - addWarning( - "VectorNodes are not supported in HTML. They can be converted via Embed Vector setting", - ); - return node; - throw new Error( - "Normally vector type nodes are converted to SVG so this code point should be unreachable.", + addWarning("Vector is not supported"); + return await htmlContainer( + { ...node, type: "RECTANGLE" } as any, + "", + [], + settings, ); default: + addWarning(`${node.type} node is not supported`); return ""; } }; diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index a969b933..1e0df04a 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -14,7 +14,7 @@ import { retrieveTopFill } from "../../common/retrieveFill"; */ export function tailwindColor(fill: SolidPaint) { const { hex, colorType, colorName, meta } = getColorInfo(fill); - const exportValue = tailwindSolidColor(fill, "solid"); + const exportValue = tailwindSolidColor(fill, "bg"); return { exportValue, colorName, diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index 884fd4e0..3b52756d 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -163,7 +163,7 @@ export function getColorInfo(fill: SolidPaint | ColorStop) { // variable if ( - localTailwindSettings.customTailwindColors && + localTailwindSettings.useColorVariables && fill.boundVariables?.color ) { colorName = variableToColorName(fill.boundVariables.color); diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index d40c9e6d..2d5df811 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -265,7 +265,7 @@ export class TailwindDefaultBuilder { this.prependAttributes(stringToClassName(this.name)); } if (this.name) { - this.addData("layer", this.name); + this.addData("layer", this.name.trim()); } if ("variantProperties" in this.node && this.node.variantProperties) { diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 57d763ab..561e8be8 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -11,12 +11,6 @@ import { renderAndAttachSVG } from "../altNodes/altNodeUtils"; import { AltNode, PluginSettings, TailwindSettings } from "types"; export let localTailwindSettings: PluginSettings; -localTailwindSettings = { - // ...existing settings... - roundTailwindValues: true, - roundingThreshold: 15, // Maximum % difference allowed for rounding (e.g., 15%) - // ...existing settings... -}; let previousExecutionCache: { style: string; text: string; @@ -80,10 +74,15 @@ const convertNode = case "SECTION": return tailwindSection(node, settings); case "VECTOR": - addWarning("VectorNodes are not supported in Tailwind"); - break; + addWarning("Vector is not supported"); + return tailwindContainer( + { ...node, type: "RECTANGLE" } as any, + "", + "", + settings, + ); default: - addWarning(`${node.type} nodes are not supported in Tailwind`); + addWarning(`${node.type} node is not supported`); } return ""; }; diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 25972488..7516b261 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -5,7 +5,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "jsx", label: "React (JSX)", - description: 'Render "class" attributes as "className"', + description: "", isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, @@ -13,7 +13,8 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "optimizeLayout", label: "Optimize layout", - description: "Attempt to auto-layout suitable element groups", + description: + "Attempt to auto-layout suitable element groups. This may increase code quality, but may not always work as expected.", isDefault: true, includedLanguages: ["HTML", "Tailwind", "Flutter", "SwiftUI"], }, @@ -21,7 +22,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "showLayerNames", label: "Layer names", - description: "Include layer names in classes", + description: "Include Figma layer names in classes.", isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, @@ -30,7 +31,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ propertyName: "roundTailwindValues", label: "Round values", description: - "Round pixel values to nearest Tailwind sizes (within a 15% range)", + "Round pixel values to nearest Tailwind sizes (within a 15% range).", isDefault: false, includedLanguages: ["Tailwind"], }, @@ -38,15 +39,16 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "roundTailwindColors", label: "Round colors", - description: "Round color values to nearest Tailwind colors", + description: "Round Figma color values to nearest Tailwind colors.", isDefault: false, includedLanguages: ["Tailwind"], }, { itemType: "individual_select", - propertyName: "customTailwindColors", - label: "Color variables", - description: "Use color variable names as custom color names", + propertyName: "useColorVariables", + label: "Color Variables", + description: + "Export code using Figma variables as colors. Example: 'bg-background' instead of 'bg-white'.", isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, @@ -54,7 +56,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "embedImages", label: "Embed Images", - description: "Convert images to Base64 and embed them in the code.", + description: "Convert Figma images to Base64 and embed them in the code.", isDefault: false, includedLanguages: ["HTML"], }, @@ -62,11 +64,10 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "embedVectors", label: "Embed Vectors", - description: "Convert vectors in the code.", + description: "Convert Figma vectors to code.", isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, - // Add your preferences data here ]; export const selectPreferenceOptions: SelectPreferenceOptions[] = [ diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 99bad9e7..96801f5e 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -87,7 +87,7 @@ const CodePanel = (props: CodePanelProps) => { const stylingPropertyNames = [ "roundTailwindValues", "roundTailwindColors", - "customTailwindColors", + "useColorVariables", "showLayerNames", "embedImages", "embedVectors", diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 9755c221..bc626fdb 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -7,12 +7,12 @@ export interface HTMLSettings { showLayerNames: boolean; embedImages: boolean; embedVectors: boolean; - customTailwindColors: boolean; + useColorVariables: boolean; } export interface TailwindSettings extends HTMLSettings { roundTailwindValues: boolean; roundTailwindColors: boolean; - customTailwindColors: boolean; + useColorVariables: boolean; customTailwindPrefix?: string; } export interface FlutterSettings { From a6653ebb6f777c4836a927249a79f989b725ff07 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 5 Mar 2025 23:43:05 -0300 Subject: [PATCH 028/134] Fix vector export --- apps/plugin/plugin-src/code.ts | 11 ++++++++++ packages/backend/src/altNodes/altNodeUtils.ts | 22 +++++++++++-------- packages/backend/src/html/htmlMain.ts | 4 +++- packages/backend/src/tailwind/tailwindMain.ts | 4 +++- packages/types/src/types.ts | 1 + 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 07978a0a..27e46678 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -103,7 +103,18 @@ const safeRun = async (settings: PluginSettings) => { const error = e as Error; console.log("error: ", error.stack); figma.ui.postMessage({ type: "error", error: error.message }); + } else { + // Handle non-standard errors or unknown error types + const errorMessage = String(e); + console.log("Unknown error: ", errorMessage); + figma.ui.postMessage({ + type: "error", + error: errorMessage || "Unknown error occurred" + }); } + + // Send a message to reset the UI state + figma.ui.postMessage({ type: "conversion-complete", success: false }); } } else { console.log( diff --git a/packages/backend/src/altNodes/altNodeUtils.ts b/packages/backend/src/altNodes/altNodeUtils.ts index 5ede70ca..50f05cc8 100644 --- a/packages/backend/src/altNodes/altNodeUtils.ts +++ b/packages/backend/src/altNodes/altNodeUtils.ts @@ -1,6 +1,7 @@ import { AltNode } from "types"; import { curry } from "../common/curry"; import { exportAsyncProxy } from "../common/exportAsyncProxy"; +import { addWarning } from "../common/commonConversionWarnings"; export const overrideReadonlyProperty = curry( (prop: K, value: any, obj: T): T => @@ -51,11 +52,6 @@ export const isSVGNode = (node: SceneNode) => { return altNode.canBeFlattened; }; -export const renderNodeAsSVG = async (node: SceneNode) => - await exportAsyncProxy(node, { - format: "SVG_STRING", - }); - export const renderAndAttachSVG = async (node: any) => { // const nodeName = `${node.type}:${node.id}`; // console.log(altNode); @@ -66,10 +62,18 @@ export const renderAndAttachSVG = async (node: any) => { // console.log(`SVG already rendered for ${nodeName}`); return node; } - // console.log(`${nodeName} can be flattened!`); - const svg = (await renderNodeAsSVG(node)) as string; - // console.log(`${svg}`); - node.svg = svg; + + try { + // console.log(`${nodeName} can be flattened!`); + const svg = (await exportAsyncProxy(node, { + format: "SVG_STRING", + })) as string; + node.svg = svg; + } catch (error) { + addWarning(`Failed rendering SVG for ${node.name}`); + console.error(`Error rendering SVG for ${node.type}:${node.id}`); + console.error(error); + } } return node; }; diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index b1d6389f..ffa053f7 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -114,7 +114,9 @@ const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { case "LINE": return htmlLine(node, settings); case "VECTOR": - addWarning("Vector is not supported"); + if (!settings.embedVectors) { + addWarning("Vector is not supported"); + } return await htmlContainer( { ...node, type: "RECTANGLE" } as any, "", diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 561e8be8..f92683d8 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -74,7 +74,9 @@ const convertNode = case "SECTION": return tailwindSection(node, settings); case "VECTOR": - addWarning("Vector is not supported"); + if (!settings.embedVectors) { + addWarning("Vector is not supported"); + } return tailwindContainer( { ...node, type: "RECTANGLE" } as any, "", diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index bc626fdb..c5622198 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -14,6 +14,7 @@ export interface TailwindSettings extends HTMLSettings { roundTailwindColors: boolean; useColorVariables: boolean; customTailwindPrefix?: string; + embedVectors: boolean; } export interface FlutterSettings { flutterGenerationMode: string; From 1ad15be5ce803a0538abefe781ac9765c396e53f Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 01:15:06 -0300 Subject: [PATCH 029/134] Experiment with styled components --- apps/plugin/package.json | 1 + apps/plugin/plugin-src/code.ts | 34 +- packages/backend/package.json | 1 + packages/backend/src/code.ts | 133 +++--- .../src/common/retrieveUI/convertToCode.ts | 2 +- .../backend/src/html/htmlDefaultBuilder.ts | 142 +++++- packages/backend/src/html/htmlMain.ts | 426 ++++++++++++++++-- packages/backend/src/html/htmlTextBuilder.ts | 64 ++- .../plugin-ui/src/codegenPreferenceOptions.ts | 14 +- .../plugin-ui/src/components/CodePanel.tsx | 40 +- packages/types/src/types.ts | 1 + pnpm-lock.yaml | 13 + 12 files changed, 731 insertions(+), 140 deletions(-) diff --git a/apps/plugin/package.json b/apps/plugin/package.json index 641d287a..9a3d3f6f 100644 --- a/apps/plugin/package.json +++ b/apps/plugin/package.json @@ -15,6 +15,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.477.0", "motion": "^12.4.9", + "nanoid": "^5.1.2", "plugin-ui": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 27e46678..33611d9a 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -32,6 +32,8 @@ export const defaultPluginSettings: PluginSettings = { customTailwindPrefix: "", embedImages: false, embedVectors: false, + exportCSS: false, + styledComponents: false, }; // A helper type guard to ensure the key belongs to the PluginSettings type @@ -107,12 +109,12 @@ const safeRun = async (settings: PluginSettings) => { // Handle non-standard errors or unknown error types const errorMessage = String(e); console.log("Unknown error: ", errorMessage); - figma.ui.postMessage({ - type: "error", - error: errorMessage || "Unknown error occurred" + figma.ui.postMessage({ + type: "error", + error: errorMessage || "Unknown error occurred", }); } - + // Send a message to reset the UI state figma.ui.postMessage({ type: "conversion-complete", success: false }); } @@ -186,11 +188,13 @@ const codegenMode = async () => { return [ { title: "Code", - code: await htmlMain( - convertedSelection, - { ...userPluginSettings, jsx: false }, - true, - ), + code: ( + await htmlMain( + convertedSelection, + { ...userPluginSettings, jsx: false }, + true, + ) + ).html, language: "HTML", }, { @@ -203,11 +207,13 @@ const codegenMode = async () => { return [ { title: "Code", - code: await htmlMain( - convertedSelection, - { ...userPluginSettings, jsx: true }, - true, - ), + code: ( + await htmlMain( + convertedSelection, + { ...userPluginSettings, jsx: true }, + true, + ) + ).html, language: "HTML", }, { diff --git a/packages/backend/package.json b/packages/backend/package.json index 3bd44a16..9669b663 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,6 +15,7 @@ "dependencies": { "@figma/plugin-typings": "^1.108.0", "js-base64": "^3.7.7", + "nanoid": "^5.1.2", "react": "19.0.0", "react-dom": "19.0.0", "types": "workspace:*" diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 50c213cd..2c682e93 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -8,10 +8,19 @@ import { clearWarnings, warnings, } from "./common/commonConversionWarnings"; -import { generateHTMLPreview } from "./html/htmlMain"; import { postConversionComplete, postEmptyMessage } from "./messaging"; import { PluginSettings } from "types"; import { convertToCode } from "./common/retrieveUI/convertToCode"; +import { generateHTMLPreview } from "./html/htmlMain"; + +export const generateId = (length: number = 4): string => { + const chars = "1234567890abcdefghijklmnopqrstuvwxyz"; + return Array.from({ length }, () => + chars.charAt(Math.floor(Math.random() * chars.length)) + ).join(''); +}; +// Keep track of node names to identify duplicates +const nodeNameRegistry: Map = new Map(); // Helper function to add parent references to all children in the node tree const addParentReferences = (node: any) => { @@ -48,38 +57,52 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { ); }); - // Only fetch the Figma node if we have gradients, optimizeLayout is enabled, or it's an instance - if ( - hasGradient || - optimizeLayout || - node.type === "INSTANCE" || - node.type === "TEXT" - ) { - try { - const figmaNode = figma.getNodeById(node.id); - if (figmaNode) { - // Handle gradients if needed + try { + const figmaNode = figma.getNodeById(node.id); + if (figmaNode) { + // Ensure node has a unique name - store directly on node + if (figmaNode.name) { + const cleanName = figmaNode.name.trim(); + + // Track names for uniqueness + const count = nodeNameRegistry.get(cleanName) || 0; + nodeNameRegistry.set(cleanName, count + 1); + + // For first occurrence, use original name; for duplicates, add suffix + node.uniqueName = + count === 0 + ? cleanName + : `${cleanName}_${generateId()}`; + } + + // Handle additional node properties + if ( + hasGradient || + optimizeLayout || + node.type === "INSTANCE" || + node.type === "TEXT" + ) { + // Handle gradients if (hasGradient) { GRADIENT_PROPERTIES.forEach((propName) => { const property = node[propName]; - if (property && Array.isArray(property) && property.length > 0) { - // We already know there's a gradient in at least one property - if ( - property.some( - (item: any) => - item.type && item.type.startsWith("GRADIENT_"), - ) && - propName in figmaNode - ) { - // Replace with the actual property that contains proper gradient transforms - node[propName] = JSON.parse( - JSON.stringify((figmaNode as any)[propName]), - ); - } + if ( + property && + Array.isArray(property) && + property.length > 0 && + property.some( + (item) => item.type && item.type.startsWith("GRADIENT_"), + ) && + propName in figmaNode + ) { + node[propName] = JSON.parse( + JSON.stringify((figmaNode as any)[propName]), + ); } }); } + // Handle text-specific properties if (figmaNode.type === "TEXT") { node.styledTextSegments = figmaNode.getStyledTextSegments([ "fontName", @@ -111,45 +134,36 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } // Extract component metadata from instances - if (figmaNode.type === "INSTANCE") { - if (figmaNode.variantProperties) { - node.variantProperties = figmaNode.variantProperties; - } + if ( + node.type === "INSTANCE" && + "variantProperties" in figmaNode && + figmaNode.variantProperties + ) { + node.variantProperties = figmaNode.variantProperties; } + } - if ("width" in figmaNode) { - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; - } + // Always copy size and position + if ("width" in figmaNode) { + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; } - } catch (e) { - // Silently fail if there's an error accessing the Figma node } - } else { - const figmaNode = figma.getNodeById(node.id); - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; + } catch (e) { + // Silently fail if there's an error accessing the Figma node } - if (!node.layoutMode) { - node.layoutMode = "NONE"; - } - if (!node.layoutGrow) { - node.layoutGrow = 0; - } - if (!node.layoutSizingHorizontal) { - node.layoutSizingHorizontal = "FIXED"; - } - if (!node.layoutSizingVertical) { - node.layoutSizingVertical = "FIXED"; - } - + // Set default layout properties if missing + if (!node.layoutMode) node.layoutMode = "NONE"; + if (!node.layoutGrow) node.layoutGrow = 0; + if (!node.layoutSizingHorizontal) node.layoutSizingHorizontal = "FIXED"; + if (!node.layoutSizingVertical) node.layoutSizingVertical = "FIXED"; + // If layout sizing is HUG but there are no children, set it to FIXED - const hasChildren = node.children && Array.isArray(node.children) && node.children.length > 0; + const hasChildren = + node.children && Array.isArray(node.children) && node.children.length > 0; if (node.layoutSizingHorizontal === "HUG" && !hasChildren) { node.layoutSizingHorizontal = "FIXED"; } @@ -176,6 +190,9 @@ export const nodesToJSON = async ( nodes: ReadonlyArray, optimizeLayout: boolean = false, ): Promise => { + // Reset name registry for each conversion + nodeNameRegistry.clear(); + const nodeJson = (await Promise.all( nodes.map( async (node) => diff --git a/packages/backend/src/common/retrieveUI/convertToCode.ts b/packages/backend/src/common/retrieveUI/convertToCode.ts index a8298be3..fc96319e 100644 --- a/packages/backend/src/common/retrieveUI/convertToCode.ts +++ b/packages/backend/src/common/retrieveUI/convertToCode.ts @@ -17,6 +17,6 @@ export const convertToCode = async ( return await swiftuiMain(nodes, settings); case "HTML": default: - return await htmlMain(nodes, settings); + return (await htmlMain(nodes, settings)).html; } }; diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index d5cfda84..892c7782 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -28,31 +28,91 @@ import { formatStyleAttribute, } from "../common/commonFormatAttributes"; import { HTMLSettings } from "types"; +import { + cssCollection, + generateUniqueClassName, + getSvelteClassName, + stylesToCSS, +} from "./htmlMain"; export class HtmlDefaultBuilder { styles: Array; data: Array; node: SceneNode; settings: HTMLSettings; + cssClassName: string | null = null; get name() { + if (this.settings.htmlGenerationMode === "styled-components") { + return this.settings.showLayerNames + ? (this.node as any).uniqueName || this.node.name + : ""; + } return this.settings.showLayerNames ? this.node.name : ""; } + get visible() { return this.node.visible; } + get isJSX() { - return this.settings.jsx; + return this.settings.htmlGenerationMode === "jsx"; } + get optimizeLayout() { return this.settings.optimizeLayout; } + get exportCSS() { + return this.settings.htmlGenerationMode === "svelte"; + } + + get useStyledComponents() { + return this.settings.htmlGenerationMode === "styled-components"; + } + + get useInlineStyles() { + return ( + this.settings.htmlGenerationMode === "html" || + this.settings.htmlGenerationMode === "jsx" + ); + } + + // Get the appropriate HTML element based on node type + get htmlElement(): string { + if (this.node.type === "TEXT") return "p"; + return "div"; + } + constructor(node: SceneNode, settings: HTMLSettings) { this.node = node; this.settings = settings; this.styles = []; this.data = []; + + // For both Svelte and styled-components, use similar naming pattern + if ( + this.settings.htmlGenerationMode === "svelte" || + this.settings.htmlGenerationMode === "styled-components" + ) { + // Always generate a unique classname that relates to the node name + const nodeName = (this.node as any).uniqueName || this.node.name; + + // Clean the name and create a valid CSS class name + let baseClassName = nodeName + ? nodeName.replace(/[^a-zA-Z0-9\s_-]/g, "") + .replace(/\s+/g, "-") + .toLowerCase() + : this.node.type.toLowerCase(); + + // Make sure it's valid + if (!/^[a-z]/i.test(baseClassName)) { + baseClassName = `${this.node.type.toLowerCase()}-${baseClassName}`; + } + + // For Svelte, use the same prefix style as styled-components for consistency + this.cssClassName = generateUniqueClassName(baseClassName); + } } commonPositionStyles(): this { @@ -329,14 +389,27 @@ export class HtmlDefaultBuilder { build(additionalStyle: Array = []): string { this.addStyles(...additionalStyle); - let classAttribute = ""; + // Different handling based on generation mode + const mode = this.settings.htmlGenerationMode || "html"; + + // Early return for styled-components with no other attributes + if ( + mode === "styled-components" && + !this.data.length && + this.styles.length > 0 && + this.cssClassName + ) { + this.storeStyles(); + return ""; // Return empty string as we're using the component directly + } + + let classNames: string[] = []; if (this.name) { this.addData("layer", this.name.trim()); const layerNameClass = stringToClassName(this.name.trim()); - classAttribute = formatClassAttribute( - layerNameClass === "" ? [] : [layerNameClass], - this.isJSX, - ); + if (layerNameClass !== "") { + classNames.push(layerNameClass); + } } if ("variantProperties" in this.node && this.node.variantProperties) { @@ -346,9 +419,66 @@ export class HtmlDefaultBuilder { .forEach((d) => this.data.push(d)); } + // For Svelte mode, we use classes + if (mode === "svelte" && this.styles.length > 0 && this.cssClassName) { + classNames.push(this.cssClassName); + this.storeStyles(); + this.styles = []; // Clear inline styles for Svelte + } + // For styled-components, we need the class but keep styles for the component + else if ( + mode === "styled-components" && + this.styles.length > 0 && + this.cssClassName + ) { + classNames.push(this.cssClassName); + this.storeStyles(); + // Keep styles for styled-components + } + const dataAttributes = this.data.join(""); + + // Class attributes + const classAttribute = + mode === "styled-components" + ? formatClassAttribute( + classNames.filter((c) => c !== this.cssClassName), + this.isJSX, + ) + : formatClassAttribute(classNames, this.isJSX); + + // Style attribute const styleAttribute = formatStyleAttribute(this.styles, this.isJSX); return `${dataAttributes}${classAttribute}${styleAttribute}`; } + + // Extract style storage into a method to avoid duplication + private storeStyles(): void { + if (!this.cssClassName || this.styles.length === 0) return; + + // Convert to CSS format if needed + const cssStyles = stylesToCSS(this.styles, this.isJSX); + + // Both modes use the standard div/span elements, no need for semantic HTML inference + // which causes conflicts with duplicate tag selectors + let element = this.node.type === "TEXT" ? "p" : "div"; + + // Only override for really obvious cases + if ((this.node as any).name?.toLowerCase().includes("button")) { + element = "button"; + } else if ((this.node as any).name?.toLowerCase().includes("img") || + (this.node as any).name?.toLowerCase().includes("image")) { + element = "img"; + } + + cssCollection[this.cssClassName] = { + styles: cssStyles, + nodeName: (this.node as any).uniqueName || + this.node.name?.replace(/[^a-zA-Z0-9]/g, "") || + undefined, + nodeType: this.node.type, + element: element, + }; + } } diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index ffa053f7..f6bc014b 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -19,6 +19,8 @@ import { nodeHasImageFill, } from "../common/images"; import { addWarning } from "../common/commonConversionWarnings"; +import { customAlphabet } from "nanoid"; +import { generateId } from "../code"; const selfClosingTags = ["img"]; @@ -26,22 +28,328 @@ export let isPreviewGlobal = false; let previousExecutionCache: { style: string; text: string }[]; +// Define better type for the output +export interface HtmlOutput { + html: string; + css?: string; +} + +// Define HTML generation modes for better type safety +export type HtmlGenerationMode = + | "html" + | "jsx" + | "styled-components" + | "svelte"; + +// CSS Collection for external stylesheet or styled-components +interface CSSCollection { + [className: string]: { + styles: string[]; + nodeName?: string; + nodeType?: string; + element?: string; // Base HTML element to use + }; +} + +export let cssCollection: CSSCollection = {}; + +// Generate a unique class name with a prefix +export function generateUniqueClassName(prefix = "figma"): string { + // Sanitize the prefix to ensure valid CSS class + const sanitizedPrefix = + prefix.replace(/[^a-zA-Z0-9_-]/g, "").replace(/^[0-9_-]/, "f") || // Ensure it doesn't start with a number or special char + "figma"; + + return `${sanitizedPrefix}-${generateId()}`; +} + +// Convert styles to CSS format +export function stylesToCSS(styles: string[], isJSX: boolean): string[] { + return styles + .map((style) => { + // Skip empty styles + if (!style.trim()) return ""; + + // Handle JSX format if needed + if (isJSX) { + return style.replace(/^([a-zA-Z0-9]+):/, (match, prop) => { + // Convert camelCase to kebab-case for CSS + return ( + prop + .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2") + .toLowerCase() + ":" + ); + }); + } + return style; + }) + .filter(Boolean); // Remove empty entries +} + +// Get proper component name from node info +export function getComponentName( + node: any, + className?: string, + nodeType = "div", +): string { + // Start with Styled prefix + let name = "Styled"; + + // Use uniqueName if available, otherwise use name + const nodeName: string = node.uniqueName || node.name; + + // Try to use node name first + if (nodeName && nodeName.length > 0) { + // Clean up the node name and capitalize first letter + const cleanName = nodeName + .replace(/[^a-zA-Z0-9]/g, "") + .replace(/^[a-z]/, (match) => match.toUpperCase()); + + name += cleanName || nodeType.charAt(0).toUpperCase() + nodeType.slice(1); + } + // Fall back to className if provided + else if (className) { + const parts = className.split("-"); + if (parts.length > 0 && parts[0]) { + name += parts[0].charAt(0).toUpperCase() + parts[0].slice(1); + } else { + name += nodeType.charAt(0).toUpperCase() + nodeType.slice(1); + } + } + // Last resort + else { + name += nodeType.charAt(0).toUpperCase() + nodeType.slice(1); + } + + return name; +} + +// Get the collected CSS as a string with improved formatting +export function getCollectedCSS(): string { + if (Object.keys(cssCollection).length === 0) { + return ""; + } + + return Object.entries(cssCollection) + .map(([className, { styles }]) => { + if (!styles.length) return ""; + return `.${className} {\n ${styles.join(";\n ")}${styles.length ? ";" : ""}\n}`; + }) + .filter(Boolean) + .join("\n\n"); +} + +// Generate styled-components with improved naming and formatting +export function generateStyledComponents(): string { + const components: string[] = []; + + Object.entries(cssCollection).forEach( + ([className, { styles, nodeName, nodeType, element }]) => { + // Skip if no styles + if (!styles.length) return; + + // Determine base HTML element - defaults to div + const baseElement = element || (nodeType === "TEXT" ? "p" : "div"); + const componentName = getComponentName( + { name: nodeName }, + className, + baseElement, + ); + + const styledComponent = `const ${componentName} = styled.${baseElement}\` + ${styles.join(";\n ")}${styles.length ? ";" : ""} +\`;`; + + components.push(styledComponent); + }, + ); + + if (components.length === 0) { + return ""; + } + + return `${components.join("\n\n")}`; +} + +// Get a valid React component name from a layer name +export function getReactComponentName(node: any): string { + // Use uniqueName if available, otherwise use name + const name: string = node?.uniqueName || node?.name; + + // Default name if nothing valid is provided + if (!name || name.trim() === "") { + return "App"; + } + + // Convert to PascalCase + let componentName = name + .replace(/[^a-zA-Z0-9_]/g, " ") // Replace non-alphanumeric chars with spaces + .split(/\s+/) // Split by spaces + .map((part) => + part ? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() : "", + ) + .join(""); + + // Ensure it starts with uppercase letter (React component convention) + componentName = + componentName.charAt(0).toUpperCase() + componentName.slice(1); + + // Ensure it's a valid identifier - if it starts with a number, prefix with 'Component' + if (/^[0-9]/.test(componentName)) { + componentName = "Component" + componentName; + } + + // If we ended up with nothing valid, use the default + return componentName || "App"; +} + +// Get a Svelte-friendly component name +export function getSvelteElementName( + elementType: string, + nodeName?: string, +): string { + // For Svelte, use semantic element names where possible + if (elementType === "TEXT" || elementType === "p") { + return "p"; + } else if (elementType === "img" || elementType === "IMAGE") { + return "img"; + } else if ( + nodeName && + (nodeName.toLowerCase().includes("button") || + nodeName.toLowerCase().includes("btn")) + ) { + return "button"; + } else if (nodeName && nodeName.toLowerCase().includes("link")) { + return "a"; + } else { + return "div"; // Default element + } +} + +// Generate semantic class names for Svelte +export function getSvelteClassName(prefix?: string, nodeType?: string): string { + if (!prefix) { + return nodeType?.toLowerCase() || "element"; + } + + // Clean and format the prefix + return prefix + .replace(/[^a-zA-Z0-9_-]/g, "-") + .replace(/-{2,}/g, "-") // Replace multiple hyphens with a single one + .replace(/^-+|-+$/g, "") // Remove leading/trailing hyphens + .toLowerCase(); +} + +// Generate component code based on the specified mode +function generateComponentCode( + html: string, + sceneNode: Array, + mode: HtmlGenerationMode, +): string { + switch (mode) { + case "styled-components": + return generateReactComponent(html, sceneNode, true); + case "svelte": + return generateSvelteComponent(html, sceneNode); + case "html": + case "jsx": + default: + return html; + } +} + +// Generate React component from HTML, with optional styled-components +function generateReactComponent( + html: string, + sceneNode: Array, + useStyledComponents: boolean = false, +): string { + const styledComponentsCode = useStyledComponents + ? generateStyledComponents() + : ""; + const componentName = getReactComponentName(sceneNode[0]); + + const imports = ['import React from "react";']; + + if (useStyledComponents) { + imports.push('import styled from "styled-components";'); + } + + return `${imports.join("\n")} +${styledComponentsCode ? `\n${styledComponentsCode}` : ""} + +const ${componentName} = () => { + return ( +${indentString(html, 4)} + ); +}; + +export default ${componentName}; +`; +} + +// Generate Svelte component from the collected styles and HTML +function generateSvelteComponent( + html: string, + sceneNode: Array, +): string { + const componentName = getReactComponentName(sceneNode[0]); + + // Build CSS classes similar to styled-components but for Svelte + const cssRules: string[] = []; + + Object.entries(cssCollection).forEach(([className, { styles }]) => { + if (!styles.length) return; + + // Always use class selector to avoid conflicts + cssRules.push( + `.${className} {\n ${styles.join(";\n ")}${styles.length ? ";" : ""}\n}`, + ); + }); + + return `${html} + +`; +} + export const htmlMain = async ( sceneNode: Array, settings: PluginSettings, isPreview: boolean = false, -): Promise => { +): Promise => { isPreviewGlobal = isPreview; previousExecutionCache = []; + cssCollection = {}; - let result = await htmlWidgetGenerator(sceneNode, settings); + let htmlContent = await htmlWidgetGenerator(sceneNode, settings); // remove the initial \n that is made in Container. - if (result.length > 0 && result.startsWith("\n")) { - result = result.slice(1, result.length); + if (htmlContent.length > 0 && htmlContent.startsWith("\n")) { + htmlContent = htmlContent.slice(1, htmlContent.length); } - return result; + // Always return an object with html property + const output: HtmlOutput = { html: htmlContent }; + + // Handle different HTML generation modes + const mode = settings.htmlGenerationMode || "html"; + + if (mode !== "html") { + // Generate component code for non-html modes + output.html = generateComponentCode(htmlContent, sceneNode, mode); + + // For svelte mode, we don't need separate CSS as it's included in the component + if (mode === "svelte" && Object.keys(cssCollection).length > 0) { + // CSS is already included in the Svelte component + } + } else if (Object.keys(cssCollection).length > 0) { + // For plain HTML with CSS, include CSS separately + output.css = getCollectedCSS(); + } + + return output; }; export const generateHTMLPreview = async ( @@ -49,26 +357,22 @@ export const generateHTMLPreview = async ( settings: PluginSettings, code?: string, ): Promise => { - // const htmlCodeAlreadyGenerated = - // settings.framework === "HTML" && settings.jsx === false && code; - const htmlCode = - // htmlCodeAlreadyGenerated - // ? code : - await htmlMain( - nodes, - { - ...settings, - jsx: false, - }, - true, - ); + const result = await htmlMain( + nodes, + { + ...settings, + htmlGenerationMode: "html", + jsx: false, + }, + true, + ); return { size: { width: nodes[0].width, height: nodes[0].height, }, - content: htmlCode, + content: result.html, }; }; @@ -77,7 +381,6 @@ const htmlWidgetGenerator = async ( settings: HTMLSettings, ): Promise => { console.log("htmlWidgetGenerator", sceneNode); - // filter non visible nodes. This is necessary at this step because conversion already happened. const promiseOfConvertedCode = getVisibleNodes(sceneNode).map( convertNode(settings), @@ -87,8 +390,6 @@ const htmlWidgetGenerator = async ( }; const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { - console.log("converting", node); - if (settings.embedVectors && (node as any).canBeFlattened) { const altNode = await renderAndAttachSVG(node); if (altNode.svg) { @@ -137,7 +438,6 @@ const htmlWrapSVG = ( const builder = new HtmlDefaultBuilder(node, settings) .addData("svg-wrapper") .position(); - return `\n\n${node.svg ?? ""}
`; }; @@ -158,16 +458,13 @@ const htmlGroup = async ( if (builder.styles) { const attr = builder.build(); - const generator = await htmlWidgetGenerator(node.children, settings); - return `\n${indentString(generator)}\n
`; } - return await htmlWidgetGenerator(node.children, settings); }; -// this was split from htmlText to help the UI part, where the style is needed (without

). +// For htmlText and htmlContainer, use the htmlGenerationMode to determine styling approach const htmlText = (node: TextNode, settings: HTMLSettings): string => { let layoutBuilder = new HtmlTextBuilder(node, settings) .commonPositionStyles() @@ -177,9 +474,45 @@ const htmlText = (node: TextNode, settings: HTMLSettings): string => { const styledHtml = layoutBuilder.getTextSegments(node); previousExecutionCache.push(...styledHtml); + const mode = settings.htmlGenerationMode || "html"; + + // For styled-components mode + if (mode === "styled-components") { + const componentName = layoutBuilder.cssClassName + ? getComponentName(node, layoutBuilder.cssClassName, "p") + : getComponentName(node, undefined, "p"); + + if (styledHtml.length === 1) { + return `\n<${componentName}>${styledHtml[0].text}`; + } else { + const content = styledHtml + .map((style) => { + const tag = + style.openTypeFeatures.SUBS === true + ? "sub" + : style.openTypeFeatures.SUPS === true + ? "sup" + : "span"; + + if (style.componentName) { + return `<${style.componentName}>${style.text}`; + } + return `<${tag}>${style.text}`; + }) + .join(""); + + return `\n<${componentName}>${content}`; + } + } + + // Standard HTML/CSS approach for HTML, React or Svelte let content = ""; if (styledHtml.length === 1) { - layoutBuilder.addStyles(styledHtml[0].style); + // For HTML and React modes, we use inline styles + if (mode === "html" || mode === "jsx") { + layoutBuilder.addStyles(styledHtml[0].style); + } + content = styledHtml[0].text; const additionalTag = @@ -191,10 +524,14 @@ const htmlText = (node: TextNode, settings: HTMLSettings): string => { if (additionalTag) { content = `<${additionalTag}>${content}`; + } else if (mode === "svelte" && styledHtml[0].className) { + // Use span just like styled-components for consistency + content = `${content}`; } } else { content = styledHtml .map((style) => { + // Always use span for multi-segment text in Svelte mode const tag = style.openTypeFeatures.SUBS === true ? "sub" @@ -202,11 +539,17 @@ const htmlText = (node: TextNode, settings: HTMLSettings): string => { ? "sup" : "span"; + // Use class name for Svelte with same approach as styled-components + if (mode === "svelte" && style.className) { + return `${style.text}`; + } + return `<${tag} style="${style.style}">${style.text}`; }) .join(""); } + // Always use div as container to be consistent with styled-components return `\n${content}
`; }; @@ -252,8 +595,6 @@ const htmlContainer = async ( settings: HTMLSettings, ): Promise => { // ignore the view when size is zero or less - // while technically it shouldn't get less than 0, due to rounding errors, - // it can get to values like: -0.000004196293048153166 if (node.width <= 0 || node.height <= 0) { return children; } @@ -267,23 +608,17 @@ const htmlContainer = async ( let src = ""; if (nodeHasImageFill(node)) { + // ...existing image handling code... const altNode = node as AltNode; const hasChildren = "children" in node && node.children.length > 0; let imgUrl = ""; - // TODO: This overrides the embedImages setting to only happen when HTML is selected but - // really this should be more of a global setting that isn't tied to a specific framework. - // It's being disabled in this way so the HTML preview will only embed images when it's HTML outuput. - // The reason this is so important is that it's a costly operation an it will slow down - // the generation of code for other languages and display different results in the preview - // than what the output will look like. if ( settings.embedImages && (settings as PluginSettings).framework === "HTML" ) { imgUrl = (await exportNodeAsBase64PNG(altNode, hasChildren)) ?? ""; } else { - // addWarning("Some images were exported as placeholder URLs"); imgUrl = getPlaceholderImage(node.width, node.height); } @@ -292,14 +627,26 @@ const htmlContainer = async ( formatWithJSX("background-image", settings.jsx, `url(${imgUrl})`), ); } else { - // if node has NO children tag = "img"; src = ` src="${imgUrl}"`; } } const build = builder.build(additionalStyles); + const mode = settings.htmlGenerationMode || "html"; + + // For styled-components mode + if (mode === "styled-components" && builder.cssClassName) { + const componentName = getComponentName(node, builder.cssClassName); + if (children) { + return `\n<${componentName}>${indentString(children)}\n`; + } else { + return `\n<${componentName} ${src}/>`; + } + } + + // Standard HTML approach for HTML, React, or Svelte if (children) { return `\n<${tag}${build}${src}>${indentString(children)}\n`; } else if (selfClosingTags.includes(tag) || settings.jsx) { @@ -308,6 +655,7 @@ const htmlContainer = async ( return `\n<${tag}${build}${src}>`; } } + return children; }; @@ -340,7 +688,7 @@ export const htmlCodeGenTextStyles = (settings: HTMLSettings) => { const result = previousExecutionCache .map( (style) => - `// ${style.text}\n${style.style.split(settings.jsx ? "," : ";").join(";\n")}`, + `// ${style.text}\n${style.style.split(settings.htmlGenerationMode === "jsx" ? "," : ";").join(";\n")}`, ) .join("\n---\n"); diff --git a/packages/backend/src/html/htmlTextBuilder.ts b/packages/backend/src/html/htmlTextBuilder.ts index 5e0d60f8..cf101766 100644 --- a/packages/backend/src/html/htmlTextBuilder.ts +++ b/packages/backend/src/html/htmlTextBuilder.ts @@ -6,16 +6,30 @@ import { commonLineHeight, } from "../common/commonTextHeightSpacing"; import { HTMLSettings, StyledTextSegmentSubset } from "types"; +import { + cssCollection, + generateUniqueClassName, + stylesToCSS, + getComponentName, + getSvelteClassName, +} from "./htmlMain"; export class HtmlTextBuilder extends HtmlDefaultBuilder { constructor(node: TextNode, settings: HTMLSettings) { super(node, settings); } + // Override htmlElement to ensure text nodes use paragraph elements + get htmlElement(): string { + return "p"; + } + getTextSegments(node: TextNode): { style: string; text: string; openTypeFeatures: { [key: string]: boolean }; + className?: string; + componentName?: string; }[] { const segments = (node as any) .styledTextSegments as StyledTextSegmentSubset[]; @@ -23,7 +37,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { return []; } - return segments.map((segment) => { + return segments.map((segment, index) => { // Prepare additional CSS properties from layer blur and drop shadow effects. const additionalStyles: { [key: string]: string } = {}; @@ -58,11 +72,57 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { ); const charsWithLineBreak = segment.characters.split("\n").join("
"); - return { + const result: any = { style: styleAttributes, text: charsWithLineBreak, openTypeFeatures: segment.openTypeFeatures, }; + + // Add class name and component name for Svelte or styled-components modes + const mode = this.settings.htmlGenerationMode; + if ((mode === "svelte" || mode === "styled-components") && styleAttributes) { + // Create a consistent naming scheme for both modes + const uniqueName = (node as any).uniqueName || node.name || "text"; + + // Create segment name with index for uniqueness + const segmentPrefix = index > 0 + ? `${uniqueName.replace(/[^a-zA-Z0-9]/g, "")}-${index}` + : uniqueName.replace(/[^a-zA-Z0-9]/g, ""); + + // Always generate a unique className for consistent styling + const className = generateUniqueClassName(segmentPrefix); + result.className = className; + + // Convert styles to CSS format + const cssStyles = stylesToCSS( + styleAttributes + .split(this.isJSX ? "," : ";") + .map((style) => style.trim()) + .filter((style) => style), + this.isJSX + ); + + // In both modes, use span for text segments to avoid selector conflicts + const elementTag = "span"; + + // Store in cssCollection with consistent metadata + cssCollection[className] = { + styles: cssStyles, + nodeName: segmentPrefix, + nodeType: "TEXT", + element: elementTag, + }; + + if (mode === "styled-components") { + result.componentName = getComponentName( + { name: segmentPrefix }, + className, + elementTag + ); + } + } + + return result; }); } diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 7516b261..0347dfac 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -7,7 +7,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ label: "React (JSX)", description: "", isDefault: false, - includedLanguages: ["HTML", "Tailwind"], + includedLanguages: ["Tailwind"], }, { itemType: "individual_select", @@ -71,6 +71,18 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ ]; export const selectPreferenceOptions: SelectPreferenceOptions[] = [ + { + itemType: "select", + propertyName: "htmlGenerationMode", + label: "Mode", + options: [ + { label: "HTML", value: "html" }, + { label: "React (JSX)", value: "jsx" }, + { label: "Svelte", value: "svelte" }, + { label: "styled-components", value: "styled-components" }, + ], + includedLanguages: ["HTML"], + }, { itemType: "select", propertyName: "flutterGenerationMode", diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 96801f5e..2f60c731 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -85,6 +85,8 @@ const CodePanel = (props: CodePanelProps) => { // Define preference grouping based on property names const essentialPropertyNames = ["jsx", "optimizeLayout"]; const stylingPropertyNames = [ + "styledComponents", + "exportCSS", "roundTailwindValues", "roundTailwindColors", "useColorVariables", @@ -139,27 +141,9 @@ const CodePanel = (props: CodePanelProps) => { onPreferenceChanged={onPreferenceChanged} /> - {/* Styling preferences with custom prefix for Tailwind */} - {(stylingPreferences.length > 0 || - selectedFramework === "Tailwind") && ( - - {selectedFramework === "Tailwind" && ( - - )} - - )} - {/* Framework-specific options */} {selectableSettingsFiltered.length > 0 && ( -
+

{selectedFramework} Options

@@ -193,6 +177,24 @@ const CodePanel = (props: CodePanelProps) => {
)} + + {/* Styling preferences with custom prefix for Tailwind */} + {(stylingPreferences.length > 0 || + selectedFramework === "Tailwind") && ( + + {selectedFramework === "Tailwind" && ( + + )} + + )}
)} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index c5622198..0bfc11da 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -8,6 +8,7 @@ export interface HTMLSettings { embedImages: boolean; embedVectors: boolean; useColorVariables: boolean; + htmlGenerationMode: "html" | "jsx" | "styled-components" | "svelte"; } export interface TailwindSettings extends HTMLSettings { roundTailwindValues: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ac4f6fe..756ec514 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: motion: specifier: ^12.4.9 version: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + nanoid: + specifier: ^5.1.2 + version: 5.1.2 plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui @@ -175,6 +178,9 @@ importers: js-base64: specifier: ^3.7.7 version: 3.7.7 + nanoid: + specifier: ^5.1.2 + version: 5.1.2 react: specifier: 19.0.0 version: 19.0.0 @@ -2364,6 +2370,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.2: + resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5184,6 +5195,8 @@ snapshots: nanoid@3.3.8: {} + nanoid@5.1.2: {} + natural-compare@1.4.0: {} next@14.2.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0): From c8ce626e19024811370c6d866059d6302f0ca113 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 01:56:08 -0300 Subject: [PATCH 030/134] Experiment 2 --- packages/backend/src/code.ts | 218 ++++++++++-------- .../backend/src/html/htmlDefaultBuilder.ts | 48 ++-- packages/backend/src/html/htmlMain.ts | 21 +- packages/backend/src/html/htmlTextBuilder.ts | 29 ++- .../src/tailwind/tailwindDefaultBuilder.ts | 2 +- 5 files changed, 183 insertions(+), 135 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 2c682e93..9ec40fc9 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -1,3 +1,4 @@ +import { btoa } from "js-base64"; import { convertNodesToAltNodes } from "./altNodes/altConversion"; import { retrieveGenericSolidUIColors, @@ -13,14 +14,8 @@ import { PluginSettings } from "types"; import { convertToCode } from "./common/retrieveUI/convertToCode"; import { generateHTMLPreview } from "./html/htmlMain"; -export const generateId = (length: number = 4): string => { - const chars = "1234567890abcdefghijklmnopqrstuvwxyz"; - return Array.from({ length }, () => - chars.charAt(Math.floor(Math.random() * chars.length)) - ).join(''); -}; -// Keep track of node names to identify duplicates -const nodeNameRegistry: Map = new Map(); +// Keep track of node names for sequential numbering +const nodeNameCounters: Map = new Map(); // Helper function to add parent references to all children in the node tree const addParentReferences = (node: any) => { @@ -57,102 +52,135 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { ); }); - try { - const figmaNode = figma.getNodeById(node.id); - if (figmaNode) { - // Ensure node has a unique name - store directly on node - if (figmaNode.name) { - const cleanName = figmaNode.name.trim(); - - // Track names for uniqueness - const count = nodeNameRegistry.get(cleanName) || 0; - nodeNameRegistry.set(cleanName, count + 1); - - // For first occurrence, use original name; for duplicates, add suffix - node.uniqueName = - count === 0 - ? cleanName - : `${cleanName}_${generateId()}`; - } + // Ensure node has a unique name with simple numbering + const cleanName = node.name.trim(); - // Handle additional node properties - if ( - hasGradient || - optimizeLayout || - node.type === "INSTANCE" || - node.type === "TEXT" - ) { - // Handle gradients - if (hasGradient) { - GRADIENT_PROPERTIES.forEach((propName) => { - const property = node[propName]; - if ( - property && - Array.isArray(property) && - property.length > 0 && - property.some( - (item) => item.type && item.type.startsWith("GRADIENT_"), - ) && - propName in figmaNode - ) { - node[propName] = JSON.parse( - JSON.stringify((figmaNode as any)[propName]), - ); - } - }); - } + // Track names with simple counter + const count = nodeNameCounters.get(cleanName) || 0; + nodeNameCounters.set(cleanName, count + 1); - // Handle text-specific properties - if (figmaNode.type === "TEXT") { - node.styledTextSegments = figmaNode.getStyledTextSegments([ - "fontName", - "fills", - "fontSize", - "fontWeight", - "hyperlink", - "indentation", - "letterSpacing", - "lineHeight", - "listOptions", - "textCase", - "textDecoration", - "textStyleId", - "fillStyleId", - "openTypeFeatures", - ]); - Object.assign(node, node.style); - if (!node.textAutoResize) { - node.textAutoResize = "NONE"; - } - } + // For first occurrence, use original name; for duplicates, add sequential suffix + node.uniqueName = + count === 0 + ? cleanName + : `${cleanName}_${count.toString().padStart(2, "0")}`; - // Extract inferredAutoLayout if optimizeLayout is enabled - if (optimizeLayout && "inferredAutoLayout" in figmaNode) { - node.inferredAutoLayout = JSON.parse( - JSON.stringify((figmaNode as any).inferredAutoLayout), - ); - } + console.log("going inside", node); + // Handle additional node properties + if ( + hasGradient || + optimizeLayout || + node.type === "INSTANCE" || + node.type === "TEXT" + ) { + const figmaNode = figma.getNodeById(node.id); - // Extract component metadata from instances + if (!figmaNode) { + return; + } + + // Handle gradients + if (hasGradient) { + GRADIENT_PROPERTIES.forEach((propName) => { + const property = node[propName]; if ( - node.type === "INSTANCE" && - "variantProperties" in figmaNode && - figmaNode.variantProperties + property && + Array.isArray(property) && + property.length > 0 && + property.some( + (item) => item.type && item.type.startsWith("GRADIENT_"), + ) && + propName in figmaNode ) { - node.variantProperties = figmaNode.variantProperties; + node[propName] = JSON.parse( + JSON.stringify((figmaNode as any)[propName]), + ); } + }); + } + console.log("eee"); + + // Handle text-specific properties + if (figmaNode.type === "TEXT") { + // Get the text segments + const styledTextSegments = figmaNode.getStyledTextSegments([ + "fontName", + "fills", + "fontSize", + "fontWeight", + "hyperlink", + "indentation", + "letterSpacing", + "lineHeight", + "listOptions", + "textCase", + "textDecoration", + "textStyleId", + "fillStyleId", + "openTypeFeatures", + ]); + + // Assign unique IDs to each segment + if (styledTextSegments.length > 0) { + const baseSegmentName = (node.uniqueName || node.name) + .replace(/[^a-zA-Z0-9_-]/g, "") + .toLowerCase(); + // Add a uniqueId to each segment + node.styledTextSegments = styledTextSegments.map( + (segment: any, index) => { + const mutableSegment = Object.assign({}, segment); + // For single segments, don't add index suffix + if (styledTextSegments.length === 1) { + mutableSegment.uniqueId = `${baseSegmentName}_span`; + } else { + // For multiple segments, add index suffix + mutableSegment.uniqueId = `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; + } + console.log("after"); + return mutableSegment; + }, + ); } - // Always copy size and position - if ("width" in figmaNode) { - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; + Object.assign(node, node.style); + if (!node.textAutoResize) { + node.textAutoResize = "NONE"; } } - } catch (e) { - // Silently fail if there's an error accessing the Figma node + + // Extract inferredAutoLayout if optimizeLayout is enabled + if (optimizeLayout && "inferredAutoLayout" in figmaNode) { + node.inferredAutoLayout = JSON.parse( + JSON.stringify((figmaNode as any).inferredAutoLayout), + ); + } + + // Extract component metadata from instances + if ( + node.type === "INSTANCE" && + "variantProperties" in figmaNode && + figmaNode.variantProperties + ) { + node.variantProperties = figmaNode.variantProperties; + } + + console.log("figmaNode", figmaNode); + // Always copy size and position + if ("width" in figmaNode) { + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; + } + } else { + // Hopefully one day this won't be needed anymore. + const figmaNode = figma.getNodeById(node.id); + if (figmaNode && "width" in figmaNode) { + node.width = (figmaNode as any).width; + node.height = (figmaNode as any).height; + node.x = (figmaNode as any).x; + node.y = (figmaNode as any).y; + } } // Set default layout properties if missing @@ -190,8 +218,8 @@ export const nodesToJSON = async ( nodes: ReadonlyArray, optimizeLayout: boolean = false, ): Promise => { - // Reset name registry for each conversion - nodeNameRegistry.clear(); + // Reset name counters for each conversion + nodeNameCounters.clear(); const nodeJson = (await Promise.all( nodes.map( diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 892c7782..da6cc673 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -90,27 +90,29 @@ export class HtmlDefaultBuilder { this.styles = []; this.data = []; - // For both Svelte and styled-components, use similar naming pattern + // For both Svelte and styled-components, use sequential class names if ( this.settings.htmlGenerationMode === "svelte" || this.settings.htmlGenerationMode === "styled-components" ) { - // Always generate a unique classname that relates to the node name - const nodeName = (this.node as any).uniqueName || this.node.name; - + // Use uniqueName (which already has _01, _02 suffixes) if available + let baseClassName = + (this.node as any).uniqueName || + this.node.name || + this.node.type.toLowerCase(); + // Clean the name and create a valid CSS class name - let baseClassName = nodeName - ? nodeName.replace(/[^a-zA-Z0-9\s_-]/g, "") - .replace(/\s+/g, "-") - .toLowerCase() - : this.node.type.toLowerCase(); - + baseClassName = baseClassName + .replace(/[^a-zA-Z0-9\s_-]/g, "") + .replace(/\s+/g, "-") + .toLowerCase(); + // Make sure it's valid if (!/^[a-z]/i.test(baseClassName)) { baseClassName = `${this.node.type.toLowerCase()}-${baseClassName}`; } - // For Svelte, use the same prefix style as styled-components for consistency + // Generate unique class name with simple counter suffix this.cssClassName = generateUniqueClassName(baseClassName); } } @@ -406,9 +408,12 @@ export class HtmlDefaultBuilder { let classNames: string[] = []; if (this.name) { this.addData("layer", this.name.trim()); - const layerNameClass = stringToClassName(this.name.trim()); - if (layerNameClass !== "") { - classNames.push(layerNameClass); + + if (mode !== "svelte" && mode !== "styled-components") { + const layerNameClass = stringToClassName(this.name.trim()); + if (layerNameClass !== "") { + classNames.push(layerNameClass); + } } } @@ -467,16 +472,19 @@ export class HtmlDefaultBuilder { // Only override for really obvious cases if ((this.node as any).name?.toLowerCase().includes("button")) { element = "button"; - } else if ((this.node as any).name?.toLowerCase().includes("img") || - (this.node as any).name?.toLowerCase().includes("image")) { - element = "img"; + } else if ( + (this.node as any).name?.toLowerCase().includes("img") || + (this.node as any).name?.toLowerCase().includes("image") + ) { + element = "img"; } cssCollection[this.cssClassName] = { styles: cssStyles, - nodeName: (this.node as any).uniqueName || - this.node.name?.replace(/[^a-zA-Z0-9]/g, "") || - undefined, + nodeName: + (this.node as any).uniqueName || + this.node.name?.replace(/[^a-zA-Z0-9]/g, "") || + undefined, nodeType: this.node.type, element: element, }; diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index f6bc014b..abfbb67d 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -19,8 +19,6 @@ import { nodeHasImageFill, } from "../common/images"; import { addWarning } from "../common/commonConversionWarnings"; -import { customAlphabet } from "nanoid"; -import { generateId } from "../code"; const selfClosingTags = ["img"]; @@ -53,14 +51,28 @@ interface CSSCollection { export let cssCollection: CSSCollection = {}; -// Generate a unique class name with a prefix +// Instance counters for class name generation - we keep this but primarily as a fallback +const classNameCounters: Map = new Map(); + +// Generate a class name - prefer direct uniqueId, but fall back to counter-based if needed export function generateUniqueClassName(prefix = "figma"): string { // Sanitize the prefix to ensure valid CSS class const sanitizedPrefix = prefix.replace(/[^a-zA-Z0-9_-]/g, "").replace(/^[0-9_-]/, "f") || // Ensure it doesn't start with a number or special char "figma"; - return `${sanitizedPrefix}-${generateId()}`; + // Most of the time, we'll just use the prefix directly as it's pre-generated to be unique + // But keep the counter logic as a fallback + const count = classNameCounters.get(sanitizedPrefix) || 0; + classNameCounters.set(sanitizedPrefix, count + 1); + + // Only add suffix if this isn't the first instance + return count === 0 ? sanitizedPrefix : `${sanitizedPrefix}_${count.toString().padStart(2, "0")}`; +} + +// Reset all class name counters - call this at the start of processing +export function resetClassNameCounters(): void { + classNameCounters.clear(); } // Convert styles to CSS format @@ -322,6 +334,7 @@ export const htmlMain = async ( isPreviewGlobal = isPreview; previousExecutionCache = []; cssCollection = {}; + resetClassNameCounters(); // Reset counters for each new generation let htmlContent = await htmlWidgetGenerator(sceneNode, settings); diff --git a/packages/backend/src/html/htmlTextBuilder.ts b/packages/backend/src/html/htmlTextBuilder.ts index cf101766..d870d562 100644 --- a/packages/backend/src/html/htmlTextBuilder.ts +++ b/packages/backend/src/html/htmlTextBuilder.ts @@ -80,17 +80,16 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { // Add class name and component name for Svelte or styled-components modes const mode = this.settings.htmlGenerationMode; - if ((mode === "svelte" || mode === "styled-components") && styleAttributes) { - // Create a consistent naming scheme for both modes - const uniqueName = (node as any).uniqueName || node.name || "text"; + if ( + (mode === "svelte" || mode === "styled-components") && + styleAttributes + ) { + // Use the pre-assigned uniqueId from the segment if available, + // or generate one if not (as a fallback) + const segmentName = (segment as any).uniqueId || + `${((node as any).uniqueName || node.name || "text").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase()}_text_${(index + 1).toString().padStart(2, "0")}`; - // Create segment name with index for uniqueness - const segmentPrefix = index > 0 - ? `${uniqueName.replace(/[^a-zA-Z0-9]/g, "")}-${index}` - : uniqueName.replace(/[^a-zA-Z0-9]/g, ""); - - // Always generate a unique className for consistent styling - const className = generateUniqueClassName(segmentPrefix); + const className = generateUniqueClassName(segmentName); result.className = className; // Convert styles to CSS format @@ -99,7 +98,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { .split(this.isJSX ? "," : ";") .map((style) => style.trim()) .filter((style) => style), - this.isJSX + this.isJSX, ); // In both modes, use span for text segments to avoid selector conflicts @@ -108,16 +107,16 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { // Store in cssCollection with consistent metadata cssCollection[className] = { styles: cssStyles, - nodeName: segmentPrefix, + nodeName: segmentName, nodeType: "TEXT", - element: elementTag, + element: elementTag, }; if (mode === "styled-components") { result.componentName = getComponentName( - { name: segmentPrefix }, + { name: segmentName }, className, - elementTag + elementTag, ); } } diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 2d5df811..79e3d79c 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -125,7 +125,7 @@ export class TailwindDefaultBuilder { if (commonIsAbsolutePosition(node, optimizeLayout)) { const { x, y } = getCommonPositionValue(node); - console.log("x", x, y); + console.log("x", x, y, "node", node); const parsedX = numberToFixedString(x); const parsedY = numberToFixedString(y); From 9bf600729cf1953d0b99b2224135f36f42eedd12 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 02:25:42 -0300 Subject: [PATCH 031/134] Improve even more --- apps/plugin/plugin-src/code.ts | 56 +++++++++++++++++-- manifest.json | 2 + packages/backend/src/code.ts | 12 +--- packages/backend/src/html/htmlMain.ts | 33 +++++------ .../src/tailwind/tailwindDefaultBuilder.ts | 2 +- packages/backend/src/tailwind/tailwindMain.ts | 5 +- .../plugin-ui/src/codegenPreferenceOptions.ts | 32 ++++++----- .../plugin-ui/src/components/CodePanel.tsx | 53 ++++++++---------- .../src/components/FrameworkTabs.tsx | 43 ++++++++++++++ .../src/components/SelectableToggle.tsx | 30 +++++----- packages/types/src/types.ts | 2 +- 11 files changed, 171 insertions(+), 99 deletions(-) create mode 100644 packages/plugin-ui/src/components/FrameworkTabs.tsx diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 33611d9a..d466258f 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -19,7 +19,6 @@ let userPluginSettings: PluginSettings; export const defaultPluginSettings: PluginSettings = { framework: "HTML", - jsx: false, optimizeLayout: true, showLayerNames: false, inlineStyle: true, @@ -32,8 +31,8 @@ export const defaultPluginSettings: PluginSettings = { customTailwindPrefix: "", embedImages: false, embedVectors: false, - exportCSS: false, - styledComponents: false, + htmlGenerationMode: "html", + tailwindGenerationMode: "jsx", }; // A helper type guard to ensure the key belongs to the PluginSettings type @@ -191,7 +190,7 @@ const codegenMode = async () => { code: ( await htmlMain( convertedSelection, - { ...userPluginSettings, jsx: false }, + { ...userPluginSettings, htmlGenerationMode: "html" }, true, ) ).html, @@ -210,7 +209,7 @@ const codegenMode = async () => { code: ( await htmlMain( convertedSelection, - { ...userPluginSettings, jsx: true }, + { ...userPluginSettings, htmlGenerationMode: "jsx" }, true, ) ).html, @@ -222,6 +221,50 @@ const codegenMode = async () => { language: "HTML", }, ]; + + case "html_svelte": + return [ + { + title: "Code", + code: ( + await htmlMain( + convertedSelection, + { ...userPluginSettings, htmlGenerationMode: "svelte" }, + true, + ) + ).html, + language: "HTML", + }, + { + title: "Text Styles", + code: htmlCodeGenTextStyles(userPluginSettings), + language: "HTML", + }, + ]; + + case "html_styled_components": + return [ + { + title: "Code", + code: ( + await htmlMain( + convertedSelection, + { + ...userPluginSettings, + htmlGenerationMode: "styled-components", + }, + true, + ) + ).html, + language: "HTML", + }, + { + title: "Text Styles", + code: htmlCodeGenTextStyles(userPluginSettings), + language: "HTML", + }, + ]; + case "tailwind": case "tailwind_jsx": return [ @@ -229,7 +272,8 @@ const codegenMode = async () => { title: "Code", code: await tailwindMain(convertedSelection, { ...userPluginSettings, - jsx: language === "tailwind_jsx", + tailwindGenerationMode: + language === "tailwind_jsx" ? "jsx" : "html", }), language: "HTML", }, diff --git a/manifest.json b/manifest.json index b9c29cc0..888938bf 100644 --- a/manifest.json +++ b/manifest.json @@ -13,6 +13,8 @@ "codegenLanguages": [ { "label": "HTML", "value": "html" }, { "label": "React (JSX)", "value": "html_jsx" }, + { "label": "Svelte", "value": "html_svelte" }, + { "label": "Styled Components", "value": "html_styled_components" }, { "label": "Tailwind", "value": "tailwind" }, { "label": "Tailwind (JSX)", "value": "tailwind_jsx" }, { "label": "Flutter", "value": "flutter" }, diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 9ec40fc9..2486e702 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -1,4 +1,3 @@ -import { btoa } from "js-base64"; import { convertNodesToAltNodes } from "./altNodes/altConversion"; import { retrieveGenericSolidUIColors, @@ -65,7 +64,6 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { ? cleanName : `${cleanName}_${count.toString().padStart(2, "0")}`; - console.log("going inside", node); // Handle additional node properties if ( hasGradient || @@ -98,7 +96,6 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } }); } - console.log("eee"); // Handle text-specific properties if (figmaNode.type === "TEXT") { @@ -136,7 +133,6 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { // For multiple segments, add index suffix mutableSegment.uniqueId = `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; } - console.log("after"); return mutableSegment; }, ); @@ -163,8 +159,6 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { ) { node.variantProperties = figmaNode.variantProperties; } - - console.log("figmaNode", figmaNode); // Always copy size and position if ("width" in figmaNode) { node.width = (figmaNode as any).width; @@ -267,11 +261,7 @@ export const run = async (settings: PluginSettings) => { } const code = await convertToCode(convertedSelection, settings); - const htmlPreview = await generateHTMLPreview( - convertedSelection, - settings, - code, - ); + const htmlPreview = await generateHTMLPreview(convertedSelection, settings); const colors = retrieveGenericSolidUIColors(framework); const gradients = retrieveGenericGradients(framework, settings); diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index abfbb67d..16a1aeb2 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -65,9 +65,11 @@ export function generateUniqueClassName(prefix = "figma"): string { // But keep the counter logic as a fallback const count = classNameCounters.get(sanitizedPrefix) || 0; classNameCounters.set(sanitizedPrefix, count + 1); - + // Only add suffix if this isn't the first instance - return count === 0 ? sanitizedPrefix : `${sanitizedPrefix}_${count.toString().padStart(2, "0")}`; + return count === 0 + ? sanitizedPrefix + : `${sanitizedPrefix}_${count.toString().padStart(2, "0")}`; } // Reset all class name counters - call this at the start of processing @@ -260,9 +262,9 @@ function generateComponentCode( ): string { switch (mode) { case "styled-components": - return generateReactComponent(html, sceneNode, true); + return generateReactComponent(html, sceneNode); case "svelte": - return generateSvelteComponent(html, sceneNode); + return generateSvelteComponent(html); case "html": case "jsx": default: @@ -274,18 +276,15 @@ function generateComponentCode( function generateReactComponent( html: string, sceneNode: Array, - useStyledComponents: boolean = false, ): string { - const styledComponentsCode = useStyledComponents - ? generateStyledComponents() - : ""; + const styledComponentsCode = generateStyledComponents(); + const componentName = getReactComponentName(sceneNode[0]); - const imports = ['import React from "react";']; - - if (useStyledComponents) { - imports.push('import styled from "styled-components";'); - } + const imports = [ + 'import React from "react";', + 'import styled from "styled-components";', + ]; return `${imports.join("\n")} ${styledComponentsCode ? `\n${styledComponentsCode}` : ""} @@ -301,12 +300,7 @@ export default ${componentName}; } // Generate Svelte component from the collected styles and HTML -function generateSvelteComponent( - html: string, - sceneNode: Array, -): string { - const componentName = getReactComponentName(sceneNode[0]); - +function generateSvelteComponent(html: string): string { // Build CSS classes similar to styled-components but for Svelte const cssRules: string[] = []; @@ -368,7 +362,6 @@ export const htmlMain = async ( export const generateHTMLPreview = async ( nodes: SceneNode[], settings: PluginSettings, - code?: string, ): Promise => { const result = await htmlMain( nodes, diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 79e3d79c..0e72f947 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -48,7 +48,7 @@ export class TailwindDefaultBuilder { return this.node.visible ?? true; } get isJSX() { - return this.settings.jsx; + return this.settings.tailwindGenerationMode === "jsx"; } get optimizeLayout() { return this.settings.optimizeLayout; diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index f92683d8..3d95f7b4 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -246,7 +246,10 @@ export const tailwindContainer = ( // Generate appropriate HTML if (children) { return `\n<${tag}${build}${src}>${indentString(children)}\n`; - } else if (SELF_CLOSING_TAGS.includes(tag) || settings.jsx) { + } else if ( + SELF_CLOSING_TAGS.includes(tag) || + settings.tailwindGenerationMode === "jsx" + ) { return `\n<${tag}${build}${src} />`; } else { return `\n<${tag}${build}${src}>`; diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 0347dfac..41acc3c8 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -3,11 +3,11 @@ import { LocalCodegenPreferenceOptions, SelectPreferenceOptions } from "types"; export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ { itemType: "individual_select", - propertyName: "jsx", - label: "React (JSX)", - description: "", + propertyName: "showLayerNames", + label: "Layer names", + description: "Include Figma layer names in classes.", isDefault: false, - includedLanguages: ["Tailwind"], + includedLanguages: ["HTML", "Tailwind"], }, { itemType: "individual_select", @@ -18,14 +18,6 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ isDefault: true, includedLanguages: ["HTML", "Tailwind", "Flutter", "SwiftUI"], }, - { - itemType: "individual_select", - propertyName: "showLayerNames", - label: "Layer names", - description: "Include Figma layer names in classes.", - isDefault: false, - includedLanguages: ["HTML", "Tailwind"], - }, { itemType: "individual_select", propertyName: "roundTailwindValues", @@ -56,7 +48,8 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "embedImages", label: "Embed Images", - description: "Convert Figma images to Base64 and embed them in the code.", + description: + "Convert Figma images to Base64 and embed them in the code. This may be slow. If there are too many images, it could freeze Figma.", isDefault: false, includedLanguages: ["HTML"], }, @@ -64,7 +57,8 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ itemType: "individual_select", propertyName: "embedVectors", label: "Embed Vectors", - description: "Convert Figma vectors to code.", + description: + "Convert Figma vectors to code. This is faster than embedding images, but still slower than not embedding.", isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, @@ -83,6 +77,16 @@ export const selectPreferenceOptions: SelectPreferenceOptions[] = [ ], includedLanguages: ["HTML"], }, + { + itemType: "select", + propertyName: "tailwindGenerationMode", + label: "Mode", + options: [ + { label: "HTML", value: "html" }, + { label: "React (JSX)", value: "jsx" }, + ], + includedLanguages: ["Tailwind"], + }, { itemType: "select", propertyName: "flutterGenerationMode", diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 2f60c731..5d8013dc 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -12,6 +12,7 @@ import { CopyButton } from "./CopyButton"; import EmptyState from "./EmptyState"; import SettingsGroup from "./SettingsGroup"; import CustomPrefixInput from "./CustomPrefixInput"; +import FrameworkTabs from "./FrameworkTabs"; interface CodePanelProps { code: string; @@ -83,7 +84,7 @@ const CodePanel = (props: CodePanelProps) => { ); // Define preference grouping based on property names - const essentialPropertyNames = ["jsx", "optimizeLayout"]; + const essentialPropertyNames = ["jsx"]; const stylingPropertyNames = [ "styledComponents", "exportCSS", @@ -91,6 +92,7 @@ const CodePanel = (props: CodePanelProps) => { "roundTailwindColors", "useColorVariables", "showLayerNames", + "optimizeLayout", "embedImages", "embedVectors", ]; @@ -143,38 +145,27 @@ const CodePanel = (props: CodePanelProps) => { {/* Framework-specific options */} {selectableSettingsFiltered.length > 0 && ( -
-

+

+

{selectedFramework} Options

-
- {selectableSettingsFiltered.map((preference) => ( -
- {preference.options.map((option) => ( - { - onPreferenceChanged( - preference.propertyName, - option.value, - ); - }} - buttonClass="bg-blue-100 dark:bg-black dark:ring-blue-800" - checkClass="bg-blue-400 dark:bg-black dark:bg-blue-500 dark:border-blue-500 ring-blue-300 border-blue-400" - /> - ))} -
- ))} -
+ {selectableSettingsFiltered.map((preference) => { + // Regular toggle buttons for other options + return ( + option.isDefault) + ?.value ?? + "") as string + } + onChange={(value) => { + onPreferenceChanged(preference.propertyName, value); + }} + /> + ); + })}
)} diff --git a/packages/plugin-ui/src/components/FrameworkTabs.tsx b/packages/plugin-ui/src/components/FrameworkTabs.tsx new file mode 100644 index 00000000..e39b9de4 --- /dev/null +++ b/packages/plugin-ui/src/components/FrameworkTabs.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +type Option = { + value: string; + label: string; +}; + +interface FrameworkTabsProps { + options: Option[]; + selectedValue: string; + onChange: (value: string) => void; +} + +const FrameworkTabs: React.FC = ({ + options, + selectedValue, + onChange, +}) => { + return ( +
+
+ {options.map((option) => { + const isSelected = option.value === selectedValue; + return ( + + ); + })} +
+
+ ); +}; + +export default FrameworkTabs; diff --git a/packages/plugin-ui/src/components/SelectableToggle.tsx b/packages/plugin-ui/src/components/SelectableToggle.tsx index faacea84..bcc55c19 100644 --- a/packages/plugin-ui/src/components/SelectableToggle.tsx +++ b/packages/plugin-ui/src/components/SelectableToggle.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Check, HelpCircle } from "lucide-react"; +import { cn } from "../lib/utils"; type SelectableToggleProps = { onSelect: (isSelected: boolean) => void; @@ -28,20 +29,19 @@ const SelectableToggle = ({
- {/* Tooltip */} + {/* Enhanced tooltip */} {showTooltip && description && ( -
- {description} +
+

+ {description} +

)} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 0bfc11da..a4fccee8 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -2,7 +2,6 @@ import "@figma/plugin-typings"; // Settings export type Framework = "Flutter" | "SwiftUI" | "HTML" | "Tailwind"; export interface HTMLSettings { - jsx: boolean; optimizeLayout: boolean; showLayerNames: boolean; embedImages: boolean; @@ -11,6 +10,7 @@ export interface HTMLSettings { htmlGenerationMode: "html" | "jsx" | "styled-components" | "svelte"; } export interface TailwindSettings extends HTMLSettings { + tailwindGenerationMode: "html" | "jsx"; roundTailwindValues: boolean; roundTailwindColors: boolean; useColorVariables: boolean; From 7c82b17cf6957737fd2455c63b5a2cda2445e9ff Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 03:06:34 -0300 Subject: [PATCH 032/134] Small tweaks --- apps/plugin/plugin-src/code.ts | 4 ++-- manifest.json | 1 + packages/backend/src/code.ts | 16 +++++++++------- packages/backend/src/html/htmlMain.ts | 13 +++++++++---- .../src/tailwind/tailwindDefaultBuilder.ts | 2 -- packages/backend/src/tailwind/tailwindMain.ts | 1 - 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index d466258f..08831db8 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -139,8 +139,8 @@ const standardMode = async () => { safeRun(userPluginSettings); }); - // Listen for document changes - figma.on("documentchange", () => { + // Listen for page changes + figma.on("currentpagechange", () => { console.log("[DEBUG] documentchange event triggered"); // Node: This was causing an infinite load when you try to export a background image from a group that contains children. // The reason for this is that the code will temporarily hide the children of the group in order to export a clean image diff --git a/manifest.json b/manifest.json index 888938bf..083d8ee0 100644 --- a/manifest.json +++ b/manifest.json @@ -7,6 +7,7 @@ "editorType": ["figma", "dev"], "capabilities": ["inspect", "codegen", "vscode"], "permissions": [], + "documentAccess": "dynamic-page", "networkAccess": { "allowedDomains": ["https://placehold.co"] }, diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 2486e702..2e58bae7 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -36,7 +36,7 @@ const GRADIENT_PROPERTIES = ["fills", "strokes", "effects"]; * @param node The node to process * @param optimizeLayout Whether to extract and include inferredAutoLayout data */ -const processNodeData = (node: any, optimizeLayout: boolean) => { +const processNodeData = async (node: any, optimizeLayout: boolean) => { if (node.id) { // Check if we need to fetch the Figma node at all const hasGradient = GRADIENT_PROPERTIES.some((propName) => { @@ -71,7 +71,7 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { node.type === "INSTANCE" || node.type === "TEXT" ) { - const figmaNode = figma.getNodeById(node.id); + const figmaNode = await figma.getNodeByIdAsync(node.id); if (!figmaNode) { return; @@ -168,7 +168,7 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { } } else { // Hopefully one day this won't be needed anymore. - const figmaNode = figma.getNodeById(node.id); + const figmaNode = await figma.getNodeByIdAsync(node.id); if (figmaNode && "width" in figmaNode) { node.width = (figmaNode as any).width; node.height = (figmaNode as any).height; @@ -196,9 +196,9 @@ const processNodeData = (node: any, optimizeLayout: boolean) => { // Process children recursively if (node.children && Array.isArray(node.children)) { - node.children.forEach((child: any) => - processNodeData(child, optimizeLayout), - ); + for (const child of node.children) { + await processNodeData(child, optimizeLayout); + } } }; @@ -227,7 +227,9 @@ export const nodesToJSON = async ( )) as SceneNode[]; // Process gradients and inferredAutoLayout in the JSON tree before adding parent references - nodeJson.forEach((node) => processNodeData(node, optimizeLayout)); + for (const node of nodeJson) { + await processNodeData(node, optimizeLayout); + } // Add parent references to all children in the node tree nodeJson.forEach((node) => addParentReferences(node)); diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 16a1aeb2..c2f85881 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -368,7 +368,6 @@ export const generateHTMLPreview = async ( { ...settings, htmlGenerationMode: "html", - jsx: false, }, true, ); @@ -386,7 +385,6 @@ const htmlWidgetGenerator = async ( sceneNode: ReadonlyArray, settings: HTMLSettings, ): Promise => { - console.log("htmlWidgetGenerator", sceneNode); // filter non visible nodes. This is necessary at this step because conversion already happened. const promiseOfConvertedCode = getVisibleNodes(sceneNode).map( convertNode(settings), @@ -630,7 +628,11 @@ const htmlContainer = async ( if (hasChildren) { builder.addStyles( - formatWithJSX("background-image", settings.jsx, `url(${imgUrl})`), + formatWithJSX( + "background-image", + settings.htmlGenerationMode === "jsx", + `url(${imgUrl})`, + ), ); } else { tag = "img"; @@ -655,7 +657,10 @@ const htmlContainer = async ( // Standard HTML approach for HTML, React, or Svelte if (children) { return `\n<${tag}${build}${src}>${indentString(children)}\n`; - } else if (selfClosingTags.includes(tag) || settings.jsx) { + } else if ( + selfClosingTags.includes(tag) || + settings.htmlGenerationMode === "jsx" + ) { return `\n<${tag}${build}${src} />`; } else { return `\n<${tag}${build}${src}>`; diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 0e72f947..e0d0731b 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -122,10 +122,8 @@ export class TailwindDefaultBuilder { position(): this { const { node, optimizeLayout } = this; - if (commonIsAbsolutePosition(node, optimizeLayout)) { const { x, y } = getCommonPositionValue(node); - console.log("x", x, y, "node", node); const parsedX = numberToFixedString(x); const parsedY = numberToFixedString(y); diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 3d95f7b4..8bee9378 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -48,7 +48,6 @@ const tailwindWidgetGenerator = async ( const convertNode = (settings: TailwindSettings) => async (node: SceneNode): Promise => { - console.log("altNode", node); if (settings.embedVectors && (node as any).canBeFlattened) { const altNode = await renderAndAttachSVG(node); if (altNode.svg) { From 791b04aa4757669f278396499d7f9d174a1f3570 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 13:09:40 -0300 Subject: [PATCH 033/134] Fix variables --- packages/backend/src/code.ts | 95 ++++++++++++++++--- .../backend/src/html/htmlDefaultBuilder.ts | 2 +- .../src/tailwind/builderImpl/tailwindColor.ts | 1 + .../backend/src/tailwind/conversionTables.ts | 20 ++-- .../src/tailwind/tailwindDefaultBuilder.ts | 1 + 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 2e58bae7..6a975ea8 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -12,6 +12,7 @@ import { postConversionComplete, postEmptyMessage } from "./messaging"; import { PluginSettings } from "types"; import { convertToCode } from "./common/retrieveUI/convertToCode"; import { generateHTMLPreview } from "./html/htmlMain"; +import { variableToColorName } from "./tailwind/conversionTables"; // Keep track of node names for sequential numbering const nodeNameCounters: Map = new Map(); @@ -31,12 +32,63 @@ const addParentReferences = (node: any) => { // Define all property paths that might contain gradients const GRADIENT_PROPERTIES = ["fills", "strokes", "effects"]; +/** + * Process color variables in a paint style and add pre-computed variable names + * @param paint The paint style to process (fill or stroke) + */ +const processColorVariables = async (paint: Paint) => { + if ( + paint.type === "GRADIENT_ANGULAR" || + paint.type === "GRADIENT_DIAMOND" || + paint.type === "GRADIENT_LINEAR" || + paint.type === "GRADIENT_RADIAL" + ) { + for (const stop of paint.gradientStops) { + if (stop.boundVariables?.color) { + (stop as any).variableColorName = await variableToColorName( + stop.boundVariables.color, + ); + } + } + } else if (paint.type === "SOLID" && paint.boundVariables?.color) { + // Pre-compute and store the variable name + (paint as any).variableColorName = await variableToColorName( + paint.boundVariables.color, + ); + } +}; + +const getColorVariables = async (node: any, settings: PluginSettings) => { + if (settings.useColorVariables) { + // Process color variables in fills and strokes + if (node.fills && Array.isArray(node.fills)) { + for (const fill of node.fills) { + await processColorVariables(fill); + } + } + + if (node.strokes && Array.isArray(node.strokes)) { + for (const stroke of node.strokes) { + await processColorVariables(stroke); + } + } + // Process color variables in effects if they exist + if (node.effects && Array.isArray(node.effects)) { + for (const effect of node.effects) { + if (effect.color) { + await processColorVariables(effect); + } + } + } + } +}; + /** * Recursively process node and its children to update with data not available in JSON * @param node The node to process * @param optimizeLayout Whether to extract and include inferredAutoLayout data */ -const processNodeData = async (node: any, optimizeLayout: boolean) => { +const processNodeData = async (node: any, settings: PluginSettings) => { if (node.id) { // Check if we need to fetch the Figma node at all const hasGradient = GRADIENT_PROPERTIES.some((propName) => { @@ -67,7 +119,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { // Handle additional node properties if ( hasGradient || - optimizeLayout || + settings.optimizeLayout || node.type === "INSTANCE" || node.type === "TEXT" ) { @@ -100,7 +152,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { // Handle text-specific properties if (figmaNode.type === "TEXT") { // Get the text segments - const styledTextSegments = figmaNode.getStyledTextSegments([ + let styledTextSegments = figmaNode.getStyledTextSegments([ "fontName", "fills", "fontSize", @@ -122,20 +174,32 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { const baseSegmentName = (node.uniqueName || node.name) .replace(/[^a-zA-Z0-9_-]/g, "") .toLowerCase(); + // Add a uniqueId to each segment - node.styledTextSegments = styledTextSegments.map( - (segment: any, index) => { + styledTextSegments = await Promise.all( + styledTextSegments.map(async (segment, index) => { const mutableSegment = Object.assign({}, segment); + + if (settings.useColorVariables && segment.fills) { + mutableSegment.fills = segment.fills.map((d) => ({ ...d })); + for (const fill of mutableSegment.fills) { + await processColorVariables(fill); + } + } + // For single segments, don't add index suffix if (styledTextSegments.length === 1) { - mutableSegment.uniqueId = `${baseSegmentName}_span`; + (mutableSegment as any).uniqueId = `${baseSegmentName}_span`; } else { // For multiple segments, add index suffix - mutableSegment.uniqueId = `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; + (mutableSegment as any).uniqueId = + `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; } return mutableSegment; - }, + }), ); + + node.styledTextSegments = styledTextSegments; } Object.assign(node, node.style); @@ -145,7 +209,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { } // Extract inferredAutoLayout if optimizeLayout is enabled - if (optimizeLayout && "inferredAutoLayout" in figmaNode) { + if (settings.optimizeLayout && "inferredAutoLayout" in figmaNode) { node.inferredAutoLayout = JSON.parse( JSON.stringify((figmaNode as any).inferredAutoLayout), ); @@ -159,6 +223,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { ) { node.variantProperties = figmaNode.variantProperties; } + // Always copy size and position if ("width" in figmaNode) { node.width = (figmaNode as any).width; @@ -177,6 +242,8 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { } } + await getColorVariables(node, settings); + // Set default layout properties if missing if (!node.layoutMode) node.layoutMode = "NONE"; if (!node.layoutGrow) node.layoutGrow = 0; @@ -197,7 +264,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { // Process children recursively if (node.children && Array.isArray(node.children)) { for (const child of node.children) { - await processNodeData(child, optimizeLayout); + await processNodeData(child, settings); } } }; @@ -210,7 +277,7 @@ const processNodeData = async (node: any, optimizeLayout: boolean) => { */ export const nodesToJSON = async ( nodes: ReadonlyArray, - optimizeLayout: boolean = false, + settings: PluginSettings, ): Promise => { // Reset name counters for each conversion nodeNameCounters.clear(); @@ -228,7 +295,7 @@ export const nodesToJSON = async ( // Process gradients and inferredAutoLayout in the JSON tree before adding parent references for (const node of nodeJson) { - await processNodeData(node, optimizeLayout); + await processNodeData(node, settings); } // Add parent references to all children in the node tree @@ -240,7 +307,7 @@ export const nodesToJSON = async ( export const run = async (settings: PluginSettings) => { clearWarnings(); - const { framework, optimizeLayout } = settings; + const { framework } = settings; const selection = figma.currentPage.selection; if (selection.length > 1) { @@ -249,7 +316,7 @@ export const run = async (settings: PluginSettings) => { ); } - const nodeJson = await nodesToJSON(selection, optimizeLayout); + const nodeJson = await nodesToJSON(selection, settings); console.log("nodeJson", nodeJson); // Now we work directly with the JSON nodes diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index da6cc673..eb0d0e9c 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -315,7 +315,7 @@ export class HtmlDefaultBuilder { const { node, settings } = this; const { width, height } = htmlSizePartial( node, - settings.jsx, + settings.htmlGenerationMode === "jsx", settings.optimizeLayout, ); diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 1e0df04a..bbfd6202 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -61,6 +61,7 @@ export const tailwindSolidColor = ( fill: SolidPaint | ColorStop, kind: TailwindColorType, ): string => { + console.log("fill is", fill); const { colorName } = getColorInfo(fill); const effectiveOpacity = calculateEffectiveOpacity(fill); diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index 3b52756d..296c3c68 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -140,11 +140,10 @@ export const nearestColorFromRgb = (color: RGB) => { return { name, value }; }; -export const variableToColorName = (alias: VariableAlias) => { +export const variableToColorName = async (alias: VariableAlias) => { return ( - figma.variables - .getVariableById(alias.id) - ?.name.replaceAll("/", "-") + (await figma.variables.getVariableByIdAsync(alias.id))?.name + .replaceAll("/", "-") .replaceAll(" ", "-") || alias.id.toLowerCase().replaceAll(":", "-") ); }; @@ -161,12 +160,15 @@ export function getColorInfo(fill: SolidPaint | ColorStop) { let hex: string = "#" + rgbTo6hex(fill.color); let meta: string = ""; + console.log( + "(fill as any).variableColorName", + fill, + (fill as any).variableColorName, + ); // variable - if ( - localTailwindSettings.useColorVariables && - fill.boundVariables?.color - ) { - colorName = variableToColorName(fill.boundVariables.color); + if ((fill as any).variableColorName) { + // Use pre-computed variable name if available + colorName = (fill as any).variableColorName; // || variableToColorName(fill.boundVariables.color); colorType = "variable"; meta = "custom"; diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index e0d0731b..9b740062 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -94,6 +94,7 @@ export class TailwindDefaultBuilder { } commonShapeStyles(): this { + console.log("this.node is", this.node); this.customColor((this.node as MinimalFillsMixin).fills, "bg"); this.radius(); this.shadow(); From e53287a11d34b51282da59d0fcdd0cc8ed4d7fd7 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 13:29:21 -0300 Subject: [PATCH 034/134] Fix color --- .../backend/src/html/builderImpl/htmlColor.ts | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 3defdc16..e23bb0e0 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -1,24 +1,25 @@ import { HTMLSettings } from "types"; import { numberToFixedString } from "../../common/numToAutoFixed"; import { retrieveTopFill } from "../../common/retrieveFill"; -import { variableToColorName } from "../../tailwind/conversionTables"; import { getGradientTransformCoordinates } from "../../common/color"; /** * Helper to process a color with variable binding if present */ -const processColorWithVariable = ( - color: RGB, - opacity: number = 1, - boundVariable?: VariableAlias, - useCustomColors: boolean = false, -): string => { - if (useCustomColors && boundVariable) { - const varName = variableToColorName(boundVariable); - const fallbackColor = htmlColor(color, opacity); +const processColorWithVariable = (fill: { + color: RGB; + opacity?: number; + variableColorName?: string; +}): string => { + const opacity = fill.opacity ?? 1; + + if (fill.variableColorName) { + const varName = fill.variableColorName; + console.log("varName is", varName); + const fallbackColor = htmlColor(fill.color, opacity); return `var(--${varName}, ${fallbackColor})`; } - return htmlColor(color, opacity); + return htmlColor(fill.color, opacity); }; /** @@ -26,12 +27,16 @@ const processColorWithVariable = ( */ const getColorAndVariable = ( fill: Paint, -): { color: RGB; opacity: number; boundVariable?: VariableAlias } => { +): { + color: RGB; + opacity: number; + variableColorName?: string; +} => { if (fill.type === "SOLID") { return { color: fill.color, opacity: fill.opacity ?? 1, - boundVariable: fill.boundVariables?.color, + variableColorName: (fill as any).variableColorName, }; } else if ( (fill.type === "GRADIENT_LINEAR" || @@ -44,7 +49,7 @@ const getColorAndVariable = ( return { color: firstStop.color, opacity: fill.opacity ?? 1, - boundVariable: firstStop.boundVariables?.color, + variableColorName: (firstStop as any).variableColorName, }; } return { color: { r: 0, g: 0, b: 0 }, opacity: 0 }; @@ -55,16 +60,10 @@ export const htmlColorFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"] | undefined, settings: HTMLSettings, ): string => { - const useCustomColors = settings.useColorVariables === true; const fill = retrieveTopFill(fills); if (fill) { - const { color, opacity, boundVariable } = getColorAndVariable(fill); - return processColorWithVariable( - color, - opacity, - boundVariable, - useCustomColors, - ); + const colorInfo = getColorAndVariable(fill); + return processColorWithVariable(colorInfo); } return ""; }; @@ -91,19 +90,21 @@ export const htmlColor = (color: RGB, alpha: number = 1): string => { }; // Process a single gradient stop with proper color and position -const processGradientStop = ( +const processGradientStop = async ( stop: ColorStop, useCustomColors: boolean, fillOpacity: number = 1, positionMultiplier: number = 100, unit: string = "%", -): string => { - const color = processColorWithVariable( - stop.color, - stop.color.a * fillOpacity, - stop.boundVariables?.color, - useCustomColors, - ); +): Promise => { + const fillInfo = { + color: stop.color, + opacity: stop.color.a * fillOpacity, + boundVariables: stop.boundVariables, + variableColorName: (stop as any).variableColorName, + }; + + const color = processColorWithVariable(fillInfo); const position = `${(stop.position * positionMultiplier).toFixed(0)}${unit}`; return `${color} ${position}`; }; @@ -129,10 +130,10 @@ const processGradientStops = ( .join(", "); }; -export const htmlGradientFromFills = ( +export const htmlGradientFromFills = async ( fills: ReadonlyArray | PluginAPI["mixed"], settings: HTMLSettings, -): string => { +): Promise => { const useCustomColors = settings.useColorVariables === true; const fill = retrieveTopFill(fills); if (!fill) return ""; @@ -140,9 +141,9 @@ export const htmlGradientFromFills = ( case "GRADIENT_LINEAR": return htmlLinearGradient(fill, useCustomColors); case "GRADIENT_ANGULAR": - return htmlAngularGradient(fill, useCustomColors); + return await htmlAngularGradient(fill, useCustomColors); case "GRADIENT_RADIAL": - return htmlRadialGradient(fill, useCustomColors); + return await htmlRadialGradient(fill, useCustomColors); case "GRADIENT_DIAMOND": return htmlDiamondGradient(fill, useCustomColors); // Added diamond gradient case default: @@ -168,10 +169,10 @@ export const cssGradientAngle = (angle: number): number => { return cssAngle < 0 ? cssAngle + 360 : cssAngle; }; -export const htmlLinearGradient = ( +export const htmlLinearGradient = async ( fill: GradientPaint, useCustomColors: boolean, -): string => { +): Promise => { const figmaAngle = gradientAngle2(fill); const angle = cssGradientAngle(figmaAngle).toFixed(0); const mappedFill = processGradientStops( From 95aff6b277d417edd09e914e17620f64139510df Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 15:34:15 -0300 Subject: [PATCH 035/134] Fix styled higlighting --- packages/backend/src/html/htmlMain.ts | 7 ++----- packages/plugin-ui/src/components/CodePanel.tsx | 13 ++++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index c2f85881..749b7217 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -289,14 +289,11 @@ function generateReactComponent( return `${imports.join("\n")} ${styledComponentsCode ? `\n${styledComponentsCode}` : ""} -const ${componentName} = () => { +export const ${componentName} = () => { return ( ${indentString(html, 4)} ); -}; - -export default ${componentName}; -`; +};`; } // Generate Svelte component from the collected styles and HTML diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 5d8013dc..1c36cdce 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -199,11 +199,14 @@ const CodePanel = (props: CodePanelProps) => { ) : ( Date: Thu, 6 Mar 2025 16:51:01 -0300 Subject: [PATCH 036/134] Fiz z positioning --- packages/backend/src/code.ts | 28 +++++++++++++++++++ .../backend/src/html/htmlDefaultBuilder.ts | 6 ++++ 2 files changed, 34 insertions(+) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 6a975ea8..e12020c1 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -266,7 +266,35 @@ const processNodeData = async (node: any, settings: PluginSettings) => { for (const child of node.children) { await processNodeData(child, settings); } + + // Handle itemReverseZIndex for absolute-positioned children + if ("itemReverseZIndex" in node && node.itemReverseZIndex) { + const absoluteChildren = node.children.filter( + (child: SceneNode) => + "layoutPositioning" in child && + child.layoutPositioning === "ABSOLUTE", + ); + const reversedAbsolute = [...absoluteChildren].reverse(); + let index = 0; + node.children = node.children.map((child: SceneNode) => { + if ( + "layoutPositioning" in child && + child.layoutPositioning === "ABSOLUTE" + ) { + return reversedAbsolute[index++]; + } else { + return child; + } + }); + } } + + // Process children recursively + // if (node.children && Array.isArray(node.children)) { + // for (const child of node.children) { + // await processNodeData(child, settings); + // } + // } }; /** diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index eb0d0e9c..f97990ff 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -122,6 +122,12 @@ export class HtmlDefaultBuilder { this.autoLayoutPadding(); this.position(); this.blend(); + + // Add z-index if we have a custom value from the itemReverseZIndex handling + if ((this.node as any).customZIndex !== undefined) { + this.addStyles(`z-index: ${(this.node as any).customZIndex}`); + } + return this; } From 6a2b5191af5d7d818e1c06199f86106a545c8162 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 16:52:01 -0300 Subject: [PATCH 037/134] Fix text --- packages/backend/src/code.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index e12020c1..c4e87975 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -267,8 +267,12 @@ const processNodeData = async (node: any, settings: PluginSettings) => { await processNodeData(child, settings); } - // Handle itemReverseZIndex for absolute-positioned children - if ("itemReverseZIndex" in node && node.itemReverseZIndex) { + // Handle itemReverseZIndex for absolute-positioned children on AutoLayout + if ( + "children" in node && + "itemReverseZIndex" in node && + node.itemReverseZIndex + ) { const absoluteChildren = node.children.filter( (child: SceneNode) => "layoutPositioning" in child && From 0218ee24f52ec458a2230ae82e42939ec90ea198 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 18:01:22 -0300 Subject: [PATCH 038/134] Fiz z-align --- packages/backend/src/code.ts | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index c4e87975..292ec0bd 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -83,6 +83,29 @@ const getColorVariables = async (node: any, settings: PluginSettings) => { } }; +function adjustChildrenOrder(node: any) { + if (!node.itemReverseZIndex || !node.children || node.layoutMode === "NONE") { + return; + } + + const children = node.children; + const absoluteChildren = []; + const fixedChildren = []; + + // Single pass to separate absolute and fixed children + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + if (child.layoutPositioning === "ABSOLUTE") { + absoluteChildren.push(child); + } else { + fixedChildren.unshift(child); // Add to beginning to maintain original order + } + } + + // Combine the arrays (reversed absolute children + original order fixed children) + node.children = [...absoluteChildren, ...fixedChildren]; +} + /** * Recursively process node and its children to update with data not available in JSON * @param node The node to process @@ -267,30 +290,7 @@ const processNodeData = async (node: any, settings: PluginSettings) => { await processNodeData(child, settings); } - // Handle itemReverseZIndex for absolute-positioned children on AutoLayout - if ( - "children" in node && - "itemReverseZIndex" in node && - node.itemReverseZIndex - ) { - const absoluteChildren = node.children.filter( - (child: SceneNode) => - "layoutPositioning" in child && - child.layoutPositioning === "ABSOLUTE", - ); - const reversedAbsolute = [...absoluteChildren].reverse(); - let index = 0; - node.children = node.children.map((child: SceneNode) => { - if ( - "layoutPositioning" in child && - child.layoutPositioning === "ABSOLUTE" - ) { - return reversedAbsolute[index++]; - } else { - return child; - } - }); - } + adjustChildrenOrder(node); } // Process children recursively From 5da861cf4066ac9e7003baacc9b89c1fa91a8ce4 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 18:26:36 -0300 Subject: [PATCH 039/134] Fix absolute positioning --- packages/backend/src/code.ts | 18 +++-- .../backend/src/common/nodeWidthHeight.ts | 75 ++++++++++++------- .../backend/src/html/htmlDefaultBuilder.ts | 9 ++- .../src/tailwind/tailwindDefaultBuilder.ts | 3 +- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 292ec0bd..4c1b9c48 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -144,6 +144,7 @@ const processNodeData = async (node: any, settings: PluginSettings) => { hasGradient || settings.optimizeLayout || node.type === "INSTANCE" || + node.type === "COMPONENT" || node.type === "TEXT" ) { const figmaNode = await figma.getNodeByIdAsync(node.id); @@ -239,11 +240,7 @@ const processNodeData = async (node: any, settings: PluginSettings) => { } // Extract component metadata from instances - if ( - node.type === "INSTANCE" && - "variantProperties" in figmaNode && - figmaNode.variantProperties - ) { + if ("variantProperties" in figmaNode && figmaNode.variantProperties) { node.variantProperties = figmaNode.variantProperties; } @@ -290,6 +287,17 @@ const processNodeData = async (node: any, settings: PluginSettings) => { await processNodeData(child, settings); } + if ( + node.layoutMode !== "NONE" && + "children" in node && + node.children.some( + (d: any) => + "layoutPositioning" in d && d.layoutPositioning === "ABSOLUTE", + ) + ) { + (node as any).isRelative = true; + } + adjustChildrenOrder(node); } diff --git a/packages/backend/src/common/nodeWidthHeight.ts b/packages/backend/src/common/nodeWidthHeight.ts index 119e5f2b..bd471ed4 100644 --- a/packages/backend/src/common/nodeWidthHeight.ts +++ b/packages/backend/src/common/nodeWidthHeight.ts @@ -22,30 +22,53 @@ export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { ? node.parent.inferredAutoLayout?.layoutMode : node.parent.layoutMode; - const isWidthFill = - (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutGrow === 1) || - (parentLayoutMode === "VERTICAL" && nodeAuto.layoutAlign === "STRETCH"); - const isHeightFill = - (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutAlign === "STRETCH") || - (parentLayoutMode === "VERTICAL" && nodeAuto.layoutGrow === 1); - const modesSwapped = parentLayoutMode === "HORIZONTAL"; - const primaryAxisMode = modesSwapped - ? "counterAxisSizingMode" - : "primaryAxisSizingMode"; - const counterAxisMode = modesSwapped - ? "primaryAxisSizingMode" - : "counterAxisSizingMode"; - - return { - width: isWidthFill - ? "fill" - : "layoutMode" in nodeAuto && nodeAuto[primaryAxisMode] === "AUTO" - ? null - : node.width, - height: isHeightFill - ? "fill" - : "layoutMode" in nodeAuto && nodeAuto[counterAxisMode] === "AUTO" - ? null - : node.height, - }; + // Check for explicit layout sizing properties + if ( + "layoutSizingHorizontal" in nodeAuto && + "layoutSizingVertical" in nodeAuto + ) { + const width = + nodeAuto.layoutSizingHorizontal === "FILL" + ? "fill" + : nodeAuto.layoutSizingHorizontal === "HUG" + ? null + : node.width; + + const height = + nodeAuto.layoutSizingVertical === "FILL" + ? "fill" + : nodeAuto.layoutSizingVertical === "HUG" + ? null + : node.height; + + return { width, height }; + } + + return { width: node.width, height: node.height }; + + // const isWidthFill = + // (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutGrow === 1) || + // (parentLayoutMode === "VERTICAL" && nodeAuto.layoutAlign === "STRETCH"); + // const isHeightFill = + // (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutAlign === "STRETCH") || + // (parentLayoutMode === "VERTICAL" && nodeAuto.layoutGrow === 1); + // const modesSwapped = parentLayoutMode === "HORIZONTAL"; + // const primaryAxisMode = modesSwapped + // ? "counterAxisSizingMode" + // : "primaryAxisSizingMode"; + // const counterAxisMode = modesSwapped + // ? "primaryAxisSizingMode" + // : "counterAxisSizingMode"; + + // return { + // width: isWidthFill + // ? "fill" + // : "layoutMode" in nodeAuto && nodeAuto[primaryAxisMode] === "AUTO" + // ? null + // : node.width, + // height: isHeightFill + // ? "fill" + // : "layoutMode" in nodeAuto && nodeAuto[counterAxisMode] === "AUTO" + // ? null + // : node.height, }; diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index f97990ff..e0b6dc0f 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -122,12 +122,12 @@ export class HtmlDefaultBuilder { this.autoLayoutPadding(); this.position(); this.blend(); - + // Add z-index if we have a custom value from the itemReverseZIndex handling if ((this.node as any).customZIndex !== undefined) { this.addStyles(`z-index: ${(this.node as any).customZIndex}`); } - + return this; } @@ -242,7 +242,8 @@ export class HtmlDefaultBuilder { node.type === "GROUP" || ("layoutMode" in node && ((optimizeLayout ? node.inferredAutoLayout : null) ?? node) - ?.layoutMode === "NONE") + ?.layoutMode === "NONE") || + (node as any).isRelative ) { this.addStyles(formatWithJSX("position", isJSX, "relative")); } @@ -424,6 +425,8 @@ export class HtmlDefaultBuilder { } if ("variantProperties" in this.node && this.node.variantProperties) { + console.log("this.node.variantProperties", this.node.variantProperties); + Object.entries(this.node.variantProperties) ?.map((prop) => formatDataAttribute(prop[0], prop[1])) .sort() diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 9b740062..d34b4bae 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -144,7 +144,8 @@ export class TailwindDefaultBuilder { node.type === "GROUP" || ("layoutMode" in node && ((optimizeLayout ? node.inferredAutoLayout : null) ?? node) - ?.layoutMode === "NONE") + ?.layoutMode === "NONE") || + (node as any).isRelative ) { this.addAttributes("relative"); } From 705bf39c0478324f0cd69fee6a8b86bdb3e8934b Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 21:27:09 -0300 Subject: [PATCH 040/134] Add gradient to tailwind --- .../backend/src/html/builderImpl/htmlColor.ts | 105 +++++++++--------- .../src/tailwind/builderImpl/tailwindColor.ts | 38 +++++-- 2 files changed, 83 insertions(+), 60 deletions(-) diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index e23bb0e0..2b1cde2e 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -1,7 +1,6 @@ import { HTMLSettings } from "types"; import { numberToFixedString } from "../../common/numToAutoFixed"; import { retrieveTopFill } from "../../common/retrieveFill"; -import { getGradientTransformCoordinates } from "../../common/color"; /** * Helper to process a color with variable binding if present @@ -90,13 +89,12 @@ export const htmlColor = (color: RGB, alpha: number = 1): string => { }; // Process a single gradient stop with proper color and position -const processGradientStop = async ( +const processGradientStop = ( stop: ColorStop, - useCustomColors: boolean, fillOpacity: number = 1, positionMultiplier: number = 100, unit: string = "%", -): Promise => { +): string => { const fillInfo = { color: stop.color, opacity: stop.color.a * fillOpacity, @@ -112,40 +110,32 @@ const processGradientStop = async ( // Process all gradient stops for any gradient type const processGradientStops = ( stops: ReadonlyArray, - useCustomColors: boolean, fillOpacity: number = 1, positionMultiplier: number = 100, unit: string = "%", ): string => { return stops .map((stop) => - processGradientStop( - stop, - useCustomColors, - fillOpacity, - positionMultiplier, - unit, - ), + processGradientStop(stop, fillOpacity, positionMultiplier, unit), ) .join(", "); }; -export const htmlGradientFromFills = async ( +export const htmlGradientFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"], settings: HTMLSettings, -): Promise => { - const useCustomColors = settings.useColorVariables === true; +): string => { const fill = retrieveTopFill(fills); if (!fill) return ""; switch (fill.type) { case "GRADIENT_LINEAR": - return htmlLinearGradient(fill, useCustomColors); + return htmlLinearGradient(fill); case "GRADIENT_ANGULAR": - return await htmlAngularGradient(fill, useCustomColors); + return htmlAngularGradient(fill); case "GRADIENT_RADIAL": - return await htmlRadialGradient(fill, useCustomColors); + return htmlRadialGradient(fill); // Updated to use radial gradient function case "GRADIENT_DIAMOND": - return htmlDiamondGradient(fill, useCustomColors); // Added diamond gradient case + return htmlDiamondGradient(fill); default: return ""; } @@ -169,15 +159,11 @@ export const cssGradientAngle = (angle: number): number => { return cssAngle < 0 ? cssAngle + 360 : cssAngle; }; -export const htmlLinearGradient = async ( - fill: GradientPaint, - useCustomColors: boolean, -): Promise => { +export const htmlLinearGradient = (fill: GradientPaint): string => { const figmaAngle = gradientAngle2(fill); const angle = cssGradientAngle(figmaAngle).toFixed(0); const mappedFill = processGradientStops( fill.gradientStops, - useCustomColors, fill.opacity ?? 1, 100, "%", @@ -187,47 +173,64 @@ export const htmlLinearGradient = async ( export const invertYCoordinate = (y: number): number => 1 - y; -export const htmlRadialGradient = ( - fill: GradientPaint, - useCustomColors: boolean, -): string => { +export const htmlAngularGradient = (fill: GradientPaint): string => { + const angle = gradientAngle2(fill).toFixed(0); + // Extract matrix components + const a = fill.gradientTransform[0][0]; + const b = fill.gradientTransform[0][1]; + const tx = fill.gradientTransform[0][2]; + const c = fill.gradientTransform[1][0]; + const d = fill.gradientTransform[1][1]; + const ty = fill.gradientTransform[1][2]; + // Compute center by transforming (0.5, 0.5) + const centerX = (a * 0.5 + b * 0.5 + tx) * 100; + const centerY = (c * 0.5 + d * 0.5 + ty) * 100; + const centerXPercent = centerX.toFixed(2); + const centerYPercent = centerY.toFixed(2); const mappedFill = processGradientStops( fill.gradientStops, - useCustomColors, fill.opacity ?? 1, - 100, - "%", + 360, + "deg", ); - const { centerX, centerY, radiusX, radiusY } = - getGradientTransformCoordinates(fill.gradientTransform); - return `radial-gradient(${radiusX}% ${radiusY}% at ${centerX}% ${centerY}%, ${mappedFill})`; + return `conic-gradient(from ${angle}deg at ${centerXPercent}% ${centerYPercent}%, ${mappedFill})`; }; -export const htmlAngularGradient = ( - fill: GradientPaint, - useCustomColors: boolean, -): string => { - const angle = gradientAngle2(fill).toFixed(0); - const centerX = (fill.gradientTransform[0][2] * 100).toFixed(2); - const centerY = (fill.gradientTransform[1][2] * 100).toFixed(2); - const mappedFill = processGradientStops( +export const htmlRadialGradient = (fill: GradientPaint): string => { + const [[a, b, tx], [c, d, ty]] = fill.gradientTransform; + + // Calculate inverse of the linear part of the gradientTransform matrix + const det = a * d - b * c; + if (Math.abs(det) < 1e-6) return ""; // Avoid division by zero + + const invDet = 1 / det; + const invA = d * invDet; + const invB = -b * invDet; + const invC = -c * invDet; + const invD = a * invDet; + + // Calculate center by solving inverse transform for (0.5, 0.5) + const cx = (invA * (0.5 - tx) + invB * (0.5 - ty)) * 100; + const cy = (invC * (0.5 - tx) + invD * (0.5 - ty)) * 100; + + // Calculate column vectors of inverse matrix + const col1Length = Math.sqrt(invA ** 2 + invC ** 2) * 100; + const col2Length = Math.sqrt(invB ** 2 + invD ** 2) * 100; + + // Get radii as half lengths of column vectors (sorted) + const radii = [col1Length / 2, col2Length / 2].sort((a, b) => b - a); + + const mappedStops = processGradientStops( fill.gradientStops, - useCustomColors, fill.opacity ?? 1, - 360, - "deg", ); - return `conic-gradient(from ${angle}deg at ${centerX}% ${centerY}%, ${mappedFill})`; + return `radial-gradient(ellipse ${radii[0].toFixed(2)}% ${radii[1].toFixed(2)}% at ${cx.toFixed(2)}% ${cy.toFixed(2)}%, ${mappedStops})`; }; // Added function for diamond gradient -export const htmlDiamondGradient = ( - fill: GradientPaint, - useCustomColors: boolean, -): string => { +export const htmlDiamondGradient = (fill: GradientPaint): string => { const stops = processGradientStops( fill.gradientStops, - useCustomColors, fill.opacity ?? 1, 50, // Adjusted multiplier for diamond gradient "%", diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index bbfd6202..15de851d 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -8,6 +8,13 @@ import { TailwindColorType } from "types"; import { addWarning } from "../../common/commonConversionWarnings"; import { retrieveTopFill } from "../../common/retrieveFill"; +// Import the HTML gradient functions +import { + htmlAngularGradient, + htmlRadialGradient, + htmlDiamondGradient, +} from "../../html/builderImpl/htmlColor"; + /** * Get a tailwind color value object * @param fill @@ -132,20 +139,33 @@ export const tailwindGradientFromFills = ( return tailwindGradient(fill); } - // Show warning if there's a non-linear gradient - if ( - fill.type === "GRADIENT_ANGULAR" || - fill.type === "GRADIENT_RADIAL" || - fill.type === "GRADIENT_DIAMOND" - ) { - addWarning( - "Gradients are not fully supported in Tailwind except for Linear Gradients.", - ); + // Use arbitrary values with HTML-based gradient syntax for other gradient types + if (fill.type === "GRADIENT_ANGULAR") { + return tailwindArbitraryGradient(htmlAngularGradient(fill)); + } + + if (fill.type === "GRADIENT_RADIAL") { + return tailwindArbitraryGradient(htmlRadialGradient(fill)); + } + + if (fill.type === "GRADIENT_DIAMOND") { + return tailwindArbitraryGradient(htmlDiamondGradient(fill)); } return ""; }; +/** + * Converts CSS gradient syntax to Tailwind arbitrary value syntax + * @param cssGradient The CSS gradient string (e.g., "radial-gradient(...)") + * @returns Tailwind class with arbitrary value (e.g., "bg-[radial-gradient(...)]") + */ +const tailwindArbitraryGradient = (cssGradient: string): string => { + // Replace spaces with underscores for Tailwind compatibility + const tailwindValue = cssGradient.replace(/\s+/g, "_"); + return `bg-[${tailwindValue}]`; +}; + export const tailwindGradient = (fill: GradientPaint): string => { const direction = gradientDirection(gradientAngle(fill)); From 4ffdb1add69f3e023c20c02daa873b3b06dc273c Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 21:36:58 -0300 Subject: [PATCH 041/134] Fix gradient --- packages/backend/src/tailwind/builderImpl/tailwindColor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 15de851d..00b323c4 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -149,7 +149,8 @@ export const tailwindGradientFromFills = ( } if (fill.type === "GRADIENT_DIAMOND") { - return tailwindArbitraryGradient(htmlDiamondGradient(fill)); + // Diamond is too complex, it is going to create 3 linear gradients, which gets too weird in Tailwind. + return ""; } return ""; From db60ab93c56887d05054ec33bdf502fd88507325 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 21:41:01 -0300 Subject: [PATCH 042/134] Clean up --- packages/backend/src/code.ts | 2 +- packages/backend/src/common/retrieveUI/retrieveColors.ts | 5 ++--- packages/backend/src/html/builderImpl/htmlColor.ts | 6 +----- packages/backend/src/tailwind/builderImpl/tailwindColor.ts | 2 -- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 4c1b9c48..2d59fab1 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -372,7 +372,7 @@ export const run = async (settings: PluginSettings) => { const code = await convertToCode(convertedSelection, settings); const htmlPreview = await generateHTMLPreview(convertedSelection, settings); const colors = retrieveGenericSolidUIColors(framework); - const gradients = retrieveGenericGradients(framework, settings); + const gradients = retrieveGenericGradients(framework); postConversionComplete({ code, diff --git a/packages/backend/src/common/retrieveUI/retrieveColors.ts b/packages/backend/src/common/retrieveUI/retrieveColors.ts index 4ce01345..b50a14ac 100644 --- a/packages/backend/src/common/retrieveUI/retrieveColors.ts +++ b/packages/backend/src/common/retrieveUI/retrieveColors.ts @@ -76,7 +76,6 @@ const convertSolidColor = ( export const retrieveGenericLinearGradients = ( framework: Framework, - settings: HTMLSettings, ): Array => { const selectionColors = figma.getSelectionColors(); const colorStr: Array = []; @@ -89,7 +88,7 @@ export const retrieveGenericLinearGradients = ( exportValue = flutterGradient(paint); break; case "HTML": - exportValue = htmlGradientFromFills([paint], settings); + exportValue = htmlGradientFromFills(paint); break; case "Tailwind": exportValue = tailwindGradient(paint); @@ -99,7 +98,7 @@ export const retrieveGenericLinearGradients = ( break; } colorStr.push({ - cssPreview: htmlGradientFromFills([paint], settings), + cssPreview: htmlGradientFromFills(paint), exportValue, }); } diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 2b1cde2e..6eec999a 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -121,11 +121,7 @@ const processGradientStops = ( .join(", "); }; -export const htmlGradientFromFills = ( - fills: ReadonlyArray | PluginAPI["mixed"], - settings: HTMLSettings, -): string => { - const fill = retrieveTopFill(fills); +export const htmlGradientFromFills = (fill: Paint): string => { if (!fill) return ""; switch (fill.type) { case "GRADIENT_LINEAR": diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 00b323c4..83d13733 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -5,14 +5,12 @@ import { nearestValue, } from "../conversionTables"; import { TailwindColorType } from "types"; -import { addWarning } from "../../common/commonConversionWarnings"; import { retrieveTopFill } from "../../common/retrieveFill"; // Import the HTML gradient functions import { htmlAngularGradient, htmlRadialGradient, - htmlDiamondGradient, } from "../../html/builderImpl/htmlColor"; /** From d9c92f4fa1dee071b80ce715cee855ae60df3002 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 22:07:13 -0300 Subject: [PATCH 043/134] Fix padding and border --- packages/backend/src/code.ts | 29 ++++++++++++------- .../src/html/builderImpl/htmlAutoLayout.ts | 4 +-- .../backend/src/html/builderImpl/htmlColor.ts | 1 - .../backend/src/html/htmlDefaultBuilder.ts | 11 +++++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 2d59fab1..69f9e692 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -235,7 +235,7 @@ const processNodeData = async (node: any, settings: PluginSettings) => { // Extract inferredAutoLayout if optimizeLayout is enabled if (settings.optimizeLayout && "inferredAutoLayout" in figmaNode) { node.inferredAutoLayout = JSON.parse( - JSON.stringify((figmaNode as any).inferredAutoLayout), + JSON.stringify(figmaNode.inferredAutoLayout), ); } @@ -246,22 +246,31 @@ const processNodeData = async (node: any, settings: PluginSettings) => { // Always copy size and position if ("width" in figmaNode) { - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; + node.width = figmaNode.width; + node.height = figmaNode.height; + node.x = figmaNode.x; + node.y = figmaNode.y; } } else { // Hopefully one day this won't be needed anymore. const figmaNode = await figma.getNodeByIdAsync(node.id); - if (figmaNode && "width" in figmaNode) { - node.width = (figmaNode as any).width; - node.height = (figmaNode as any).height; - node.x = (figmaNode as any).x; - node.y = (figmaNode as any).y; + if (figmaNode) { + if ("width" in figmaNode) { + node.width = figmaNode.width; + node.height = figmaNode.height; + node.x = figmaNode.x; + node.y = figmaNode.y; + } } } + if ("individualStrokeWeights" in node) { + node.strokeTopWeight = node.individualStrokeWeights.top; + node.strokeBottomWeight = node.individualStrokeWeights.bottom; + node.strokeLeftWeight = node.individualStrokeWeights.left; + node.strokeRightWeight = node.individualStrokeWeights.right; + } + await getColorVariables(node, settings); // Set default layout properties if missing diff --git a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts index 8ffe58cf..8bbcad5f 100644 --- a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts +++ b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts @@ -1,4 +1,4 @@ -import { HTMLSettings, PluginSettings } from "types"; +import { HTMLSettings } from "types"; import { formatMultipleJSXArray } from "../../common/parseJSX"; const getFlexDirection = (node: InferredAutoLayoutResult): string => @@ -60,5 +60,5 @@ export const htmlAutoLayoutProps = ( gap: getGap(autoLayout), display: getFlex(node, autoLayout), }, - settings.jsx, + settings.htmlGenerationMode === "jsx", ); diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 6eec999a..2d2e7fb5 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -14,7 +14,6 @@ const processColorWithVariable = (fill: { if (fill.variableColorName) { const varName = fill.variableColorName; - console.log("varName is", varName); const fallbackColor = htmlColor(fill.color, opacity); return `var(--${varName}, ${fallbackColor})`; } diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index e0b6dc0f..4b443491 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -298,7 +298,7 @@ export class HtmlDefaultBuilder { paint.type === "GRADIENT_ANGULAR" || paint.type === "GRADIENT_DIAMOND" ) { - return htmlGradientFromFills([paint], this.settings); + return htmlGradientFromFills(paint); } return ""; // Handle other paint types safely @@ -347,10 +347,15 @@ export class HtmlDefaultBuilder { autoLayoutPadding(): this { const { node, isJSX, optimizeLayout } = this; - if ("paddingLeft" in node) { + if ( + "paddingLeft" in node || + "paddingRight" in node || + "paddingTop" in node || + "paddingBottom" in node + ) { this.addStyles( ...htmlPadding( - (optimizeLayout ? node.inferredAutoLayout : null) ?? node, + (optimizeLayout ? (node as any).inferredAutoLayout : null) ?? node, isJSX, ), ); From 50cedca10dbd49ae6af58f03132de72b361ff3b4 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Thu, 6 Mar 2025 22:18:03 -0300 Subject: [PATCH 044/134] Allow changing background --- packages/plugin-ui/src/components/Preview.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/plugin-ui/src/components/Preview.tsx b/packages/plugin-ui/src/components/Preview.tsx index 46b4b9f6..9cc7461b 100644 --- a/packages/plugin-ui/src/components/Preview.tsx +++ b/packages/plugin-ui/src/components/Preview.tsx @@ -5,6 +5,7 @@ import { Minimize2, MonitorSmartphone, Smartphone, + Circle, } from "lucide-react"; const Preview: React.FC<{ @@ -13,6 +14,7 @@ const Preview: React.FC<{ const [expanded, setExpanded] = useState(false); const [viewMode, setViewMode] = useState<"desktop" | "mobile">("desktop"); const [animationClass, setAnimationClass] = useState(""); + const [bgColor, setBgColor] = useState<"white" | "black">("white"); // Define consistent dimensions regardless of mode const containerWidth = expanded ? 320 : 240; @@ -63,6 +65,16 @@ const Preview: React.FC<{ Preview
+ {/* Background Color Toggle */} + + {/* View Mode Toggle */}
+
+ )} + )}
From 3cc5b2d4eaf30bdf168788855363055d729c4fcc Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 14:10:48 -0300 Subject: [PATCH 047/134] Even faster memoization --- packages/backend/src/code.ts | 6 +++--- packages/plugin-ui/src/components/CodePanel.tsx | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 6525b164..519fc442 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -38,14 +38,14 @@ const addParentReferences = (node: any) => { } }; -const variableCache = new Map>(); +const variableCache = new Map(); const memoizedVariableToColorName = async ( variableId: string, ): Promise => { if (!variableCache.has(variableId)) { - const promise = variableToColorName(variableId); - variableCache.set(variableId, promise); + const colorName = await variableToColorName(variableId); + variableCache.set(variableId, colorName); } return variableCache.get(variableId)!; }; diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index be41219d..35dd5150 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -4,10 +4,9 @@ import { PluginSettings, SelectPreferenceOptions, } from "types"; -import { useMemo, useState, useEffect } from "react"; +import { useMemo, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coldarkDark as theme } from "react-syntax-highlighter/dist/esm/styles/prism"; -import SelectableToggle from "./SelectableToggle"; import { CopyButton } from "./CopyButton"; import EmptyState from "./EmptyState"; import SettingsGroup from "./SettingsGroup"; From 782dcaaf4c1ce6a8374e60eb86b723151d233705 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 20:36:47 -0300 Subject: [PATCH 048/134] Add min/max width/height. --- packages/backend/src/code.ts | 6 ++ .../backend/src/common/nodeWidthHeight.ts | 27 +++--- .../src/flutter/builderImpl/flutterSize.ts | 23 ++++- .../backend/src/flutter/flutterContainer.ts | 15 ++- packages/backend/src/flutter/flutterMain.ts | 3 +- .../backend/src/flutter/flutterTextBuilder.ts | 32 ++++++- .../backend/src/html/builderImpl/htmlSize.ts | 29 +++++- .../backend/src/html/htmlDefaultBuilder.ts | 7 +- .../src/swiftui/builderImpl/swiftuiSize.ts | 54 +++++------ .../src/swiftui/swiftuiDefaultBuilder.ts | 12 ++- packages/backend/src/swiftui/swiftuiMain.ts | 1 + .../backend/src/swiftui/swiftuiTextBuilder.ts | 4 +- .../src/tailwind/builderImpl/tailwindSize.ts | 91 ++++++++++++++----- .../backend/src/tailwind/conversionTables.ts | 24 ++++- .../src/tailwind/tailwindDefaultBuilder.ts | 9 +- 15 files changed, 249 insertions(+), 88 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 519fc442..10a18203 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -333,6 +333,12 @@ const processNodePair = async ( if (!jsonNode.layoutSizingHorizontal) jsonNode.layoutSizingHorizontal = "FIXED"; if (!jsonNode.layoutSizingVertical) jsonNode.layoutSizingVertical = "FIXED"; + if (!jsonNode.primaryAxisAlignItems) { + jsonNode.primaryAxisAlignItems = "MIN"; + } + if (!jsonNode.counterAxisAlignItems) { + jsonNode.counterAxisAlignItems = "MIN"; + } // If layout sizing is HUG but there are no children, set it to FIXED const hasChildren = diff --git a/packages/backend/src/common/nodeWidthHeight.ts b/packages/backend/src/common/nodeWidthHeight.ts index bd471ed4..dff8fb77 100644 --- a/packages/backend/src/common/nodeWidthHeight.ts +++ b/packages/backend/src/common/nodeWidthHeight.ts @@ -1,27 +1,11 @@ import { Size } from "types"; export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { - const hasLayout = - "layoutAlign" in node && node.parent && "layoutMode" in node.parent; - - if (!hasLayout) { - return { width: node.width, height: node.height }; - } - const nodeAuto = (optimizeLayout && "inferredAutoLayout" in node ? node.inferredAutoLayout : null) ?? node; - if ("layoutMode" in nodeAuto && nodeAuto.layoutMode === "NONE") { - return { width: node.width, height: node.height }; - } - - // const parentLayoutMode = node.parent.layoutMode; - const parentLayoutMode = optimizeLayout - ? node.parent.inferredAutoLayout?.layoutMode - : node.parent.layoutMode; - // Check for explicit layout sizing properties if ( "layoutSizingHorizontal" in nodeAuto && @@ -44,6 +28,17 @@ export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { return { width, height }; } + if ("layoutMode" in nodeAuto && nodeAuto.layoutMode === "NONE") { + return { width: node.width, height: node.height }; + } + + const hasLayout = + "layoutAlign" in node && node.parent && "layoutMode" in node.parent; + + if (!hasLayout) { + return { width: node.width, height: node.height }; + } + return { width: node.width, height: node.height }; // const isWidthFill = diff --git a/packages/backend/src/flutter/builderImpl/flutterSize.ts b/packages/backend/src/flutter/builderImpl/flutterSize.ts index 0cf193e7..5877d2bb 100644 --- a/packages/backend/src/flutter/builderImpl/flutterSize.ts +++ b/packages/backend/src/flutter/builderImpl/flutterSize.ts @@ -11,7 +11,7 @@ export const flutterSizeWH = (node: SceneNode): string => { export const flutterSize = ( node: SceneNode, optimizeLayout: boolean, -): { width: string; height: string; isExpanded: boolean } => { +): { width: string; height: string; isExpanded: boolean; constraints: Record } => { const size = nodeSize(node, optimizeLayout); let isExpanded: boolean = false; @@ -53,5 +53,24 @@ export const flutterSize = ( } } - return { width: propWidth, height: propHeight, isExpanded }; + // Handle min/max constraints + const constraints: Record = {}; + + if (node.minWidth !== undefined && node.minWidth !== null) { + constraints.minWidth = numberToFixedString(node.minWidth); + } + + if (node.maxWidth !== undefined && node.maxWidth !== null) { + constraints.maxWidth = numberToFixedString(node.maxWidth); + } + + if (node.minHeight !== undefined && node.minHeight !== null) { + constraints.minHeight = numberToFixedString(node.minHeight); + } + + if (node.maxHeight !== undefined && node.maxHeight !== null) { + constraints.maxHeight = numberToFixedString(node.maxHeight); + } + + return { width: propWidth, height: propHeight, isExpanded, constraints }; }; diff --git a/packages/backend/src/flutter/flutterContainer.ts b/packages/backend/src/flutter/flutterContainer.ts index a82e3419..aac9f96e 100644 --- a/packages/backend/src/flutter/flutterContainer.ts +++ b/packages/backend/src/flutter/flutterContainer.ts @@ -28,7 +28,10 @@ export const flutterContainer = ( // ignore for Groups const propBoxDecoration = getDecoration(node); - const { width, height, isExpanded } = flutterSize(node, optimizeLayout); + const { width, height, isExpanded, constraints } = flutterSize( + node, + optimizeLayout, + ); const clipBehavior = "clipsContent" in node && node.clipsContent === true @@ -46,6 +49,8 @@ export const flutterContainer = ( } let result: string; + const hasConstraints = constraints && Object.keys(constraints).length > 0; + if (width || height || propBoxDecoration || clipBehavior) { const parsedDecoration = skipDefaultProperty( propBoxDecoration, @@ -69,6 +74,14 @@ export const flutterContainer = ( result = child; } + // Apply constraints if any exist + if (hasConstraints) { + result = generateWidgetCode("ConstrainedBox", { + constraints: generateWidgetCode("BoxConstraints", constraints), + child: result, + }); + } + // Add Expanded() when parent is a Row/Column and width is full. if (isExpanded) { result = generateWidgetCode("Expanded", { diff --git a/packages/backend/src/flutter/flutterMain.ts b/packages/backend/src/flutter/flutterMain.ts index 9433b958..8dffb577 100644 --- a/packages/backend/src/flutter/flutterMain.ts +++ b/packages/backend/src/flutter/flutterMain.ts @@ -239,7 +239,6 @@ const makeRowColumn = ( // mainAxisSize: getFlex(node, autoLayout), mainAxisAlignment: getMainAxisAlignment(autoLayout), crossAxisAlignment: getCrossAxisAlignment(autoLayout), - children: [children], }; // Add spacing parameter if itemSpacing is set @@ -249,6 +248,8 @@ const makeRowColumn = ( addWarning("Flutter doesn't support negative itemSpacing"); } + widgetProps.children = [children]; + return generateWidgetCode(rowOrColumn, widgetProps); }; diff --git a/packages/backend/src/flutter/flutterTextBuilder.ts b/packages/backend/src/flutter/flutterTextBuilder.ts index b70f8d4e..40c334e3 100644 --- a/packages/backend/src/flutter/flutterTextBuilder.ts +++ b/packages/backend/src/flutter/flutterTextBuilder.ts @@ -168,7 +168,34 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder { textAutoSize(node: TextNode): this { let result = this.child; - + + // Get constraints for the node + const constraints: Record = {}; + + if (node.minWidth !== undefined && node.minWidth !== null) { + constraints.minWidth = numberToFixedString(node.minWidth); + } + + if (node.maxWidth !== undefined && node.maxWidth !== null) { + constraints.maxWidth = numberToFixedString(node.maxWidth); + } + + if (node.minHeight !== undefined && node.minHeight !== null) { + constraints.minHeight = numberToFixedString(node.minHeight); + } + + if (node.maxHeight !== undefined && node.maxHeight !== null) { + constraints.maxHeight = numberToFixedString(node.maxHeight); + } + + const hasConstraints = Object.keys(constraints).length > 0; + if (hasConstraints) { + result = generateWidgetCode("ConstrainedBox", { + constraints: generateWidgetCode("BoxConstraints", constraints), + child: result, + }); + } + switch (node.textAutoResize) { case "WIDTH_AND_HEIGHT": break; @@ -187,9 +214,8 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder { }); break; } - + result = wrapTextWithLayerBlur(node, result); - this.child = result; return this; } diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index 97e9151d..d6a3a7c1 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -6,11 +6,12 @@ export const htmlSizePartial = ( node: SceneNode, isJsx: boolean, optimizeLayout: boolean, -): { width: string; height: string } => { +): { width: string; height: string; constraints: string[] } => { if (isPreviewGlobal && node.parent === undefined) { return { width: formatWithJSX("width", isJsx, "100%"), height: formatWithJSX("height", isJsx, "100%"), + constraints: [], }; } @@ -50,5 +51,29 @@ export const htmlSizePartial = ( } } - return { width: w, height: h }; + // Handle min/max width/height constraints + const constraints = []; + + if (node.maxWidth !== undefined && node.maxWidth !== null) { + constraints.push(formatWithJSX("max-width", isJsx, node.maxWidth)); + } + + if (node.minWidth !== undefined && node.minWidth !== null) { + constraints.push(formatWithJSX("min-width", isJsx, node.minWidth)); + } + + if (node.maxHeight !== undefined && node.maxHeight !== null) { + constraints.push(formatWithJSX("max-height", isJsx, node.maxHeight)); + } + + if (node.minHeight !== undefined && node.minHeight !== null) { + constraints.push(formatWithJSX("min-height", isJsx, node.minHeight)); + } + + // Return constraints separately instead of appending to width/height + return { + width: w, + height: h, + constraints: constraints, + }; }; diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 364dcefd..7febce2e 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -320,7 +320,7 @@ export class HtmlDefaultBuilder { size(): this { const { node, settings } = this; - const { width, height } = htmlSizePartial( + const { width, height, constraints } = htmlSizePartial( node, settings.htmlGenerationMode === "jsx", settings.optimizeLayout, @@ -342,6 +342,11 @@ export class HtmlDefaultBuilder { this.addStyles(width, height); } + // Add constraints as separate styles + if (constraints.length > 0) { + this.addStyles(...constraints); + } + return this; } diff --git a/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts b/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts index c6f1d646..4b7fc13b 100644 --- a/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts +++ b/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts @@ -3,39 +3,39 @@ import { numberToFixedString } from "../../common/numToAutoFixed"; export const swiftuiSize = ( node: SceneNode, - optimizeLayout: boolean = false, -): { width: string; height: string } => { - const size = nodeSize(node, optimizeLayout); + optimize: boolean = false, +): { width: string; height: string; constraints: string[] } => { + const size = nodeSize(node, optimize); - // if width is set as maxWidth, height must also be set as maxHeight (not height) - const shouldExtend = size.height === "fill" || size.width === "fill"; + const constraintProps: string[] = []; + let width = ""; + let height = ""; - // this cast will always be true, since nodeWidthHeight was called with false to relative. - let propWidth = ""; + // Handle width and height if (typeof size.width === "number") { - const w = numberToFixedString(size.width); - - if (shouldExtend) { - propWidth = `minWidth: ${w}, maxWidth: ${w}`; - } else { - propWidth = `width: ${w}`; - } - } else if (size.width === "fill") { - propWidth = `maxWidth: .infinity`; + width = `width: ${numberToFixedString(size.width)}`; } - - let propHeight = ""; if (typeof size.height === "number") { - const h = numberToFixedString(size.height); + height = `height: ${numberToFixedString(size.height)}`; + } - if (shouldExtend) { - propHeight = `minHeight: ${h}, maxHeight: ${h}`; - } else { - propHeight = `height: ${h}`; - } - } else if (size.height === "fill") { - propHeight = `maxHeight: .infinity`; + // Handle min/max constraints + if (node.minWidth !== undefined && node.minWidth !== null) { + constraintProps.push(`minWidth: ${numberToFixedString(node.minWidth)}`); + } + if (node.maxWidth !== undefined && node.maxWidth !== null) { + constraintProps.push(`maxWidth: ${numberToFixedString(node.maxWidth)}`); + } + if (node.minHeight !== undefined && node.minHeight !== null) { + constraintProps.push(`minHeight: ${numberToFixedString(node.minHeight)}`); + } + if (node.maxHeight !== undefined && node.maxHeight !== null) { + constraintProps.push(`maxHeight: ${numberToFixedString(node.maxHeight)}`); } - return { width: propWidth, height: propHeight }; + return { + width, + height, + constraints: constraintProps, + }; }; diff --git a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts index 3648877f..a990adc4 100644 --- a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts @@ -139,10 +139,14 @@ export class SwiftuiDefaultBuilder { } size(node: SceneNode, optimize: boolean): this { - const { width, height } = swiftuiSize(node, optimize); - const sizes = [width, height].filter((d) => d); - if (sizes.length > 0) { - this.pushModifier([`frame`, sizes.join(", ")]); + const { width, height, constraints } = swiftuiSize(node, optimize); + if (width || height) { + this.pushModifier([`frame`, [width, height].filter(Boolean).join(", ")]); + } + + // Add constraints if any exist + if (constraints.length > 0) { + this.pushModifier([`frame`, constraints.join(", ")]); } return this; diff --git a/packages/backend/src/swiftui/swiftuiMain.ts b/packages/backend/src/swiftui/swiftuiMain.ts index ebf331cf..94a6610f 100644 --- a/packages/backend/src/swiftui/swiftuiMain.ts +++ b/packages/backend/src/swiftui/swiftuiMain.ts @@ -197,6 +197,7 @@ const createDirectionalStack = ( const getLayoutAlignment = ( inferredAutoLayout: InferredAutoLayoutResult, ): string => { + console.log("inferred is", inferredAutoLayout); switch (inferredAutoLayout.counterAxisAlignItems) { case "MIN": return inferredAutoLayout.layoutMode === "VERTICAL" ? ".leading" : ".top"; diff --git a/packages/backend/src/swiftui/swiftuiTextBuilder.ts b/packages/backend/src/swiftui/swiftuiTextBuilder.ts index 56307bd3..549e50d8 100644 --- a/packages/backend/src/swiftui/swiftuiTextBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiTextBuilder.ts @@ -179,7 +179,7 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { }; wrapTextAutoResize = (node: TextNode): string => { - const { width, height } = swiftuiSize(node); + const { width, height, constraints } = swiftuiSize(node); let comp: string[] = []; switch (node.textAutoResize) { @@ -194,6 +194,8 @@ export class SwiftuiTextBuilder extends SwiftuiDefaultBuilder { break; } + comp.push(...constraints); + if (comp.length > 0) { const align = this.textAlignment(node); return `.frame(${comp.join(", ")}${align})`; diff --git a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts index af7a183a..c3fef8c1 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts @@ -1,62 +1,103 @@ import { pxToLayoutSize } from "../conversionTables"; import { nodeSize } from "../../common/nodeWidthHeight"; -import { formatWithJSX } from "../../common/parseJSX"; +import { numberToFixedString } from "../../common/numToAutoFixed"; +import { TailwindSettings } from "types"; + +/** + * Formats a size value into a Tailwind class + * Uses Tailwind's standard classes if there's a good match, otherwise uses arbitrary values + */ +const formatTailwindSizeValue = ( + size: number, + prefix: string, + settings?: TailwindSettings, +): string => { + // Try to find a matching Tailwind size class + if (settings?.roundTailwindValues) { + const tailwindSize = pxToLayoutSize(size); + + // If we found a matching Tailwind class, use it + if (!tailwindSize.startsWith("[")) { + return `${prefix}-${tailwindSize}`; + } + } + + // No matching class or rounding disabled, use arbitrary value + const sizeFixed = numberToFixedString(size); + if (sizeFixed === "0") { + return `${prefix}-0`; + } else { + return `${prefix}-[${sizeFixed}px]`; + } +}; export const tailwindSizePartial = ( node: SceneNode, optimizeLayout: boolean, -): { width: string; height: string } => { + settings?: TailwindSettings, +): { width: string; height: string; constraints: string } => { const size = nodeSize(node, optimizeLayout); - const nodeParent = (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent ? node.parent.inferredAutoLayout : null) ?? node.parent; let w = ""; - if ( - typeof size.width === "number" && - "layoutSizingHorizontal" in node && - node.layoutSizingHorizontal === "FIXED" - ) { - w = `w-${pxToLayoutSize(size.width)}`; + if (typeof size.width === "number") { + w = formatTailwindSizeValue(size.width, "w", settings); } else if (size.width === "fill") { if ( nodeParent && "layoutMode" in nodeParent && nodeParent.layoutMode === "HORIZONTAL" ) { - w = `grow shrink basis-0`; + w = "flex-1"; } else { - w = `self-stretch`; + w = "self-stretch"; } } let h = ""; if (typeof size.height === "number") { - h = `h-${pxToLayoutSize(size.height)}`; + h = formatTailwindSizeValue(size.height, "h", settings); } else if (size.height === "fill") { if ( - size.height === "fill" && nodeParent && "layoutMode" in nodeParent && nodeParent.layoutMode === "VERTICAL" ) { - h = `grow shrink basis-0`; + h = "flex-1"; } else { - h = `self-stretch`; + h = "self-stretch"; } } - return { width: w, height: h }; -}; + // Handle min/max constraints in tailwind + const constraints = []; -export const htmlSizePartialForTailwind = ( - node: SceneNode, - isJSX: boolean, -): [string, string] => { - return [ - formatWithJSX("width", isJSX, node.width), - formatWithJSX("height", isJSX, node.height), - ]; + if (node.maxWidth !== undefined && node.maxWidth !== null) { + constraints.push(formatTailwindSizeValue(node.maxWidth, "max-w", settings)); + } + + if (node.minWidth !== undefined && node.minWidth !== null) { + constraints.push(formatTailwindSizeValue(node.minWidth, "min-w", settings)); + } + + if (node.maxHeight !== undefined && node.maxHeight !== null) { + constraints.push( + formatTailwindSizeValue(node.maxHeight, "max-h", settings), + ); + } + + if (node.minHeight !== undefined && node.minHeight !== null) { + constraints.push( + formatTailwindSizeValue(node.minHeight, "min-h", settings), + ); + } + + return { + width: w, + height: h, + constraints: constraints.join(" "), + }; }; diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index cd1a39c7..654db9fe 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -111,11 +111,29 @@ export const pxToBlur = (value: number): string | null => { }; export const pxToLayoutSize = (value: number): string => { - const tailwindValue = pxToTailwind(value, config.layoutSize); - if (tailwindValue) { - return tailwindValue; + // First check if it's a direct match to avoid rounding errors + const exactValue = Object.keys(config.layoutSize) + .map(Number) + .find((size) => Math.abs(value - size) < 0.05); + + if (exactValue !== undefined) { + return (config.layoutSize as any)[exactValue]; + } + + // If not an exact match but rounding is enabled, check for a close match + if (localTailwindSettings.roundTailwindValues) { + const thresholdValue = nearestValueWithThreshold( + value, + Object.keys(config.layoutSize).map(Number), + 15, // 15% threshold for layout sizes + ); + + if (thresholdValue !== null) { + return (config.layoutSize as any)[thresholdValue]; + } } + // No match found, return arbitrary value return `[${numberToFixedString(value)}px]`; }; diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index d34b4bae..7ccdf3f7 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -187,8 +187,8 @@ export class TailwindDefaultBuilder { // must be called before Position, because of the hasFixedSize attribute. size(): this { - const { node, optimizeLayout } = this; - const { width, height } = tailwindSizePartial(node, optimizeLayout); + const { node, optimizeLayout, settings } = this; + const { width, height, constraints } = tailwindSizePartial(node, optimizeLayout, settings); if (node.type === "TEXT") { switch (node.textAutoResize) { @@ -206,6 +206,11 @@ export class TailwindDefaultBuilder { this.addAttributes(width, height); } + // Add any min/max constraints + if (constraints) { + this.addAttributes(constraints); + } + return this; } From 8fc5523eafbfaf9a5f7f75bb9f789d97a1094660 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 21:38:10 -0300 Subject: [PATCH 049/134] Add custom base font --- apps/plugin/plugin-src/code.ts | 3 +- apps/plugin/ui-src/App.tsx | 2 +- .../src/tailwind/builderImpl/tailwindSize.ts | 11 +- .../backend/src/tailwind/conversionTables.ts | 42 +- packages/plugin-ui/src/PluginUI.tsx | 6 +- .../plugin-ui/src/components/CodePanel.tsx | 9 +- .../src/components/CustomPrefixInput.tsx | 413 +++++++++++------- .../src/components/SettingsGroup.tsx | 4 +- .../src/components/TailwindSettings.tsx | 245 +++++++++++ packages/types/src/types.ts | 1 + 10 files changed, 532 insertions(+), 204 deletions(-) create mode 100644 packages/plugin-ui/src/components/TailwindSettings.tsx diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 08831db8..2590c4b7 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -33,6 +33,7 @@ export const defaultPluginSettings: PluginSettings = { embedVectors: false, htmlGenerationMode: "html", tailwindGenerationMode: "jsx", + baseFontSize: 16, }; // A helper type guard to ensure the key belongs to the PluginSettings type @@ -175,7 +176,7 @@ const codegenMode = async () => { node, ); - const nodeJson = await nodesToJSON([node]); + const nodeJson = await nodesToJSON([node], userPluginSettings); const convertedSelection = await convertIntoNodes(nodeJson, null); console.log( "[DEBUG] codegen.generate - Converted selection:", diff --git a/apps/plugin/ui-src/App.tsx b/apps/plugin/ui-src/App.tsx index d7cca718..1f5e80a2 100644 --- a/apps/plugin/ui-src/App.tsx +++ b/apps/plugin/ui-src/App.tsx @@ -124,7 +124,7 @@ export default function App() { }; const handlePreferencesChange = ( key: keyof PluginSettings, - value: boolean | string, + value: boolean | string | number, ) => { if (state.settings && state.settings[key] === value) { // do nothing diff --git a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts index c3fef8c1..60208415 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts @@ -12,14 +12,11 @@ const formatTailwindSizeValue = ( prefix: string, settings?: TailwindSettings, ): string => { - // Try to find a matching Tailwind size class - if (settings?.roundTailwindValues) { - const tailwindSize = pxToLayoutSize(size); + const tailwindSize = pxToLayoutSize(size); - // If we found a matching Tailwind class, use it - if (!tailwindSize.startsWith("[")) { - return `${prefix}-${tailwindSize}`; - } + // If we found a matching Tailwind class, use it + if (!tailwindSize.startsWith("[")) { + return `${prefix}-${tailwindSize}`; } // No matching class or rounding disabled, use arbitrary value diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index 654db9fe..e8f591f2 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -44,7 +44,7 @@ export const exactValue = ( /** * convert pixel values to Tailwind attributes. * by default, Tailwind uses rem, while Figma uses px. - * Therefore, a conversion is necessary. Rem = Pixel / 16.abs + * Therefore, a conversion is necessary. Rem = Pixel / baseFontSize * Then, find in the corresponding table the closest value. */ const pxToRemToTailwind = ( @@ -52,7 +52,9 @@ const pxToRemToTailwind = ( conversionMap: Record, ): string => { const keys = Object.keys(conversionMap).map((d) => +d); - const remValue = value / 16; + // Use the configured base font size or fall back to default 16px + const baseFontSize = localTailwindSettings.baseFontSize || 16; + const remValue = value / baseFontSize; const convertedValue = exactValue(remValue, keys); if (convertedValue) { @@ -76,6 +78,8 @@ const pxToTailwind = ( const keys = Object.keys(conversionMap).map((d) => +d); const convertedValue = exactValue(value, keys); + console.log("convertedValue", convertedValue); + if (convertedValue) { return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { @@ -111,30 +115,16 @@ export const pxToBlur = (value: number): string | null => { }; export const pxToLayoutSize = (value: number): string => { - // First check if it's a direct match to avoid rounding errors - const exactValue = Object.keys(config.layoutSize) - .map(Number) - .find((size) => Math.abs(value - size) < 0.05); - - if (exactValue !== undefined) { - return (config.layoutSize as any)[exactValue]; - } - - // If not an exact match but rounding is enabled, check for a close match - if (localTailwindSettings.roundTailwindValues) { - const thresholdValue = nearestValueWithThreshold( - value, - Object.keys(config.layoutSize).map(Number), - 15, // 15% threshold for layout sizes - ); - - if (thresholdValue !== null) { - return (config.layoutSize as any)[thresholdValue]; - } - } - - // No match found, return arbitrary value - return `[${numberToFixedString(value)}px]`; + // Scale the input value according to the base font size ratio + const baseFontSize = localTailwindSettings.baseFontSize || 16; + // If baseFontSize is different than 16, we need to adjust the pixel value + // For example, with baseFontSize=14, 7px should match with the key for 8px (w-2) + const scaledValue = (value * 16) / baseFontSize; + + // Use pxToTailwind directly with the scaled value, since the keys in config.layoutSize + // are likely in pixels based on a 16px base font size + const result = pxToTailwind(scaledValue, config.layoutSize); + return result !== null ? result : `[${numberToFixedString(value)}px]`; }; export const nearestOpacity = (nodeOpacity: number): number => { diff --git a/packages/plugin-ui/src/PluginUI.tsx b/packages/plugin-ui/src/PluginUI.tsx index fede84a3..9546508e 100644 --- a/packages/plugin-ui/src/PluginUI.tsx +++ b/packages/plugin-ui/src/PluginUI.tsx @@ -31,7 +31,7 @@ type PluginUIProps = { settings: PluginSettings | null; onPreferenceChanged: ( key: keyof PluginSettings, - value: boolean | string, + value: boolean | string | number, ) => void; colors: SolidColorConversion[]; gradients: LinearGradientConversion[]; @@ -97,9 +97,9 @@ export const PluginUI = (props: PluginUIProps) => { {isEmpty === false && props.htmlPreview && ( )} - + {warnings.length > 0 && } - + void; } @@ -202,9 +203,9 @@ const CodePanel = (props: CodePanelProps) => { onPreferenceChanged={onPreferenceChanged} > {selectedFramework === "Tailwind" && ( - )} diff --git a/packages/plugin-ui/src/components/CustomPrefixInput.tsx b/packages/plugin-ui/src/components/CustomPrefixInput.tsx index 2d6bd669..fe9caa9d 100644 --- a/packages/plugin-ui/src/components/CustomPrefixInput.tsx +++ b/packages/plugin-ui/src/components/CustomPrefixInput.tsx @@ -1,177 +1,270 @@ import React, { useState, useRef, useEffect } from "react"; import { HelpCircle, Check } from "lucide-react"; -interface CustomPrefixInputProps { - initialValue: string; - onValueChange: (value: string) => void; +interface FormFieldProps { + // Common props + label: string; + initialValue: string | number; + onValueChange: (value: string | number) => void; + placeholder?: string; + helpText?: string; + + // Validation props + type?: "text" | "number"; + min?: number; + max?: number; + suffix?: string; + + // For text input validation + disallowedPattern?: RegExp; + disallowedMessage?: string; + + // Optional preview (for text inputs) + showPreview?: boolean; + previewExamples?: string[]; + previewTransform?: (value: string, example: string) => React.ReactNode; } -const CustomPrefixInput = React.memo(({ initialValue, onValueChange }: CustomPrefixInputProps) => { - // Use internal state to manage the input value - const [inputValue, setInputValue] = useState(initialValue); - const [isFocused, setIsFocused] = useState(false); - const [hasChanges, setHasChanges] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const inputRef = useRef(null); - - // Update internal state when initialValue changes (from parent) - useEffect(() => { - setInputValue(initialValue); - setHasChanges(false); - }, [initialValue]); - - const examples = ["flex"]; - const hasInvalidChars = /\s/.test(inputValue); - - const handleChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - setInputValue(newValue); - setHasChanges(newValue !== initialValue); - }; - - const applyChanges = () => { - if (hasInvalidChars) return; - - onValueChange(inputValue); - setHasChanges(false); - - // Show success indicator briefly - setShowSuccess(true); - setTimeout(() => setShowSuccess(false), 1500); - }; - - const handleBlur = () => { - setIsFocused(false); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - applyChanges(); - inputRef.current?.blur(); - } - }; - - return ( -
-
- - -
- -
- Add a prefix to all generated Tailwind classes. -
- Useful for avoiding conflicts with existing CSS. -
-
+const FormField = React.memo( + ({ + label, + initialValue, + onValueChange, + placeholder, + helpText, + type = "text", + min, + max, + suffix, + disallowedPattern = /\s/, + disallowedMessage = "Input cannot contain spaces", + showPreview = false, + previewExamples = ["flex"], + previewTransform, + }: FormFieldProps) => { + // Use internal state to manage the input value + const [inputValue, setInputValue] = useState(String(initialValue)); + const [isFocused, setIsFocused] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [hasError, setHasError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const inputRef = useRef(null); + + // Update internal state when initialValue changes (from parent) + useEffect(() => { + setInputValue(String(initialValue)); + setHasChanges(false); + setHasError(false); + setErrorMessage(""); + }, [initialValue]); + + const validateInput = (value: string): boolean => { + // Text validation + if (type === "text") { + if (disallowedPattern && disallowedPattern.test(value)) { + setHasError(true); + setErrorMessage(disallowedMessage); + return false; + } + setHasError(false); + setErrorMessage(""); + return true; + } + + // Number validation + if (type === "number") { + // Check for non-numeric characters + if (/[^0-9]/.test(value)) { + setHasError(true); + setErrorMessage("Only numbers are allowed"); + return false; + } + + const numValue = parseInt(value, 10); + + if (isNaN(numValue)) { + setHasError(true); + setErrorMessage("Please enter a valid number"); + return false; + } + + if (min !== undefined && numValue < min) { + setHasError(true); + setErrorMessage(`Minimum value is ${min}`); + return false; + } + + if (max !== undefined && numValue > max) { + setHasError(true); + setErrorMessage(`Maximum value is ${max}`); + return false; + } + + setHasError(false); + setErrorMessage(""); + return true; + } + + return true; + }; + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + validateInput(newValue); + setHasChanges(newValue !== String(initialValue)); + }; + + const applyChanges = () => { + if (hasError) return; + + if (type === "number") { + const numValue = parseInt(inputValue, 10); + if (!isNaN(numValue)) { + onValueChange(numValue); + } + } else { + onValueChange(inputValue); + } + + setHasChanges(false); + + // Show success indicator briefly + setShowSuccess(true); + setTimeout(() => setShowSuccess(false), 1500); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + applyChanges(); + inputRef.current?.blur(); + } + }; + + // Default preview transform for text prefixes + const defaultPreviewTransform = (value: string, example: string) => ( +
+
+ {value} + {example} +
+ +
+ {example}
- - {showSuccess && ( - - Applied - - )}
+ ); -
-
- setIsFocused(true)} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - placeholder="e.g., tw-" - className={`p-1.5 px-2.5 border rounded-md text-sm w-full transition-all focus:outline-none ${ - hasInvalidChars - ? "border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20" - : isFocused - ? "border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800" - : "border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500" - }`} - /> - - {hasInvalidChars && ( -

- Prefix cannot contain spaces -

+ const renderPreview = previewTransform || defaultPreviewTransform; + + return ( +
+
+ + + {helpText && ( +
+ +
+ {helpText} +
+
+
+ )} + + {showSuccess && ( + + Applied + )}
- - {hasChanges && ( - - )} -
- {inputValue && !hasInvalidChars && ( -
-

- Preview{hasChanges ? " (not applied yet)" : ""}: -

-
- {examples.map((example) => ( -
-
- - {inputValue} - - - {example} - -
- - → +
+
+
+ setIsFocused(true)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + placeholder={placeholder} + className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-none ${ + suffix ? "rounded-l-md" : "rounded-md" + } ${ + hasError + ? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20" + : isFocused + ? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800" + : "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500" + }`} + /> + + {suffix && ( + + {suffix} -
- {example} -
-
- ))} + )} +
+ + {hasError && ( +

{errorMessage}

+ )}
- + {hasChanges && ( -

- Press Enter or click Done to apply changes -

+ )}
- )} -
- ); -}); - -CustomPrefixInput.displayName = "CustomPrefixInput"; - -// Add a keyframe for fade-in-out animation -if (typeof document !== 'undefined') { - const style = document.createElement('style'); - style.innerHTML = ` - @keyframes fadeInOut { - 0% { opacity: 0; } - 20% { opacity: 1; } - 80% { opacity: 1; } - 100% { opacity: 0; } - } - .animate-fade-in-out { - animation: fadeInOut 1.5s ease-in-out; - } - `; - document.head.appendChild(style); -} -export default CustomPrefixInput; + {showPreview && inputValue && !hasError && ( +
+

+ Preview{hasChanges ? " (not applied yet)" : ""}: +

+
+ {previewExamples.map((example) => ( + + {renderPreview(inputValue, example)} + + ))} +
+ + {hasChanges && ( +

+ Press Enter or click Done to apply changes +

+ )} +
+ )} +
+ ); + }, +); + +FormField.displayName = "FormField"; + +export default FormField; diff --git a/packages/plugin-ui/src/components/SettingsGroup.tsx b/packages/plugin-ui/src/components/SettingsGroup.tsx index 8115d31d..2137c191 100644 --- a/packages/plugin-ui/src/components/SettingsGroup.tsx +++ b/packages/plugin-ui/src/components/SettingsGroup.tsx @@ -35,14 +35,14 @@ const SettingsGroup: React.FC = ({
{alwaysExpanded ? (
- + {title}
) : ( + )} +
+
+ ); +}; + +interface TailwindSettingsProps { + settings: PluginSettings | null; + onPreferenceChanged: ( + key: keyof PluginSettings, + value: boolean | string | number, + ) => void; +} + +export const TailwindSettings: React.FC = ({ + settings, + onPreferenceChanged, +}) => { + if (!settings) return null; + + const handleCustomPrefixChange = (newValue: string) => { + onPreferenceChanged("customTailwindPrefix", newValue); + }; + const handleBaseFontSizeChange = (value: number) => { + onPreferenceChanged("baseFontSize", value); + }; + + return ( +
+

+ Advanced Settings +

+ + {/* Advanced Settings Section */} +
+ {/* Class name prefix setting */} +
+ { + handleCustomPrefixChange(d as any); + }} + placeholder="e.g., tw-" + helpText="Add a prefix to all generated Tailwind classes. Useful for avoiding conflicts with existing CSS. Default is empty." + type="text" + showPreview={true} + /> +

+ Add a custom prefix to all Tailwind classes (e.g. "tw-") +

+
+ + {/* Base font size setting */} +
+ { + handleBaseFontSizeChange(d as any); + }} + placeholder="16" + suffix="px" + type="number" + min={1} + max={100} + /> +

+ Use this value to calculate rem values (default: 16px) +

+
+
+
+ ); +}; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index a4fccee8..6cac3a3f 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -16,6 +16,7 @@ export interface TailwindSettings extends HTMLSettings { useColorVariables: boolean; customTailwindPrefix?: string; embedVectors: boolean; + baseFontSize: number; } export interface FlutterSettings { flutterGenerationMode: string; From 813563b6df0b24390f3728cb807fc2407deff278 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 21:38:13 -0300 Subject: [PATCH 050/134] Update dependencies --- apps/debug/next-env.d.ts | 2 +- apps/debug/package.json | 2 +- apps/plugin/package.json | 18 +- package.json | 4 +- packages/backend/package.json | 4 +- packages/eslint-config-custom/package.json | 6 +- packages/plugin-ui/package.json | 4 +- packages/types/package.json | 2 +- pnpm-lock.yaml | 1683 +++++++++----------- 9 files changed, 746 insertions(+), 979 deletions(-) diff --git a/apps/debug/next-env.d.ts b/apps/debug/next-env.d.ts index a4a7b3f5..52e831b4 100644 --- a/apps/debug/next-env.d.ts +++ b/apps/debug/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/debug/package.json b/apps/debug/package.json index 2a589dec..933ddf9a 100644 --- a/apps/debug/package.json +++ b/apps/debug/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "backend": "workspace:*", - "next": "^14.2.24", + "next": "^15.2.1", "plugin-ui": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/apps/plugin/package.json b/apps/plugin/package.json index 9a3d3f6f..ecdd83c7 100644 --- a/apps/plugin/package.json +++ b/apps/plugin/package.json @@ -13,9 +13,9 @@ "@figma/plugin-typings": "^1.108.0", "backend": "workspace:*", "clsx": "^2.1.1", - "lucide-react": "^0.477.0", - "motion": "^12.4.9", - "nanoid": "^5.1.2", + "lucide-react": "^0.479.0", + "motion": "^12.4.10", + "nanoid": "^5.1.3", "plugin-ui": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -23,18 +23,18 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@types/node": "^20.17.21", + "@types/node": "^22.13.9", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.26.0", + "@typescript-eslint/parser": "^8.26.0", "@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react-swc": "^3.8.0", "autoprefixer": "^10.4.20", - "concurrently": "^8.2.2", - "esbuild": "^0.23.1", + "concurrently": "^9.1.2", + "esbuild": "^0.25.0", "eslint-config-custom": "workspace:*", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "postcss": "^8.5.3", "tailwindcss": "3.4.6", diff --git a/package.json b/package.json index 41e66f7a..8a8cc0c2 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "format": "prettier --write \"**/*.{ts,tsx,css,md}\"" }, "devDependencies": { - "eslint": "^9.21.0", + "eslint": "^9.22.0", "eslint-config-custom": "workspace:*", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "turbo": "^2.4.4", "typescript": "^5.8.2" } diff --git a/packages/backend/package.json b/packages/backend/package.json index 9669b663..fa412603 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,7 +15,7 @@ "dependencies": { "@figma/plugin-typings": "^1.108.0", "js-base64": "^3.7.7", - "nanoid": "^5.1.2", + "nanoid": "^5.1.3", "react": "19.0.0", "react-dom": "19.0.0", "types": "workspace:*" @@ -23,7 +23,7 @@ "devDependencies": { "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "eslint": "^9.21.0", + "eslint": "^9.22.0", "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", "tsup": "^8.4.0", diff --git a/packages/eslint-config-custom/package.json b/packages/eslint-config-custom/package.json index 513f6bd8..f905f154 100644 --- a/packages/eslint-config-custom/package.json +++ b/packages/eslint-config-custom/package.json @@ -4,10 +4,10 @@ "main": "index.js", "license": "MIT", "dependencies": { - "eslint-config-next": "^14.2.24", - "eslint-config-prettier": "^9.1.0", + "eslint-config-next": "^15.2.1", + "eslint-config-prettier": "^10.1.1", "eslint-config-turbo": "^2.4.4", - "eslint-plugin-react": "7.35.0" + "eslint-plugin-react": "7.37.4" }, "publishConfig": { "access": "public" diff --git a/packages/plugin-ui/package.json b/packages/plugin-ui/package.json index 7c61b749..041aee42 100644 --- a/packages/plugin-ui/package.json +++ b/packages/plugin-ui/package.json @@ -15,7 +15,7 @@ "@types/react-syntax-highlighter": "15.5.13", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", - "lucide-react": "^0.477.0", + "lucide-react": "^0.479.0", "react": "^19.0.0", "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^3.0.2", @@ -23,7 +23,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "eslint": "^9.21.0", + "eslint": "^9.22.0", "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", "types": "workspace:*", diff --git a/packages/types/package.json b/packages/types/package.json index 7bffd087..c8e011dd 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -19,7 +19,7 @@ "tsconfig": "workspace:*" }, "devDependencies": { - "eslint": "^9.21.0", + "eslint": "^9.22.0", "eslint-config-custom": "workspace:*", "typescript": "^5.8.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 756ec514..e36aa4dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,14 +9,14 @@ importers: .: devDependencies: eslint: - specifier: ^9.21.0 - version: 9.21.0(jiti@1.21.7) + specifier: ^9.22.0 + version: 9.22.0(jiti@1.21.7) eslint-config-custom: specifier: workspace:* version: link:packages/eslint-config-custom prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 turbo: specifier: ^2.4.4 version: 2.4.4 @@ -30,8 +30,8 @@ importers: specifier: workspace:* version: link:../../packages/backend next: - specifier: ^14.2.24 - version: 14.2.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^15.2.1 + version: 15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui @@ -85,14 +85,14 @@ importers: specifier: ^2.1.1 version: 2.1.1 lucide-react: - specifier: ^0.477.0 - version: 0.477.0(react@19.0.0) + specifier: ^0.479.0 + version: 0.479.0(react@19.0.0) motion: - specifier: ^12.4.9 - version: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^12.4.10 + version: 12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nanoid: - specifier: ^5.1.2 - version: 5.1.2 + specifier: ^5.1.3 + version: 5.1.3 plugin-ui: specifier: workspace:* version: link:../../packages/plugin-ui @@ -110,8 +110,8 @@ importers: version: 1.0.7(tailwindcss@3.4.6) devDependencies: '@types/node': - specifier: ^20.17.21 - version: 20.17.21 + specifier: ^22.13.9 + version: 22.13.9 '@types/react': specifier: ^19.0.10 version: 19.0.10 @@ -119,35 +119,35 @@ importers: specifier: ^19.0.4 version: 19.0.4(@types/react@19.0.10) '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) + specifier: ^8.26.0 + version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) + specifier: ^8.26.0 + version: 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@5.4.14(@types/node@20.17.21)) + version: 4.3.4(vite@5.4.14(@types/node@22.13.9)) '@vitejs/plugin-react-swc': specifier: ^3.8.0 - version: 3.8.0(@swc/helpers@0.5.12)(vite@5.4.14(@types/node@20.17.21)) + version: 3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.9)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.3) concurrently: - specifier: ^8.2.2 - version: 8.2.2 + specifier: ^9.1.2 + version: 9.1.2 esbuild: - specifier: ^0.23.1 - version: 0.23.1 + specifier: ^0.25.0 + version: 0.25.0 eslint-config-custom: specifier: workspace:* version: link:../../packages/eslint-config-custom eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@9.21.0(jiti@1.21.7)) + specifier: ^5.2.0 + version: 5.2.0(eslint@9.22.0(jiti@1.21.7)) eslint-plugin-react-refresh: specifier: ^0.4.19 - version: 0.4.19(eslint@9.21.0(jiti@1.21.7)) + version: 0.4.19(eslint@9.22.0(jiti@1.21.7)) postcss: specifier: ^8.5.3 version: 8.5.3 @@ -165,10 +165,10 @@ importers: version: 5.8.2 vite: specifier: ^5.4.14 - version: 5.4.14(@types/node@20.17.21) + version: 5.4.14(@types/node@22.13.9) vite-plugin-singlefile: specifier: ^2.1.0 - version: 2.1.0(rollup@4.34.8)(vite@5.4.14(@types/node@20.17.21)) + version: 2.1.0(rollup@4.34.9)(vite@5.4.14(@types/node@22.13.9)) packages/backend: dependencies: @@ -179,8 +179,8 @@ importers: specifier: ^3.7.7 version: 3.7.7 nanoid: - specifier: ^5.1.2 - version: 5.1.2 + specifier: ^5.1.3 + version: 5.1.3 react: specifier: 19.0.0 version: 19.0.0 @@ -198,8 +198,8 @@ importers: specifier: ^19.0.4 version: 19.0.4(@types/react@19.0.10) eslint: - specifier: ^9.21.0 - version: 9.21.0(jiti@1.21.7) + specifier: ^9.22.0 + version: 9.22.0(jiti@1.21.7) eslint-config-custom: specifier: workspace:* version: link:../eslint-config-custom @@ -208,7 +208,7 @@ importers: version: link:../tsconfig tsup: specifier: ^8.4.0 - version: 8.4.0(@swc/core@1.11.5(@swc/helpers@0.5.12))(jiti@1.21.7)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.0) + version: 8.4.0(@swc/core@1.11.8(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.0) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -216,17 +216,17 @@ importers: packages/eslint-config-custom: dependencies: eslint-config-next: - specifier: ^14.2.24 - version: 14.2.24(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) + specifier: ^15.2.1 + version: 15.2.1(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@9.21.0(jiti@1.21.7)) + specifier: ^10.1.1 + version: 10.1.1(eslint@9.22.0(jiti@1.21.7)) eslint-config-turbo: specifier: ^2.4.4 - version: 2.4.4(eslint@9.21.0(jiti@1.21.7))(turbo@2.4.4) + version: 2.4.4(eslint@9.22.0(jiti@1.21.7))(turbo@2.4.4) eslint-plugin-react: - specifier: 7.35.0 - version: 7.35.0(eslint@9.21.0(jiti@1.21.7)) + specifier: 7.37.4 + version: 7.37.4(eslint@9.22.0(jiti@1.21.7)) packages/plugin-ui: dependencies: @@ -246,8 +246,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 lucide-react: - specifier: ^0.477.0 - version: 0.477.0(react@19.0.0) + specifier: ^0.479.0 + version: 0.479.0(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -265,8 +265,8 @@ importers: version: 1.0.7(tailwindcss@3.4.6) devDependencies: eslint: - specifier: ^9.21.0 - version: 9.21.0(jiti@1.21.7) + specifier: ^9.22.0 + version: 9.22.0(jiti@1.21.7) eslint-config-custom: specifier: workspace:* version: link:../eslint-config-custom @@ -298,8 +298,8 @@ importers: version: link:../tsconfig devDependencies: eslint: - specifier: ^9.21.0 - version: 9.21.0(jiti@1.21.7) + specifier: ^9.22.0 + version: 9.22.0(jiti@1.21.7) eslint-config-custom: specifier: workspace:* version: link:../eslint-config-custom @@ -400,18 +400,15 @@ packages: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.0': resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} engines: {node: '>=18'} @@ -424,12 +421,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.0': resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} engines: {node: '>=18'} @@ -442,12 +433,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.0': resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} engines: {node: '>=18'} @@ -460,12 +445,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.0': resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} engines: {node: '>=18'} @@ -478,12 +457,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.0': resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} engines: {node: '>=18'} @@ -496,12 +469,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.0': resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} engines: {node: '>=18'} @@ -514,12 +481,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.0': resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} engines: {node: '>=18'} @@ -532,12 +493,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} engines: {node: '>=18'} @@ -550,12 +505,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.0': resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} engines: {node: '>=18'} @@ -568,12 +517,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.0': resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} engines: {node: '>=18'} @@ -586,12 +529,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.0': resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} engines: {node: '>=18'} @@ -604,12 +541,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.0': resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} engines: {node: '>=18'} @@ -622,12 +553,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.0': resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} engines: {node: '>=18'} @@ -640,12 +565,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.0': resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} engines: {node: '>=18'} @@ -658,12 +577,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.0': resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} engines: {node: '>=18'} @@ -676,12 +589,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.0': resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} engines: {node: '>=18'} @@ -694,12 +601,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.0': resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} engines: {node: '>=18'} @@ -718,24 +619,12 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.25.0': resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} engines: {node: '>=18'} @@ -748,12 +637,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} engines: {node: '>=18'} @@ -766,12 +649,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.0': resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} engines: {node: '>=18'} @@ -784,12 +661,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.0': resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} engines: {node: '>=18'} @@ -802,12 +673,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.0': resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} engines: {node: '>=18'} @@ -820,12 +685,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.0': resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} engines: {node: '>=18'} @@ -846,6 +705,10 @@ packages: resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.1.0': + resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.12.0': resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -854,8 +717,8 @@ packages: resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.21.0': - resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} + '@eslint/js@9.22.0': + resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -889,6 +752,111 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -911,62 +879,56 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@next/env@14.2.24': - resolution: {integrity: sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==} + '@next/env@15.2.1': + resolution: {integrity: sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==} - '@next/eslint-plugin-next@14.2.24': - resolution: {integrity: sha512-FDL3qs+5DML0AJz56DCVr+KnFYivxeAX73En8QbPw9GjJZ6zbfvqDy+HrarHFzbsIASn7y8y5ySJ/lllSruNVQ==} + '@next/eslint-plugin-next@15.2.1': + resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==} - '@next/swc-darwin-arm64@14.2.24': - resolution: {integrity: sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==} + '@next/swc-darwin-arm64@15.2.1': + resolution: {integrity: sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.24': - resolution: {integrity: sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==} + '@next/swc-darwin-x64@15.2.1': + resolution: {integrity: sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.24': - resolution: {integrity: sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==} + '@next/swc-linux-arm64-gnu@15.2.1': + resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.24': - resolution: {integrity: sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==} + '@next/swc-linux-arm64-musl@15.2.1': + resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.24': - resolution: {integrity: sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==} + '@next/swc-linux-x64-gnu@15.2.1': + resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.24': - resolution: {integrity: sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==} + '@next/swc-linux-x64-musl@15.2.1': + resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.24': - resolution: {integrity: sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==} + '@next/swc-win32-arm64-msvc@15.2.1': + resolution: {integrity: sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.24': - resolution: {integrity: sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.24': - resolution: {integrity: sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==} + '@next/swc-win32-x64-msvc@15.2.1': + resolution: {integrity: sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -991,98 +953,98 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.34.8': - resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} + '@rollup/rollup-android-arm-eabi@4.34.9': + resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.8': - resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} + '@rollup/rollup-android-arm64@4.34.9': + resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.8': - resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} + '@rollup/rollup-darwin-arm64@4.34.9': + resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.8': - resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} + '@rollup/rollup-darwin-x64@4.34.9': + resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.8': - resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} + '@rollup/rollup-freebsd-arm64@4.34.9': + resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.8': - resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} + '@rollup/rollup-freebsd-x64@4.34.9': + resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': - resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==} + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': + resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.34.8': - resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==} + '@rollup/rollup-linux-arm-musleabihf@4.34.9': + resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.8': - resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} + '@rollup/rollup-linux-arm64-gnu@4.34.9': + resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.8': - resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} + '@rollup/rollup-linux-arm64-musl@4.34.9': + resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': - resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': + resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': - resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': + resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.34.8': - resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} + '@rollup/rollup-linux-riscv64-gnu@4.34.9': + resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.8': - resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} + '@rollup/rollup-linux-s390x-gnu@4.34.9': + resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.8': - resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} + '@rollup/rollup-linux-x64-gnu@4.34.9': + resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.8': - resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} + '@rollup/rollup-linux-x64-musl@4.34.9': + resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.34.8': - resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} + '@rollup/rollup-win32-arm64-msvc@4.34.9': + resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.8': - resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} + '@rollup/rollup-win32-ia32-msvc@4.34.9': + resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.8': - resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} + '@rollup/rollup-win32-x64-msvc@4.34.9': + resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} cpu: [x64] os: [win32] @@ -1092,68 +1054,68 @@ packages: '@rushstack/eslint-patch@1.10.5': resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==} - '@swc/core-darwin-arm64@1.11.5': - resolution: {integrity: sha512-GEd1hzEx0mSGkJYMFMGLnrGgjL2rOsOsuYWyjyiA3WLmhD7o+n/EWBDo6mzD/9aeF8dzSPC0TnW216gJbvrNzA==} + '@swc/core-darwin-arm64@1.11.8': + resolution: {integrity: sha512-rrSsunyJWpHN+5V1zumndwSSifmIeFQBK9i2RMQQp15PgbgUNxHK5qoET1n20pcUrmZeT6jmJaEWlQchkV//Og==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.11.5': - resolution: {integrity: sha512-toz04z9wAClVvQSEY3xzrgyyeWBAfMWcKG4K0ugNvO56h/wczi2ZHRlnAXZW1tghKBk3z6MXqa/srfXgNhffKw==} + '@swc/core-darwin-x64@1.11.8': + resolution: {integrity: sha512-44goLqQuuo0HgWnG8qC+ZFw/qnjCVVeqffhzFr9WAXXotogVaxM8ze6egE58VWrfEc8me8yCcxOYL9RbtjhS/Q==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.5': - resolution: {integrity: sha512-5SjmKxXdwbBpsYGTpgeXOXMIjS563/ntRGn8Zc12H/c4VfPrRLGhgbJ/48z2XVFyBLcw7BCHZyFuVX1+ZI3W0Q==} + '@swc/core-linux-arm-gnueabihf@1.11.8': + resolution: {integrity: sha512-Mzo8umKlhTWwF1v8SLuTM1z2A+P43UVhf4R8RZDhzIRBuB2NkeyE+c0gexIOJBuGSIATryuAF4O4luDu727D1w==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.11.5': - resolution: {integrity: sha512-pydIlInHRzRIwB0NHblz3Dx58H/bsi0I5F2deLf9iOmwPNuOGcEEZF1Qatc7YIjP5DFbXK+Dcz+pMUZb2cc2MQ==} + '@swc/core-linux-arm64-gnu@1.11.8': + resolution: {integrity: sha512-EyhO6U+QdoGYC1MeHOR0pyaaSaKYyNuT4FQNZ1eZIbnuueXpuICC7iNmLIOfr3LE5bVWcZ7NKGVPlM2StJEcgA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.11.5': - resolution: {integrity: sha512-LhBHKjkZq5tJF1Lh0NJFpx7ROnCWLckrlIAIdSt9XfOV+zuEXJQOj+NFcM1eNk17GFfFyUMOZyGZxzYq5dveEQ==} + '@swc/core-linux-arm64-musl@1.11.8': + resolution: {integrity: sha512-QU6wOkZnS6/QuBN1MHD6G2BgFxB0AclvTVGbqYkRA7MsVkcC29PffESqzTXnypzB252/XkhQjoB2JIt9rPYf6A==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.11.5': - resolution: {integrity: sha512-dCi4xkxXlsk5sQYb3i413Cfh7+wMJeBYTvBZTD5xh+/DgRtIcIJLYJ2tNjWC4/C2i5fj+Ze9bKNSdd8weRWZ3A==} + '@swc/core-linux-x64-gnu@1.11.8': + resolution: {integrity: sha512-r72onUEIU1iJi9EUws3R28pztQ/eM3EshNpsPRBfuLwKy+qn3et55vXOyDhIjGCUph5Eg2Yn8H3h6MTxDdLd+w==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.11.5': - resolution: {integrity: sha512-K0AC4TreM5Oo/tXNXnE/Gf5+5y/HwUdd7xvUjOpZddcX/RlsbYOKWLgOtA3fdFIuta7XC+vrGKmIhm5l70DSVQ==} + '@swc/core-linux-x64-musl@1.11.8': + resolution: {integrity: sha512-294k8cLpO103++f4ZUEDr3vnBeUfPitW6G0a3qeVZuoXFhFgaW7ANZIWknUc14WiLOMfMecphJAEiy9C8OeYSw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.11.5': - resolution: {integrity: sha512-wzum8sYUsvPY7kgUfuqVYTgIPYmBC8KPksoNM1fz5UfhudU0ciQuYvUBD47GIGOevaoxhLkjPH4CB95vh1mJ9w==} + '@swc/core-win32-arm64-msvc@1.11.8': + resolution: {integrity: sha512-EbjOzQ+B85rumHyeesBYxZ+hq3ZQn+YAAT1ZNE9xW1/8SuLoBmHy/K9YniRGVDq/2NRmp5kI5+5h5TX0asIS9A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.11.5': - resolution: {integrity: sha512-lco7mw0TPRTpVPR6NwggJpjdUkAboGRkLrDHjIsUaR+Y5+0m5FMMkHOMxWXAbrBS5c4ph7QErp4Lma4r9Mn5og==} + '@swc/core-win32-ia32-msvc@1.11.8': + resolution: {integrity: sha512-Z+FF5kgLHfQWIZ1KPdeInToXLzbY0sMAashjd/igKeP1Lz0qKXVAK+rpn6ASJi85Fn8wTftCGCyQUkRVn0bTDg==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.11.5': - resolution: {integrity: sha512-E+DApLSC6JRK8VkDa4bNsBdD7Qoomx1HvKVZpOXl9v94hUZI5GMExl4vU5isvb+hPWL7rZ0NeI7ITnVLgLJRbA==} + '@swc/core-win32-x64-msvc@1.11.8': + resolution: {integrity: sha512-j6B6N0hChCeAISS6xp/hh6zR5CSCr037BAjCxNLsT8TGe5D+gYZ57heswUWXRH8eMKiRDGiLCYpPB2pkTqxCSw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.11.5': - resolution: {integrity: sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA==} + '@swc/core@1.11.8': + resolution: {integrity: sha512-UAL+EULxrc0J73flwYHfu29mO8CONpDJiQv1QPDXsyCvDUcEhqAqUROVTgC+wtJCFFqMQdyr4stAA5/s0KSOmA==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '*' @@ -1164,11 +1126,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.12': - resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} - - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@swc/types@0.1.19': resolution: {integrity: sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==} @@ -1197,9 +1156,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.17.21': - resolution: {integrity: sha512-yw1WZ94lZpdZbpnaF+WRvlN/Sx2EZWe/YZVdK4mC4u02/ql6Ozen8qbRJhOtltOxCg97/kpijhGs5X6STwkvbg==} - '@types/node@22.13.9': resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} @@ -1217,109 +1173,51 @@ packages: '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/eslint-plugin@8.25.0': - resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==} + '@typescript-eslint/eslint-plugin@8.26.0': + resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.25.0': - resolution: {integrity: sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==} + '@typescript-eslint/parser@8.26.0': + resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/scope-manager@8.25.0': - resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} + '@typescript-eslint/scope-manager@8.26.0': + resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/type-utils@8.25.0': - resolution: {integrity: sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==} + '@typescript-eslint/type-utils@8.26.0': + resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.25.0': - resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} + '@typescript-eslint/types@8.26.0': + resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/typescript-estree@8.25.0': - resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} + '@typescript-eslint/typescript-estree@8.26.0': + resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - - '@typescript-eslint/utils@8.25.0': - resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} + '@typescript-eslint/utils@8.26.0': + resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.25.0': - resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} + '@typescript-eslint/visitor-keys@8.26.0': + resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react-swc@3.8.0': @@ -1338,8 +1236,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1387,10 +1285,6 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -1433,8 +1327,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.10.2: - resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1485,8 +1379,8 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} callsites@3.1.0: @@ -1497,8 +1391,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001701: - resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} + caniuse-lite@1.0.30001702: + resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1539,6 +1433,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -1549,9 +1450,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concurrently@8.2.2: - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} - engines: {node: ^14.13.0 || >=16.0.0} + concurrently@9.1.2: + resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} + engines: {node: '>=18'} hasBin: true consola@3.4.0: @@ -1591,10 +1492,6 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1623,13 +1520,13 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -1648,8 +1545,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.109: - resolution: {integrity: sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==} + electron-to-chromium@1.5.113: + resolution: {integrity: sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1698,11 +1595,6 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.25.0: resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} engines: {node: '>=18'} @@ -1716,17 +1608,17 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@14.2.24: - resolution: {integrity: sha512-9r1ujK++Pgpfixr5+DQ6rXDIQmSzuDbBlAQYMkJRMz9KWqovX7ESUTC0EAyBfOCl3ubkoeplw+aoXDuih3A8fw==} + eslint-config-next@15.2.1: + resolution: {integrity: sha512-mhsprz7l0no8X+PdDnVHF4dZKu9YBJp2Rf6ztWbXBLJ4h6gxmW//owbbGJMBVUU+PibGJDAqZhW4pt8SC8HSow==} peerDependencies: - eslint: ^7.23.0 || ^8.0.0 + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' peerDependenciesMeta: typescript: optional: true - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + eslint-config-prettier@10.1.1: + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -1790,25 +1682,19 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - - eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705: - resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react-refresh@0.4.19: resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==} peerDependencies: eslint: '>=8.40' - eslint-plugin-react@7.35.0: - resolution: {integrity: sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==} + eslint-plugin-react@7.37.4: + resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 @@ -1819,8 +1705,8 @@ packages: eslint: '>6.6.0' turbo: '>2.0.0' - eslint-scope@8.2.0: - resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: @@ -1831,8 +1717,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.21.0: - resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} + eslint@9.22.0: + resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1864,6 +1750,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1922,8 +1812,8 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@12.4.9: - resolution: {integrity: sha512-c+nDhfiNUwi8G4BrhrP2hjPsDHzIKRbUhDlcK7oC5kXY4QK1IrT/kuhY4BgK6h2ujDrZ8ocvFrG2X8+b1m/MkQ==} + framer-motion@12.4.10: + resolution: {integrity: sha512-3Msuyjcr1Pb5hjkn4EJcRe1HumaveP0Gbv4DBMKTPKcV/1GSMkQXj+Uqgneys+9DPcZM18Hac9qY9iUEF5LZtg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1982,11 +1872,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -2003,10 +1888,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2082,6 +1963,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2201,10 +2085,6 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2308,8 +2188,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.477.0: - resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==} + lucide-react@0.479.0: + resolution: {integrity: sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2339,14 +2219,14 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.4.5: - resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} + motion-dom@12.4.10: + resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==} - motion-utils@12.0.0: - resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} + motion-utils@12.4.10: + resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==} - motion@12.4.9: - resolution: {integrity: sha512-lOT2+X9b3yvDEC+pAClTzLSW/D5T/zZweO+UN1lMe86WtGFQIbHU/VjEhwGREW0QryG9KECB1uK3QJo8G3NGag==} + motion@12.4.10: + resolution: {integrity: sha512-AM21Lyfn7ZHO+nBuHJEA2REFgS3kUM83CLZnzM0ZY1/sVeKGkCtV4LF4O/YsQXyZ9mrUrrnTaUkKquS4eaIYjg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2365,34 +2245,37 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.9: + resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.2: - resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==} + nanoid@5.1.3: + resolution: {integrity: sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==} engines: {node: ^18 || >=20} hasBin: true natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@14.2.24: - resolution: {integrity: sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==} - engines: {node: '>=18.17.0'} + next@15.2.1: + resolution: {integrity: sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true '@playwright/test': optional: true + babel-plugin-react-compiler: + optional: true sass: optional: true @@ -2484,10 +2367,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2578,8 +2457,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.5.2: - resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true @@ -2678,8 +2557,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.34.8: - resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} + rollup@4.34.9: + resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2725,6 +2604,10 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2757,9 +2640,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -2772,9 +2654,6 @@ packages: space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - spawn-command@0.0.2: - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} - stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -2829,13 +2708,13 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: '@babel/core': optional: true @@ -2904,12 +2783,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-api-utils@2.0.1: resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} engines: {node: '>=18.12'} @@ -3007,9 +2880,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -3253,10 +3123,12 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@esbuild/aix-ppc64@0.21.5': + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.23.1': + '@esbuild/aix-ppc64@0.21.5': optional: true '@esbuild/aix-ppc64@0.25.0': @@ -3265,144 +3137,96 @@ snapshots: '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.23.1': - optional: true - '@esbuild/android-arm64@0.25.0': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.23.1': - optional: true - '@esbuild/android-arm@0.25.0': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.23.1': - optional: true - '@esbuild/android-x64@0.25.0': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.23.1': - optional: true - '@esbuild/darwin-arm64@0.25.0': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.23.1': - optional: true - '@esbuild/darwin-x64@0.25.0': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.23.1': - optional: true - '@esbuild/freebsd-arm64@0.25.0': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.23.1': - optional: true - '@esbuild/freebsd-x64@0.25.0': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.23.1': - optional: true - '@esbuild/linux-arm64@0.25.0': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.23.1': - optional: true - '@esbuild/linux-arm@0.25.0': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.23.1': - optional: true - '@esbuild/linux-ia32@0.25.0': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.23.1': - optional: true - '@esbuild/linux-loong64@0.25.0': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.23.1': - optional: true - '@esbuild/linux-mips64el@0.25.0': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.23.1': - optional: true - '@esbuild/linux-ppc64@0.25.0': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.23.1': - optional: true - '@esbuild/linux-riscv64@0.25.0': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.23.1': - optional: true - '@esbuild/linux-s390x@0.25.0': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.23.1': - optional: true - '@esbuild/linux-x64@0.25.0': optional: true @@ -3412,66 +3236,45 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.23.1': - optional: true - '@esbuild/netbsd-x64@0.25.0': optional: true - '@esbuild/openbsd-arm64@0.23.1': - optional: true - '@esbuild/openbsd-arm64@0.25.0': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.23.1': - optional: true - '@esbuild/openbsd-x64@0.25.0': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.23.1': - optional: true - '@esbuild/sunos-x64@0.25.0': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.23.1': - optional: true - '@esbuild/win32-arm64@0.25.0': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.23.1': - optional: true - '@esbuild/win32-ia32@0.25.0': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.23.1': - optional: true - '@esbuild/win32-x64@0.25.0': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0(jiti@1.21.7))': dependencies: - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -3484,6 +3287,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.1.0': {} + '@eslint/core@0.12.0': dependencies: '@types/json-schema': 7.0.15 @@ -3502,7 +3307,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.21.0': {} + '@eslint/js@9.22.0': {} '@eslint/object-schema@2.1.6': {} @@ -3526,6 +3331,81 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3552,37 +3432,34 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@next/env@14.2.24': {} + '@next/env@15.2.1': {} - '@next/eslint-plugin-next@14.2.24': + '@next/eslint-plugin-next@15.2.1': dependencies: - glob: 10.3.10 - - '@next/swc-darwin-arm64@14.2.24': - optional: true + fast-glob: 3.3.1 - '@next/swc-darwin-x64@14.2.24': + '@next/swc-darwin-arm64@15.2.1': optional: true - '@next/swc-linux-arm64-gnu@14.2.24': + '@next/swc-darwin-x64@15.2.1': optional: true - '@next/swc-linux-arm64-musl@14.2.24': + '@next/swc-linux-arm64-gnu@15.2.1': optional: true - '@next/swc-linux-x64-gnu@14.2.24': + '@next/swc-linux-arm64-musl@15.2.1': optional: true - '@next/swc-linux-x64-musl@14.2.24': + '@next/swc-linux-x64-gnu@15.2.1': optional: true - '@next/swc-win32-arm64-msvc@14.2.24': + '@next/swc-linux-x64-musl@15.2.1': optional: true - '@next/swc-win32-ia32-msvc@14.2.24': + '@next/swc-win32-arm64-msvc@15.2.1': optional: true - '@next/swc-win32-x64-msvc@14.2.24': + '@next/swc-win32-x64-msvc@15.2.1': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3602,125 +3479,119 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.34.8': + '@rollup/rollup-android-arm-eabi@4.34.9': optional: true - '@rollup/rollup-android-arm64@4.34.8': + '@rollup/rollup-android-arm64@4.34.9': optional: true - '@rollup/rollup-darwin-arm64@4.34.8': + '@rollup/rollup-darwin-arm64@4.34.9': optional: true - '@rollup/rollup-darwin-x64@4.34.8': + '@rollup/rollup-darwin-x64@4.34.9': optional: true - '@rollup/rollup-freebsd-arm64@4.34.8': + '@rollup/rollup-freebsd-arm64@4.34.9': optional: true - '@rollup/rollup-freebsd-x64@4.34.8': + '@rollup/rollup-freebsd-x64@4.34.9': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.8': + '@rollup/rollup-linux-arm-musleabihf@4.34.9': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.8': + '@rollup/rollup-linux-arm64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.8': + '@rollup/rollup-linux-arm64-musl@4.34.9': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.8': + '@rollup/rollup-linux-riscv64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.8': + '@rollup/rollup-linux-s390x-gnu@4.34.9': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.8': + '@rollup/rollup-linux-x64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-x64-musl@4.34.8': + '@rollup/rollup-linux-x64-musl@4.34.9': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.8': + '@rollup/rollup-win32-arm64-msvc@4.34.9': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.8': + '@rollup/rollup-win32-ia32-msvc@4.34.9': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.8': + '@rollup/rollup-win32-x64-msvc@4.34.9': optional: true '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.5': {} - '@swc/core-darwin-arm64@1.11.5': + '@swc/core-darwin-arm64@1.11.8': optional: true - '@swc/core-darwin-x64@1.11.5': + '@swc/core-darwin-x64@1.11.8': optional: true - '@swc/core-linux-arm-gnueabihf@1.11.5': + '@swc/core-linux-arm-gnueabihf@1.11.8': optional: true - '@swc/core-linux-arm64-gnu@1.11.5': + '@swc/core-linux-arm64-gnu@1.11.8': optional: true - '@swc/core-linux-arm64-musl@1.11.5': + '@swc/core-linux-arm64-musl@1.11.8': optional: true - '@swc/core-linux-x64-gnu@1.11.5': + '@swc/core-linux-x64-gnu@1.11.8': optional: true - '@swc/core-linux-x64-musl@1.11.5': + '@swc/core-linux-x64-musl@1.11.8': optional: true - '@swc/core-win32-arm64-msvc@1.11.5': + '@swc/core-win32-arm64-msvc@1.11.8': optional: true - '@swc/core-win32-ia32-msvc@1.11.5': + '@swc/core-win32-ia32-msvc@1.11.8': optional: true - '@swc/core-win32-x64-msvc@1.11.5': + '@swc/core-win32-x64-msvc@1.11.8': optional: true - '@swc/core@1.11.5(@swc/helpers@0.5.12)': + '@swc/core@1.11.8(@swc/helpers@0.5.15)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.19 optionalDependencies: - '@swc/core-darwin-arm64': 1.11.5 - '@swc/core-darwin-x64': 1.11.5 - '@swc/core-linux-arm-gnueabihf': 1.11.5 - '@swc/core-linux-arm64-gnu': 1.11.5 - '@swc/core-linux-arm64-musl': 1.11.5 - '@swc/core-linux-x64-gnu': 1.11.5 - '@swc/core-linux-x64-musl': 1.11.5 - '@swc/core-win32-arm64-msvc': 1.11.5 - '@swc/core-win32-ia32-msvc': 1.11.5 - '@swc/core-win32-x64-msvc': 1.11.5 - '@swc/helpers': 0.5.12 + '@swc/core-darwin-arm64': 1.11.8 + '@swc/core-darwin-x64': 1.11.8 + '@swc/core-linux-arm-gnueabihf': 1.11.8 + '@swc/core-linux-arm64-gnu': 1.11.8 + '@swc/core-linux-arm64-musl': 1.11.8 + '@swc/core-linux-x64-gnu': 1.11.8 + '@swc/core-linux-x64-musl': 1.11.8 + '@swc/core-win32-arm64-msvc': 1.11.8 + '@swc/core-win32-ia32-msvc': 1.11.8 + '@swc/core-win32-x64-msvc': 1.11.8 + '@swc/helpers': 0.5.15 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.12': + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - optional: true - - '@swc/helpers@0.5.5': - dependencies: - '@swc/counter': 0.1.3 - tslib: 2.8.1 '@swc/types@0.1.19': dependencies: @@ -3757,10 +3628,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@20.17.21': - dependencies: - undici-types: 6.19.8 - '@types/node@22.13.9': dependencies: undici-types: 6.20.0 @@ -3779,33 +3646,15 @@ snapshots: '@types/unist@2.0.11': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/utils': 7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.21.0(jiti@1.21.7) - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.8.2) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 - eslint: 9.21.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/type-utils': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 + eslint: 9.22.0(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -3814,87 +3663,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': + '@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 - eslint: 9.21.0(jiti@1.21.7) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.18.0': + '@typescript-eslint/scope-manager@8.26.0': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - - '@typescript-eslint/scope-manager@8.25.0': - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - - '@typescript-eslint/type-utils@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) - '@typescript-eslint/utils': 7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - debug: 4.4.0 - eslint: 9.21.0(jiti@1.21.7) - ts-api-utils: 1.4.3(typescript@5.8.2) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) debug: 4.4.0 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) ts-api-utils: 2.0.1(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.18.0': {} - - '@typescript-eslint/types@8.25.0': {} - - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.8.2)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@5.8.2) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.25.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -3905,61 +3707,45 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': + '@typescript-eslint/utils@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) - eslint: 9.21.0(jiti@1.21.7) - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - eslint: 9.21.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + eslint: 9.22.0(jiti@1.21.7) typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - - '@typescript-eslint/visitor-keys@8.25.0': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.25.0 + '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.12)(vite@5.4.14(@types/node@20.17.21))': + '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.9))': dependencies: - '@swc/core': 1.11.5(@swc/helpers@0.5.12) - vite: 5.4.14(@types/node@20.17.21) + '@swc/core': 1.11.8(@swc/helpers@0.5.15) + vite: 5.4.14(@types/node@22.13.9) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.3.4(vite@5.4.14(@types/node@20.17.21))': + '@vitejs/plugin-react@4.3.4(vite@5.4.14(@types/node@22.13.9))': dependencies: '@babel/core': 7.26.9 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.14(@types/node@20.17.21) + vite: 5.4.14(@types/node@22.13.9) transitivePeerDependencies: - supports-color - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.14.0 + acorn: 8.14.1 - acorn@8.14.0: {} + acorn@8.14.1: {} ajv@6.12.6: dependencies: @@ -3993,7 +3779,7 @@ snapshots: array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 array-includes@3.1.8: @@ -4005,8 +3791,6 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 - array-union@2.1.0: {} - array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.8 @@ -4064,7 +3848,7 @@ snapshots: autoprefixer@10.4.20(postcss@8.5.3): dependencies: browserslist: 4.24.4 - caniuse-lite: 1.0.30001701 + caniuse-lite: 1.0.30001702 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -4075,7 +3859,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.10.2: {} + axe-core@4.10.3: {} axobject-query@4.1.0: {} @@ -4098,8 +3882,8 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001701 - electron-to-chromium: 1.5.109 + caniuse-lite: 1.0.30001702 + electron-to-chromium: 1.5.113 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) @@ -4126,7 +3910,7 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 @@ -4135,7 +3919,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001701: {} + caniuse-lite@1.0.30001702: {} chalk@4.1.2: dependencies: @@ -4180,20 +3964,30 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + comma-separated-tokens@1.0.8: {} commander@4.1.1: {} concat-map@0.0.1: {} - concurrently@8.2.2: + concurrently@9.1.2: dependencies: chalk: 4.1.2 - date-fns: 2.30.0 lodash: 4.17.21 rxjs: 7.8.2 shell-quote: 1.8.2 - spawn-command: 0.0.2 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 @@ -4220,26 +4014,22 @@ snapshots: data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-offset@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.26.9 - debug@3.2.7: dependencies: ms: 2.1.3 @@ -4262,11 +4052,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - didyoumean@1.2.2: {} + detect-libc@2.0.3: + optional: true - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -4284,7 +4073,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.109: {} + electron-to-chromium@1.5.113: {} emoji-regex@8.0.0: {} @@ -4301,7 +4090,7 @@ snapshots: arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 @@ -4356,7 +4145,7 @@ snapshots: es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -4419,33 +4208,6 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.23.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 - esbuild@0.25.0: optionalDependencies: '@esbuild/aix-ppc64': 0.25.0 @@ -4478,19 +4240,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@14.2.24(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2): + eslint-config-next@15.2.1(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2): dependencies: - '@next/eslint-plugin-next': 14.2.24 + '@next/eslint-plugin-next': 15.2.1 '@rushstack/eslint-patch': 1.10.5 - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - eslint: 9.21.0(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + eslint: 9.22.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@1.21.7)) - eslint-plugin-react: 7.35.0(eslint@9.21.0(jiti@1.21.7)) - eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@9.21.0(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@1.21.7)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@1.21.7)) + eslint-plugin-react: 7.37.4(eslint@9.22.0(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.22.0(jiti@1.21.7)) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -4498,14 +4260,14 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@9.1.0(eslint@9.21.0(jiti@1.21.7)): + eslint-config-prettier@10.1.1(eslint@9.22.0(jiti@1.21.7)): dependencies: - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) - eslint-config-turbo@2.4.4(eslint@9.21.0(jiti@1.21.7))(turbo@2.4.4): + eslint-config-turbo@2.4.4(eslint@9.22.0(jiti@1.21.7))(turbo@2.4.4): dependencies: - eslint: 9.21.0(jiti@1.21.7) - eslint-plugin-turbo: 2.4.4(eslint@9.21.0(jiti@1.21.7))(turbo@2.4.4) + eslint: 9.22.0(jiti@1.21.7) + eslint-plugin-turbo: 2.4.4(eslint@9.22.0(jiti@1.21.7))(turbo@2.4.4) turbo: 2.4.4 eslint-import-resolver-node@0.3.9: @@ -4516,33 +4278,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)): + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.1 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) get-tsconfig: 4.10.0 is-bun-module: 1.3.0 stable-hash: 0.0.4 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) - eslint: 9.21.0(jiti@1.21.7) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) + eslint: 9.22.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -4551,9 +4313,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -4565,23 +4327,23 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.22.0(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.10.2 + axe-core: 4.10.3 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -4590,19 +4352,15 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@4.6.2(eslint@9.21.0(jiti@1.21.7)): - dependencies: - eslint: 9.21.0(jiti@1.21.7) - - eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@9.21.0(jiti@1.21.7)): + eslint-plugin-react-hooks@5.2.0(eslint@9.22.0(jiti@1.21.7)): dependencies: - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) - eslint-plugin-react-refresh@0.4.19(eslint@9.21.0(jiti@1.21.7)): + eslint-plugin-react-refresh@0.4.19(eslint@9.22.0(jiti@1.21.7)): dependencies: - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) - eslint-plugin-react@7.35.0(eslint@9.21.0(jiti@1.21.7)): + eslint-plugin-react@7.37.4(eslint@9.22.0(jiti@1.21.7)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -4610,7 +4368,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -4624,13 +4382,13 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.4.4(eslint@9.21.0(jiti@1.21.7))(turbo@2.4.4): + eslint-plugin-turbo@2.4.4(eslint@9.22.0(jiti@1.21.7))(turbo@2.4.4): dependencies: dotenv: 16.0.3 - eslint: 9.21.0(jiti@1.21.7) + eslint: 9.22.0(jiti@1.21.7) turbo: 2.4.4 - eslint-scope@8.2.0: + eslint-scope@8.3.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -4639,14 +4397,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.21.0(jiti@1.21.7): + eslint@9.22.0(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 + '@eslint/config-helpers': 0.1.0 '@eslint/core': 0.12.0 '@eslint/eslintrc': 3.3.0 - '@eslint/js': 9.21.0 + '@eslint/js': 9.22.0 '@eslint/plugin-kit': 0.2.7 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -4658,7 +4417,7 @@ snapshots: cross-spawn: 7.0.6 debug: 4.4.0 escape-string-regexp: 4.0.0 - eslint-scope: 8.2.0 + eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 esquery: 1.6.0 @@ -4682,8 +4441,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 esquery@1.6.0: @@ -4700,6 +4459,14 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4757,10 +4524,10 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + framer-motion@12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - motion-dom: 12.4.5 - motion-utils: 12.0.0 + motion-dom: 12.4.10 + motion-utils: 12.4.10 tslib: 2.8.1 optionalDependencies: react: 19.0.0 @@ -4774,7 +4541,7 @@ snapshots: function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 hasown: 2.0.2 @@ -4806,7 +4573,7 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 @@ -4822,14 +4589,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.10: - dependencies: - foreground-child: 3.3.1 - jackspeak: 2.3.6 - minimatch: 9.0.5 - minipass: 7.1.2 - path-scurry: 1.11.1 - glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -4848,15 +4607,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4924,13 +4674,16 @@ snapshots: is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.3.2: + optional: true + is-async-function@2.1.1: dependencies: async-function: 1.0.0 - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -4945,7 +4698,7 @@ snapshots: is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-bun-module@1.3.0: @@ -4960,13 +4713,13 @@ snapshots: is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-decimal@1.0.4: {} @@ -4975,13 +4728,13 @@ snapshots: is-finalizationregistry@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -4996,14 +4749,14 @@ snapshots: is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -5012,16 +4765,16 @@ snapshots: is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 @@ -5033,11 +4786,11 @@ snapshots: is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 isarray@2.0.5: {} @@ -5053,12 +4806,6 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -5146,7 +4893,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.477.0(react@19.0.0): + lucide-react@0.479.0(react@19.0.0): dependencies: react: 19.0.0 @@ -5171,15 +4918,15 @@ snapshots: minipass@7.1.2: {} - motion-dom@12.4.5: + motion-dom@12.4.10: dependencies: - motion-utils: 12.0.0 + motion-utils: 12.4.10 - motion-utils@12.0.0: {} + motion-utils@12.4.10: {} - motion@12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + motion@12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - framer-motion: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + framer-motion: 12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tslib: 2.8.1 optionalDependencies: react: 19.0.0 @@ -5193,33 +4940,33 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.8: {} + nanoid@3.3.9: {} - nanoid@5.1.2: {} + nanoid@5.1.3: {} natural-compare@1.4.0: {} - next@14.2.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 14.2.24 - '@swc/helpers': 0.5.5 + '@next/env': 15.2.1 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001701 - graceful-fs: 4.2.11 + caniuse-lite: 1.0.30001702 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.1(react@19.0.0) + styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.24 - '@next/swc-darwin-x64': 14.2.24 - '@next/swc-linux-arm64-gnu': 14.2.24 - '@next/swc-linux-arm64-musl': 14.2.24 - '@next/swc-linux-x64-gnu': 14.2.24 - '@next/swc-linux-x64-musl': 14.2.24 - '@next/swc-win32-arm64-msvc': 14.2.24 - '@next/swc-win32-ia32-msvc': 14.2.24 - '@next/swc-win32-x64-msvc': 14.2.24 + '@next/swc-darwin-arm64': 15.2.1 + '@next/swc-darwin-x64': 15.2.1 + '@next/swc-linux-arm64-gnu': 15.2.1 + '@next/swc-linux-arm64-musl': 15.2.1 + '@next/swc-linux-x64-gnu': 15.2.1 + '@next/swc-linux-x64-musl': 15.2.1 + '@next/swc-win32-arm64-msvc': 15.2.1 + '@next/swc-win32-x64-msvc': 15.2.1 + sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -5241,7 +4988,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -5269,7 +5016,7 @@ snapshots: object.values@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -5322,8 +5069,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-type@4.0.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5377,19 +5122,19 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.9 picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.9 picocolors: 1.1.1 source-map-js: 1.2.1 prelude-ls@1.2.1: {} - prettier@3.5.2: {} + prettier@3.5.3: {} prismjs@1.27.0: {} @@ -5490,29 +5235,29 @@ snapshots: reusify@1.1.0: {} - rollup@4.34.8: + rollup@4.34.9: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.8 - '@rollup/rollup-android-arm64': 4.34.8 - '@rollup/rollup-darwin-arm64': 4.34.8 - '@rollup/rollup-darwin-x64': 4.34.8 - '@rollup/rollup-freebsd-arm64': 4.34.8 - '@rollup/rollup-freebsd-x64': 4.34.8 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.8 - '@rollup/rollup-linux-arm-musleabihf': 4.34.8 - '@rollup/rollup-linux-arm64-gnu': 4.34.8 - '@rollup/rollup-linux-arm64-musl': 4.34.8 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.8 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8 - '@rollup/rollup-linux-riscv64-gnu': 4.34.8 - '@rollup/rollup-linux-s390x-gnu': 4.34.8 - '@rollup/rollup-linux-x64-gnu': 4.34.8 - '@rollup/rollup-linux-x64-musl': 4.34.8 - '@rollup/rollup-win32-arm64-msvc': 4.34.8 - '@rollup/rollup-win32-ia32-msvc': 4.34.8 - '@rollup/rollup-win32-x64-msvc': 4.34.8 + '@rollup/rollup-android-arm-eabi': 4.34.9 + '@rollup/rollup-android-arm64': 4.34.9 + '@rollup/rollup-darwin-arm64': 4.34.9 + '@rollup/rollup-darwin-x64': 4.34.9 + '@rollup/rollup-freebsd-arm64': 4.34.9 + '@rollup/rollup-freebsd-x64': 4.34.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.9 + '@rollup/rollup-linux-arm-musleabihf': 4.34.9 + '@rollup/rollup-linux-arm64-gnu': 4.34.9 + '@rollup/rollup-linux-arm64-musl': 4.34.9 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.9 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.9 + '@rollup/rollup-linux-riscv64-gnu': 4.34.9 + '@rollup/rollup-linux-s390x-gnu': 4.34.9 + '@rollup/rollup-linux-x64-gnu': 4.34.9 + '@rollup/rollup-linux-x64-musl': 4.34.9 + '@rollup/rollup-win32-arm64-msvc': 4.34.9 + '@rollup/rollup-win32-ia32-msvc': 4.34.9 + '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 run-parallel@1.2.0: @@ -5526,7 +5271,7 @@ snapshots: safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -5538,7 +5283,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -5570,6 +5315,33 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5585,14 +5357,14 @@ snapshots: side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 @@ -5608,7 +5380,10 @@ snapshots: signal-exit@4.1.0: {} - slash@3.0.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true source-map-js@1.2.1: {} @@ -5618,8 +5393,6 @@ snapshots: space-separated-tokens@1.1.5: {} - spawn-command@0.0.2: {} - stable-hash@0.0.4: {} streamsearch@1.1.0: {} @@ -5645,7 +5418,7 @@ snapshots: string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -5666,7 +5439,7 @@ snapshots: string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.23.9 @@ -5676,7 +5449,7 @@ snapshots: string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -5698,7 +5471,7 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.1(react@19.0.0): + styled-jsx@5.1.6(react@19.0.0): dependencies: client-only: 0.0.1 react: 19.0.0 @@ -5785,10 +5558,6 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.8.2): - dependencies: - typescript: 5.8.2 - ts-api-utils@2.0.1(typescript@5.8.2): dependencies: typescript: 5.8.2 @@ -5804,7 +5573,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(@swc/core@1.11.5(@swc/helpers@0.5.12))(jiti@1.21.7)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.0): + tsup@8.4.0(@swc/core@1.11.8(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.0) cac: 6.7.14 @@ -5816,14 +5585,14 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.3)(yaml@2.7.0) resolve-from: 5.0.0 - rollup: 4.34.8 + rollup: 4.34.9 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 tinyglobby: 0.2.12 tree-kill: 1.2.2 optionalDependencies: - '@swc/core': 1.11.5(@swc/helpers@0.5.12) + '@swc/core': 1.11.8(@swc/helpers@0.5.15) postcss: 8.5.3 typescript: 5.8.2 transitivePeerDependencies: @@ -5865,7 +5634,7 @@ snapshots: typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 @@ -5900,13 +5669,11 @@ snapshots: unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.19.8: {} - undici-types@6.20.0: {} update-browserslist-db@1.1.3(browserslist@4.24.4): @@ -5921,19 +5688,19 @@ snapshots: util-deprecate@1.0.2: {} - vite-plugin-singlefile@2.1.0(rollup@4.34.8)(vite@5.4.14(@types/node@20.17.21)): + vite-plugin-singlefile@2.1.0(rollup@4.34.9)(vite@5.4.14(@types/node@22.13.9)): dependencies: micromatch: 4.0.8 - rollup: 4.34.8 - vite: 5.4.14(@types/node@20.17.21) + rollup: 4.34.9 + vite: 5.4.14(@types/node@22.13.9) - vite@5.4.14(@types/node@20.17.21): + vite@5.4.14(@types/node@22.13.9): dependencies: esbuild: 0.21.5 postcss: 8.5.3 - rollup: 4.34.8 + rollup: 4.34.9 optionalDependencies: - '@types/node': 20.17.21 + '@types/node': 22.13.9 fsevents: 2.3.3 webidl-conversions@4.0.2: {} @@ -5954,7 +5721,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.1.1 @@ -5979,7 +5746,7 @@ snapshots: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 for-each: 0.3.5 gopd: 1.2.0 has-tostringtag: 1.0.2 From 8449fb2c4bc012006cd4a9c71d4ff74cd37498b0 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 22:12:25 -0300 Subject: [PATCH 051/134] Add ring and custom border values to Tailwind --- .../tailwind/builderImpl/tailwindBorder.ts | 96 +++++++++++++++---- .../backend/src/tailwind/conversionTables.ts | 10 +- .../backend/src/tailwind/tailwindConfig.ts | 69 ++++++++----- .../src/tailwind/tailwindDefaultBuilder.ts | 11 ++- packages/plugin-ui/src/components/Preview.tsx | 3 +- packages/types/src/types.ts | 2 +- 6 files changed, 141 insertions(+), 50 deletions(-) diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts index 8639702f..c90095bd 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts @@ -1,38 +1,92 @@ import { getCommonRadius } from "../../common/commonRadius"; import { commonStroke } from "../../common/commonStroke"; -import { nearestValue, pxToBorderRadius } from "../conversionTables"; +import { + nearestValue, + pxToBorderRadius, + pxToBorderWidth, + pxToRing, +} from "../conversionTables"; +import { numberToFixedString } from "../../common/numToAutoFixed"; +import { addWarning } from "../../common/commonConversionWarnings"; + +const getBorder = (weight: number, kind: string, isRing: boolean = false) => { + // Use ring utilities for outside strokes + if (isRing) { + // Special case: ring (without width) is 3px in Tailwind + if (weight === 3) { + return "ring"; + } + + const ringWidth = pxToRing(weight); + if (ringWidth === null) { + return `ring-[${numberToFixedString(weight)}px]`; + } else if (ringWidth === "3") { + // Ring is 3px + return `ring`; + } else { + return `ring-${ringWidth}`; + } + } + + // Special case: border (without width) is 1px in Tailwind + if (weight === 1) { + return `border${kind}`; + } + + // Use border utilities for default and inside strokes + const borderWidth = pxToBorderWidth(weight); + if (borderWidth === null) { + return `border${kind}-[${numberToFixedString(weight)}px]`; + } else if (borderWidth === "DEFAULT") { + // Border is 1px + return `border${kind}`; + } else { + return `border${kind}-${borderWidth}`; + } +}; /** * https://tailwindcss.com/docs/border-width/ * example: border-2 */ -export const tailwindBorderWidth = (node: SceneNode): string => { +export const tailwindBorderWidth = ( + node: SceneNode, +): { + isRing: boolean; + property: string; +} => { const commonBorder = commonStroke(node); if (!commonBorder) { - return ""; + return { + isRing: false, + property: "", + }; } - const getBorder = (weight: number, kind: string) => { - const allowedValues = [1, 2, 4, 8]; - console.log("weight", weight); - const nearest = nearestValue(weight, allowedValues); - console.log("nearest", nearest); - - if (nearest === 1) { - // special case - return `border${kind}`; - } else { - return `border${kind}-${nearest}`; - } - }; + // Check if stroke is outside + const isRing = + "strokeAlign" in node && + (node.strokeAlign === "OUTSIDE" || node.strokeAlign === "CENTER"); if ("all" in commonBorder) { if (commonBorder.all === 0) { - return ""; + return { + isRing: false, + property: "", + }; } - return getBorder(commonBorder.all, ""); + return { + isRing, + property: getBorder(commonBorder.all, "", isRing), + }; + } else { + addWarning( + 'Non-uniform borders are only supported with strokeAlign set to "inside". Will paint inside.', + ); } + // If borders are non-uniform, always use border utilities for better control + // regardless of whether the stroke is outside or not const comp = []; if (commonBorder.left !== 0) { comp.push(getBorder(commonBorder.left, "-l")); @@ -46,7 +100,11 @@ export const tailwindBorderWidth = (node: SceneNode): string => { if (commonBorder.bottom !== 0) { comp.push(getBorder(commonBorder.bottom, "-b")); } - return comp.join(" "); + + return { + isRing, + property: comp.join(" "), + }; }; /** diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index e8f591f2..ad947b17 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -78,7 +78,7 @@ const pxToTailwind = ( const keys = Object.keys(conversionMap).map((d) => +d); const convertedValue = exactValue(value, keys); - console.log("convertedValue", convertedValue); + console.log("convertedValue", convertedValue, value, keys); if (convertedValue) { return conversionMap[convertedValue]; @@ -110,6 +110,14 @@ export const pxToBorderRadius = (value: number): string => { return pxToRemToTailwind(value, config.borderRadius); }; +export const pxToBorderWidth = (value: number): string | null => { + return pxToTailwind(value, config.border); +}; + +export const pxToRing = (value: number): string | null => { + return pxToTailwind(value, config.ring); +}; + export const pxToBlur = (value: number): string | null => { return pxToTailwind(value, config.blur); }; diff --git a/packages/backend/src/tailwind/tailwindConfig.ts b/packages/backend/src/tailwind/tailwindConfig.ts index 5660ae41..15e11f11 100644 --- a/packages/backend/src/tailwind/tailwindConfig.ts +++ b/packages/backend/src/tailwind/tailwindConfig.ts @@ -1,5 +1,5 @@ const layoutSize = { - // '0: 0', + "0": "0", 1: "px", 2: "0.5", 4: "1", @@ -37,7 +37,7 @@ const layoutSize = { }; const borderRadius = { - // 0: "none", + 0: "none", 0.125: "sm", 0.25: "", 0.375: "md", @@ -356,37 +356,54 @@ const fontWeight: Record = { 600: "semibold", 700: "bold", 800: "extrabold", - 900: "black" + 900: "black", }; const fontFamily = { sans: [ - 'ui-sans-serif', - 'system-ui', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - 'Noto Color Emoji' + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", ], serif: [ - 'ui-serif', - 'Georgia', - 'Cambria', - 'Times New Roman', - 'Times', - 'serif' + "ui-serif", + "Georgia", + "Cambria", + "Times New Roman", + "Times", + "serif", ], mono: [ - 'ui-monospace', - 'SFMono-Regular', - 'Menlo', - 'Monaco', - 'Consolas', - 'Liberation Mono', - 'Courier New', - 'monospace' - ] + "ui-monospace", + "SFMono-Regular", + "Menlo", + "Monaco", + "Consolas", + "Liberation Mono", + "Courier New", + "monospace", + ], +}; + +const border = { + 0: "0", + 1: "1", + 2: "2", + 4: "4", + 8: "8", +}; + +const ring = { + 0: "0", + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 8: "8", }; export const config = { @@ -400,4 +417,6 @@ export const config = { color, fontWeight, fontFamily, + border, + ring, }; diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 7ccdf3f7..8d46a52c 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -114,8 +114,9 @@ export class TailwindDefaultBuilder { border(): this { if ("strokes" in this.node) { - this.addAttributes(tailwindBorderWidth(this.node)); - this.customColor(this.node.strokes, "border"); + const { isRing, property } = tailwindBorderWidth(this.node); + this.addAttributes(property); + this.customColor(this.node.strokes, isRing ? "ring" : "border"); } return this; @@ -188,7 +189,11 @@ export class TailwindDefaultBuilder { // must be called before Position, because of the hasFixedSize attribute. size(): this { const { node, optimizeLayout, settings } = this; - const { width, height, constraints } = tailwindSizePartial(node, optimizeLayout, settings); + const { width, height, constraints } = tailwindSizePartial( + node, + optimizeLayout, + settings, + ); if (node.type === "TEXT") { switch (node.textAutoResize) { diff --git a/packages/plugin-ui/src/components/Preview.tsx b/packages/plugin-ui/src/components/Preview.tsx index 9cc7461b..447d01c1 100644 --- a/packages/plugin-ui/src/components/Preview.tsx +++ b/packages/plugin-ui/src/components/Preview.tsx @@ -169,7 +169,8 @@ const Preview: React.FC<{ {/* Footer with size info */}
- {props.htmlPreview.size.width}×{props.htmlPreview.size.height}px + {props.htmlPreview.size.width.toFixed(0)}× + {props.htmlPreview.size.height.toFixed(0)}px
{viewMode === "mobile" ? ( diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 6cac3a3f..f5180d15 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -189,7 +189,7 @@ export interface TailwindTextConversion { contrastBlack: number; } -export type TailwindColorType = "text" | "bg" | "border" | "solid"; +export type TailwindColorType = "text" | "bg" | "border" | "ring"; export type SwiftUIModifier = [ string, From 3146e600264be0ea07f58d2b3741de00dbf47d04 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 22:28:33 -0300 Subject: [PATCH 052/134] Add variable colors to Flutter --- .../src/flutter/builderImpl/flutterColor.ts | 47 +++++++++++++------ .../plugin-ui/src/codegenPreferenceOptions.ts | 2 +- .../plugin-ui/src/components/CodePanel.tsx | 1 - 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/flutter/builderImpl/flutterColor.ts b/packages/backend/src/flutter/builderImpl/flutterColor.ts index 6cb62468..5beb4c31 100644 --- a/packages/backend/src/flutter/builderImpl/flutterColor.ts +++ b/packages/backend/src/flutter/builderImpl/flutterColor.ts @@ -35,7 +35,11 @@ export const flutterColorFromDirectFills = ( const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { - return flutterColor(fill.color, fill.opacity ?? 1.0); + return flutterColor( + fill.color, + fill.opacity ?? 1.0, + (fill as any).variableColorName + ); } else if ( fill && (fill.type === "GRADIENT_LINEAR" || @@ -43,7 +47,12 @@ export const flutterColorFromDirectFills = ( fill.type === "GRADIENT_RADIAL") ) { if (fill.gradientStops.length > 0) { - return flutterColor(fill.gradientStops[0].color, fill.opacity ?? 1.0); + const stop = fill.gradientStops[0]; + return flutterColor( + stop.color, + fill.opacity ?? 1.0, + (stop as any).variableColorName + ); } } @@ -66,7 +75,7 @@ export const flutterBoxDecorationColor = ( if (fill && fill.type === "SOLID") { const opacity = fill.opacity ?? 1.0; - return { color: flutterColor(fill.color, opacity) }; + return { color: flutterColor(fill.color, opacity, (fill as any).variableColorName) }; } else if ( fill?.type === "GRADIENT_LINEAR" || fill?.type === "GRADIENT_RADIAL" || @@ -126,7 +135,7 @@ const gradientDirection = (angle: number): string => { const flutterRadialGradient = (fill: GradientPaint): string => { const colors = fill.gradientStops - .map((d) => flutterColor(d.color, d.color.a)) + .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) .join(", "); const x = numberToFixedString(fill.gradientTransform[0][2]); @@ -144,7 +153,7 @@ const flutterRadialGradient = (fill: GradientPaint): string => { const flutterAngularGradient = (fill: GradientPaint): string => { const colors = fill.gradientStops - .map((d) => flutterColor(d.color, d.color.a)) + .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) .join(", "); const x = numberToFixedString(fill.gradientTransform[0][2]); @@ -166,7 +175,7 @@ const flutterLinearGradient = (fill: GradientPaint): string => { const y = Math.sin(radians).toFixed(2); const colors = fill.gradientStops - .map((d) => flutterColor(d.color, d.color.a)) + .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) .join(", "); return generateWidgetCode("LinearGradient", { @@ -205,21 +214,31 @@ const opacityToAlpha = (opacity: number): number => { return Math.round(opacity * 255); }; -export const flutterColor = (color: RGB, opacity: number): string => { +export const flutterColor = ( + color: RGB, + opacity: number, + variableColorName?: string +): string => { const sum = color.r + color.g + color.b; + let colorCode = ""; if (sum === 0) { - return opacity === 1 + colorCode = opacity === 1 ? "Colors.black" : `Colors.black.withValues(alpha: ${opacityToAlpha(opacity)})`; - } - - if (sum === 3) { - return opacity === 1 + } else if (sum === 3) { + colorCode = opacity === 1 ? "Colors.white" : `Colors.white.withValues(alpha: ${opacityToAlpha(opacity)})`; + } else { + // Always use full 8-digit hex which includes alpha channel + colorCode = `Color(0x${rgbTo8hex(color, opacity).toUpperCase()})`; } - // Always use full 8-digit hex which includes alpha channel - return `Color(0x${rgbTo8hex(color, opacity).toUpperCase()})`; + // Add variable name as a comment if it exists + if (variableColorName) { + return `${colorCode} /* ${variableColorName} */`; + } + + return colorCode; }; diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 41acc3c8..7ca4bada 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -42,7 +42,7 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ description: "Export code using Figma variables as colors. Example: 'bg-background' instead of 'bg-white'.", isDefault: false, - includedLanguages: ["HTML", "Tailwind"], + includedLanguages: ["HTML", "Tailwind", "Flutter"], }, { itemType: "individual_select", diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 994f6434..b72e4dab 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -129,7 +129,6 @@ const CodePanel = (props: CodePanelProps) => { stylingPreferences: frameworkPreferences.filter((p) => stylingPropertyNames.includes(p.propertyName), ), - selectableSettingsFiltered: selectPreferenceOptions.filter((p) => p.includedLanguages?.includes(selectedFramework), ), From 469f9fe1425c7794ecef4b01793a40163a1e961f Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 23:00:02 -0300 Subject: [PATCH 053/134] Add flex-wrap, fix HUG --- packages/backend/src/code.ts | 5 ++- .../backend/src/common/commonChildrenOrder.ts | 5 --- .../backend/src/common/nodeWidthHeight.ts | 31 ++++++++++--------- .../src/html/builderImpl/htmlAutoLayout.ts | 23 ++++++++++++++ .../backend/src/html/builderImpl/htmlSize.ts | 1 + .../builderImpl/tailwindAutoLayout.ts | 23 ++++++++++++++ .../src/tailwind/builderImpl/tailwindColor.ts | 1 - .../backend/src/tailwind/conversionTables.ts | 7 ----- .../src/tailwind/tailwindTextBuilder.ts | 2 -- .../plugin-ui/src/components/CodePanel.tsx | 5 --- 10 files changed, 67 insertions(+), 36 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 10a18203..cf9386ec 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -452,7 +452,10 @@ export const run = async (settings: PluginSettings) => { const { framework } = settings; const selection = figma.currentPage.selection; - if (selection.length > 1) { + if (selection.length === 0) { + postEmptyMessage(); + return; + } else if (selection.length > 1) { addWarning( "Ungrouped elements may have incorrect positioning. If this happens, try wrapping the selection in a Frame or Group.", ); diff --git a/packages/backend/src/common/commonChildrenOrder.ts b/packages/backend/src/common/commonChildrenOrder.ts index d7a93965..506870fb 100644 --- a/packages/backend/src/common/commonChildrenOrder.ts +++ b/packages/backend/src/common/commonChildrenOrder.ts @@ -18,11 +18,6 @@ export const commonSortChildrenWhenInferredAutoLayout = ( // NONE is a bug from Figma. case "NONE": case "VERTICAL": - console.log( - "ordering", - children.map((c) => c.name), - children.sort((a, b) => a.y - b.y).map((c) => c.name), - ); return children.sort((a, b) => a.y - b.y); } } diff --git a/packages/backend/src/common/nodeWidthHeight.ts b/packages/backend/src/common/nodeWidthHeight.ts index dff8fb77..abaff57d 100644 --- a/packages/backend/src/common/nodeWidthHeight.ts +++ b/packages/backend/src/common/nodeWidthHeight.ts @@ -1,34 +1,35 @@ import { Size } from "types"; export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { - const nodeAuto = - (optimizeLayout && "inferredAutoLayout" in node - ? node.inferredAutoLayout - : null) ?? node; - - // Check for explicit layout sizing properties - if ( - "layoutSizingHorizontal" in nodeAuto && - "layoutSizingVertical" in nodeAuto - ) { + if ("layoutSizingHorizontal" in node && "layoutSizingVertical" in node) { const width = - nodeAuto.layoutSizingHorizontal === "FILL" + node.layoutSizingHorizontal === "FILL" ? "fill" - : nodeAuto.layoutSizingHorizontal === "HUG" + : node.layoutSizingHorizontal === "HUG" ? null : node.width; const height = - nodeAuto.layoutSizingVertical === "FILL" + node.layoutSizingVertical === "FILL" ? "fill" - : nodeAuto.layoutSizingVertical === "HUG" + : node.layoutSizingVertical === "HUG" ? null : node.height; return { width, height }; } - if ("layoutMode" in nodeAuto && nodeAuto.layoutMode === "NONE") { + const nodeAuto = + (optimizeLayout && "inferredAutoLayout" in node + ? node.inferredAutoLayout + : null) ?? node; + + if ( + nodeAuto && + typeof nodeAuto === "object" && + "layoutMode" in nodeAuto && + nodeAuto.layoutMode === "NONE" + ) { return { width: node.width, height: node.height }; } diff --git a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts index 8bbcad5f..bac3ffe4 100644 --- a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts +++ b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts @@ -37,6 +37,27 @@ const getGap = (node: InferredAutoLayoutResult): string | number => ? node.itemSpacing : ""; +const getFlexWrap = (node: InferredAutoLayoutResult): string => + node.layoutWrap === "WRAP" ? "wrap" : ""; + +const getAlignContent = (node: InferredAutoLayoutResult): string => { + if (node.layoutWrap !== "WRAP") return ""; + + switch (node.counterAxisAlignItems) { + case undefined: + case "MIN": + return "flex-start"; + case "CENTER": + return "center"; + case "MAX": + return "flex-end"; + case "BASELINE": + return "baseline"; + default: + return "normal"; + } +}; + const getFlex = ( node: SceneNode, autoLayout: InferredAutoLayoutResult, @@ -59,6 +80,8 @@ export const htmlAutoLayoutProps = ( "align-items": getAlignItems(autoLayout), gap: getGap(autoLayout), display: getFlex(node, autoLayout), + "flex-wrap": getFlexWrap(autoLayout), + "align-content": getAlignContent(autoLayout), }, settings.htmlGenerationMode === "jsx", ); diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index d6a3a7c1..fdf86cc1 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -16,6 +16,7 @@ export const htmlSizePartial = ( } const size = nodeSize(node, optimizeLayout); + console.log("size", size); const nodeParent = (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent ? node.parent.inferredAutoLayout diff --git a/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts b/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts index 84b0ca82..73fda204 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindAutoLayout.ts @@ -36,6 +36,27 @@ const getGap = (node: InferredAutoLayoutResult): string => ? `gap-${pxToLayoutSize(node.itemSpacing)}` : ""; +const getFlexWrap = (node: InferredAutoLayoutResult): string => + node.layoutWrap === "WRAP" ? "flex-wrap" : ""; + +const getAlignContent = (node: InferredAutoLayoutResult): string => { + if (node.layoutWrap !== "WRAP") return ""; + + switch (node.counterAxisAlignItems) { + case undefined: + case "MIN": + return "content-start"; + case "CENTER": + return "content-center"; + case "MAX": + return "content-end"; + case "BASELINE": + return "content-baseline"; + default: + return "content-normal"; + } +}; + const getFlex = ( node: SceneNode, autoLayout: InferredAutoLayoutResult, @@ -56,6 +77,8 @@ export const tailwindAutoLayoutProps = ( getJustifyContent(autoLayout), getAlignItems(autoLayout), getGap(autoLayout), + getFlexWrap(autoLayout), + getAlignContent(autoLayout), ].filter(Boolean); return classes.join(" "); diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 83d13733..31ececde 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -66,7 +66,6 @@ export const tailwindSolidColor = ( fill: SolidPaint | ColorStop, kind: TailwindColorType, ): string => { - console.log("fill is", fill); const { colorName } = getColorInfo(fill); const effectiveOpacity = calculateEffectiveOpacity(fill); diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index ad947b17..78b0882e 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -78,8 +78,6 @@ const pxToTailwind = ( const keys = Object.keys(conversionMap).map((d) => +d); const convertedValue = exactValue(value, keys); - console.log("convertedValue", convertedValue, value, keys); - if (convertedValue) { return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { @@ -176,11 +174,6 @@ export function getColorInfo(fill: SolidPaint | ColorStop) { let hex: string = "#" + rgbTo6hex(fill.color); let meta: string = ""; - console.log( - "(fill as any).variableColorName", - fill, - (fill as any).variableColorName, - ); // variable if ((fill as any).variableColorName) { // Use pre-computed variable name if available diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index d404d9d5..822c9b7e 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -58,8 +58,6 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { .filter(Boolean) .join(" "); - console.log("styleClasses", styleClasses, segment); - const charsWithLineBreak = segment.characters.split("\n").join("
"); return { style: styleClasses, diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index b72e4dab..1ee07632 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -135,11 +135,6 @@ const CodePanel = (props: CodePanelProps) => { }; }, [preferenceOptions, selectPreferenceOptions, selectedFramework]); - // Handle custom prefix change - const handleCustomPrefixChange = (newValue: string) => { - onPreferenceChanged("customTailwindPrefix", newValue); - }; - return (
From 7fc482027436467ffe4fb0b9152f5a2439c32dc1 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 23:05:20 -0300 Subject: [PATCH 054/134] Clean up node width height --- .../backend/src/common/nodeWidthHeight.ts | 49 +------------------ .../src/flutter/builderImpl/flutterSize.ts | 2 +- .../backend/src/html/builderImpl/htmlSize.ts | 2 +- .../src/swiftui/builderImpl/swiftuiSize.ts | 3 +- .../src/swiftui/swiftuiDefaultBuilder.ts | 4 +- packages/backend/src/swiftui/swiftuiMain.ts | 2 +- .../src/tailwind/builderImpl/tailwindSize.ts | 2 +- 7 files changed, 8 insertions(+), 56 deletions(-) diff --git a/packages/backend/src/common/nodeWidthHeight.ts b/packages/backend/src/common/nodeWidthHeight.ts index abaff57d..670a86b5 100644 --- a/packages/backend/src/common/nodeWidthHeight.ts +++ b/packages/backend/src/common/nodeWidthHeight.ts @@ -1,6 +1,6 @@ import { Size } from "types"; -export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { +export const nodeSize = (node: SceneNode): Size => { if ("layoutSizingHorizontal" in node && "layoutSizingVertical" in node) { const width = node.layoutSizingHorizontal === "FILL" @@ -19,52 +19,5 @@ export const nodeSize = (node: SceneNode, optimizeLayout: boolean): Size => { return { width, height }; } - const nodeAuto = - (optimizeLayout && "inferredAutoLayout" in node - ? node.inferredAutoLayout - : null) ?? node; - - if ( - nodeAuto && - typeof nodeAuto === "object" && - "layoutMode" in nodeAuto && - nodeAuto.layoutMode === "NONE" - ) { - return { width: node.width, height: node.height }; - } - - const hasLayout = - "layoutAlign" in node && node.parent && "layoutMode" in node.parent; - - if (!hasLayout) { - return { width: node.width, height: node.height }; - } - return { width: node.width, height: node.height }; - - // const isWidthFill = - // (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutGrow === 1) || - // (parentLayoutMode === "VERTICAL" && nodeAuto.layoutAlign === "STRETCH"); - // const isHeightFill = - // (parentLayoutMode === "HORIZONTAL" && nodeAuto.layoutAlign === "STRETCH") || - // (parentLayoutMode === "VERTICAL" && nodeAuto.layoutGrow === 1); - // const modesSwapped = parentLayoutMode === "HORIZONTAL"; - // const primaryAxisMode = modesSwapped - // ? "counterAxisSizingMode" - // : "primaryAxisSizingMode"; - // const counterAxisMode = modesSwapped - // ? "primaryAxisSizingMode" - // : "counterAxisSizingMode"; - - // return { - // width: isWidthFill - // ? "fill" - // : "layoutMode" in nodeAuto && nodeAuto[primaryAxisMode] === "AUTO" - // ? null - // : node.width, - // height: isHeightFill - // ? "fill" - // : "layoutMode" in nodeAuto && nodeAuto[counterAxisMode] === "AUTO" - // ? null - // : node.height, }; diff --git a/packages/backend/src/flutter/builderImpl/flutterSize.ts b/packages/backend/src/flutter/builderImpl/flutterSize.ts index 5877d2bb..a219a8a1 100644 --- a/packages/backend/src/flutter/builderImpl/flutterSize.ts +++ b/packages/backend/src/flutter/builderImpl/flutterSize.ts @@ -12,7 +12,7 @@ export const flutterSize = ( node: SceneNode, optimizeLayout: boolean, ): { width: string; height: string; isExpanded: boolean; constraints: Record } => { - const size = nodeSize(node, optimizeLayout); + const size = nodeSize(node); let isExpanded: boolean = false; const nodeParent = diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index fdf86cc1..bc6df727 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -15,7 +15,7 @@ export const htmlSizePartial = ( }; } - const size = nodeSize(node, optimizeLayout); + const size = nodeSize(node); console.log("size", size); const nodeParent = (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent diff --git a/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts b/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts index 4b7fc13b..d0fe784b 100644 --- a/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts +++ b/packages/backend/src/swiftui/builderImpl/swiftuiSize.ts @@ -3,9 +3,8 @@ import { numberToFixedString } from "../../common/numToAutoFixed"; export const swiftuiSize = ( node: SceneNode, - optimize: boolean = false, ): { width: string; height: string; constraints: string[] } => { - const size = nodeSize(node, optimize); + const size = nodeSize(node); const constraintProps: string[] = []; let width = ""; diff --git a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts index a990adc4..2033e96f 100644 --- a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts @@ -138,8 +138,8 @@ export class SwiftuiDefaultBuilder { return this; } - size(node: SceneNode, optimize: boolean): this { - const { width, height, constraints } = swiftuiSize(node, optimize); + size(node: SceneNode): this { + const { width, height, constraints } = swiftuiSize(node); if (width || height) { this.pushModifier([`frame`, [width, height].filter(Boolean).join(", ")]); } diff --git a/packages/backend/src/swiftui/swiftuiMain.ts b/packages/backend/src/swiftui/swiftuiMain.ts index 94a6610f..71d5226f 100644 --- a/packages/backend/src/swiftui/swiftuiMain.ts +++ b/packages/backend/src/swiftui/swiftuiMain.ts @@ -127,7 +127,7 @@ export const swiftuiContainer = ( const result = new SwiftuiDefaultBuilder(kind) .shapeForeground(node) .autoLayoutPadding(node, localSettings.optimizeLayout) - .size(node, localSettings.optimizeLayout) + .size(node) .shapeBackground(node) .cornerRadius(node) .shapeBorder(node) diff --git a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts index 60208415..e514af82 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts @@ -33,7 +33,7 @@ export const tailwindSizePartial = ( optimizeLayout: boolean, settings?: TailwindSettings, ): { width: string; height: string; constraints: string } => { - const size = nodeSize(node, optimizeLayout); + const size = nodeSize(node); const nodeParent = (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent ? node.parent.inferredAutoLayout From 3dc4cef3c9c49e1976591824eff342569c7495cb Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 23:42:59 -0300 Subject: [PATCH 055/134] Improve README --- README.md | 80 ++++++++++++++----- .../src/html/builderImpl/htmlAutoLayout.ts | 2 +- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0ab9378c..8aa1a839 100644 --- a/README.md +++ b/README.md @@ -13,42 +13,47 @@

-Most _design to code_ plugins are bad, some are even paid. This project aims to raise the bar by generating **responsive** layouts in [Tailwind](https://tailwindcss.com/), [Flutter](https://flutter.github.io/) and [SwiftUI](https://developer.apple.com/xcode/swiftui/). The plan is to eventually add support for [Jetpack Compose](https://developer.android.com/jetpack/compose) and possibly standard HTML or other frameworks like [React Native](https://reactnative.dev/), [Bootstrap](https://getbootstrap.com/) or [Fluent](https://www.microsoft.com/design/fluent/). Feedback, ideas and partnerships are appreciated! +Turning Figma designs into usable code can be a challenge, often requiring time-consuming manual work. Figma to Code simplifies that process. This plugin generates responsive layouts in `HTML`, `React (JSX)`, `Svelte`, `styled-components`, `Tailwind`, `Flutter`, and `SwiftUI` directly from your designs. Your feedback and ideas are always welcome. ![Gif showing the conversion](assets/lossy_gif.gif) ## How it works -This plugin takes an unconventional approach to improve code quality: it optimizes the layout before the conversion to code even begins. The standard Figma [Nodes](https://www.figma.com/plugin-docs/api/nodes/) (what represents each layer) is a joy to work with, but it can't modify a layer without modifying the user project. For this reason, I decided to virtualize it, remaking the official implementation and naming them `AltNodes`. During the process of converting a `Node` into an `AltNode`, the plugin does the following: +The plugin uses a sophisticated multi-step process to transform your Figma designs into clean, optimized code: + +1. **Node Conversion**: First, the plugin converts Figma's native nodes into JSON representations, preserving all necessary properties while adding optimizations and parent references. + +2. **Intermediate Representation**: The JSON nodes are then transformed into `AltNodes` - a custom virtual representation that can be manipulated without affecting your original design. + +3. **Layout Optimization**: The plugin analyzes and optimizes layouts, detecting patterns like auto-layouts, responsive constraints and color variables. + +4. **Code Generation**: Finally, the optimized structure is transformed into the target framework's code, with special handling for each framework's unique patterns and best practices. If a feature is unsupported, the plugin will provide a warning. ![Conversion Workflow](assets/workflow.png) -That process can also be seen as an [Intermediate Representation](https://en.wikipedia.org/wiki/Intermediate_representation) and might allow this plugin to, one day, live outside Figma. +This intermediate representation approach allows for sophisticated transformations and optimizations before any code is generated, resulting in cleaner, more maintainable output. ## Hard cases -When finding the unknown (a `Group` or `Frame` with more than one child and no vertical or horizontal alignment), Tailwind mode uses [insets](https://tailwindcss.com/docs/top-right-bottom-left/#app) for best cases and `left`, `top` from standard CSS for the worst cases. Flutter mode uses `Stack` and `Positioned.fill`. Both are usually not recommended and can easily defeat the responsiveness. In many scenarios, just wrapping some elements in a `Group` or `Frame` can solve: +Converting visual designs to code inevitably encounters complex edge cases. Here are some challenges the plugin handles: -![Conversion Workflow](assets/examples.png) +1. **Complex Layouts**: When working with mixed positioning (absolute + auto-layout), the plugin has to make intelligent decisions about how to structure the resulting code. It detects parent-child relationships and z-index ordering to produce the most accurate representation. -**Tip**: Instead of selecting the whole page, you can also select individual items. This can be useful for both debugging and componentization. For example: you can use the plugin to generate the code of a single element and then replicate it using a for-loop. +2. **Text Styling**: Rich text with multiple styles requires breaking into multiple elements while preserving layout relationships. -### Todo +3. **Color Variables**: The plugin detects and processes color variables, allowing for theme-consistent output. -- Vectors (tricky in HTML, unsupported in Flutter) -- Images (they are local, how to support them?) -- Line/Star/Polygon (todo. Rectangle and Ellipse were prioritized and are more common) -- The source code is fully commented and there are more than 30 "todo"s there +4. **Gradients and Effects**: Different frameworks handle gradients and effects in unique ways, requiring specialized conversion logic. -### Tailwind limitations +![Conversion Workflow](assets/examples.png) -- **Width:** Tailwind has a maximum width of 384px. If an item passes this, the width will be set to `w-full` (unless it is already relative like `w-1/2`, `w-1/3`, etc). This is usually a feature, but be careful: if most layers in your project are larger than 384px, the plugin's result might be less than optimal. +**Tip**: Instead of selecting the whole page, you can also select individual items. This can be useful for both debugging and componentization. For example: you can use the plugin to generate the code of a single element and then replicate it using a for-loop. -### Flutter limits and ideas +### Todo -- **Stack:** in some simpler cases, a `Stack` could be replaced with a `Container` and a `BoxDecoration`. Discover those cases and optimize them. -- **Material Styles**: text could be matched to existing Material styles (like outputting `Headline6` when text size is 20). -- **Identify Buttons**: the plugin could identify specific buttons and output them instead of always using `Container` or `Material`. +- Vectors (possible to enable in HTML and Tailwind) +- Images (possible to enable to inline them in HTML and Tailwind) +- Line/Star/Polygon ## How to build the project @@ -70,16 +75,49 @@ The plugin is organized as a monorepo. There are several packages: - `ui-src` - loads the common `plugin-ui` and compiles to `index.html` - `apps/debug` - This is a debug mode plugin that is a more convenient way to see all the UI elements. -The plugin is built using Turbo which in turn builds the internal packages. +### Development Workflow + +The project uses [Turborepo](https://turbo.build/) for managing the monorepo, and each package is compiled using [esbuild](https://esbuild.github.io/) for fast development cycles. Only modified files are recompiled when changes are made, making the development process more efficient. + +#### Running the Project + +You have two main options for development: + +1. **Root development mode** (includes debug UI): + + ```bash + pnpm dev + ``` + + This runs the plugin in dev mode and also starts a Next.js server for the debug UI. You can access the debug UI at `http://localhost:3000`. + +2. **Plugin-only development mode**: + + ```bash + cd apps/plugin + pnpm dev + ``` + + This focuses only on the plugin without the Next.js debug UI. Use this when you're making changes specifically to the plugin. + +#### Where to Make Changes + +Most of your development work will happen in these directories: + +- `packages/backend` - For plugin backend +- `packages/plugin-ui` - For plugin UI +- `apps/plugin/` - The main plugin result that combines the backend and UI and is called by Figma. + +You'll rarely need to modify files directly in the `apps/` directory, as they mostly contain build configuration. #### Commands `pnpm run ...` - `dev` - runs the app in dev mode. This can be run in the Figma editor. -- `build` -- `build:watch` -- `lint` +- `build` - builds the project for production +- `build:watch` - builds and watches for changes +- `lint` - runs ESLint - `format` - formats with prettier (warning: may edit files!) #### Debug mode diff --git a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts index bac3ffe4..289df9d9 100644 --- a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts +++ b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts @@ -42,7 +42,7 @@ const getFlexWrap = (node: InferredAutoLayoutResult): string => const getAlignContent = (node: InferredAutoLayoutResult): string => { if (node.layoutWrap !== "WRAP") return ""; - + switch (node.counterAxisAlignItems) { case undefined: case "MIN": From 82fe0b543e437a83e40746e1f456f4e887adba9d Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Fri, 7 Mar 2025 23:44:34 -0300 Subject: [PATCH 056/134] Improve README --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8aa1a839..1c063e75 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

-Turning Figma designs into usable code can be a challenge, often requiring time-consuming manual work. Figma to Code simplifies that process. This plugin generates responsive layouts in `HTML`, `React (JSX)`, `Svelte`, `styled-components`, `Tailwind`, `Flutter`, and `SwiftUI` directly from your designs. Your feedback and ideas are always welcome. +Converting Figma designs into usable code can be a challenge, often requiring time-consuming manual work. Figma to Code simplifies that process. This plugin generates responsive layouts in `HTML`, `React (JSX)`, `Svelte`, `styled-components`, `Tailwind`, `Flutter`, and `SwiftUI` directly from your designs. Your feedback and ideas are always welcome. ![Gif showing the conversion](assets/lossy_gif.gif) @@ -39,11 +39,9 @@ Converting visual designs to code inevitably encounters complex edge cases. Here 1. **Complex Layouts**: When working with mixed positioning (absolute + auto-layout), the plugin has to make intelligent decisions about how to structure the resulting code. It detects parent-child relationships and z-index ordering to produce the most accurate representation. -2. **Text Styling**: Rich text with multiple styles requires breaking into multiple elements while preserving layout relationships. +2. **Color Variables**: The plugin detects and processes color variables, allowing for theme-consistent output. -3. **Color Variables**: The plugin detects and processes color variables, allowing for theme-consistent output. - -4. **Gradients and Effects**: Different frameworks handle gradients and effects in unique ways, requiring specialized conversion logic. +3. **Gradients and Effects**: Different frameworks handle gradients and effects in unique ways, requiring specialized conversion logic. ![Conversion Workflow](assets/examples.png) From c477282ace9a58105597c55959b966fe8a1ff59a Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Sun, 9 Mar 2025 01:42:24 -0300 Subject: [PATCH 057/134] Add background-blend-mode --- packages/backend/src/code.ts | 29 +++++++-- .../backend/src/common/convertFontWeight.ts | 2 +- .../backend/src/html/builderImpl/htmlColor.ts | 49 +++++++++++++++ .../backend/src/html/builderImpl/htmlSize.ts | 1 - .../backend/src/html/htmlDefaultBuilder.ts | 61 ++++++++----------- .../src/tailwind/builderImpl/tailwindBlend.ts | 24 ++++++++ .../src/tailwind/tailwindDefaultBuilder.ts | 19 ++++-- .../src/tailwind/tailwindTextBuilder.ts | 4 +- 8 files changed, 140 insertions(+), 49 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index cf9386ec..25878cb2 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -44,8 +44,12 @@ const memoizedVariableToColorName = async ( variableId: string, ): Promise => { if (!variableCache.has(variableId)) { - const colorName = await variableToColorName(variableId); + const colorName = (await variableToColorName(variableId)).replaceAll( + ",", + "", + ); variableCache.set(variableId, colorName); + return colorName; } return variableCache.get(variableId)!; }; @@ -255,9 +259,18 @@ const processNodePair = async ( const mutableSegment = Object.assign({}, segment); if (settings.useColorVariables && segment.fills) { - mutableSegment.fills = segment.fills.map((d) => ({ ...d })); - await Promise.all( - mutableSegment.fills.map((fill) => processColorVariables(fill)), + mutableSegment.fills = await Promise.all( + segment.fills.map(async (d) => { + if ( + d.blendMode !== "PASS_THROUGH" && + d.blendMode !== "NORMAL" + ) { + addWarning("BlendMode is not supported in Text colors"); + } + const fill = { ...d }; + await processColorVariables(fill); + return fill; + }), ); } @@ -298,8 +311,12 @@ const processNodePair = async ( if ("width" in figmaNode) { jsonNode.width = figmaNode.width; jsonNode.height = figmaNode.height; - jsonNode.x = figmaNode.x; - jsonNode.y = figmaNode.y; + // jsonNode.x = figmaNode.x; + // jsonNode.y = figmaNode.y; + } + + if ("rotation" in jsonNode) { + jsonNode.rotation = jsonNode.rotation * (180 / Math.PI); } if ("individualStrokeWeights" in jsonNode) { diff --git a/packages/backend/src/common/convertFontWeight.ts b/packages/backend/src/common/convertFontWeight.ts index 7306161a..3231dd51 100644 --- a/packages/backend/src/common/convertFontWeight.ts +++ b/packages/backend/src/common/convertFontWeight.ts @@ -3,7 +3,7 @@ import { FontWeightNumber } from "types"; // Convert generic named weights to numbers, which is the way tailwind understands export const convertFontWeight = (weight: string): FontWeightNumber | null => { // change extra-light to extralight - weight = weight.replace(" ", "").replace("-", "").toLowerCase(); + weight = weight.replaceAll(" ", "").replaceAll("-", "").toLowerCase(); switch (weight) { case "thin": return "100"; diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 2d2e7fb5..73b0c6db 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -243,3 +243,52 @@ export const htmlDiamondGradient = (fill: GradientPaint): string => { ) .join(", "); }; + +export const buildBackgroundValues = ( + paintArray: ReadonlyArray | PluginAPI["mixed"], + settings: HTMLSettings, +): string => { + if (paintArray === figma.mixed) { + return ""; + } + + // If only one fill, just use plain color/gradient + if (paintArray.length === 1) { + const paint = paintArray[0]; + if (paint.type === "SOLID") { + return htmlColorFromFills(paintArray, settings); + } else if ( + paint.type === "GRADIENT_LINEAR" || + paint.type === "GRADIENT_RADIAL" || + paint.type === "GRADIENT_ANGULAR" || + paint.type === "GRADIENT_DIAMOND" + ) { + return htmlGradientFromFills(paint); + } + return ""; + } + + // Reverse the array to match CSS layering (first is top-most in CSS) + const styles = [...paintArray].reverse().map((paint, index) => { + if (paint.type === "SOLID") { + // For multiple fills, always convert solid colors to linear gradients + // to ensure proper layering in CSS backgrounds + const color = htmlColorFromFills([paint], settings); + if (index === 0) { + return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`; + } + + return color; + } else if ( + paint.type === "GRADIENT_LINEAR" || + paint.type === "GRADIENT_RADIAL" || + paint.type === "GRADIENT_ANGULAR" || + paint.type === "GRADIENT_DIAMOND" + ) { + return htmlGradientFromFills(paint); + } + return ""; // Handle other paint types safely + }); + + return styles.filter((value) => value !== "").join(", "); +}; diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index bc6df727..faffc414 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -16,7 +16,6 @@ export const htmlSizePartial = ( } const size = nodeSize(node); - console.log("size", size); const nodeParent = (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent ? node.parent.inferredAutoLayout diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 7febce2e..e9eede9b 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -7,8 +7,8 @@ import { htmlBlendMode, } from "./builderImpl/htmlBlend"; import { + buildBackgroundValues, htmlColorFromFills, - htmlGradientFromFills, } from "./builderImpl/htmlColor"; import { htmlPadding } from "./builderImpl/htmlPadding"; import { htmlSizePartial } from "./builderImpl/htmlSize"; @@ -31,7 +31,6 @@ import { HTMLSettings } from "types"; import { cssCollection, generateUniqueClassName, - getSvelteClassName, stylesToCSS, } from "./htmlMain"; @@ -122,12 +121,6 @@ export class HtmlDefaultBuilder { this.autoLayoutPadding(); this.position(); this.blend(); - - // Add z-index if we have a custom value from the itemReverseZIndex handling - if ((this.node as any).customZIndex !== undefined) { - this.addStyles(`z-index: ${(this.node as any).customZIndex}`); - } - return this; } @@ -229,8 +222,11 @@ export class HtmlDefaultBuilder { position(): this { const { node, optimizeLayout, isJSX } = this; - if (commonIsAbsolutePosition(node, optimizeLayout)) { + const isAbsolutePosition = commonIsAbsolutePosition(node, optimizeLayout); + console.log("isAbsolutePosition", isAbsolutePosition, "for node", node); + if (isAbsolutePosition) { const { x, y } = getCommonPositionValue(node); + console.log("x, y, are", x, y); this.addStyles( formatWithJSX("left", isJSX, x), @@ -267,44 +263,39 @@ export class HtmlDefaultBuilder { return this; } - const backgroundValues = this.buildBackgroundValues(paintArray); + const backgroundValues = buildBackgroundValues(paintArray, this.settings); if (backgroundValues) { this.addStyles(formatWithJSX("background", this.isJSX, backgroundValues)); + + // Add blend mode property if multiple fills exist with different blend modes + if (paintArray !== figma.mixed) { + const blendModes = this.buildBackgroundBlendModes(paintArray); + if (blendModes) { + this.addStyles( + formatWithJSX("background-blend-mode", this.isJSX, blendModes), + ); + } + } } return this; } - buildBackgroundValues( - paintArray: ReadonlyArray | PluginAPI["mixed"], - ): string { - if (paintArray === figma.mixed) { + buildBackgroundBlendModes(paintArray: ReadonlyArray): string { + if (paintArray.length === 0) { return ""; } - // If one fill and it's a solid, return the solid RGB color - if (paintArray.length === 1 && paintArray[0].type === "SOLID") { - return htmlColorFromFills(paintArray, this.settings); - } - - // If multiple fills, deal with gradients and convert solid colors to a "dumb" linear-gradient - const styles = paintArray.map((paint) => { - if (paint.type === "SOLID") { - const color = htmlColorFromFills([paint], this.settings); - return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`; - } else if ( - paint.type === "GRADIENT_LINEAR" || - paint.type === "GRADIENT_RADIAL" || - paint.type === "GRADIENT_ANGULAR" || - paint.type === "GRADIENT_DIAMOND" - ) { - return htmlGradientFromFills(paint); + // Reverse the array to match the background order + const blendModes = [...paintArray].reverse().map((paint) => { + if (paint.blendMode === "PASS_THROUGH") { + return "normal"; } - return ""; // Handle other paint types safely + return paint.blendMode?.toLowerCase(); }); - return styles.filter((value) => value !== "").join(", "); + return blendModes.join(", "); } shadow(): this { @@ -374,7 +365,7 @@ export class HtmlDefaultBuilder { formatWithJSX( "filter", this.isJSX, - `blur(${numberToFixedString(blur.radius)}px)`, + `blur(${numberToFixedString(blur.radius / 2)}px)`, ), ); } @@ -387,7 +378,7 @@ export class HtmlDefaultBuilder { formatWithJSX( "backdrop-filter", this.isJSX, - `blur(${numberToFixedString(backgroundBlur.radius)}px)`, + `blur(${numberToFixedString(backgroundBlur.radius / 2)}px)`, ), ); } diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBlend.ts b/packages/backend/src/tailwind/builderImpl/tailwindBlend.ts index 8ad679e8..668f0284 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBlend.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBlend.ts @@ -55,6 +55,30 @@ export const tailwindBlendMode = (node: MinimalBlendMixin): string => { return ""; }; +/** + * Convert a Figma background blend mode to a Tailwind bg-blend-* class + * + * @param paintArray The array of paint fills that may have blend modes + * @returns Tailwind background blend mode class if applicable + */ +export const tailwindBackgroundBlendMode = ( + paintArray: ReadonlyArray, +): string => { + if (paintArray.length === 0) { + return ""; + } + + // Get the top fill's blend mode (in Figma, the last item is the top one) + const topFill = paintArray[paintArray.length - 1]; + if (topFill.blendMode === "NORMAL" || topFill.blendMode === "PASS_THROUGH") { + return ""; + } + + const blendMode = + topFill.blendMode?.toLowerCase()?.replaceAll("_", "-") || "normal"; + return `bg-blend-${blendMode}`; +}; + /** * https://tailwindcss.com/docs/visibility/ * example: invisible diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 8d46a52c..ae23b0c5 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -8,6 +8,7 @@ import { tailwindRotation, tailwindOpacity, tailwindBlendMode, + tailwindBackgroundBlendMode, } from "./builderImpl/tailwindBlend"; import { tailwindBorderWidth, @@ -167,6 +168,14 @@ export class TailwindDefaultBuilder { let gradient = ""; if (kind === "bg") { gradient = tailwindGradientFromFills(paint); + + // Add background blend mode class if applicable + if (paint !== figma.mixed) { + const blendModeClass = tailwindBackgroundBlendMode(paint); + if (blendModeClass) { + this.addAttributes(blendModeClass); + } + } } if (gradient) { this.addAttributes(gradient); @@ -234,9 +243,11 @@ export class TailwindDefaultBuilder { blur() { const { node } = this; if ("effects" in node && node.effects.length > 0) { - const blur = node.effects.find((e) => e.type === "LAYER_BLUR"); + const blur = node.effects.find( + (e) => e.type === "LAYER_BLUR" && e.visible, + ); if (blur) { - const blurValue = pxToBlur(blur.radius); + const blurValue = pxToBlur(blur.radius / 2); if (blurValue) { this.addAttributes( blurValue === "blur" ? "blur" : `blur-${blurValue}`, @@ -245,10 +256,10 @@ export class TailwindDefaultBuilder { } const backgroundBlur = node.effects.find( - (e) => e.type === "BACKGROUND_BLUR", + (e) => e.type === "BACKGROUND_BLUR" && e.visible, ); if (backgroundBlur) { - const backgroundBlurValue = pxToBlur(backgroundBlur.radius); + const backgroundBlurValue = pxToBlur(backgroundBlur.radius / 2); if (backgroundBlurValue) { this.addAttributes( `backdrop-blur${ diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index 822c9b7e..66ee2882 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -137,8 +137,8 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { } const value = node.fontName.style - .replace("italic", "") - .replace(" ", "") + .replaceAll("italic", "") + .replaceAll(" ", "") .toLowerCase(); this.addAttributes(`font-${value}`); From a4c640265ebf76322e0e4a87291358c92e11983c Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Sun, 9 Mar 2025 01:55:42 -0300 Subject: [PATCH 058/134] Make it even faster. No more `inferredAutoLayout`. --- packages/backend/src/code.ts | 38 ++----- packages/backend/src/common/commonPosition.ts | 104 +----------------- .../plugin-ui/src/codegenPreferenceOptions.ts | 9 -- 3 files changed, 15 insertions(+), 136 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 25878cb2..19506505 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -26,18 +26,6 @@ let nodeCacheHits = 0; // Keep track of node names for sequential numbering const nodeNameCounters: Map = new Map(); -// Helper function to add parent references to all children in the node tree -const addParentReferences = (node: any) => { - if (node.children && node.children.length > 0) { - for (const child of node.children) { - // Add parent reference to the child - child.parent = node; - // Recursively process this child's children - addParentReferences(child); - } - } -}; - const variableCache = new Map(); const memoizedVariableToColorName = async ( @@ -171,14 +159,21 @@ function adjustChildrenOrder(node: any) { * @param jsonNode The JSON node to process * @param figmaNode The corresponding Figma node * @param settings Plugin settings + * @param parentNode Optional parent node reference to set */ const processNodePair = async ( jsonNode: any, figmaNode: SceneNode, settings: PluginSettings, + parentNode?: any ) => { if (!jsonNode.id) return; + // Set parent reference if parent is provided + if (parentNode) { + jsonNode.parent = parentNode; + } + // Ensure node has a unique name with simple numbering const cleanName = jsonNode.name.trim(); @@ -295,13 +290,6 @@ const processNodePair = async ( } } - // Extract inferredAutoLayout if optimizeLayout is enabled - if (settings.optimizeLayout && "inferredAutoLayout" in figmaNode) { - jsonNode.inferredAutoLayout = JSON.parse( - JSON.stringify(figmaNode.inferredAutoLayout), - ); - } - // Extract component metadata from instances if ("variantProperties" in figmaNode && figmaNode.variantProperties) { jsonNode.variantProperties = figmaNode.variantProperties; @@ -311,8 +299,8 @@ const processNodePair = async ( if ("width" in figmaNode) { jsonNode.width = figmaNode.width; jsonNode.height = figmaNode.height; - // jsonNode.x = figmaNode.x; - // jsonNode.y = figmaNode.y; + jsonNode.x = figmaNode.x; + jsonNode.y = figmaNode.y; } if ("rotation" in jsonNode) { @@ -388,6 +376,7 @@ const processNodePair = async ( jsonNode.children[i], figmaNode.children[i], settings, + jsonNode // Pass the current node as parent for its children ); } @@ -444,13 +433,6 @@ export const nodesToJSON = async ( `[benchmark][inside nodesToJSON] Process node pairs: ${Date.now() - processNodesStart}ms`, ); - const addParentStart = Date.now(); - // Add parent references to all children in the node tree - nodeJson.forEach((node) => addParentReferences(node)); - console.log( - `[benchmark][inside nodesToJSON] addParentReferences: ${Date.now() - addParentStart}ms`, - ); - return nodeJson; }; diff --git a/packages/backend/src/common/commonPosition.ts b/packages/backend/src/common/commonPosition.ts index ce97d203..ad4d9fe4 100644 --- a/packages/backend/src/common/commonPosition.ts +++ b/packages/backend/src/common/commonPosition.ts @@ -1,97 +1,3 @@ -import { LayoutMode } from "types"; -import { parentCoordinates } from "./parentCoordinates"; - -export const commonPosition = ( - node: SceneNode & DimensionAndPositionMixin, -): LayoutMode => { - // if node is same size as height, position is not necessary - - // detect if Frame's width is same as Child when Frame has Padding. - // warning: this may return true even when false, if size is same, but position is different. However, it would be an unexpected layout. - let hPadding = 0; - let vPadding = 0; - if (node.parent && "layoutMode" in node.parent) { - hPadding = node.parent.paddingLeft + node.parent.paddingRight; - vPadding = node.parent.paddingTop + node.parent.paddingBottom; - } - - if ( - !node.parent || - !("width" in node.parent) || - (node.width === node.parent.width - hPadding && - node.height === node.parent.height - vPadding) - ) { - return ""; - } - - // position is absolute, parent is relative - // return "absolute inset-0 m-auto "; - - const [parentX, parentY] = parentCoordinates(node.parent); - - // if view is too small, anything will be detected; this is necessary to reduce the tolerance. - let threshold = 8; - if (node.width < 16 || node.height < 16) { - threshold = 1; - } - - // < 4 is a threshold. If === is used, there can be rounding errors (28.002 !== 28) - const centerX = - Math.abs(2 * (node.x - parentX) + node.width - node.parent.width) < - threshold; - const centerY = - Math.abs(2 * (node.y - parentY) + node.height - node.parent.height) < - threshold; - - const minX = node.x - parentX < threshold; - const minY = node.y - parentY < threshold; - - const maxX = node.parent.width - (node.x - parentX + node.width) < threshold; - const maxY = - node.parent.height - (node.y - parentY + node.height) < threshold; - - // this needs to be on top, because Tailwind is incompatible with Center, so this will give preference. - if (minX && minY) { - // x left, y top - return "TopStart"; - } else if (minX && maxY) { - // x left, y bottom - return "BottomStart"; - } else if (maxX && minY) { - // x right, y top - return "TopEnd"; - } else if (maxX && maxY) { - // x right, y bottom - return "BottomEnd"; - } - - if (centerX && centerY) { - return "Center"; - } - - if (centerX) { - if (minY) { - // x center, y top - return "TopCenter"; - } - if (maxY) { - // x center, y bottom - return "BottomCenter"; - } - } else if (centerY) { - if (minX) { - // x left, y center - return "CenterStart"; - } - if (maxX) { - // x right, y center - return "CenterEnd"; - } - } - - return "Absolute"; -}; - export const getCommonPositionValue = ( node: SceneNode, ): { x: number; y: number } => { @@ -112,6 +18,10 @@ export const commonIsAbsolutePosition = ( node: SceneNode, optimizeLayout: boolean, ) => { + if ("layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE") { + return true; + } + // No position when parent is inferred auto layout. if ( optimizeLayout && @@ -130,11 +40,7 @@ export const commonIsAbsolutePosition = ( "layoutMode" in node.parent && node.parent.layoutMode === "NONE"; const hasNoLayoutMode = !("layoutMode" in node.parent); - if ( - ("layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE") || - parentLayoutIsNone || - hasNoLayoutMode - ) { + if (parentLayoutIsNone || hasNoLayoutMode) { return true; } diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 7ca4bada..d4e3c39c 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -9,15 +9,6 @@ export const preferenceOptions: LocalCodegenPreferenceOptions[] = [ isDefault: false, includedLanguages: ["HTML", "Tailwind"], }, - { - itemType: "individual_select", - propertyName: "optimizeLayout", - label: "Optimize layout", - description: - "Attempt to auto-layout suitable element groups. This may increase code quality, but may not always work as expected.", - isDefault: true, - includedLanguages: ["HTML", "Tailwind", "Flutter", "SwiftUI"], - }, { itemType: "individual_select", propertyName: "roundTailwindValues", From 42150da05fba62742b2b599ab775d74b448ce5a5 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Sun, 9 Mar 2025 02:37:24 -0300 Subject: [PATCH 059/134] Fix compile --- packages/backend/src/code.ts | 3 --- packages/plugin-ui/src/components/CodePanel.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 19506505..9a504274 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -21,7 +21,6 @@ let getStyledTextSegmentsTime = 0; let getStyledTextSegmentsCalls = 0; let processColorVariablesTime = 0; let processColorVariablesCalls = 0; -let nodeCacheHits = 0; // Keep track of node names for sequential numbering const nodeNameCounters: Map = new Map(); @@ -406,7 +405,6 @@ export const nodesToJSON = async ( ): Promise => { // Reset name counters for each conversion nodeNameCounters.clear(); - nodeCacheHits = 0; const exportJsonStart = Date.now(); // First get the JSON representation of nodes @@ -444,7 +442,6 @@ export const run = async (settings: PluginSettings) => { getStyledTextSegmentsCalls = 0; processColorVariablesTime = 0; processColorVariablesCalls = 0; - nodeCacheHits = 0; variableCache.clear(); clearWarnings(); diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 1ee07632..583e226e 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -12,7 +12,7 @@ import EmptyState from "./EmptyState"; import SettingsGroup from "./SettingsGroup"; import CustomPrefixInput from "./CustomPrefixInput"; import FrameworkTabs from "./FrameworkTabs"; -import { TailwindSettings } from "./tailwindSettings"; +import { TailwindSettings } from "./TailwindSettings"; interface CodePanelProps { code: string; From 2c9445d438eb593df7f205986d4be664f73e11ff Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 00:57:49 -0300 Subject: [PATCH 060/134] Fix radius --- packages/backend/src/altNodes/altNodeUtils.ts | 2 - packages/backend/src/common/commonRadius.ts | 19 +++ .../backend/src/common/exportAsyncProxy.ts | 2 +- .../src/html/builderImpl/htmlBorderRadius.ts | 51 ++++---- .../backend/src/html/htmlDefaultBuilder.ts | 2 - .../tailwind/builderImpl/tailwindBorder.ts | 2 +- packages/plugin-ui/src/components/Preview.tsx | 112 ++++++++++++------ 7 files changed, 123 insertions(+), 67 deletions(-) diff --git a/packages/backend/src/altNodes/altNodeUtils.ts b/packages/backend/src/altNodes/altNodeUtils.ts index 50f05cc8..150e5d53 100644 --- a/packages/backend/src/altNodes/altNodeUtils.ts +++ b/packages/backend/src/altNodes/altNodeUtils.ts @@ -56,8 +56,6 @@ export const renderAndAttachSVG = async (node: any) => { // const nodeName = `${node.type}:${node.id}`; // console.log(altNode); if (node.canBeFlattened) { - console.log("altNode is", node); - if (node.svg) { // console.log(`SVG already rendered for ${nodeName}`); return node; diff --git a/packages/backend/src/common/commonRadius.ts b/packages/backend/src/common/commonRadius.ts index 90ece57a..5f8070eb 100644 --- a/packages/backend/src/common/commonRadius.ts +++ b/packages/backend/src/common/commonRadius.ts @@ -1,6 +1,25 @@ import { CornerRadius } from "types"; export const getCommonRadius = (node: SceneNode): CornerRadius => { + if ("rectangleCornerRadii" in node) { + const [topLeft, topRight, bottomRight, bottomLeft] = + node.rectangleCornerRadii as any; + if ( + topLeft === topRight && + topLeft === bottomRight && + topLeft === bottomLeft + ) { + return { all: topLeft }; + } + + return { + topLeft, + topRight, + bottomRight, + bottomLeft, + }; + } + if ( "cornerRadius" in node && node.cornerRadius !== figma.mixed && diff --git a/packages/backend/src/common/exportAsyncProxy.ts b/packages/backend/src/common/exportAsyncProxy.ts index 1d68ca59..04af43ab 100644 --- a/packages/backend/src/common/exportAsyncProxy.ts +++ b/packages/backend/src/common/exportAsyncProxy.ts @@ -22,7 +22,7 @@ export const exportAsyncProxy = async < } const figmaNode = (await figma.getNodeByIdAsync(node.id)) as ExportMixin; - console.log("getting figma id for", figmaNode); + // console.log("getting figma id for", figmaNode); if (figmaNode.exportAsync === undefined) { // console.log(node); diff --git a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts index f2f7fc39..16d39260 100644 --- a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts +++ b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts @@ -2,13 +2,13 @@ import { getCommonRadius } from "../../common/commonRadius"; import { formatWithJSX } from "../../common/parseJSX"; export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { - const radius = getCommonRadius(node); if (node.type === "ELLIPSE") { return [formatWithJSX("border-radius", isJsx, 9999)]; } + const radius = getCommonRadius(node); + let comp: string[] = []; - let cornerValues: number[] = [0, 0, 0, 0]; let singleCorner: number = 0; if ("all" in radius) { @@ -17,21 +17,28 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { } singleCorner = radius.all; comp.push(formatWithJSX("border-radius", isJsx, radius.all)); - } else if ("topLeftRadius" in node) { - cornerValues = handleIndividualRadius(node); - comp.push( - ...cornerValues - .filter((d) => d > 0) - .map((value, index) => { - const property = [ - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - ][index]; - return formatWithJSX(property, isJsx, value); - }), - ); + } else { + const cornerValues = [ + radius.topLeft, + radius.topRight, + radius.bottomRight, + radius.bottomLeft, + ]; + + // Map each corner value to its corresponding CSS property + const cornerProperties = [ + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + ]; + + // Add CSS properties for non-zero corner values + for (let i = 0; i < 4; i++) { + if (cornerValues[i] > 0) { + comp.push(formatWithJSX(cornerProperties[i], isJsx, cornerValues[i])); + } + } } if ( @@ -45,13 +52,3 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { return comp; }; - -const handleIndividualRadius = (node: RectangleCornerMixin): number[] => { - const cornerValues = [ - node.topLeftRadius, - node.topRightRadius, - node.bottomRightRadius, - node.bottomLeftRadius, - ]; - return cornerValues; -}; diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index e9eede9b..6c721e9f 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -223,10 +223,8 @@ export class HtmlDefaultBuilder { position(): this { const { node, optimizeLayout, isJSX } = this; const isAbsolutePosition = commonIsAbsolutePosition(node, optimizeLayout); - console.log("isAbsolutePosition", isAbsolutePosition, "for node", node); if (isAbsolutePosition) { const { x, y } = getCommonPositionValue(node); - console.log("x, y, are", x, y); this.addStyles( formatWithJSX("left", isJSX, x), diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts index c90095bd..57d4e159 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts @@ -142,7 +142,7 @@ export const tailwindBorderRadius = (node: SceneNode): string => { return `rounded${getRadius(radius.all)}`; } - // todo optimize for tr/tl/br/bl instead of t/r/l/b + // todo optimize for t/r/l/b instead of tr/tl/br/bl let comp: string[] = []; if (radius.topLeft !== 0) { comp.push(`rounded-tl${getRadius(radius.topLeft)}`); diff --git a/packages/plugin-ui/src/components/Preview.tsx b/packages/plugin-ui/src/components/Preview.tsx index 447d01c1..65c8d0f4 100644 --- a/packages/plugin-ui/src/components/Preview.tsx +++ b/packages/plugin-ui/src/components/Preview.tsx @@ -6,13 +6,17 @@ import { MonitorSmartphone, Smartphone, Circle, + Ruler, + MonitorIcon, } from "lucide-react"; const Preview: React.FC<{ htmlPreview: HTMLPreview; }> = (props) => { const [expanded, setExpanded] = useState(false); - const [viewMode, setViewMode] = useState<"desktop" | "mobile">("desktop"); + const [viewMode, setViewMode] = useState<"desktop" | "mobile" | "precision">( + "desktop", + ); const [animationClass, setAnimationClass] = useState(""); const [bgColor, setBgColor] = useState<"white" | "black">("white"); @@ -22,27 +26,26 @@ const Preview: React.FC<{ // Calculate content dimensions based on view mode const contentWidth = - viewMode === "desktop" ? containerWidth : Math.floor(containerWidth * 0.4); // Narrower for mobile - - // Adjust scale factor based on view mode - const scaleFactor = viewMode === "desktop" - ? Math.min( - containerWidth / props.htmlPreview.size.width, - containerHeight / props.htmlPreview.size.height, - ) - : Math.min( - contentWidth / props.htmlPreview.size.width, - containerHeight / props.htmlPreview.size.height, - ); + ? containerWidth + : viewMode === "mobile" + ? Math.floor(containerWidth * 0.4) // Narrower for mobile + : containerWidth; // For precision, use container width for the outer frame + + const scaleFactor = Math.min( + containerWidth / props.htmlPreview.size.width, + containerHeight / props.htmlPreview.size.height, + ); // Add animation when changing view mode useEffect(() => { - setAnimationClass( - viewMode === "desktop" - ? "animate-slide-in-left" - : "animate-slide-in-right", - ); + if (viewMode === "desktop") { + setAnimationClass("animate-slide-in-left"); + } else if (viewMode === "mobile") { + setAnimationClass("animate-slide-in-right"); + } else { + setAnimationClass("animate-fade-in"); + } const timer = setTimeout(() => setAnimationClass(""), 300); // Remove animation class after it completes return () => clearTimeout(timer); }, [viewMode]); @@ -65,15 +68,19 @@ const Preview: React.FC<{ Preview
- {/* Background Color Toggle */} - + {/* Background Color Toggle - Only show in desktop and mobile modes */} + {viewMode !== "precision" && ( + + )} {/* View Mode Toggle */}
@@ -101,6 +108,18 @@ const Preview: React.FC<{ > +
{/* Expand/Collapse Button */} @@ -134,26 +153,46 @@ const Preview: React.FC<{ height: viewMode === "mobile" ? Math.min(containerHeight * 0.9, containerHeight) - : containerHeight, + : containerHeight, // Use full container height for both desktop and precision transition: "width 0.3s ease, height 0.3s ease", }} > - {/* Device frame - just a border for mobile, no status bar or home indicator */} + {/* Device frame - no background for precision mode */}
- {/* Content - no padding needed anymore */} + {/* Content */}
Mobile view + ) : viewMode === "precision" ? ( + + + Precision view + ) : ( - + Desktop view )} From 4dd5448a158e1d31a4a581fbf6d501da7f0fa993 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 01:16:42 -0300 Subject: [PATCH 061/134] Comment responsive preview (for now) --- packages/plugin-ui/src/PluginUI.tsx | 19 ++- packages/plugin-ui/src/components/Preview.tsx | 130 ++++++++---------- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/packages/plugin-ui/src/PluginUI.tsx b/packages/plugin-ui/src/PluginUI.tsx index 9546508e..fc2feee8 100644 --- a/packages/plugin-ui/src/PluginUI.tsx +++ b/packages/plugin-ui/src/PluginUI.tsx @@ -43,10 +43,17 @@ const frameworks: Framework[] = ["HTML", "Tailwind", "Flutter", "SwiftUI"]; export const PluginUI = (props: PluginUIProps) => { const [showAbout, setShowAbout] = useState(false); + const [previewExpanded, setPreviewExpanded] = useState(false); + const [previewViewMode, setPreviewViewMode] = useState< + "desktop" | "mobile" | "precision" + >("precision"); + const [previewBgColor, setPreviewBgColor] = useState<"white" | "black">( + "white", + ); + if (props.isLoading) return ; const isEmpty = props.code === ""; - const warnings = props.warnings ?? []; return ( @@ -95,7 +102,15 @@ export const PluginUI = (props: PluginUIProps) => { ) : (
{isEmpty === false && props.htmlPreview && ( - + )} {warnings.length > 0 && } diff --git a/packages/plugin-ui/src/components/Preview.tsx b/packages/plugin-ui/src/components/Preview.tsx index 65c8d0f4..691af1f5 100644 --- a/packages/plugin-ui/src/components/Preview.tsx +++ b/packages/plugin-ui/src/components/Preview.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import { HTMLPreview } from "types"; import { Maximize2, @@ -7,57 +7,49 @@ import { Smartphone, Circle, Ruler, - MonitorIcon, + Monitor, } from "lucide-react"; +import { cn } from "../lib/utils"; +// Update the component props to receive state from parent const Preview: React.FC<{ htmlPreview: HTMLPreview; + expanded: boolean; + setExpanded: React.Dispatch>; + viewMode: "desktop" | "mobile" | "precision"; + setViewMode: React.Dispatch< + React.SetStateAction<"desktop" | "mobile" | "precision"> + >; + bgColor: "white" | "black"; + setBgColor: React.Dispatch>; }> = (props) => { - const [expanded, setExpanded] = useState(false); - const [viewMode, setViewMode] = useState<"desktop" | "mobile" | "precision">( - "desktop", - ); - const [animationClass, setAnimationClass] = useState(""); - const [bgColor, setBgColor] = useState<"white" | "black">("white"); + const { + htmlPreview, + expanded, + setExpanded, + viewMode, + setViewMode, + bgColor, + setBgColor, + } = props; // Define consistent dimensions regardless of mode const containerWidth = expanded ? 320 : 240; const containerHeight = expanded ? 180 : 120; + // Calculate scale factor first to use in content width calculation + const scaleFactor = Math.min( + containerWidth / htmlPreview.size.width, + containerHeight / htmlPreview.size.height, + ); + // Calculate content dimensions based on view mode const contentWidth = viewMode === "desktop" ? containerWidth : viewMode === "mobile" ? Math.floor(containerWidth * 0.4) // Narrower for mobile - : containerWidth; // For precision, use container width for the outer frame - - const scaleFactor = Math.min( - containerWidth / props.htmlPreview.size.width, - containerHeight / props.htmlPreview.size.height, - ); - - // Add animation when changing view mode - useEffect(() => { - if (viewMode === "desktop") { - setAnimationClass("animate-slide-in-left"); - } else if (viewMode === "mobile") { - setAnimationClass("animate-slide-in-right"); - } else { - setAnimationClass("animate-fade-in"); - } - const timer = setTimeout(() => setAnimationClass(""), 300); // Remove animation class after it completes - return () => clearTimeout(timer); - }, [viewMode]); - - // Add animation when changing size - useEffect(() => { - const timer = setTimeout(() => setAnimationClass("animate-scale-in"), 50); - return () => { - clearTimeout(timer); - setAnimationClass(""); - }; - }, [expanded]); + : htmlPreview.size.width * scaleFactor; // For precision, use scaled original width return (
@@ -69,21 +61,18 @@ const Preview: React.FC<{
{/* Background Color Toggle - Only show in desktop and mobile modes */} - {viewMode !== "precision" && ( - - )} + + {/* View Mode Toggle */} -
+ {/*
-
+
*/} {/* Expand/Collapse Button */} + + {/* Hidden setting for using old plugin version */} +
+ +

+ The new version is up to 100x faster, but might still cause some issues. + If you encounter problems, you can switch to the old version + (and please report issues so they can be fixed). +

+
+
{/* Footer */} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 6f0779c7..55d04ccb 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -30,7 +30,7 @@ export interface PluginSettings FlutterSettings, SwiftUISettings { framework: Framework; - inlineStyle: boolean; + useOldPluginVersion2025: boolean; responsiveRoot: boolean; } // Messaging diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e36aa4dd..8b3c1f76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + copy-to-clipboard: + specifier: ^3.3.3 + version: 3.3.3 lucide-react: specifier: ^0.479.0 version: 0.479.0(react@19.0.0) @@ -110,8 +113,8 @@ importers: version: 1.0.7(tailwindcss@3.4.6) devDependencies: '@types/node': - specifier: ^22.13.9 - version: 22.13.9 + specifier: ^22.13.10 + version: 22.13.10 '@types/react': specifier: ^19.0.10 version: 19.0.10 @@ -126,19 +129,19 @@ importers: version: 8.26.0(eslint@9.22.0(jiti@1.21.7))(typescript@5.8.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@5.4.14(@types/node@22.13.9)) + version: 4.3.4(vite@5.4.14(@types/node@22.13.10)) '@vitejs/plugin-react-swc': specifier: ^3.8.0 - version: 3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.9)) + version: 3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.10)) autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.5.3) + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.3) concurrently: specifier: ^9.1.2 version: 9.1.2 esbuild: - specifier: ^0.25.0 - version: 0.25.0 + specifier: ^0.25.1 + version: 0.25.1 eslint-config-custom: specifier: workspace:* version: link:../../packages/eslint-config-custom @@ -165,10 +168,10 @@ importers: version: 5.8.2 vite: specifier: ^5.4.14 - version: 5.4.14(@types/node@22.13.9) + version: 5.4.14(@types/node@22.13.10) vite-plugin-singlefile: - specifier: ^2.1.0 - version: 2.1.0(rollup@4.34.9)(vite@5.4.14(@types/node@22.13.9)) + specifier: ^2.2.0 + version: 2.2.0(rollup@4.35.0)(vite@5.4.14(@types/node@22.13.10)) packages/backend: dependencies: @@ -409,8 +412,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -421,8 +424,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -433,8 +436,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -445,8 +448,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -457,8 +460,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -469,8 +472,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -481,8 +484,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -493,8 +496,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -505,8 +508,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -517,8 +520,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -529,8 +532,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -541,8 +544,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -553,8 +556,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -565,8 +568,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -577,8 +580,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -589,8 +592,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -601,14 +604,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -619,14 +622,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -637,8 +640,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -649,8 +652,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -661,8 +664,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -673,8 +676,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -685,8 +688,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -958,96 +961,191 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.35.0': + resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.34.9': resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.35.0': + resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.34.9': resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.35.0': + resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.34.9': resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.35.0': + resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.34.9': resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.35.0': + resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.34.9': resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.35.0': + resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.35.0': + resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.34.9': resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.35.0': + resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.34.9': resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.35.0': + resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.34.9': resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.35.0': + resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.35.0': + resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': + resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.34.9': resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.35.0': + resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.34.9': resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.35.0': + resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.34.9': resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.35.0': + resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.34.9': resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.35.0': + resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.34.9': resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.35.0': + resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.34.9': resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.35.0': + resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.34.9': resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.35.0': + resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==} + cpu: [x64] + os: [win32] + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1156,6 +1254,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@22.13.10': + resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} + '@types/node@22.13.9': resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} @@ -1323,6 +1424,13 @@ packages: peerDependencies: postcss: ^8.1.0 + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1394,6 +1502,9 @@ packages: caniuse-lite@1.0.30001702: resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} + caniuse-lite@1.0.30001703: + resolution: {integrity: sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1545,8 +1656,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.113: - resolution: {integrity: sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==} + electron-to-chromium@1.5.114: + resolution: {integrity: sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1595,8 +1706,8 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} hasBin: true @@ -2562,6 +2673,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.35.0: + resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2895,11 +3011,11 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite-plugin-singlefile@2.1.0: - resolution: {integrity: sha512-7tJo+UgZABlKpY/nubth/wxJ4+pUGREPnEwNOknxwl2MM0zTvF14KTU4Ln1lc140gjLLV5mjDrvuoquU7OZqCg==} + vite-plugin-singlefile@2.2.0: + resolution: {integrity: sha512-Ik1wXmJaGzeQtUeIV7JprDUqqy6DlLzXAY27Blei5peE4c9VJF+Kp9xWDJeuX0RJUZmFbIAuw1/RAh06A+Ql7w==} engines: {node: '>18.0.0'} peerDependencies: - rollup: ^4.28.1 + rollup: ^4.35.0 vite: ^5.4.11 || ^6.0.0 vite@5.4.14: @@ -3131,145 +3247,145 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.25.0': + '@esbuild/aix-ppc64@0.25.1': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.25.0': + '@esbuild/android-arm64@0.25.1': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.25.0': + '@esbuild/android-arm@0.25.1': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.25.0': + '@esbuild/android-x64@0.25.1': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.25.0': + '@esbuild/darwin-arm64@0.25.1': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.25.0': + '@esbuild/darwin-x64@0.25.1': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.25.0': + '@esbuild/freebsd-arm64@0.25.1': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.25.0': + '@esbuild/freebsd-x64@0.25.1': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.25.0': + '@esbuild/linux-arm64@0.25.1': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.25.0': + '@esbuild/linux-arm@0.25.1': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.25.0': + '@esbuild/linux-ia32@0.25.1': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.25.0': + '@esbuild/linux-loong64@0.25.1': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.25.0': + '@esbuild/linux-mips64el@0.25.1': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/linux-ppc64@0.25.1': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/linux-riscv64@0.25.1': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-s390x@0.25.1': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/netbsd-arm64@0.25.1': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openbsd-arm64@0.25.1': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/openbsd-x64@0.25.1': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.25.0': + '@esbuild/sunos-x64@0.25.1': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.25.0': + '@esbuild/win32-arm64@0.25.1': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.25.0': + '@esbuild/win32-ia32@0.25.1': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.25.0': + '@esbuild/win32-x64@0.25.1': optional: true '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0(jiti@1.21.7))': @@ -3482,60 +3598,117 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.34.9': optional: true + '@rollup/rollup-android-arm-eabi@4.35.0': + optional: true + '@rollup/rollup-android-arm64@4.34.9': optional: true + '@rollup/rollup-android-arm64@4.35.0': + optional: true + '@rollup/rollup-darwin-arm64@4.34.9': optional: true + '@rollup/rollup-darwin-arm64@4.35.0': + optional: true + '@rollup/rollup-darwin-x64@4.34.9': optional: true + '@rollup/rollup-darwin-x64@4.35.0': + optional: true + '@rollup/rollup-freebsd-arm64@4.34.9': optional: true + '@rollup/rollup-freebsd-arm64@4.35.0': + optional: true + '@rollup/rollup-freebsd-x64@4.34.9': optional: true + '@rollup/rollup-freebsd-x64@4.35.0': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.35.0': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.34.9': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.35.0': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.34.9': optional: true + '@rollup/rollup-linux-arm64-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-arm64-musl@4.34.9': optional: true + '@rollup/rollup-linux-arm64-musl@4.35.0': + optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.34.9': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.34.9': optional: true + '@rollup/rollup-linux-s390x-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.34.9': optional: true + '@rollup/rollup-linux-x64-gnu@4.35.0': + optional: true + '@rollup/rollup-linux-x64-musl@4.34.9': optional: true + '@rollup/rollup-linux-x64-musl@4.35.0': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.34.9': optional: true + '@rollup/rollup-win32-arm64-msvc@4.35.0': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.34.9': optional: true + '@rollup/rollup-win32-ia32-msvc@4.35.0': + optional: true + '@rollup/rollup-win32-x64-msvc@4.34.9': optional: true + '@rollup/rollup-win32-x64-msvc@4.35.0': + optional: true + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.5': {} @@ -3628,6 +3801,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node@22.13.10': + dependencies: + undici-types: 6.20.0 + '@types/node@22.13.9': dependencies: undici-types: 6.20.0 @@ -3723,21 +3900,21 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.9))': + '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.15)(vite@5.4.14(@types/node@22.13.10))': dependencies: '@swc/core': 1.11.8(@swc/helpers@0.5.15) - vite: 5.4.14(@types/node@22.13.9) + vite: 5.4.14(@types/node@22.13.10) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.3.4(vite@5.4.14(@types/node@22.13.9))': + '@vitejs/plugin-react@4.3.4(vite@5.4.14(@types/node@22.13.10))': dependencies: '@babel/core': 7.26.9 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.14(@types/node@22.13.9) + vite: 5.4.14(@types/node@22.13.10) transitivePeerDependencies: - supports-color @@ -3855,6 +4032,16 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001703 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -3882,14 +4069,14 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001702 - electron-to-chromium: 1.5.113 + caniuse-lite: 1.0.30001703 + electron-to-chromium: 1.5.114 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) - bundle-require@5.1.0(esbuild@0.25.0): + bundle-require@5.1.0(esbuild@0.25.1): dependencies: - esbuild: 0.25.0 + esbuild: 0.25.1 load-tsconfig: 0.2.5 busboy@1.6.0: @@ -3921,6 +4108,8 @@ snapshots: caniuse-lite@1.0.30001702: {} + caniuse-lite@1.0.30001703: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4073,7 +4262,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.113: {} + electron-to-chromium@1.5.114: {} emoji-regex@8.0.0: {} @@ -4208,33 +4397,33 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.0: + esbuild@0.25.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 escalade@3.2.0: {} @@ -5260,6 +5449,31 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 + rollup@4.35.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.35.0 + '@rollup/rollup-android-arm64': 4.35.0 + '@rollup/rollup-darwin-arm64': 4.35.0 + '@rollup/rollup-darwin-x64': 4.35.0 + '@rollup/rollup-freebsd-arm64': 4.35.0 + '@rollup/rollup-freebsd-x64': 4.35.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.35.0 + '@rollup/rollup-linux-arm-musleabihf': 4.35.0 + '@rollup/rollup-linux-arm64-gnu': 4.35.0 + '@rollup/rollup-linux-arm64-musl': 4.35.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.35.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.35.0 + '@rollup/rollup-linux-riscv64-gnu': 4.35.0 + '@rollup/rollup-linux-s390x-gnu': 4.35.0 + '@rollup/rollup-linux-x64-gnu': 4.35.0 + '@rollup/rollup-linux-x64-musl': 4.35.0 + '@rollup/rollup-win32-arm64-msvc': 4.35.0 + '@rollup/rollup-win32-ia32-msvc': 4.35.0 + '@rollup/rollup-win32-x64-msvc': 4.35.0 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5575,12 +5789,12 @@ snapshots: tsup@8.4.0(@swc/core@1.11.8(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.5.3)(typescript@5.8.2)(yaml@2.7.0): dependencies: - bundle-require: 5.1.0(esbuild@0.25.0) + bundle-require: 5.1.0(esbuild@0.25.1) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.0 debug: 4.4.0 - esbuild: 0.25.0 + esbuild: 0.25.1 joycon: 3.1.1 picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.3)(yaml@2.7.0) @@ -5688,19 +5902,19 @@ snapshots: util-deprecate@1.0.2: {} - vite-plugin-singlefile@2.1.0(rollup@4.34.9)(vite@5.4.14(@types/node@22.13.9)): + vite-plugin-singlefile@2.2.0(rollup@4.35.0)(vite@5.4.14(@types/node@22.13.10)): dependencies: micromatch: 4.0.8 - rollup: 4.34.9 - vite: 5.4.14(@types/node@22.13.9) + rollup: 4.35.0 + vite: 5.4.14(@types/node@22.13.10) - vite@5.4.14(@types/node@22.13.9): + vite@5.4.14(@types/node@22.13.10): dependencies: esbuild: 0.21.5 postcss: 8.5.3 - rollup: 4.34.9 + rollup: 4.35.0 optionalDependencies: - '@types/node': 22.13.9 + '@types/node': 22.13.10 fsevents: 2.3.3 webidl-conversions@4.0.2: {} From 672ad5c55f5a724632b2453ae9164e0e0b18bbc0 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 12:19:54 -0300 Subject: [PATCH 066/134] Fix --- packages/backend/src/code.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 28e6ec30..02a0fb83 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -468,7 +468,6 @@ export const run = async (settings: PluginSettings) => { if (useOldPluginVersion2025) { convertedSelection = oldConvertNodesToAltNodes(selection, null); console.log("convertedSelection", convertedSelection); - console.log("convertedSelection", convertedSelection[0].children[0]); } else { const nodeJson = await nodesToJSON(selection, settings); console.log(`[benchmark] nodesToJSON: ${Date.now() - nodeToJSONStart}ms`); From fd771395586fa489602a5e8d6fb99d9411228209 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 13:08:17 -0300 Subject: [PATCH 067/134] Debug --- apps/plugin/plugin-src/code.ts | 5 +-- packages/backend/src/code.ts | 40 +++++-------------- .../src/html/builderImpl/htmlBorderRadius.ts | 27 +++++++------ .../backend/src/html/htmlDefaultBuilder.ts | 1 - .../tailwind/builderImpl/tailwindBorder.ts | 1 - packages/backend/src/tailwind/tailwindMain.ts | 5 ++- 6 files changed, 30 insertions(+), 49 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index c8fa2d78..3fe67a9b 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -198,10 +198,7 @@ const standardMode = async () => { } try { - result.newConversion = await convertNodesToAltNodes( - result.json || [], - null, - ); + result.newConversion = await nodesToJSON(nodes, userPluginSettings); } catch (error) { console.error("Error in new conversion:", error); } diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 02a0fb83..a592cfe4 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -187,39 +187,19 @@ const processNodePair = async ( ? cleanName : `${cleanName}_${count.toString().padStart(2, "0")}`; - // Check if we need to handle gradients - const hasGradient = GRADIENT_PROPERTIES.some((propName) => { + GRADIENT_PROPERTIES.forEach((propName) => { const property = jsonNode[propName]; - return ( + if ( + propName in figmaNode && property && - Array.isArray(property) && - property.length > 0 && - property.some( - (item: any) => item.type && item.type.startsWith("GRADIENT_"), - ) - ); + property.some((item: any) => item.type.startsWith("GRADIENT_")) + ) { + jsonNode[propName] = JSON.parse( + JSON.stringify((figmaNode as any)[propName]), + ); + } }); - // Handle gradients - if (hasGradient) { - GRADIENT_PROPERTIES.forEach((propName) => { - const property = jsonNode[propName]; - if ( - property && - Array.isArray(property) && - property.length > 0 && - property.some( - (item) => item.type && item.type.startsWith("GRADIENT_"), - ) && - propName in figmaNode - ) { - jsonNode[propName] = JSON.parse( - JSON.stringify((figmaNode as any)[propName]), - ); - } - }); - } - // Handle text-specific properties if (figmaNode.type === "TEXT") { const getSegmentsStart = Date.now(); @@ -376,7 +356,7 @@ const processNodePair = async ( jsonNode.children[i], figmaNode.children[i], settings, - jsonNode, // Pass the current node as parent for its children + jsonNode, ); } diff --git a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts index 16d39260..33451874 100644 --- a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts +++ b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts @@ -2,18 +2,29 @@ import { getCommonRadius } from "../../common/commonRadius"; import { formatWithJSX } from "../../common/parseJSX"; export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { + let comp: string[] = []; + + if ( + "children" in node && + node.children.length > 0 && + "clipsContent" in node && + node.clipsContent === true + ) { + comp.push(formatWithJSX("overflow", isJsx, "hidden")); + } + if (node.type === "ELLIPSE") { - return [formatWithJSX("border-radius", isJsx, 9999)]; + comp.push(formatWithJSX("border-radius", isJsx, 9999)); + return comp; } const radius = getCommonRadius(node); - let comp: string[] = []; let singleCorner: number = 0; if ("all" in radius) { if (radius.all === 0) { - return []; + return comp; } singleCorner = radius.all; comp.push(formatWithJSX("border-radius", isJsx, radius.all)); @@ -41,14 +52,6 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { } } - if ( - "children" in node && - "clipsContent" in node && - node.children.length > 0 && - node.clipsContent === true - ) { - comp.push(formatWithJSX("overflow", isJsx, "hidden")); - } - + console.log("comp was", comp); return comp; }; diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 510ecccf..8f6469c2 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -262,7 +262,6 @@ export class HtmlDefaultBuilder { position(): this { const { node, optimizeLayout, isJSX } = this; const isAbsolutePosition = commonIsAbsolutePosition(node, optimizeLayout); - console.log("node is absolute", isAbsolutePosition, node, node.parent); if (isAbsolutePosition) { const { x, y } = getCommonPositionValue(node); diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts index ed48f0e7..23459cc4 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts @@ -67,7 +67,6 @@ export const tailwindBorderWidth = ( // Check stroke alignment and layout mode const strokeAlign = "strokeAlign" in node ? node.strokeAlign : "INSIDE"; - const layoutMode = "layoutMode" in node ? node.layoutMode : "NONE"; if ("all" in commonBorder) { if (commonBorder.all === 0) { diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 8bee9378..8e960747 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -179,7 +179,10 @@ const tailwindFrame = async ( ); const childrenStr = await tailwindWidgetGenerator(sortedChildren, settings); - const clipsContentClass = node.clipsContent ? "overflow-hidden" : ""; + const clipsContentClass = + node.clipsContent && "children" in node && node.children.length > 0 + ? "overflow-hidden" + : ""; let layoutProps = ""; if (node.layoutMode !== "NONE") { From 80a2f65d2c90345c7d7cfee9b0846f27cce17b45 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 13:31:04 -0300 Subject: [PATCH 068/134] Try to fix --- apps/plugin/plugin-src/code.ts | 18 +++++++++++------- apps/plugin/ui-src/App.tsx | 1 - packages/backend/src/code.ts | 16 ++++++++-------- .../src/html/builderImpl/htmlAutoLayout.ts | 17 ++++++++--------- packages/backend/src/html/htmlMain.ts | 19 +++++-------------- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 3fe67a9b..974166b1 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -8,12 +8,6 @@ import { htmlMain, postSettingsChanged, } from "backend"; -import { convertNodesToAltNodes } from "backend/src/altNodes/altConversion"; -import { - disableParent, - oldConvertNodesToAltNodes, - setDisableParent, -} from "backend/src/altNodes/oldAltConversion"; import { nodesToJSON } from "backend/src/code"; import { retrieveGenericSolidUIColors } from "backend/src/common/retrieveUI/retrieveColors"; import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain"; @@ -198,7 +192,17 @@ const standardMode = async () => { } try { - result.newConversion = await nodesToJSON(nodes, userPluginSettings); + const newNodes = await nodesToJSON(nodes, userPluginSettings); + const removeParent = (node: any) => { + if (node.parent) { + delete node.parent; + } + if (node.children) { + node.children.forEach(removeParent); + } + }; + newNodes.forEach(removeParent); + result.newConversion = newNodes; } catch (error) { console.error("Error in new conversion:", error); } diff --git a/apps/plugin/ui-src/App.tsx b/apps/plugin/ui-src/App.tsx index 90549a1b..0a4045a7 100644 --- a/apps/plugin/ui-src/App.tsx +++ b/apps/plugin/ui-src/App.tsx @@ -104,7 +104,6 @@ export default function App() { break; case "selection-json": - console.log("selection json"); const json = event.data.pluginMessage.data; copy(JSON.stringify(json, null, 2)); diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index a592cfe4..1a40790b 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -163,12 +163,17 @@ function adjustChildrenOrder(node: any) { */ const processNodePair = async ( jsonNode: any, - figmaNode: SceneNode, settings: PluginSettings, parentNode?: any, ) => { if (!jsonNode.id) return; + const figmaNode = await figma.getNodeByIdAsync(jsonNode.id); + + if (!figmaNode) { + return; + } + // Set parent reference if parent is provided if (parentNode) { jsonNode.parent = parentNode; @@ -352,12 +357,7 @@ const processNodePair = async ( // ); for (let i = 0; i < jsonNode.children.length; i++) { - await processNodePair( - jsonNode.children[i], - figmaNode.children[i], - settings, - jsonNode, - ); + await processNodePair(jsonNode.children[i], settings, jsonNode); } if ( @@ -409,7 +409,7 @@ export const nodesToJSON = async ( // Now process each top-level node pair (JSON node + Figma node) const processNodesStart = Date.now(); for (let i = 0; i < nodes.length; i++) { - await processNodePair(nodeJson[i], nodes[i], settings); + await processNodePair(nodeJson[i], settings); } console.log( `[benchmark][inside nodesToJSON] Process node pairs: ${Date.now() - processNodesStart}ms`, diff --git a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts index 289df9d9..2fb246c3 100644 --- a/packages/backend/src/html/builderImpl/htmlAutoLayout.ts +++ b/packages/backend/src/html/builderImpl/htmlAutoLayout.ts @@ -69,19 +69,18 @@ const getFlex = ( : "inline-flex"; export const htmlAutoLayoutProps = ( - node: SceneNode, - autoLayout: InferredAutoLayoutResult, + node: SceneNode & InferredAutoLayoutResult, settings: HTMLSettings, ): string[] => formatMultipleJSXArray( { - "flex-direction": getFlexDirection(autoLayout), - "justify-content": getJustifyContent(autoLayout), - "align-items": getAlignItems(autoLayout), - gap: getGap(autoLayout), - display: getFlex(node, autoLayout), - "flex-wrap": getFlexWrap(autoLayout), - "align-content": getAlignContent(autoLayout), + "flex-direction": getFlexDirection(node), + "justify-content": getJustifyContent(node), + "align-items": getAlignItems(node), + gap: getGap(node), + display: getFlex(node, node), + "flex-wrap": getFlexWrap(node), + "align-content": getAlignContent(node), }, settings.htmlGenerationMode === "jsx", ); diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 749b7217..adc17e0c 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -564,22 +564,13 @@ const htmlFrame = async ( ); if (node.layoutMode !== "NONE") { - const rowColumn = htmlAutoLayoutProps(node, node, settings); + const rowColumn = htmlAutoLayoutProps(node, settings); return await htmlContainer(node, childrenStr, rowColumn, settings); - } else { - if (settings.optimizeLayout && node.inferredAutoLayout !== null) { - const rowColumn = htmlAutoLayoutProps( - node, - node.inferredAutoLayout, - settings, - ); - return await htmlContainer(node, childrenStr, rowColumn, settings); - } - - // node.layoutMode === "NONE" && node.children.length > 1 - // children needs to be absolute - return await htmlContainer(node, childrenStr, [], settings); } + + // node.layoutMode === "NONE" && node.children.length > 1 + // children needs to be absolute + return await htmlContainer(node, childrenStr, [], settings); }; // properties named propSomething always take care of "," From bfa2da488b4c394671243bffffeb1c04f31b363f Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Mon, 10 Mar 2025 13:43:55 -0300 Subject: [PATCH 069/134] No more optimize layout --- apps/plugin/plugin-src/code.ts | 1 - packages/backend/src/code.ts | 16 +++++----- .../backend/src/common/commonChildrenOrder.ts | 25 ---------------- packages/backend/src/common/commonPosition.ts | 5 +--- .../src/flutter/builderImpl/flutterSize.ts | 23 ++++++++------- .../backend/src/flutter/flutterContainer.ts | 15 ++-------- .../src/flutter/flutterDefaultBuilder.ts | 8 ++--- packages/backend/src/flutter/flutterMain.ts | 22 ++++---------- .../backend/src/html/builderImpl/htmlSize.ts | 6 +--- .../backend/src/html/htmlDefaultBuilder.ts | 23 ++++----------- packages/backend/src/html/htmlMain.ts | 6 +--- .../src/swiftui/swiftuiDefaultBuilder.ts | 16 ++++------ packages/backend/src/swiftui/swiftuiMain.ts | 29 ++++--------------- .../src/tailwind/builderImpl/tailwindSize.ts | 6 +--- .../src/tailwind/tailwindDefaultBuilder.ts | 27 ++++------------- packages/backend/src/tailwind/tailwindMain.ts | 12 +------- .../plugin-ui/src/components/CodePanel.tsx | 1 - packages/types/src/types.ts | 1 - 18 files changed, 61 insertions(+), 181 deletions(-) delete mode 100644 packages/backend/src/common/commonChildrenOrder.ts diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 974166b1..2b10d3a0 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -19,7 +19,6 @@ let userPluginSettings: PluginSettings; export const defaultPluginSettings: PluginSettings = { framework: "HTML", - optimizeLayout: true, showLayerNames: false, useOldPluginVersion2025: false, responsiveRoot: false, diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 1a40790b..a592cfe4 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -163,17 +163,12 @@ function adjustChildrenOrder(node: any) { */ const processNodePair = async ( jsonNode: any, + figmaNode: SceneNode, settings: PluginSettings, parentNode?: any, ) => { if (!jsonNode.id) return; - const figmaNode = await figma.getNodeByIdAsync(jsonNode.id); - - if (!figmaNode) { - return; - } - // Set parent reference if parent is provided if (parentNode) { jsonNode.parent = parentNode; @@ -357,7 +352,12 @@ const processNodePair = async ( // ); for (let i = 0; i < jsonNode.children.length; i++) { - await processNodePair(jsonNode.children[i], settings, jsonNode); + await processNodePair( + jsonNode.children[i], + figmaNode.children[i], + settings, + jsonNode, + ); } if ( @@ -409,7 +409,7 @@ export const nodesToJSON = async ( // Now process each top-level node pair (JSON node + Figma node) const processNodesStart = Date.now(); for (let i = 0; i < nodes.length; i++) { - await processNodePair(nodeJson[i], settings); + await processNodePair(nodeJson[i], nodes[i], settings); } console.log( `[benchmark][inside nodesToJSON] Process node pairs: ${Date.now() - processNodesStart}ms`, diff --git a/packages/backend/src/common/commonChildrenOrder.ts b/packages/backend/src/common/commonChildrenOrder.ts deleted file mode 100644 index 506870fb..00000000 --- a/packages/backend/src/common/commonChildrenOrder.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const commonSortChildrenWhenInferredAutoLayout = ( - node: SceneNode & ChildrenMixin, - optimize: boolean, -) => { - if (node.children.length <= 1) { - return node.children; - } - - if ( - optimize && - "inferredAutoLayout" in node && - node.inferredAutoLayout !== null - ) { - const children = [...node.children]; - switch (node.inferredAutoLayout.layoutMode) { - case "HORIZONTAL": - return children.sort((a, b) => a.x - b.x); - // NONE is a bug from Figma. - case "NONE": - case "VERTICAL": - return children.sort((a, b) => a.y - b.y); - } - } - return node.children; -}; diff --git a/packages/backend/src/common/commonPosition.ts b/packages/backend/src/common/commonPosition.ts index 8515b931..7899b4e7 100644 --- a/packages/backend/src/common/commonPosition.ts +++ b/packages/backend/src/common/commonPosition.ts @@ -14,10 +14,7 @@ export const getCommonPositionValue = ( }; }; -export const commonIsAbsolutePosition = ( - node: SceneNode, - optimizeLayout: boolean, -) => { +export const commonIsAbsolutePosition = (node: SceneNode) => { if ("layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE") { return true; } diff --git a/packages/backend/src/flutter/builderImpl/flutterSize.ts b/packages/backend/src/flutter/builderImpl/flutterSize.ts index a219a8a1..ccd4f2bd 100644 --- a/packages/backend/src/flutter/builderImpl/flutterSize.ts +++ b/packages/backend/src/flutter/builderImpl/flutterSize.ts @@ -3,22 +3,23 @@ import { numberToFixedString } from "../../common/numToAutoFixed"; // Used in tests. export const flutterSizeWH = (node: SceneNode): string => { - const fSize = flutterSize(node, false); + const fSize = flutterSize(node); const size = fSize.width + fSize.height; return size; }; export const flutterSize = ( node: SceneNode, - optimizeLayout: boolean, -): { width: string; height: string; isExpanded: boolean; constraints: Record } => { +): { + width: string; + height: string; + isExpanded: boolean; + constraints: Record; +} => { const size = nodeSize(node); let isExpanded: boolean = false; - const nodeParent = - (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent - ? node.parent.inferredAutoLayout - : null) ?? node.parent; + const nodeParent = node.parent; // this cast will always be true, since nodeWidthHeight was called with false to relative. let propWidth = ""; @@ -55,19 +56,19 @@ export const flutterSize = ( // Handle min/max constraints const constraints: Record = {}; - + if (node.minWidth !== undefined && node.minWidth !== null) { constraints.minWidth = numberToFixedString(node.minWidth); } - + if (node.maxWidth !== undefined && node.maxWidth !== null) { constraints.maxWidth = numberToFixedString(node.maxWidth); } - + if (node.minHeight !== undefined && node.minHeight !== null) { constraints.minHeight = numberToFixedString(node.minHeight); } - + if (node.maxHeight !== undefined && node.maxHeight !== null) { constraints.maxHeight = numberToFixedString(node.maxHeight); } diff --git a/packages/backend/src/flutter/flutterContainer.ts b/packages/backend/src/flutter/flutterContainer.ts index aac9f96e..df4ae110 100644 --- a/packages/backend/src/flutter/flutterContainer.ts +++ b/packages/backend/src/flutter/flutterContainer.ts @@ -14,11 +14,7 @@ import { numberToFixedString } from "../common/numToAutoFixed"; import { getCommonRadius } from "../common/commonRadius"; import { commonStroke } from "../common/commonStroke"; -export const flutterContainer = ( - node: SceneNode, - child: string, - optimizeLayout: boolean, -): string => { +export const flutterContainer = (node: SceneNode, child: string): string => { // ignore the view when size is zero or less // while technically it shouldn't get less than 0, due to rounding errors, // it can get to values like: -0.000004196293048153166 @@ -28,10 +24,7 @@ export const flutterContainer = ( // ignore for Groups const propBoxDecoration = getDecoration(node); - const { width, height, isExpanded, constraints } = flutterSize( - node, - optimizeLayout, - ); + const { width, height, isExpanded, constraints } = flutterSize(node); const clipBehavior = "clipsContent" in node && node.clipsContent === true @@ -43,9 +36,7 @@ export const flutterContainer = ( // [propPadding] will be "padding: const EdgeInsets.symmetric(...)" or "" let propPadding = ""; if ("paddingLeft" in node) { - propPadding = flutterPadding( - (optimizeLayout ? node.inferredAutoLayout : null) ?? node, - ); + propPadding = flutterPadding(node); } let result: string; diff --git a/packages/backend/src/flutter/flutterDefaultBuilder.ts b/packages/backend/src/flutter/flutterDefaultBuilder.ts index c25a5063..47d1c581 100644 --- a/packages/backend/src/flutter/flutterDefaultBuilder.ts +++ b/packages/backend/src/flutter/flutterDefaultBuilder.ts @@ -18,8 +18,8 @@ export class FlutterDefaultBuilder { this.child = optChild; } - createContainer(node: SceneNode, optimizeLayout: boolean): this { - this.child = flutterContainer(node, this.child, optimizeLayout); + createContainer(node: SceneNode): this { + this.child = flutterContainer(node, this.child); return this; } @@ -34,8 +34,8 @@ export class FlutterDefaultBuilder { return this; } - position(node: SceneNode, optimizeLayout: boolean): this { - if (commonIsAbsolutePosition(node, optimizeLayout)) { + position(node: SceneNode): this { + if (commonIsAbsolutePosition(node)) { const { x, y } = getCommonPositionValue(node); this.child = generateWidgetCode("Positioned", { left: x, diff --git a/packages/backend/src/flutter/flutterMain.ts b/packages/backend/src/flutter/flutterMain.ts index 8dffb577..162e45e5 100644 --- a/packages/backend/src/flutter/flutterMain.ts +++ b/packages/backend/src/flutter/flutterMain.ts @@ -11,7 +11,6 @@ import { getCrossAxisAlignment, getMainAxisAlignment, } from "./builderImpl/flutterAutoLayout"; -import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; import { PluginSettings } from "types"; import { addWarning } from "../common/commonConversionWarnings"; import { getPlaceholderImage } from "../common/images"; @@ -152,9 +151,9 @@ const flutterContainer = (node: SceneNode, child: string): string => { } const builder = new FlutterDefaultBuilder(propChild) - .createContainer(node, localSettings.optimizeLayout) + .createContainer(node) .blendAttr(node) - .position(node, localSettings.optimizeLayout); + .position(node); return builder.child; }; @@ -163,24 +162,15 @@ const flutterText = (node: TextNode): string => { const builder = new FlutterTextBuilder().createText(node); previousExecutionCache.push(builder.child); - return builder - .blendAttr(node) - .textAutoSize(node) - .position(node, localSettings.optimizeLayout).child; + return builder.blendAttr(node).textAutoSize(node).position(node).child; }; const flutterFrame = ( node: SceneNode & BaseFrameMixin & MinimalBlendMixin, ): string => { - // Sort children according to layout direction - const sortedChildren = commonSortChildrenWhenInferredAutoLayout( - node, - localSettings.optimizeLayout, - ); - // Check if any direct children need absolute positioning - const hasAbsoluteChildren = sortedChildren.some( - (child) => (child as any).layoutPositioning === "ABSOLUTE", + const hasAbsoluteChildren = node.children.some( + (child: any) => (child as any).layoutPositioning === "ABSOLUTE", ); // Add warning if we need to use Stack due to absolute positioning @@ -209,7 +199,7 @@ const flutterFrame = ( const rowColumn = makeRowColumn(node, children); return flutterContainer(node, rowColumn); } else { - if (localSettings.optimizeLayout && node.inferredAutoLayout) { + if (node.inferredAutoLayout) { const rowColumn = makeRowColumn(node.inferredAutoLayout, children); return flutterContainer(node, rowColumn); } diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index faffc414..d6bbc280 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -5,7 +5,6 @@ import { isPreviewGlobal } from "../htmlMain"; export const htmlSizePartial = ( node: SceneNode, isJsx: boolean, - optimizeLayout: boolean, ): { width: string; height: string; constraints: string[] } => { if (isPreviewGlobal && node.parent === undefined) { return { @@ -16,10 +15,7 @@ export const htmlSizePartial = ( } const size = nodeSize(node); - const nodeParent = - (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent - ? node.parent.inferredAutoLayout - : null) ?? node.parent; + const nodeParent = node.parent; let w = ""; if (typeof size.width === "number") { diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 8f6469c2..12ceb5b4 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -58,10 +58,6 @@ export class HtmlDefaultBuilder { return this.settings.htmlGenerationMode === "jsx"; } - get optimizeLayout() { - return this.settings.optimizeLayout; - } - get exportCSS() { return this.settings.htmlGenerationMode === "svelte"; } @@ -260,8 +256,8 @@ export class HtmlDefaultBuilder { } position(): this { - const { node, optimizeLayout, isJSX } = this; - const isAbsolutePosition = commonIsAbsolutePosition(node, optimizeLayout); + const { node, isJSX } = this; + const isAbsolutePosition = commonIsAbsolutePosition(node); if (isAbsolutePosition) { const { x, y } = getCommonPositionValue(node); @@ -273,10 +269,7 @@ export class HtmlDefaultBuilder { } else { if ( node.type === "GROUP" || - ("layoutMode" in node && - ((optimizeLayout ? node.inferredAutoLayout : null) ?? node) - ?.layoutMode === "NONE") || - (node as any).isRelative + ("layoutMode" in node && (node as any).isRelative) ) { this.addStyles(formatWithJSX("position", isJSX, "relative")); } @@ -356,7 +349,6 @@ export class HtmlDefaultBuilder { const { width, height, constraints } = htmlSizePartial( node, settings.htmlGenerationMode === "jsx", - settings.optimizeLayout, ); if (node.type === "TEXT") { @@ -384,14 +376,9 @@ export class HtmlDefaultBuilder { } autoLayoutPadding(): this { - const { node, isJSX, optimizeLayout } = this; + const { node, isJSX } = this; if ("paddingLeft" in node) { - this.addStyles( - ...htmlPadding( - (optimizeLayout ? (node as any).inferredAutoLayout : null) ?? node, - isJSX, - ), - ); + this.addStyles(...htmlPadding(node, isJSX)); } return this; } diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index adc17e0c..2972fc38 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -3,7 +3,6 @@ import { HtmlTextBuilder } from "./htmlTextBuilder"; import { HtmlDefaultBuilder } from "./htmlDefaultBuilder"; import { htmlAutoLayoutProps } from "./builderImpl/htmlAutoLayout"; import { formatWithJSX } from "../common/parseJSX"; -import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; import { PluginSettings, HTMLPreview, @@ -558,10 +557,7 @@ const htmlFrame = async ( node: SceneNode & BaseFrameMixin, settings: HTMLSettings, ): Promise => { - const childrenStr = await htmlWidgetGenerator( - commonSortChildrenWhenInferredAutoLayout(node, settings.optimizeLayout), - settings, - ); + const childrenStr = await htmlWidgetGenerator(node.children, settings); if (node.layoutMode !== "NONE") { const rowColumn = htmlAutoLayoutProps(node, settings); diff --git a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts index 2033e96f..c082d38b 100644 --- a/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts +++ b/packages/backend/src/swiftui/swiftuiDefaultBuilder.ts @@ -36,8 +36,8 @@ export class SwiftuiDefaultBuilder { }); } - commonPositionStyles(node: SceneNode, optimizeLayout: boolean): this { - this.position(node, optimizeLayout); + commonPositionStyles(node: SceneNode): this { + this.position(node); if ("layoutAlign" in node && "opacity" in node) { this.blend(node); } @@ -75,8 +75,8 @@ export class SwiftuiDefaultBuilder { return { centerX: centerBasedX, centerY: centerBasedY }; } - position(node: SceneNode, optimizeLayout: boolean): this { - if (commonIsAbsolutePosition(node, optimizeLayout)) { + position(node: SceneNode): this { + if (commonIsAbsolutePosition(node)) { const { x, y } = getCommonPositionValue(node); const { centerX, centerY } = this.topLeftToCenterOffset( x, @@ -152,13 +152,9 @@ export class SwiftuiDefaultBuilder { return this; } - autoLayoutPadding(node: SceneNode, optimizeLayout: boolean): this { + autoLayoutPadding(node: SceneNode): this { if ("paddingLeft" in node) { - this.pushModifier( - swiftuiPadding( - (optimizeLayout ? node.inferredAutoLayout : null) ?? node, - ), - ); + this.pushModifier(swiftuiPadding(node)); } return this; } diff --git a/packages/backend/src/swiftui/swiftuiMain.ts b/packages/backend/src/swiftui/swiftuiMain.ts index 84acbf7c..28b2b7db 100644 --- a/packages/backend/src/swiftui/swiftuiMain.ts +++ b/packages/backend/src/swiftui/swiftuiMain.ts @@ -5,7 +5,6 @@ import { } from "../common/numToAutoFixed"; import { SwiftuiTextBuilder } from "./swiftuiTextBuilder"; import { SwiftuiDefaultBuilder } from "./swiftuiDefaultBuilder"; -import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; import { PluginSettings } from "types"; import { addWarning } from "../common/commonConversionWarnings"; import { getVisibleNodes } from "../common/nodeVisibility"; @@ -126,12 +125,12 @@ export const swiftuiContainer = ( const result = new SwiftuiDefaultBuilder(kind) .shapeForeground(node) - .autoLayoutPadding(node, localSettings.optimizeLayout) + .autoLayoutPadding(node) .size(node) .shapeBackground(node) .cornerRadius(node) .shapeBorder(node) - .commonPositionStyles(node, localSettings.optimizeLayout) + .commonPositionStyles(node) .effects(node) .build(kind === stack ? -2 : 0); @@ -153,9 +152,7 @@ const swiftuiText = (node: TextNode): string => { const result = new SwiftuiTextBuilder().createText(node); previousExecutionCache.push(result.build()); - return result - .commonPositionStyles(node, localSettings.optimizeLayout) - .build(); + return result.commonPositionStyles(node).build(); }; const swiftuiFrame = ( @@ -167,12 +164,7 @@ const swiftuiFrame = ( node.children.length > 1 ? indentLevel + 1 : indentLevel, ); - const anyStack = createDirectionalStack( - children, - localSettings.optimizeLayout && node.inferredAutoLayout !== null - ? node.inferredAutoLayout - : node, - ); + const anyStack = createDirectionalStack(children, node); return swiftuiContainer(node, anyStack); }; @@ -250,21 +242,12 @@ const widgetGeneratorWithLimits = ( ) => { if (node.children.length < 10) { // standard way - return swiftuiWidgetGenerator( - commonSortChildrenWhenInferredAutoLayout( - node, - localSettings.optimizeLayout, - ), - indentLevel, - ); + return swiftuiWidgetGenerator(node.children, indentLevel); } const chunk = 10; let strBuilder = ""; - const slicedChildren = commonSortChildrenWhenInferredAutoLayout( - node, - localSettings.optimizeLayout, - ).slice(0, 100); + const slicedChildren = node.children.slice(0, 100); // I believe no one should have more than 100 items in a single nesting level. If you do, please email me. if (node.children.length > 100) { diff --git a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts index e514af82..43575681 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts @@ -30,14 +30,10 @@ const formatTailwindSizeValue = ( export const tailwindSizePartial = ( node: SceneNode, - optimizeLayout: boolean, settings?: TailwindSettings, ): { width: string; height: string; constraints: string } => { const size = nodeSize(node); - const nodeParent = - (node.parent && optimizeLayout && "inferredAutoLayout" in node.parent - ? node.parent.inferredAutoLayout - : null) ?? node.parent; + const nodeParent = node.parent; let w = ""; if (typeof size.width === "number") { diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index ebfc2f86..3340e674 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -51,9 +51,6 @@ export class TailwindDefaultBuilder { get isJSX() { return this.settings.tailwindGenerationMode === "jsx"; } - get optimizeLayout() { - return this.settings.optimizeLayout; - } constructor(node: SceneNode, settings: TailwindSettings) { this.node = node; @@ -123,8 +120,8 @@ export class TailwindDefaultBuilder { } position(): this { - const { node, optimizeLayout } = this; - if (commonIsAbsolutePosition(node, optimizeLayout)) { + const { node } = this; + if (commonIsAbsolutePosition(node)) { const { x, y } = getCommonPositionValue(node); const parsedX = numberToFixedString(x); @@ -143,10 +140,7 @@ export class TailwindDefaultBuilder { this.addAttributes(`absolute`); } else if ( node.type === "GROUP" || - ("layoutMode" in node && - ((optimizeLayout ? node.inferredAutoLayout : null) ?? node) - ?.layoutMode === "NONE") || - (node as any).isRelative + ("layoutMode" in node && (node as any)).isRelative ) { this.addAttributes("relative"); } @@ -196,12 +190,8 @@ export class TailwindDefaultBuilder { // must be called before Position, because of the hasFixedSize attribute. size(): this { - const { node, optimizeLayout, settings } = this; - const { width, height, constraints } = tailwindSizePartial( - node, - optimizeLayout, - settings, - ); + const { node, settings } = this; + const { width, height, constraints } = tailwindSizePartial(node, settings); if (node.type === "TEXT") { switch (node.textAutoResize) { @@ -229,12 +219,7 @@ export class TailwindDefaultBuilder { autoLayoutPadding(): this { if ("paddingLeft" in this.node) { - this.addAttributes( - ...tailwindPadding( - (this.optimizeLayout ? this.node.inferredAutoLayout : null) ?? - this.node, - ), - ); + this.addAttributes(...tailwindPadding(this.node)); } return this; } diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index 8e960747..d2ed446c 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -1,7 +1,6 @@ import { retrieveTopFill } from "../common/retrieveFill"; import { indentString } from "../common/indentString"; import { addWarning } from "../common/commonConversionWarnings"; -import { commonSortChildrenWhenInferredAutoLayout } from "../common/commonChildrenOrder"; import { getVisibleNodes } from "../common/nodeVisibility"; import { getPlaceholderImage } from "../common/images"; import { TailwindTextBuilder } from "./tailwindTextBuilder"; @@ -173,11 +172,7 @@ const tailwindFrame = async ( node: FrameNode | InstanceNode | ComponentNode | ComponentSetNode, settings: TailwindSettings, ): Promise => { - const sortedChildren = commonSortChildrenWhenInferredAutoLayout( - node, - localTailwindSettings.optimizeLayout, - ); - const childrenStr = await tailwindWidgetGenerator(sortedChildren, settings); + const childrenStr = await tailwindWidgetGenerator(node.children, settings); const clipsContentClass = node.clipsContent && "children" in node && node.children.length > 0 @@ -187,11 +182,6 @@ const tailwindFrame = async ( if (node.layoutMode !== "NONE") { layoutProps = tailwindAutoLayoutProps(node, node); - } else if ( - localTailwindSettings.optimizeLayout && - node.inferredAutoLayout !== null - ) { - layoutProps = tailwindAutoLayoutProps(node, node.inferredAutoLayout); } // Combine classes properly, ensuring no extra spaces diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 583e226e..285f3dd2 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -116,7 +116,6 @@ const CodePanel = (props: CodePanelProps) => { "roundTailwindColors", "useColorVariables", "showLayerNames", - "optimizeLayout", "embedImages", "embedVectors", ]; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 55d04ccb..6f02854f 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -2,7 +2,6 @@ import "@figma/plugin-typings"; // Settings export type Framework = "Flutter" | "SwiftUI" | "HTML" | "Tailwind"; export interface HTMLSettings { - optimizeLayout: boolean; showLayerNames: boolean; embedImages: boolean; embedVectors: boolean; From 353a9375218fd8ebcf2aa6097ac9612991243c50 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 11 Mar 2025 02:04:33 -0300 Subject: [PATCH 070/134] Improve gradients --- packages/backend/src/code.ts | 3118 ++++++++++++++++- packages/backend/src/common/color.ts | 50 +- packages/backend/src/common/commonPosition.ts | 19 +- packages/backend/src/common/retrieveFill.ts | 8 +- .../src/flutter/builderImpl/flutterColor.ts | 192 +- packages/backend/src/flutter/flutterMain.ts | 3 +- .../src/html/builderImpl/htmlBorderRadius.ts | 1 - .../backend/src/html/builderImpl/htmlColor.ts | 159 +- .../backend/src/html/htmlDefaultBuilder.ts | 8 +- packages/backend/src/html/htmlMain.ts | 1 + packages/backend/src/html/htmlTextBuilder.ts | 7 +- .../src/swiftui/builderImpl/swiftuiColor.ts | 18 +- .../src/tailwind/builderImpl/tailwindColor.ts | 5 +- 13 files changed, 3323 insertions(+), 266 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index a592cfe4..d14c9e09 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -1,4 +1,3 @@ -import { convertNodesToAltNodes } from "./altNodes/altConversion"; import { retrieveGenericSolidUIColors, retrieveGenericLinearGradients as retrieveGenericGradients, @@ -14,6 +13,7 @@ import { convertToCode } from "./common/retrieveUI/convertToCode"; import { generateHTMLPreview } from "./html/htmlMain"; import { variableToColorName } from "./tailwind/conversionTables"; import { oldConvertNodesToAltNodes } from "./altNodes/oldAltConversion"; +import { convertNodesToAltNodes, convertNodeToAltNode } from "./altNodes/altConversion"; // Performance tracking counters let getNodeByIdAsyncTime = 0; @@ -187,19 +187,6 @@ const processNodePair = async ( ? cleanName : `${cleanName}_${count.toString().padStart(2, "0")}`; - GRADIENT_PROPERTIES.forEach((propName) => { - const property = jsonNode[propName]; - if ( - propName in figmaNode && - property && - property.some((item: any) => item.type.startsWith("GRADIENT_")) - ) { - jsonNode[propName] = JSON.parse( - JSON.stringify((figmaNode as any)[propName]), - ); - } - }); - // Handle text-specific properties if (figmaNode.type === "TEXT") { const getSegmentsStart = Date.now(); @@ -371,6 +358,13 @@ const processNodePair = async ( } adjustChildrenOrder(jsonNode); + } else if ( + "children" in figmaNode && + figmaNode.children.length !== jsonNode.children.length + ) { + addWarning( + "Error: JSON and Figma nodes have different child counts. Please report this issue.", + ); } }; @@ -456,11 +450,3103 @@ export const run = async (settings: PluginSettings) => { // Now we work directly with the JSON nodes const convertNodesStart = Date.now(); convertedSelection = await convertNodesToAltNodes(nodeJson, null); + const convertedSelection2 = [ + { + id: "I2099:38616;1739:34914", + name: "Modal", + type: "FRAME", + scrollBehavior: "SCROLLS", + boundVariables: { + minHeight: { + type: "VARIABLE_ALIAS", + id: "VariableID:ca5fdd543c7de4d7a5d043eb31c365871c484b3e/4411:779", + }, + maxHeight: { + type: "VARIABLE_ALIAS", + id: "VariableID:143b8d97896be058533bf7578cac052ff1473fe5/4411:780", + }, + size: { + x: { + type: "VARIABLE_ALIAS", + id: "VariableID:51164a6f21a5daac8ea5fa8551389d4d7864a05e/1129:893", + }, + y: { + type: "VARIABLE_ALIAS", + id: "VariableID:c7de0427328e0238030ad2c66fb3a86abcdf6421/2867:8", + }, + }, + rectangleCornerRadii: { + RECTANGLE_TOP_LEFT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", + }, + RECTANGLE_TOP_RIGHT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", + }, + RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", + }, + RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", + }, + }, + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", + }, + ], + effects: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:55268df3aca26e8ed4182c6831670c631ab2e88b/4411:298", + }, + { + type: "VARIABLE_ALIAS", + id: "VariableID:7b576d4f7cef936e728857b8d6f7952e2a6dd6fe/3157:78", + }, + { + type: "VARIABLE_ALIAS", + id: "VariableID:e7dccd708e8eb5f689ef26147d2d985b7af1bfd8/2013:336", + }, + ], + }, + children: [ + { + id: "I2099:38616;1739:61360", + name: "Content", + type: "FRAME", + scrollBehavior: "SCROLLS", + children: [ + { + id: "I2099:38616;1739:34917", + name: "Modal Container", + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + mainComponent: "Swap Modal Container#893:1", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", + }, + paddingLeft: { + type: "VARIABLE_ALIAS", + id: "VariableID:c44b7d196360345cd2e77fddb9fdbb56b074630c/10434:249", + }, + paddingTop: { + type: "VARIABLE_ALIAS", + id: "VariableID:becb73e51eeba1f8c786ade8394c2a4d29eb3ecf/10434:245", + }, + paddingRight: { + type: "VARIABLE_ALIAS", + id: "VariableID:dca108e1e0b09a774a4527df680b8306bc9208b2/10434:246", + }, + paddingBottom: { + type: "VARIABLE_ALIAS", + id: "VariableID:f6e871ff408efebce7861cdde12d1530510445e9/10434:248", + }, + minHeight: { + type: "VARIABLE_ALIAS", + id: "VariableID:d4bef8dbc813180acafb9ee86fec3ab4e9cb2983/3203:9", + }, + maxHeight: { + type: "VARIABLE_ALIAS", + id: "VariableID:5c0fdb551d4699d20f1fa1328995f4fb9d0ad8d5/3203:12", + }, + }, + componentId: "2099:38674", + isExposedInstance: true, + componentProperties: { + Type: { + value: "Fixed", + type: "VARIANT", + boundVariables: {}, + }, + Allignment: { + value: "Default (L-R)", + type: "VARIANT", + boundVariables: {}, + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664", + overriddenFields: [ + "componentProperties", + "primaryAxisSizingMode", + "layoutGrow", + "counterAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19688", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19673;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19661", + overriddenFields: [ + "componentProperties", + "counterAxisSizingMode", + "primaryAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19706", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054", + overriddenFields: [ + "layoutAlign", + "sharedPluginData", + "pluginData", + "componentProperties", + "counterAxisSizingMode", + "layoutGrow", + "paddingRight", + "paddingLeft", + "primaryAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19697", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19673", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19691", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19703;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19667", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19691;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917", + overriddenFields: [ + "topLeftRadius", + "cornerRadius", + "topRightRadius", + "name", + "bottomLeftRadius", + "bottomRightRadius", + "primaryAxisSizingMode", + "counterAxisSizingMode", + "layoutGrow", + "boundVariables", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19670", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19700", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19703", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3055", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19670;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19697;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19688;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19706;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3056", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19667;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;1732:20311", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19694", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3053", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19694;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3052", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + { + id: "I2099:38616;1739:34917;2326:19700;979:10619", + overriddenFields: ["sharedPluginData", "pluginData"], + }, + ], + children: [ + { + id: "I2099:38616;1739:34917;1732:20311", + name: "Grid", + type: "FRAME", + scrollBehavior: "SCROLLS", + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", + }, + }, + children: [ + { + id: "I2099:38616;1739:34917;1732:20312", + name: "Row", + type: "FRAME", + scrollBehavior: "SCROLLS", + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:be243c44965c9affe677211ea0cd661d873653e1/10434:247", + }, + counterAxisSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", + }, + }, + children: [ + { + id: "I2099:38616;1739:34917;2326:19661", + name: "TextImage Variation 2", + type: "INSTANCE", + scrollBehavior: "SCROLLS", + boundVariables: { + size: { + x: { + type: "VARIABLE_ALIAS", + id: "VariableID:7dde29c0269cd3c53a0f9f7c1e2cc8e86d43cf3b/3157:236", + }, + y: { + type: "VARIABLE_ALIAS", + id: "VariableID:7dde29c0269cd3c53a0f9f7c1e2cc8e86d43cf3b/3157:236", + }, + }, + }, + componentId: "2043:22065", + componentProperties: { + "↳Swap Icon/Image/Graphic#9837:448": { + value: "2092:64304", + type: "INSTANCE_SWAP", + preferredValues: [], + }, + "Custom Size": { + value: "False", + type: "VARIANT", + boundVariables: {}, + }, + "Predefined Size": { + value: "L Container", + type: "VARIANT", + boundVariables: {}, + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19661", + overriddenFields: [ + "componentProperties", + "counterAxisSizingMode", + "primaryAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [ + { + id: "I2099:38616;1739:34917;2326:19661;117:435", + name: "FaxOfHook", + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + mainComponent: + "↳Swap Icon/Image/Graphic#9837:448", + }, + componentId: "2092:64304", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [ + { + id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", + name: "Vector", + type: "VECTOR", + scrollBehavior: "SCROLLS", + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + fillOverrideTable: { + "1": null, + }, + strokes: [], + strokeWeight: 0.75, + strokeAlign: "CENTER", + strokeJoin: "ROUND", + strokeCap: "ROUND", + strokeMiterAngle: 11.478341102600098, + absoluteBoundingBox: { + x: -1897.455078125, + y: -2168.515380859375, + width: 127.45187377929688, + height: 125.89124298095703, + }, + absoluteRenderBounds: { + x: -1897.455078125, + y: -2168.515380859375, + width: 127.451904296875, + height: 125.8912353515625, + }, + constraints: { + vertical: "SCALE", + horizontal: "SCALE", + }, + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + absoluteBoundingBox: { + x: -1907.5, + y: -2177.75, + width: 150, + height: 150, + }, + absoluteRenderBounds: { + x: -1907.5, + y: -2177.75, + width: 150, + height: 150, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 1, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "FILL", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "HORIZONTAL", + itemSpacing: 10, + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1907.5, + y: -2177.75, + width: 150, + height: 150, + }, + absoluteRenderBounds: { + x: -1907.5, + y: -2177.75, + width: 150, + height: 150, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664", + name: "TextImage Variation 1", + type: "INSTANCE", + scrollBehavior: "SCROLLS", + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:3f744b5e3c8b3619411d855f7fe34dd7351a9435/3157:1061", + }, + minWidth: { + type: "VARIABLE_ALIAS", + id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", + }, + }, + componentId: "2058:19231", + exposedInstances: [ + "I2099:38616;1739:34917;2326:19664;1202:3050", + "I2099:38616;1739:34917;2326:19664;1202:3052", + "I2099:38616;1739:34917;2326:19664;1202:3053", + "I2099:38616;1739:34917;2326:19664;1202:3054", + "I2099:38616;1739:34917;2326:19664;1202:3055", + "I2099:38616;1739:34917;2326:19664;1202:3056", + ], + componentProperties: { + "↳Show Title Big#1053:9": { + value: false, + type: "BOOLEAN", + }, + "↳Show Content Small#1053:13": { + value: false, + type: "BOOLEAN", + }, + "Show Icon#1053:11": { + value: false, + type: "BOOLEAN", + }, + "Show Text#3122:8": { + value: true, + type: "BOOLEAN", + }, + "↳Show Title Small#1053:12": { + value: false, + type: "BOOLEAN", + }, + "↳Show Content#1053:10": { + value: true, + type: "BOOLEAN", + }, + "↳Show Clarification#1053:7": { + value: false, + type: "BOOLEAN", + }, + "↳Swap Icon#1053:8": { + value: "2033:1462", + type: "INSTANCE_SWAP", + preferredValues: [], + }, + Alignment: { + value: "ImageLeft-Middle", + type: "VARIANT", + boundVariables: {}, + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664", + overriddenFields: [ + "componentProperties", + "primaryAxisSizingMode", + "layoutGrow", + "counterAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3056", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3052", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054", + overriddenFields: [ + "layoutAlign", + "sharedPluginData", + "pluginData", + "componentProperties", + "counterAxisSizingMode", + "layoutGrow", + "paddingRight", + "paddingLeft", + "primaryAxisSizingMode", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3055", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3053", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3050", + name: "Placeholder", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + mainComponent: "↳Swap Icon#1053:8", + visible: "Show Icon#1053:11", + }, + boundVariables: { + size: { + x: { + type: "VARIABLE_ALIAS", + id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", + }, + y: { + type: "VARIABLE_ALIAS", + id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", + }, + }, + }, + explicitVariableModes: { + "VariableCollectionId:e2fa2f7f8d460f250b4a1269b312f661b543be85/443:278": + "146:9", + }, + componentId: "2033:1462", + isExposedInstance: true, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 37.5, + height: 37.5, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3051", + name: "Text", + type: "FRAME", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "Show Text#3122:8", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", + }, + }, + children: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3052", + name: "TitleBig", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Title Big#1053:9", + }, + componentId: "2033:2259", + isExposedInstance: true, + componentProperties: { + "Title Label#1366:0": { + value: "Title big text", + type: "TEXT", + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3052", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "HORIZONTAL", + primaryAxisSizingMode: "FIXED", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 321.5, + height: 32, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3053", + name: "TitleSmall", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Title Small#1053:12", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", + }, + }, + componentId: "2058:19206", + isExposedInstance: true, + componentProperties: { + "Title small Label#1366:1": { + value: "Title small text", + type: "TEXT", + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3053", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 18, + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 321.5, + height: 26, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054", + name: "Content", + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Content#1053:10", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", + }, + }, + componentId: "2058:19212", + isExposedInstance: true, + componentProperties: { + "Content Label#1366:2": { + value: + "You can send a fax to the dialed number or manually receive a fax by selecting one of these options.", + type: "TEXT", + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054", + overriddenFields: [ + "layoutAlign", + "sharedPluginData", + "pluginData", + "componentProperties", + "counterAxisSizingMode", + "layoutGrow", + "paddingRight", + "paddingLeft", + "primaryAxisSizingMode", + ], + }, + ], + children: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054;1053:1206", + name: "Content", + type: "TEXT", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + characters: "Content Label#1366:2", + }, + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + lineHeight: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", + }, + ], + fontFamily: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", + }, + ], + fontSize: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "OUTSIDE", + absoluteBoundingBox: { + x: -1710, + y: -2177.75, + width: 707.5, + height: 62, + }, + absoluteRenderBounds: { + x: -1709.5155029296875, + y: -2171.339599609375, + width: 681.0682373046875, + height: 53.128662109375, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + characters: + "You can send a fax to the dialed number or manually receive a fax by selecting one of these options.", + characterStyleOverrides: [], + styleOverrideTable: {}, + lineTypes: ["NONE"], + lineIndentations: [0], + style: { + fontFamily: "HP Simplified", + fontPostScriptName: + "HPSimplified-Regular", + fontStyle: "Regular", + fontWeight: 400, + textAutoResize: "HEIGHT", + fontSize: 25.5, + textAlignHorizontal: "LEFT", + textAlignVertical: "TOP", + letterSpacing: 0, + lineHeightPx: 30.600000381469727, + lineHeightPercent: 103.53753662109375, + lineHeightPercentFontSize: 120, + lineHeightUnit: "PIXELS", + }, + layoutVersion: 4, + styles: { + text: "2022:10407", + }, + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1256", + name: "Content", + visible: false, + type: "TEXT", + scrollBehavior: "SCROLLS", + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + lineHeight: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", + }, + ], + fontFamily: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", + }, + ], + fontSize: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "OUTSIDE", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 216, + height: 31, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + characters: "Content text", + characterStyleOverrides: [], + styleOverrideTable: {}, + lineTypes: ["NONE"], + lineIndentations: [0], + style: { + fontFamily: "HP Simplified", + fontPostScriptName: + "HPSimplified-Regular", + fontStyle: "Regular", + fontWeight: 400, + textAutoResize: "HEIGHT", + fontSize: 25.5, + textAlignHorizontal: "LEFT", + textAlignVertical: "TOP", + letterSpacing: 0, + lineHeightPx: 30.600000381469727, + lineHeightPercent: 103.53753662109375, + lineHeightPercentFontSize: 120, + lineHeightUnit: "PIXELS", + }, + layoutVersion: 4, + styles: { + text: "2022:10407", + }, + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1267", + name: "Content", + visible: false, + type: "TEXT", + scrollBehavior: "SCROLLS", + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + lineHeight: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", + }, + ], + fontFamily: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", + }, + ], + fontSize: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "OUTSIDE", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 216, + height: 31, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + characters: "Content text", + characterStyleOverrides: [], + styleOverrideTable: {}, + lineTypes: ["NONE"], + lineIndentations: [0], + style: { + fontFamily: "HP Simplified", + fontPostScriptName: + "HPSimplified-Regular", + fontStyle: "Regular", + fontWeight: 400, + textAutoResize: "HEIGHT", + fontSize: 25.5, + textAlignHorizontal: "LEFT", + textAlignVertical: "TOP", + letterSpacing: 0, + lineHeightPx: 30.600000381469727, + lineHeightPercent: 103.53753662109375, + lineHeightPercentFontSize: 120, + lineHeightUnit: "PIXELS", + }, + layoutVersion: 4, + styles: { + text: "2022:10407", + }, + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1283", + name: "Content", + visible: false, + type: "TEXT", + scrollBehavior: "SCROLLS", + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + lineHeight: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", + }, + ], + fontFamily: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", + }, + ], + fontSize: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "OUTSIDE", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 216, + height: 31, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + characters: "Content text", + characterStyleOverrides: [], + styleOverrideTable: {}, + lineTypes: ["NONE"], + lineIndentations: [0], + style: { + fontFamily: "HP Simplified", + fontPostScriptName: + "HPSimplified-Regular", + fontStyle: "Regular", + fontWeight: 400, + textAutoResize: "HEIGHT", + fontSize: 25.5, + textAlignHorizontal: "LEFT", + textAlignVertical: "TOP", + letterSpacing: 0, + lineHeightPx: 30.600000381469727, + lineHeightPercent: 103.53753662109375, + lineHeightPercentFontSize: 120, + lineHeightUnit: "PIXELS", + }, + layoutVersion: 4, + styles: { + text: "2022:10407", + }, + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1304", + name: "Content", + visible: false, + type: "TEXT", + scrollBehavior: "SCROLLS", + boundVariables: { + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + ], + lineHeight: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", + }, + ], + fontFamily: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", + }, + ], + fontSize: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "OUTSIDE", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 216, + height: 31, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + characters: "Content text", + characterStyleOverrides: [], + styleOverrideTable: {}, + lineTypes: ["NONE"], + lineIndentations: [0], + style: { + fontFamily: "HP Simplified", + fontPostScriptName: + "HPSimplified-Regular", + fontStyle: "Regular", + fontWeight: 400, + textAutoResize: "HEIGHT", + fontSize: 25.5, + textAlignHorizontal: "LEFT", + textAlignVertical: "TOP", + letterSpacing: 0, + lineHeightPx: 30.600000381469727, + lineHeightPercent: 103.53753662109375, + lineHeightPercentFontSize: 120, + lineHeightUnit: "PIXELS", + }, + layoutVersion: 4, + styles: { + text: "2022:10407", + }, + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 18, + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + paddingLeft: 10, + paddingRight: 10, + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + absoluteRenderBounds: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3055", + name: "ContentSmall", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Content Small#1053:13", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", + }, + }, + componentId: "2058:19218", + isExposedInstance: true, + componentProperties: { + "Content Small Label#1366:3": { + value: "Content small text", + type: "TEXT", + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3055", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 18, + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 321.5, + height: 20, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19664;1202:3056", + name: "Clarification", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Clarification#1053:7", + }, + boundVariables: { + itemSpacing: { + type: "VARIABLE_ALIAS", + id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", + }, + }, + componentId: "2058:19224", + isExposedInstance: true, + componentProperties: { + "Clarification Label#1366:4": { + value: "Clarification text", + type: "TEXT", + }, + }, + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19664;1202:3056", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 18, + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2132.75, + width: 321.5, + height: 20, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 18, + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + absoluteRenderBounds: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 1, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "HORIZONTAL", + itemSpacing: 15, + primaryAxisSizingMode: "FIXED", + counterAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + absoluteRenderBounds: { + x: -1720, + y: -2177.75, + width: 727.5, + height: 62, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 1, + minWidth: 37.5, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19667", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19667", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19667;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1678.5, + y: -2177.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19670", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19670", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19670;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1564, + y: -2177.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19688", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19688", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19688;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1907.5, + y: -2122.25, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19691", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19691", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19691;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1793, + y: -2122.25, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19694", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19694;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19694", + overriddenFields: ["visible"], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1678.5, + y: -2122.25, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19673", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19673;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + { + id: "I2099:38616;1739:34917;2326:19673", + overriddenFields: ["visible"], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1564, + y: -2122.25, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19697", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19697", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19697;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1907.5, + y: -2066.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19700", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19700", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19700;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1793, + y: -2066.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19703", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19703", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19703;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1678.5, + y: -2066.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:34917;2326:19706", + name: "Slot", + visible: false, + type: "INSTANCE", + scrollBehavior: "SCROLLS", + componentId: "2060:58189", + overrides: [ + { + id: "I2099:38616;1739:34917;2326:19706", + overriddenFields: ["visible"], + }, + { + id: "I2099:38616;1739:34917;2326:19706;979:10619", + overriddenFields: [ + "sharedPluginData", + "pluginData", + ], + }, + ], + children: [], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + cornerRadius: 4, + cornerSmoothing: 0, + strokeWeight: 2, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "HORIZONTAL", + counterAxisAlignItems: "CENTER", + primaryAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + strokeDashes: [8, 4], + absoluteBoundingBox: { + x: -1564, + y: -2066.75, + width: 98, + height: 39, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "HORIZONTAL", + itemSpacing: 37.5, + primaryAxisSizingMode: "FIXED", + layoutWrap: "WRAP", + counterAxisSpacing: 37.5, + counterAxisAlignContent: "AUTO", + absoluteBoundingBox: { + x: -1907.5, + y: -2177.75, + width: 915, + height: 150, + }, + absoluteRenderBounds: { + x: -1907.5, + y: -2177.75, + width: 915, + height: 150, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + uniqueName: "Row", + width: 915, + height: 150, + x: 0, + y: 0, + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + primaryAxisAlignItems: "MIN", + counterAxisAlignItems: "MIN", + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 37.5, + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1907.5, + y: -2177.75, + width: 915, + height: 150, + }, + absoluteRenderBounds: { + x: -1907.5, + y: -2177.75, + width: 915, + height: 150, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 0, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "HUG", + effects: [], + interactions: [], + uniqueName: "Grid", + width: 915, + height: 150, + x: 22.5, + y: 22.5, + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + primaryAxisAlignItems: "MIN", + counterAxisAlignItems: "MIN", + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + overflowDirection: "VERTICAL_SCROLLING", + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 37.5, + primaryAxisSizingMode: "FIXED", + paddingLeft: 22.5, + paddingRight: 22.5, + paddingTop: 22.5, + paddingBottom: 22.5, + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1930, + y: -2200.25, + width: 960, + height: 536.25, + }, + absoluteRenderBounds: { + x: -1930, + y: -2200.25, + width: 960, + height: 536.25, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 1, + minHeight: 136.25, + maxHeight: 536.25, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "FILL", + effects: [], + interactions: [], + uniqueName: "Modal Container", + variantProperties: { + Type: "Fixed", + Allignment: "Default (L-R)", + }, + width: 960, + height: 536.25, + x: 0, + y: 0, + primaryAxisAlignItems: "MIN", + counterAxisAlignItems: "MIN", + }, + { + id: "I2099:38616;1739:61707", + name: "Scroll Container", + type: "INSTANCE", + locked: true, + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "↳Show Scroll Container#1739:42", + }, + componentId: "438:9274", + componentProperties: { + "Show Top Gradient#1272:20": { + value: false, + type: "BOOLEAN", + }, + "Show Bottom Gradient#1272:19": { + value: true, + type: "BOOLEAN", + }, + "Show Container Bullets#2778:4": { + value: true, + type: "BOOLEAN", + }, + "Show Left Gradient#1359:0": { + value: false, + type: "BOOLEAN", + }, + "Show Right Gradient#1359:4": { + value: true, + type: "BOOLEAN", + }, + "Show Scrollbar#1272:18": { + value: true, + type: "BOOLEAN", + }, + "Show V Scrollbar#1359:8": { + value: true, + type: "BOOLEAN", + }, + "Show H Scrollbar#1359:12": { + value: true, + type: "BOOLEAN", + }, + Variation: { + value: "Vertical", + type: "VARIANT", + boundVariables: {}, + }, + }, + overrides: [], + children: [ + { + id: "I2099:38616;1739:61707;548:24039", + name: "Gradient Top", + visible: false, + type: "RECTANGLE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "Show Top Gradient#1272:20", + }, + boundVariables: { + size: { + y: { + type: "VARIABLE_ALIAS", + id: "VariableID:7de17999810183e2464dbfd32573c3ecf15a9f2e/3157:462", + }, + }, + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", + }, + { + type: "VARIABLE_ALIAS", + id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "GRADIENT_LINEAR", + gradientHandlePositions: [ + { + x: 0.5, + y: -3.0616171314629196e-17, + }, + { + x: 0.5, + y: 0.9999999999999999, + }, + { + x: 0, + y: 0, + }, + ], + gradientStops: [ + { + color: { + r: 0.0784313753247261, + g: 0.0784313753247261, + b: 0.0784313753247261, + a: 1, + }, + position: 0, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", + }, + }, + }, + { + color: { + r: 0.0784313753247261, + g: 0.0784313753247261, + b: 0.0784313753247261, + a: 0, + }, + position: 1, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", + }, + }, + }, + ], + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + styles: { + fill: "438:9260", + }, + absoluteBoundingBox: { + x: -1930, + y: -2200, + width: 960, + height: 30, + }, + absoluteRenderBounds: null, + constraints: { + vertical: "TOP", + horizontal: "LEFT_RIGHT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutPositioning: "ABSOLUTE", + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:61707;548:24040", + name: "Gradient Bottom", + type: "RECTANGLE", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "Show Bottom Gradient#1272:19", + }, + boundVariables: { + size: { + y: { + type: "VARIABLE_ALIAS", + id: "VariableID:7de17999810183e2464dbfd32573c3ecf15a9f2e/3157:462", + }, + }, + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", + }, + { + type: "VARIABLE_ALIAS", + id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "GRADIENT_LINEAR", + gradientHandlePositions: [ + { + x: 0.5, + y: -3.0616171314629196e-17, + }, + { + x: 0.5, + y: 0.9999999999999999, + }, + { + x: 0, + y: 0, + }, + ], + gradientStops: [ + { + color: { + r: 0.0784313753247261, + g: 0.0784313753247261, + b: 0.0784313753247261, + a: 0, + }, + position: 0, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", + }, + }, + }, + { + color: { + r: 0.0784313753247261, + g: 0.0784313753247261, + b: 0.0784313753247261, + a: 1, + }, + position: 1, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", + }, + }, + }, + ], + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + styles: { + fill: "438:9261", + }, + absoluteBoundingBox: { + x: -1930, + y: -1686, + width: 960, + height: 30, + }, + absoluteRenderBounds: { + x: -1930, + y: -1686, + width: 960, + height: 22, + }, + constraints: { + vertical: "BOTTOM", + horizontal: "LEFT_RIGHT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutPositioning: "ABSOLUTE", + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + effects: [], + interactions: [], + }, + { + id: "I2099:38616;1739:61707;548:24046", + name: "Scrollbar V", + type: "FRAME", + scrollBehavior: "SCROLLS", + componentPropertyReferences: { + visible: "Show Scrollbar#1272:18", + }, + boundVariables: { + paddingTop: { + type: "VARIABLE_ALIAS", + id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", + }, + paddingRight: { + type: "VARIABLE_ALIAS", + id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", + }, + paddingBottom: { + type: "VARIABLE_ALIAS", + id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", + }, + }, + children: [ + { + id: "I2099:38616;1739:61707;548:24047", + name: "Bar", + type: "RECTANGLE", + scrollBehavior: "SCROLLS", + boundVariables: { + minHeight: { + type: "VARIABLE_ALIAS", + id: "VariableID:5fdc417718534e1862bd74d2b0cae46cbc3fdd93/3157:975", + }, + size: { + x: { + type: "VARIABLE_ALIAS", + id: "VariableID:4e7717a4a0ee45c0a9977c43d56cf4cb32848299/3157:257", + }, + }, + rectangleCornerRadii: { + RECTANGLE_TOP_LEFT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", + }, + RECTANGLE_TOP_RIGHT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", + }, + RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", + }, + RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS: { + type: "VARIABLE_ALIAS", + id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", + }, + }, + fills: [ + { + type: "VARIABLE_ALIAS", + id: "VariableID:9b31d8ddc760d048c5d9e42dbce12aa5946a3041/2036:15", + }, + ], + }, + blendMode: "PASS_THROUGH", + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 0.658823549747467, + g: 0.658823549747467, + b: 0.658823549747467, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:9b31d8ddc760d048c5d9e42dbce12aa5946a3041/2036:15", + }, + }, + }, + ], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + cornerRadius: 1000, + cornerSmoothing: 0, + absoluteBoundingBox: { + x: -979, + y: -2195.5, + width: 4.5, + height: 31, + }, + absoluteRenderBounds: { + x: -979, + y: -2195.5, + width: 4.5, + height: 31, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 1, + minHeight: 15, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FILL", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: false, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + primaryAxisSizingMode: "FIXED", + counterAxisAlignItems: "MAX", + paddingRight: 4.5, + paddingTop: 4.5, + paddingBottom: 4.5, + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -979, + y: -2200, + width: 9, + height: 40, + }, + absoluteRenderBounds: { + x: -979, + y: -2200, + width: 9, + height: 40, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutSizingHorizontal: "HUG", + layoutSizingVertical: "FIXED", + effects: [], + interactions: [], + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + primaryAxisSizingMode: "FIXED", + counterAxisAlignItems: "MAX", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1930, + y: -2200, + width: 960, + height: 536, + }, + absoluteRenderBounds: { + x: -1930, + y: -2200, + width: 960, + height: 536, + }, + constraints: { + vertical: "TOP_BOTTOM", + horizontal: "LEFT_RIGHT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + layoutPositioning: "ABSOLUTE", + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + effects: [], + interactions: [], + uniqueName: "Scroll Container", + variantProperties: { + Variation: "Vertical", + }, + width: 960, + height: 536, + x: 0, + y: 0.25, + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + primaryAxisAlignItems: "MIN", + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [], + fills: [], + strokes: [], + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + itemSpacing: 10, + primaryAxisSizingMode: "FIXED", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1930, + y: -2200.25, + width: 960, + height: 536.25, + }, + absoluteRenderBounds: { + x: -1930, + y: -2200.25, + width: 960, + height: 536.25, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "STRETCH", + layoutGrow: 1, + layoutSizingHorizontal: "FILL", + layoutSizingVertical: "FILL", + effects: [], + interactions: [], + uniqueName: "Content", + width: 960, + height: 536.25, + x: 0, + y: 93.75, + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + primaryAxisAlignItems: "MIN", + counterAxisAlignItems: "MIN", + isRelative: true, + }, + ], + blendMode: "PASS_THROUGH", + clipsContent: true, + background: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 0.1411764770746231, + g: 0.1411764770746231, + b: 0.1411764770746231, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", + }, + }, + }, + ], + fills: [ + { + blendMode: "NORMAL", + type: "SOLID", + color: { + r: 0.1411764770746231, + g: 0.1411764770746231, + b: 0.1411764770746231, + a: 1, + }, + boundVariables: { + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", + }, + }, + variableColorName: "Box-Style-box4", + }, + ], + strokes: [], + cornerRadius: 4.5, + cornerSmoothing: 0, + strokeWeight: 1, + strokeAlign: "INSIDE", + backgroundColor: { + r: 0.1411764770746231, + g: 0.1411764770746231, + b: 0.1411764770746231, + a: 1, + }, + layoutMode: "VERTICAL", + counterAxisSizingMode: "FIXED", + primaryAxisSizingMode: "FIXED", + counterAxisAlignItems: "CENTER", + layoutWrap: "NO_WRAP", + absoluteBoundingBox: { + x: -1930, + y: -2294, + width: 960, + height: 720, + }, + absoluteRenderBounds: { + x: -1969, + y: -2323.10009765625, + width: 1038, + height: 789.10009765625, + }, + constraints: { + vertical: "TOP", + horizontal: "LEFT", + }, + layoutAlign: "INHERIT", + layoutGrow: 0, + minHeight: 320, + maxHeight: 720, + layoutSizingHorizontal: "FIXED", + layoutSizingVertical: "FIXED", + effects: [ + { + type: "DROP_SHADOW", + visible: true, + color: { + r: 0, + g: 0, + b: 0, + a: 0.47999998927116394, + }, + blendMode: "NORMAL", + offset: { + x: 0, + y: 9.899999618530273, + }, + radius: 39, + showShadowBehindNode: true, + boundVariables: { + radius: { + type: "VARIABLE_ALIAS", + id: "VariableID:7b576d4f7cef936e728857b8d6f7952e2a6dd6fe/3157:78", + }, + color: { + type: "VARIABLE_ALIAS", + id: "VariableID:e7dccd708e8eb5f689ef26147d2d985b7af1bfd8/2013:336", + }, + offsetY: { + type: "VARIABLE_ALIAS", + id: "VariableID:55268df3aca26e8ed4182c6831670c631ab2e88b/4411:298", + }, + }, + variableColorName: "Elevation-elevation5", + }, + ], + styles: { + effect: "438:10166", + }, + interactions: [], + uniqueName: "Modal", + width: 960, + height: 720, + x: 160, + y: 40, + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + primaryAxisAlignItems: "MIN", + }, + ]; console.log( `[benchmark] convertNodesToAltNodes: ${Date.now() - convertNodesStart}ms`, ); } + console.log("[debug] convertedSelection", { ...convertedSelection[0] }); + // ignore when nothing was selected // If the selection was empty, the converted selection will also be empty. if (convertedSelection.length === 0) { @@ -482,7 +3568,7 @@ export const run = async (settings: PluginSettings) => { const colorPanelStart = Date.now(); const colors = retrieveGenericSolidUIColors(framework); - const gradients = retrieveGenericGradients(framework); + // const gradients = retrieveGenericGradients(framework); console.log( `[benchmark] color and gradient panel: ${Date.now() - colorPanelStart}ms`, ); @@ -513,7 +3599,7 @@ export const run = async (settings: PluginSettings) => { code, htmlPreview, colors, - gradients, + gradients: [], settings, warnings: [...warnings], }); diff --git a/packages/backend/src/common/color.ts b/packages/backend/src/common/color.ts index a7c9a496..e143fec2 100644 --- a/packages/backend/src/common/color.ts +++ b/packages/backend/src/common/color.ts @@ -59,42 +59,24 @@ export const rgbToCssColor = (color: RGB | RGBA, alpha: number = 1): string => { // ---- Gradient Transformation ---- export const gradientAngle = (fill: GradientPaint): number => { - // Thanks Gleb and Liam for helping! - const decomposed = decomposeRelativeTransform( - fill.gradientTransform[0], - fill.gradientTransform[1], - ); - - return (decomposed.rotation * 180) / Math.PI; -}; - -// Calculate gradient angle for CSS (different coordinate system) -export const cssGradientAngle = (angle: number): number => { - // Normalize angle: if negative, add 360 to make it positive. - return angle < 0 ? angle + 360 : angle; + const [start, end] = fill.gradientHandlePositions; + return calculateAngle(start, end); }; -// Calculate gradient coordinates for a matrix transform -export const getGradientTransformCoordinates = ( - gradientTransform: number[][], -): { centerX: string; centerY: string; radiusX: string; radiusY: string } => { - const a = gradientTransform[0][0]; - const b = gradientTransform[0][1]; - const c = gradientTransform[1][0]; - const d = gradientTransform[1][1]; - const e = gradientTransform[0][2]; - const f = gradientTransform[1][2]; - - const scaleX = Math.sqrt(a ** 2 + b ** 2); - const scaleY = Math.sqrt(c ** 2 + d ** 2); - - const centerX = ((e * scaleX * 100) / (1 - scaleX)).toFixed(2); - const centerY = (((1 - f) * scaleY * 100) / (1 - scaleY)).toFixed(2); - - const radiusX = (scaleX * 100).toFixed(2); - const radiusY = (scaleY * 100).toFixed(2); - - return { centerX, centerY, radiusX, radiusY }; +/** + * Calculate the angle between two points in degrees + * @param start Starting point {x, y} in normalized coordinates (0-1) + * @param end Ending point {x, y} in normalized coordinates (0-1) + * @returns Angle in degrees (0-360) + */ +export const calculateAngle = ( + start: { x: number; y: number }, + end: { x: number; y: number }, +): number => { + const dx = end.x - start.x; + const dy = end.y - start.y; + let angle = Math.atan2(dy, dx) * (180 / Math.PI); + return (angle + 360) % 360; // Normalize to 0-360 degrees }; // from https://math.stackexchange.com/a/2888105 diff --git a/packages/backend/src/common/commonPosition.ts b/packages/backend/src/common/commonPosition.ts index 7899b4e7..5001088c 100644 --- a/packages/backend/src/common/commonPosition.ts +++ b/packages/backend/src/common/commonPosition.ts @@ -19,25 +19,14 @@ export const commonIsAbsolutePosition = (node: SceneNode) => { return true; } - // No position when parent is inferred auto layout. - // if ( - // optimizeLayout && - // node.parent && - // "layoutMode" in node.parent && - // node.parent.inferredAutoLayout !== null - // ) { - // return false; - // } - if (!node.parent || node.parent === undefined) { return false; } - const parentLayoutIsNone = - "layoutMode" in node.parent && node.parent.layoutMode === "NONE"; - const hasNoLayoutMode = !("layoutMode" in node.parent); - - if (parentLayoutIsNone || hasNoLayoutMode) { + if ( + ("layoutMode" in node.parent && node.parent.layoutMode === "NONE") || + !("layoutMode" in node.parent) + ) { return true; } diff --git a/packages/backend/src/common/retrieveFill.ts b/packages/backend/src/common/retrieveFill.ts index 233c2edb..2fe1cd4a 100644 --- a/packages/backend/src/common/retrieveFill.ts +++ b/packages/backend/src/common/retrieveFill.ts @@ -1,12 +1,16 @@ +import { Paint } from "../api_types"; + /** * Retrieve the first visible color that is being used by the layer, in case there are more than one. */ export const retrieveTopFill = ( - fills: ReadonlyArray | PluginAPI["mixed"] | undefined, + fills: ReadonlyArray | undefined, ): Paint | undefined => { - if (fills && fills !== figma.mixed && fills.length > 0) { + if (fills && Array.isArray(fills) && fills.length > 0) { // on Figma, the top layer is always at the last position // reverse, then try to find the first layer that is visible, if any. return [...fills].reverse().find((d) => d.visible !== false); } + + return undefined; }; diff --git a/packages/backend/src/flutter/builderImpl/flutterColor.ts b/packages/backend/src/flutter/builderImpl/flutterColor.ts index 5beb4c31..9964f520 100644 --- a/packages/backend/src/flutter/builderImpl/flutterColor.ts +++ b/packages/backend/src/flutter/builderImpl/flutterColor.ts @@ -1,12 +1,13 @@ -import { rgbTo8hex, gradientAngle } from "../../common/color"; +import { StarNode } from "./../../api_types"; +import { rgbTo8hex } from "../../common/color"; import { addWarning } from "../../common/commonConversionWarnings"; import { generateWidgetCode, numberToFixedString, } from "../../common/numToAutoFixed"; import { retrieveTopFill } from "../../common/retrieveFill"; -import { nearestValue } from "../../tailwind/conversionTables"; import { getPlaceholderImage } from "../../common/images"; +import { GradientPaint, ImagePaint, Paint } from "../../api_types"; /** * Retrieve the SOLID color for Flutter when existent, otherwise "" @@ -17,11 +18,9 @@ export const flutterColorFromFills = ( node: SceneNode, propertyPath: string, ): string => { - let fills: ReadonlyArray | PluginAPI["mixed"]; - fills = node[propertyPath as keyof SceneNode] as - | ReadonlyArray - | PluginAPI["mixed"]; - + let fills: ReadonlyArray = node[ + propertyPath as keyof SceneNode + ] as ReadonlyArray; return flutterColorFromDirectFills(fills); }; @@ -30,15 +29,15 @@ export const flutterColorFromFills = ( * @param fills The fills array to process */ export const flutterColorFromDirectFills = ( - fills: ReadonlyArray | PluginAPI["mixed"], + fills: ReadonlyArray, ): string => { const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { return flutterColor( - fill.color, - fill.opacity ?? 1.0, - (fill as any).variableColorName + fill.color, + fill.opacity ?? 1.0, + (fill as any).variableColorName, ); } else if ( fill && @@ -49,9 +48,9 @@ export const flutterColorFromDirectFills = ( if (fill.gradientStops.length > 0) { const stop = fill.gradientStops[0]; return flutterColor( - stop.color, - fill.opacity ?? 1.0, - (stop as any).variableColorName + stop.color, + fill.opacity ?? 1.0, + (stop as any).variableColorName, ); } } @@ -66,16 +65,16 @@ export const flutterBoxDecorationColor = ( node: SceneNode, propertyPath: string, ): Record => { - let fills: ReadonlyArray | PluginAPI["mixed"]; - fills = node[propertyPath as keyof SceneNode] as - | ReadonlyArray - | PluginAPI["mixed"]; + let fills: ReadonlyArray; + fills = node[propertyPath as keyof SceneNode] as ReadonlyArray; const fill = retrieveTopFill(fills); if (fill && fill.type === "SOLID") { const opacity = fill.opacity ?? 1.0; - return { color: flutterColor(fill.color, opacity, (fill as any).variableColorName) }; + return { + color: flutterColor(fill.color, opacity, (fill as any).variableColorName), + }; } else if ( fill?.type === "GRADIENT_LINEAR" || fill?.type === "GRADIENT_RADIAL" || @@ -100,13 +99,13 @@ export const flutterDecorationImage = (node: SceneNode, fill: ImagePaint) => { const fitToBoxFit = (fill: ImagePaint): string => { switch (fill.scaleMode) { case "FILL": - return "BoxFit.fill"; + return "BoxFit.cover"; // FILL in Figma covers the entire area, similar to BoxFit.cover case "FIT": - return "BoxFit.contain"; - case "CROP": - return "BoxFit.cover"; + return "BoxFit.contain"; // FIT in Figma fits the image while maintaining aspect ratio, like BoxFit.contain + case "STRETCH": + return "BoxFit.fill"; // STRETCH in Figma stretches the image, like BoxFit.fill case "TILE": - return "BoxFit.none"; + return "BoxFit.none"; // TILE doesn't have a direct equivalent, but BoxFit.none is closest default: return "BoxFit.cover"; } @@ -126,87 +125,92 @@ export const flutterGradient = (fill: GradientPaint): string => { } }; -const gradientDirection = (angle: number): string => { - const radians = (angle * Math.PI) / 180; - const x = Math.cos(radians).toFixed(2); - const y = Math.sin(radians).toFixed(2); - return `begin: Alignment(${x}, ${y}), end: Alignment(${-x}, ${-y})`; -}; - -const flutterRadialGradient = (fill: GradientPaint): string => { +/** + * Generate a Flutter LinearGradient widget + * @param fill The linear gradient fill + * @returns LinearGradient widget code + */ +const flutterLinearGradient = (fill: GradientPaint): string => { + const [start, end] = fill.gradientHandlePositions; const colors = fill.gradientStops .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) .join(", "); - - const x = numberToFixedString(fill.gradientTransform[0][2]); - const y = numberToFixedString(fill.gradientTransform[1][2]); - const scaleX = fill.gradientTransform[0][0]; - const scaleY = fill.gradientTransform[1][1]; - const r = numberToFixedString(Math.sqrt(scaleX * scaleX + scaleY * scaleY)); - - return generateWidgetCode("RadialGradient", { - center: `Alignment(${x}, ${y})`, - radius: r, + return generateWidgetCode("LinearGradient", { + begin: `Alignment(${start.x.toFixed(2)}, ${start.y.toFixed(2)})`, + end: `Alignment(${end.x.toFixed(2)}, ${end.y.toFixed(2)})`, colors: `[${colors}]`, }); }; -const flutterAngularGradient = (fill: GradientPaint): string => { +/** + * Generate a Flutter RadialGradient widget + * @param fill The radial gradient fill + * @returns RadialGradient widget code + */ +const flutterRadialGradient = (fill: GradientPaint): string => { + const [center, h1, h2] = (fill as any).gradientHandlePositions; + const radius1 = Math.sqrt((h1.x - center.x) ** 2 + (h1.y - center.y) ** 2); + const radius2 = Math.sqrt((h2.x - center.x) ** 2 + (h2.y - center.y) ** 2); + const radius = Math.max(radius1, radius2); const colors = fill.gradientStops .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) .join(", "); - - const x = numberToFixedString(fill.gradientTransform[0][2]); - const y = numberToFixedString(fill.gradientTransform[1][2]); - const startAngle = numberToFixedString(-fill.gradientTransform[0][0]); - const endAngle = numberToFixedString(-fill.gradientTransform[0][1]); - - return generateWidgetCode("SweepGradient", { - center: `Alignment(${x}, ${y})`, - startAngle: startAngle, - endAngle: endAngle, + return generateWidgetCode("RadialGradient", { + center: `Alignment(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`, + radius: radius.toFixed(2), colors: `[${colors}]`, }); }; -const flutterLinearGradient = (fill: GradientPaint): string => { - const radians = (-gradientAngle(fill) * Math.PI) / 180; - const x = Math.cos(radians).toFixed(2); - const y = Math.sin(radians).toFixed(2); +/** + * Convert Figma's normalized coordinates (0 to 1) to Flutter's Alignment (-1 to 1) + * @param x Figma's x coordinate (0 to 1) + * @param y Figma's y coordinate (0 to 1) + * @returns Flutter's Alignment string + */ +const figmaToFlutterAlignment = (x: number, y: number): string => { + const alignmentX = x * 2 - 1; + const alignmentY = y * 2 - 1; + return `Alignment(${numberToFixedString(alignmentX)}, ${numberToFixedString(alignmentY)})`; +}; + +/** + * Generate a Flutter SweepGradient widget (for angular gradients) + * @param fill The angular gradient fill + * @returns SweepGradient widget code + */ +export const flutterAngularGradient = (fill: GradientPaint): string => { + // TODO This function is not 100% perfect but gets close. It is hard to get AngularGradient in Flutter. + const [center, _, startDirection] = fill.gradientHandlePositions; + + // Center alignment + const centerAlignment = figmaToFlutterAlignment(center.x, center.y); + // Starting angle + const dx = startDirection.x - center.x; + const dy = startDirection.y - center.y; + const startAngle = -(90 * Math.PI) / 180 + Math.atan2(dy, dx); + + // Generate colors and stops const colors = fill.gradientStops - .map((d) => flutterColor(d.color, d.color.a, (d as any).variableColorName)) + .map((stop) => flutterColor(stop.color, stop.color.a)) .join(", "); - return generateWidgetCode("LinearGradient", { - begin: `Alignment(${x}, ${y})`, - end: `Alignment(${-x}, ${-y})`, + const stops = fill.gradientStops + .map((stop) => numberToFixedString(stop.position)) + .join(", "); + + // Generate SweepGradient code + return generateWidgetCode("SweepGradient", { + center: centerAlignment, + startAngle: numberToFixedString(startAngle), + endAngle: numberToFixedString(startAngle + 2 * Math.PI), colors: `[${colors}]`, + stops: `[${stops}]`, + transform: `GradientRotation(${numberToFixedString(startAngle)})`, }); }; -const gradientDirectionReadable = (angle: number): string => { - switch (nearestValue(angle, [-180, -135, -90, -45, 0, 45, 90, 135, 180])) { - case 0: - return "begin: Alignment.centerLeft, end: Alignment.centerRight"; - case 45: - return "begin: Alignment.topLeft, end: Alignment.bottomRight"; - case 90: - return "begin: Alignment.topCenter, end: Alignment.bottomCenter"; - case 135: - return "begin: Alignment.topRight, end: Alignment.bottomLeft"; - case -45: - return "begin: Alignment.bottomLeft, end: Alignment.topRight"; - case -90: - return "begin: Alignment.bottomCenter, end: Alignment.topCenter"; - case -135: - return "begin: Alignment.bottomRight, end: Alignment.topLeft"; - default: - // 180 and -180 - return "begin: Alignment.centerRight, end: Alignment.centerLeft"; - } -}; - /** * Convert opacity (0-1) to alpha (0-255) */ @@ -215,21 +219,23 @@ const opacityToAlpha = (opacity: number): number => { }; export const flutterColor = ( - color: RGB, - opacity: number, - variableColorName?: string + color: RGB, + opacity: number, + variableColorName?: string, ): string => { const sum = color.r + color.g + color.b; let colorCode = ""; if (sum === 0) { - colorCode = opacity === 1 - ? "Colors.black" - : `Colors.black.withValues(alpha: ${opacityToAlpha(opacity)})`; + colorCode = + opacity === 1 + ? "Colors.black" + : `Colors.black.withValues(alpha: ${opacityToAlpha(opacity)})`; } else if (sum === 3) { - colorCode = opacity === 1 - ? "Colors.white" - : `Colors.white.withValues(alpha: ${opacityToAlpha(opacity)})`; + colorCode = + opacity === 1 + ? "Colors.white" + : `Colors.white.withValues(alpha: ${opacityToAlpha(opacity)})`; } else { // Always use full 8-digit hex which includes alpha channel colorCode = `Color(0x${rgbTo8hex(color, opacity).toUpperCase()})`; @@ -239,6 +245,6 @@ export const flutterColor = ( if (variableColorName) { return `${colorCode} /* ${variableColorName} */`; } - + return colorCode; }; diff --git a/packages/backend/src/flutter/flutterMain.ts b/packages/backend/src/flutter/flutterMain.ts index 162e45e5..6d550e1f 100644 --- a/packages/backend/src/flutter/flutterMain.ts +++ b/packages/backend/src/flutter/flutterMain.ts @@ -89,7 +89,6 @@ const flutterWidgetGenerator = ( // filter non visible nodes. This is necessary at this step because conversion already happened. const visibleSceneNode = getVisibleNodes(sceneNode); - const sceneLen = visibleSceneNode.length; visibleSceneNode.forEach((node) => { switch (node.type) { @@ -183,7 +182,7 @@ const flutterFrame = ( } // Generate widget code for children - const children = flutterWidgetGenerator(sortedChildren); + const children = flutterWidgetGenerator(node.children); // Force Stack for any frame that has absolute positioned children if (hasAbsoluteChildren) { diff --git a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts index 33451874..3ba1094d 100644 --- a/packages/backend/src/html/builderImpl/htmlBorderRadius.ts +++ b/packages/backend/src/html/builderImpl/htmlBorderRadius.ts @@ -52,6 +52,5 @@ export const htmlBorderRadius = (node: SceneNode, isJsx: boolean): string[] => { } } - console.log("comp was", comp); return comp; }; diff --git a/packages/backend/src/html/builderImpl/htmlColor.ts b/packages/backend/src/html/builderImpl/htmlColor.ts index 73b0c6db..0b9cda0a 100644 --- a/packages/backend/src/html/builderImpl/htmlColor.ts +++ b/packages/backend/src/html/builderImpl/htmlColor.ts @@ -1,6 +1,6 @@ -import { HTMLSettings } from "types"; import { numberToFixedString } from "../../common/numToAutoFixed"; import { retrieveTopFill } from "../../common/retrieveFill"; +import { GradientPaint, Paint } from "../../api_types"; /** * Helper to process a color with variable binding if present @@ -53,10 +53,11 @@ const getColorAndVariable = ( return { color: { r: 0, g: 0, b: 0 }, opacity: 0 }; }; -// Retrieve the SOLID color or approximate gradient as HTML color +/** + * Convert fills to an HTML color string + */ export const htmlColorFromFills = ( - fills: ReadonlyArray | PluginAPI["mixed"] | undefined, - settings: HTMLSettings, + fills: ReadonlyArray | undefined, ): string => { const fill = retrieveTopFill(fills); if (fill) { @@ -66,6 +67,9 @@ export const htmlColorFromFills = ( return ""; }; +/** + * Convert RGB color to CSS color string + */ export const htmlColor = (color: RGB, alpha: number = 1): string => { if (color.r === 1 && color.g === 1 && color.b === 1 && alpha === 1) { return "white"; @@ -87,7 +91,9 @@ export const htmlColor = (color: RGB, alpha: number = 1): string => { return `rgba(${r}, ${g}, ${b}, ${a})`; }; -// Process a single gradient stop with proper color and position +/** + * Process a single gradient stop + */ const processGradientStop = ( stop: ColorStop, fillOpacity: number = 1, @@ -106,7 +112,9 @@ const processGradientStop = ( return `${color} ${position}`; }; -// Process all gradient stops for any gradient type +/** + * Process all gradient stops for a gradient + */ const processGradientStops = ( stops: ReadonlyArray, fillOpacity: number = 1, @@ -120,6 +128,9 @@ const processGradientStops = ( .join(", "); }; +/** + * Determine the appropriate gradient function based on fill type + */ export const htmlGradientFromFills = (fill: Paint): string => { if (!fill) return ""; switch (fill.type) { @@ -128,7 +139,7 @@ export const htmlGradientFromFills = (fill: Paint): string => { case "GRADIENT_ANGULAR": return htmlAngularGradient(fill); case "GRADIENT_RADIAL": - return htmlRadialGradient(fill); // Updated to use radial gradient function + return htmlRadialGradient(fill); case "GRADIENT_DIAMOND": return htmlDiamondGradient(fill); default: @@ -136,98 +147,70 @@ export const htmlGradientFromFills = (fill: Paint): string => { } }; -export const gradientAngle2 = (fill: GradientPaint): number => { - const x1 = fill.gradientTransform[0][2]; - const y1 = fill.gradientTransform[1][2]; - const x2 = fill.gradientTransform[0][0] + x1; - const y2 = fill.gradientTransform[1][0] + y1; - const dx = x2 - x1; - const dy = y1 - y2; - const radians = Math.atan2(dy, dx); - const unadjustedAngle = (radians * 180) / Math.PI; - const adjustedAngle = unadjustedAngle + 90; - return adjustedAngle; -}; - -export const cssGradientAngle = (angle: number): number => { - const cssAngle = angle; - return cssAngle < 0 ? cssAngle + 360 : cssAngle; -}; - -export const htmlLinearGradient = (fill: GradientPaint): string => { - const figmaAngle = gradientAngle2(fill); - const angle = cssGradientAngle(figmaAngle).toFixed(0); +/** + * Generate CSS linear gradient + */ +export const htmlLinearGradient = (fill: GradientPaint) => { + const [start, end] = fill.gradientHandlePositions; + const dx = end.x - start.x; + const dy = end.y - start.y; + let angle = Math.atan2(dy, dx) * (180 / Math.PI); // Angle in degrees + angle = (angle + 360) % 360; // Normalize to 0-360 + const cssAngle = (angle + 90) % 360; // Adjust for CSS convention const mappedFill = processGradientStops( fill.gradientStops, fill.opacity ?? 1, - 100, - "%", ); - return `linear-gradient(${angle}deg, ${mappedFill})`; + return `linear-gradient(${cssAngle.toFixed(0)}deg, ${mappedFill})`; }; -export const invertYCoordinate = (y: number): number => 1 - y; - -export const htmlAngularGradient = (fill: GradientPaint): string => { - const angle = gradientAngle2(fill).toFixed(0); - // Extract matrix components - const a = fill.gradientTransform[0][0]; - const b = fill.gradientTransform[0][1]; - const tx = fill.gradientTransform[0][2]; - const c = fill.gradientTransform[1][0]; - const d = fill.gradientTransform[1][1]; - const ty = fill.gradientTransform[1][2]; - // Compute center by transforming (0.5, 0.5) - const centerX = (a * 0.5 + b * 0.5 + tx) * 100; - const centerY = (c * 0.5 + d * 0.5 + ty) * 100; - const centerXPercent = centerX.toFixed(2); - const centerYPercent = centerY.toFixed(2); - const mappedFill = processGradientStops( +/** + * Generate CSS radial gradient + */ +export const htmlRadialGradient = (fill: GradientPaint) => { + const [center, h1, h2] = fill.gradientHandlePositions; + const cx = center.x * 100; // Center X as percentage + const cy = center.y * 100; // Center Y as percentage + // Calculate horizontal radius (distance from center to h1) + const rx = Math.sqrt((h1.x - center.x) ** 2 + (h1.y - center.y) ** 2) * 100; + // Calculate vertical radius (distance from center to h2) + const ry = Math.sqrt((h2.x - center.x) ** 2 + (h2.y - center.y) ** 2) * 100; + const mappedStops = processGradientStops( fill.gradientStops, fill.opacity ?? 1, - 360, - "deg", ); - return `conic-gradient(from ${angle}deg at ${centerXPercent}% ${centerYPercent}%, ${mappedFill})`; + return `radial-gradient(ellipse ${rx.toFixed(2)}% ${ry.toFixed(2)}% at ${cx.toFixed(2)}% ${cy.toFixed(2)}%, ${mappedStops})`; }; -export const htmlRadialGradient = (fill: GradientPaint): string => { - const [[a, b, tx], [c, d, ty]] = fill.gradientTransform; - - // Calculate inverse of the linear part of the gradientTransform matrix - const det = a * d - b * c; - if (Math.abs(det) < 1e-6) return ""; // Avoid division by zero - - const invDet = 1 / det; - const invA = d * invDet; - const invB = -b * invDet; - const invC = -c * invDet; - const invD = a * invDet; - - // Calculate center by solving inverse transform for (0.5, 0.5) - const cx = (invA * (0.5 - tx) + invB * (0.5 - ty)) * 100; - const cy = (invC * (0.5 - tx) + invD * (0.5 - ty)) * 100; - - // Calculate column vectors of inverse matrix - const col1Length = Math.sqrt(invA ** 2 + invC ** 2) * 100; - const col2Length = Math.sqrt(invB ** 2 + invD ** 2) * 100; - - // Get radii as half lengths of column vectors (sorted) - const radii = [col1Length / 2, col2Length / 2].sort((a, b) => b - a); - - const mappedStops = processGradientStops( +/** + * Generate CSS conic (angular) gradient + */ +export const htmlAngularGradient = (fill: GradientPaint) => { + const [center, _, startDirection] = fill.gradientHandlePositions; + const cx = center.x * 100; // Center X as percentage + const cy = center.y * 100; // Center Y as percentage + // Calculate the starting angle + const dx = startDirection.x - center.x; + const dy = startDirection.y - center.y; + let angle = Math.atan2(dy, dx) * (180 / Math.PI); // Convert to degrees + angle = (angle + 360) % 360; // Normalize to 0-360 degrees + const mappedFill = processGradientStops( fill.gradientStops, fill.opacity ?? 1, + 360, + "deg", ); - return `radial-gradient(ellipse ${radii[0].toFixed(2)}% ${radii[1].toFixed(2)}% at ${cx.toFixed(2)}% ${cy.toFixed(2)}%, ${mappedStops})`; + return `conic-gradient(from ${angle.toFixed(0)}deg at ${cx.toFixed(2)}% ${cy.toFixed(2)}%, ${mappedFill})`; }; -// Added function for diamond gradient -export const htmlDiamondGradient = (fill: GradientPaint): string => { +/** + * Generate CSS diamond gradient (approximation using four linear gradients) + */ +export const htmlDiamondGradient = (fill: GradientPaint) => { const stops = processGradientStops( fill.gradientStops, fill.opacity ?? 1, - 50, // Adjusted multiplier for diamond gradient + 50, "%", ); const gradientConfigs = [ @@ -244,19 +227,21 @@ export const htmlDiamondGradient = (fill: GradientPaint): string => { .join(", "); }; +/** + * Build CSS background value from an array of paints + */ export const buildBackgroundValues = ( paintArray: ReadonlyArray | PluginAPI["mixed"], - settings: HTMLSettings, ): string => { if (paintArray === figma.mixed) { return ""; } - // If only one fill, just use plain color/gradient + // If only one fill, use plain color or gradient if (paintArray.length === 1) { const paint = paintArray[0]; if (paint.type === "SOLID") { - return htmlColorFromFills(paintArray, settings); + return htmlColorFromFills(paintArray); } else if ( paint.type === "GRADIENT_LINEAR" || paint.type === "GRADIENT_RADIAL" || @@ -268,16 +253,14 @@ export const buildBackgroundValues = ( return ""; } - // Reverse the array to match CSS layering (first is top-most in CSS) + // For multiple fills, reverse to match CSS layering (first is top-most) const styles = [...paintArray].reverse().map((paint, index) => { if (paint.type === "SOLID") { - // For multiple fills, always convert solid colors to linear gradients - // to ensure proper layering in CSS backgrounds - const color = htmlColorFromFills([paint], settings); + // Convert solid colors to gradients for proper layering + const color = htmlColorFromFills([paint]); if (index === 0) { return `linear-gradient(0deg, ${color} 0%, ${color} 100%)`; } - return color; } else if ( paint.type === "GRADIENT_LINEAR" || diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index 12ceb5b4..a9d68f6d 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -122,6 +122,7 @@ export class HtmlDefaultBuilder { commonShapeStyles(): this { if ("fills" in this.node) { + console.log("node is", this.node); this.applyFillsToStyle( this.node.fills, this.node.type === "TEXT" ? "text" : "background", @@ -158,7 +159,7 @@ export class HtmlDefaultBuilder { } const strokes = ("strokes" in node && node.strokes) || undefined; - const color = htmlColorFromFills(strokes, settings); + const color = htmlColorFromFills(strokes as any); if (!color) { return this; } @@ -166,7 +167,6 @@ export class HtmlDefaultBuilder { "dashPattern" in node && node.dashPattern.length > 0 ? "dotted" : "solid"; const strokeAlign = "strokeAlign" in node ? node.strokeAlign : "INSIDE"; - const layoutMode = "layoutMode" in node ? node.layoutMode : "NONE"; // Function to create border value string const consolidateBorders = (border: number): string => @@ -287,13 +287,13 @@ export class HtmlDefaultBuilder { formatWithJSX( "text", this.isJSX, - htmlColorFromFills(paintArray, this.settings), + htmlColorFromFills(paintArray as any), ), ); return this; } - const backgroundValues = buildBackgroundValues(paintArray, this.settings); + const backgroundValues = buildBackgroundValues(paintArray as any); if (backgroundValues) { this.addStyles(formatWithJSX("background", this.isJSX, backgroundValues)); diff --git a/packages/backend/src/html/htmlMain.ts b/packages/backend/src/html/htmlMain.ts index 2972fc38..3c04e5df 100644 --- a/packages/backend/src/html/htmlMain.ts +++ b/packages/backend/src/html/htmlMain.ts @@ -396,6 +396,7 @@ const convertNode = (settings: HTMLSettings) => async (node: SceneNode) => { return htmlWrapSVG(altNode, settings); } } + console.log("[convert] node is", node); switch (node.type) { case "RECTANGLE": diff --git a/packages/backend/src/html/htmlTextBuilder.ts b/packages/backend/src/html/htmlTextBuilder.ts index d870d562..436577ec 100644 --- a/packages/backend/src/html/htmlTextBuilder.ts +++ b/packages/backend/src/html/htmlTextBuilder.ts @@ -52,7 +52,7 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { const styleAttributes = formatMultipleJSX( { - color: htmlColorFromFills(segment.fills, this.settings), + color: htmlColorFromFills(segment.fills as any), "font-size": segment.fontSize, "font-family": segment.fontName.family, "font-style": this.getFontStyle(segment.fontName.style), @@ -86,9 +86,10 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder { ) { // Use the pre-assigned uniqueId from the segment if available, // or generate one if not (as a fallback) - const segmentName = (segment as any).uniqueId || + const segmentName = + (segment as any).uniqueId || `${((node as any).uniqueName || node.name || "text").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase()}_text_${(index + 1).toString().padStart(2, "0")}`; - + const className = generateUniqueClassName(segmentName); result.className = className; diff --git a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts index 39b6c851..c871a5af 100644 --- a/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts +++ b/packages/backend/src/swiftui/builderImpl/swiftuiColor.ts @@ -2,8 +2,6 @@ import { retrieveTopFill } from "../../common/retrieveFill"; import { gradientAngle } from "../../common/color"; import { nearestValue } from "../../tailwind/conversionTables"; import { numberToFixedString } from "../../common/numToAutoFixed"; -import { addWarning } from "../../common/commonConversionWarnings"; -import { getPlaceholderImage } from "../../common/images"; /** * Retrieve the SwiftUI color for a Paint object @@ -74,13 +72,21 @@ export const swiftuiSolidColorFromDirectFills = ( return ""; }; +/** + * Generate a SwiftUI gradient from a GradientPaint object + * @param fill The gradient fill object from Figma + * @returns SwiftUI gradient code as a string + */ export const swiftuiGradient = (fill: GradientPaint): string => { - const direction = gradientDirection(gradientAngle(fill)); + if (fill.type !== "GRADIENT_LINEAR") { + return ""; // Only handling linear gradients here for simplicity + } + + const angle = gradientAngle(fill); + const direction = gradientDirection(angle); const colors = fill.gradientStops - .map((d) => { - return swiftuiColor(d.color, d.color.a); - }) + .map((d) => swiftuiColor(d.color, d.color.a)) .join(", "); return `LinearGradient(gradient: Gradient(colors: [${colors}]), ${direction})`; diff --git a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts index 31ececde..3c352256 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindColor.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindColor.ts @@ -12,6 +12,7 @@ import { htmlAngularGradient, htmlRadialGradient, } from "../../html/builderImpl/htmlColor"; +import { Paint } from "../../api_types"; /** * Get a tailwind color value object @@ -100,7 +101,7 @@ export const tailwindGradientStop = ( // retrieve the SOLID color for tailwind export const tailwindColorFromFills = ( - fills: ReadonlyArray | PluginAPI["mixed"], + fills: ReadonlyArray, kind: TailwindColorType, ): string => { // [when testing] fills can be undefined @@ -123,7 +124,7 @@ export const tailwindColorFromFills = ( }; export const tailwindGradientFromFills = ( - fills: ReadonlyArray | PluginAPI["mixed"], + fills: ReadonlyArray, ): string => { const fill = retrieveTopFill(fills); From 9192100ff4c2d8e1eca1d6aafb8cb4bf4d6d0186 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 11 Mar 2025 02:11:47 -0300 Subject: [PATCH 071/134] Fix https://github.com/bernaferrari/FigmaToCode/issues/196 --- packages/backend/src/code.ts | 7 +++++-- packages/backend/src/html/htmlDefaultBuilder.ts | 5 +---- packages/backend/src/tailwind/tailwindDefaultBuilder.ts | 5 +---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index d14c9e09..67464094 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -13,7 +13,10 @@ import { convertToCode } from "./common/retrieveUI/convertToCode"; import { generateHTMLPreview } from "./html/htmlMain"; import { variableToColorName } from "./tailwind/conversionTables"; import { oldConvertNodesToAltNodes } from "./altNodes/oldAltConversion"; -import { convertNodesToAltNodes, convertNodeToAltNode } from "./altNodes/altConversion"; +import { + convertNodesToAltNodes, + convertNodeToAltNode, +} from "./altNodes/altConversion"; // Performance tracking counters let getNodeByIdAsyncTime = 0; @@ -348,7 +351,7 @@ const processNodePair = async ( } if ( - jsonNode.layoutMode !== "NONE" && + jsonNode.layoutMode === "NONE" || jsonNode.children.some( (d: any) => "layoutPositioning" in d && d.layoutPositioning === "ABSOLUTE", diff --git a/packages/backend/src/html/htmlDefaultBuilder.ts b/packages/backend/src/html/htmlDefaultBuilder.ts index a9d68f6d..77e4e8d9 100644 --- a/packages/backend/src/html/htmlDefaultBuilder.ts +++ b/packages/backend/src/html/htmlDefaultBuilder.ts @@ -267,10 +267,7 @@ export class HtmlDefaultBuilder { formatWithJSX("position", isJSX, "absolute"), ); } else { - if ( - node.type === "GROUP" || - ("layoutMode" in node && (node as any).isRelative) - ) { + if (node.type === "GROUP" || (node as any).isRelative) { this.addStyles(formatWithJSX("position", isJSX, "relative")); } } diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 3340e674..e2ab2064 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -138,10 +138,7 @@ export class TailwindDefaultBuilder { } this.addAttributes(`absolute`); - } else if ( - node.type === "GROUP" || - ("layoutMode" in node && (node as any)).isRelative - ) { + } else if (node.type === "GROUP" || (node as any).isRelative) { this.addAttributes("relative"); } return this; From b696c128ed11f71f6f110bd686e1193c00f053c0 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 11 Mar 2025 04:17:26 -0300 Subject: [PATCH 072/134] Improve rotation? --- packages/backend/src/code.ts | 3119 +---------------- packages/backend/src/common/commonPosition.ts | 77 + .../backend/src/html/builderImpl/htmlBlend.ts | 5 +- 3 files changed, 100 insertions(+), 3101 deletions(-) diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 67464094..479923a3 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -17,6 +17,13 @@ import { convertNodesToAltNodes, convertNodeToAltNode, } from "./altNodes/altConversion"; +import { + HasGeometryTrait, + MinimalFillsTrait, + MinimalStrokesTrait, + Node, + Paint, +} from "./api_types"; // Performance tracking counters let getNodeByIdAsyncTime = 0; @@ -104,7 +111,7 @@ const processEffectVariables = async ( }; const getColorVariables = async ( - node: GeometryMixin, + node: HasGeometryTrait, settings: PluginSettings, ) => { // This tries to be as fast as it can, using Promise.all so it can parallelize calls. @@ -165,16 +172,16 @@ function adjustChildrenOrder(node: any) { * @param parentNode Optional parent node reference to set */ const processNodePair = async ( - jsonNode: any, + jsonNode: Node, figmaNode: SceneNode, settings: PluginSettings, - parentNode?: any, + parentNode?: Node, ) => { if (!jsonNode.id) return; // Set parent reference if parent is provided if (parentNode) { - jsonNode.parent = parentNode; + (jsonNode as any).parent = parentNode; } // Ensure node has a unique name with simple numbering @@ -221,7 +228,7 @@ const processNodePair = async ( // Add a uniqueId to each segment styledTextSegments = await Promise.all( styledTextSegments.map(async (segment, index) => { - const mutableSegment = Object.assign({}, segment); + const mutableSegment: any = Object.assign({}, segment); if (settings.useColorVariables && segment.fills) { mutableSegment.fills = await Promise.all( @@ -232,7 +239,7 @@ const processNodePair = async ( ) { addWarning("BlendMode is not supported in Text colors"); } - const fill = { ...d }; + const fill = { ...d } as Paint; await processColorVariables(fill); return fill; }), @@ -287,7 +294,7 @@ const processNodePair = async ( await getColorVariables(jsonNode, settings); // Some places check if paddingLeft exists. This makes sure they all exist, even if 0. - if (jsonNode.layoutMode) { + if ("layoutMode" in jsonNode && jsonNode.layoutMode) { if (jsonNode.paddingLeft === undefined) { jsonNode.paddingLeft = 0; } @@ -317,9 +324,11 @@ const processNodePair = async ( // If layout sizing is HUG but there are no children, set it to FIXED const hasChildren = + "children" in jsonNode && jsonNode.children && Array.isArray(jsonNode.children) && jsonNode.children.length > 0; + if (jsonNode.layoutSizingHorizontal === "HUG" && !hasChildren) { jsonNode.layoutSizingHorizontal = "FIXED"; } @@ -329,6 +338,7 @@ const processNodePair = async ( // Process children recursively if both have children if ( + "children" in jsonNode && jsonNode.children && Array.isArray(jsonNode.children) && "children" in figmaNode && @@ -380,7 +390,7 @@ const processNodePair = async ( export const nodesToJSON = async ( nodes: ReadonlyArray, settings: PluginSettings, -): Promise => { +): Promise => { // Reset name counters for each conversion nodeNameCounters.clear(); @@ -395,7 +405,7 @@ export const nodesToJSON = async ( })) as any ).document, ), - )) as SceneNode[]; + )) as Node[]; console.log("[debug] initial nodeJson", { ...nodeJson[0] }); @@ -453,3096 +463,7 @@ export const run = async (settings: PluginSettings) => { // Now we work directly with the JSON nodes const convertNodesStart = Date.now(); convertedSelection = await convertNodesToAltNodes(nodeJson, null); - const convertedSelection2 = [ - { - id: "I2099:38616;1739:34914", - name: "Modal", - type: "FRAME", - scrollBehavior: "SCROLLS", - boundVariables: { - minHeight: { - type: "VARIABLE_ALIAS", - id: "VariableID:ca5fdd543c7de4d7a5d043eb31c365871c484b3e/4411:779", - }, - maxHeight: { - type: "VARIABLE_ALIAS", - id: "VariableID:143b8d97896be058533bf7578cac052ff1473fe5/4411:780", - }, - size: { - x: { - type: "VARIABLE_ALIAS", - id: "VariableID:51164a6f21a5daac8ea5fa8551389d4d7864a05e/1129:893", - }, - y: { - type: "VARIABLE_ALIAS", - id: "VariableID:c7de0427328e0238030ad2c66fb3a86abcdf6421/2867:8", - }, - }, - rectangleCornerRadii: { - RECTANGLE_TOP_LEFT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", - }, - RECTANGLE_TOP_RIGHT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", - }, - RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", - }, - RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:f4761dc8bffff3a6dc119f76d86928372facdf02/3157:870", - }, - }, - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", - }, - ], - effects: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:55268df3aca26e8ed4182c6831670c631ab2e88b/4411:298", - }, - { - type: "VARIABLE_ALIAS", - id: "VariableID:7b576d4f7cef936e728857b8d6f7952e2a6dd6fe/3157:78", - }, - { - type: "VARIABLE_ALIAS", - id: "VariableID:e7dccd708e8eb5f689ef26147d2d985b7af1bfd8/2013:336", - }, - ], - }, - children: [ - { - id: "I2099:38616;1739:61360", - name: "Content", - type: "FRAME", - scrollBehavior: "SCROLLS", - children: [ - { - id: "I2099:38616;1739:34917", - name: "Modal Container", - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - mainComponent: "Swap Modal Container#893:1", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", - }, - paddingLeft: { - type: "VARIABLE_ALIAS", - id: "VariableID:c44b7d196360345cd2e77fddb9fdbb56b074630c/10434:249", - }, - paddingTop: { - type: "VARIABLE_ALIAS", - id: "VariableID:becb73e51eeba1f8c786ade8394c2a4d29eb3ecf/10434:245", - }, - paddingRight: { - type: "VARIABLE_ALIAS", - id: "VariableID:dca108e1e0b09a774a4527df680b8306bc9208b2/10434:246", - }, - paddingBottom: { - type: "VARIABLE_ALIAS", - id: "VariableID:f6e871ff408efebce7861cdde12d1530510445e9/10434:248", - }, - minHeight: { - type: "VARIABLE_ALIAS", - id: "VariableID:d4bef8dbc813180acafb9ee86fec3ab4e9cb2983/3203:9", - }, - maxHeight: { - type: "VARIABLE_ALIAS", - id: "VariableID:5c0fdb551d4699d20f1fa1328995f4fb9d0ad8d5/3203:12", - }, - }, - componentId: "2099:38674", - isExposedInstance: true, - componentProperties: { - Type: { - value: "Fixed", - type: "VARIANT", - boundVariables: {}, - }, - Allignment: { - value: "Default (L-R)", - type: "VARIANT", - boundVariables: {}, - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664", - overriddenFields: [ - "componentProperties", - "primaryAxisSizingMode", - "layoutGrow", - "counterAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19688", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19673;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19661", - overriddenFields: [ - "componentProperties", - "counterAxisSizingMode", - "primaryAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19706", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054", - overriddenFields: [ - "layoutAlign", - "sharedPluginData", - "pluginData", - "componentProperties", - "counterAxisSizingMode", - "layoutGrow", - "paddingRight", - "paddingLeft", - "primaryAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19697", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19673", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19691", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19703;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19667", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19691;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917", - overriddenFields: [ - "topLeftRadius", - "cornerRadius", - "topRightRadius", - "name", - "bottomLeftRadius", - "bottomRightRadius", - "primaryAxisSizingMode", - "counterAxisSizingMode", - "layoutGrow", - "boundVariables", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19670", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19700", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19703", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3055", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19670;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19697;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19688;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19706;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3056", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19667;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;1732:20311", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19694", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3053", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19694;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3052", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - { - id: "I2099:38616;1739:34917;2326:19700;979:10619", - overriddenFields: ["sharedPluginData", "pluginData"], - }, - ], - children: [ - { - id: "I2099:38616;1739:34917;1732:20311", - name: "Grid", - type: "FRAME", - scrollBehavior: "SCROLLS", - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", - }, - }, - children: [ - { - id: "I2099:38616;1739:34917;1732:20312", - name: "Row", - type: "FRAME", - scrollBehavior: "SCROLLS", - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:be243c44965c9affe677211ea0cd661d873653e1/10434:247", - }, - counterAxisSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:b1aa965163834bcfd01131ac315d7d493f241ba6/10434:244", - }, - }, - children: [ - { - id: "I2099:38616;1739:34917;2326:19661", - name: "TextImage Variation 2", - type: "INSTANCE", - scrollBehavior: "SCROLLS", - boundVariables: { - size: { - x: { - type: "VARIABLE_ALIAS", - id: "VariableID:7dde29c0269cd3c53a0f9f7c1e2cc8e86d43cf3b/3157:236", - }, - y: { - type: "VARIABLE_ALIAS", - id: "VariableID:7dde29c0269cd3c53a0f9f7c1e2cc8e86d43cf3b/3157:236", - }, - }, - }, - componentId: "2043:22065", - componentProperties: { - "↳Swap Icon/Image/Graphic#9837:448": { - value: "2092:64304", - type: "INSTANCE_SWAP", - preferredValues: [], - }, - "Custom Size": { - value: "False", - type: "VARIANT", - boundVariables: {}, - }, - "Predefined Size": { - value: "L Container", - type: "VARIANT", - boundVariables: {}, - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19661", - overriddenFields: [ - "componentProperties", - "counterAxisSizingMode", - "primaryAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [ - { - id: "I2099:38616;1739:34917;2326:19661;117:435", - name: "FaxOfHook", - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - mainComponent: - "↳Swap Icon/Image/Graphic#9837:448", - }, - componentId: "2092:64304", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [ - { - id: "I2099:38616;1739:34917;2326:19661;117:435;1658:6969", - name: "Vector", - type: "VECTOR", - scrollBehavior: "SCROLLS", - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - fillOverrideTable: { - "1": null, - }, - strokes: [], - strokeWeight: 0.75, - strokeAlign: "CENTER", - strokeJoin: "ROUND", - strokeCap: "ROUND", - strokeMiterAngle: 11.478341102600098, - absoluteBoundingBox: { - x: -1897.455078125, - y: -2168.515380859375, - width: 127.45187377929688, - height: 125.89124298095703, - }, - absoluteRenderBounds: { - x: -1897.455078125, - y: -2168.515380859375, - width: 127.451904296875, - height: 125.8912353515625, - }, - constraints: { - vertical: "SCALE", - horizontal: "SCALE", - }, - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - absoluteBoundingBox: { - x: -1907.5, - y: -2177.75, - width: 150, - height: 150, - }, - absoluteRenderBounds: { - x: -1907.5, - y: -2177.75, - width: 150, - height: 150, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 1, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "FILL", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "HORIZONTAL", - itemSpacing: 10, - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1907.5, - y: -2177.75, - width: 150, - height: 150, - }, - absoluteRenderBounds: { - x: -1907.5, - y: -2177.75, - width: 150, - height: 150, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664", - name: "TextImage Variation 1", - type: "INSTANCE", - scrollBehavior: "SCROLLS", - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:3f744b5e3c8b3619411d855f7fe34dd7351a9435/3157:1061", - }, - minWidth: { - type: "VARIABLE_ALIAS", - id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", - }, - }, - componentId: "2058:19231", - exposedInstances: [ - "I2099:38616;1739:34917;2326:19664;1202:3050", - "I2099:38616;1739:34917;2326:19664;1202:3052", - "I2099:38616;1739:34917;2326:19664;1202:3053", - "I2099:38616;1739:34917;2326:19664;1202:3054", - "I2099:38616;1739:34917;2326:19664;1202:3055", - "I2099:38616;1739:34917;2326:19664;1202:3056", - ], - componentProperties: { - "↳Show Title Big#1053:9": { - value: false, - type: "BOOLEAN", - }, - "↳Show Content Small#1053:13": { - value: false, - type: "BOOLEAN", - }, - "Show Icon#1053:11": { - value: false, - type: "BOOLEAN", - }, - "Show Text#3122:8": { - value: true, - type: "BOOLEAN", - }, - "↳Show Title Small#1053:12": { - value: false, - type: "BOOLEAN", - }, - "↳Show Content#1053:10": { - value: true, - type: "BOOLEAN", - }, - "↳Show Clarification#1053:7": { - value: false, - type: "BOOLEAN", - }, - "↳Swap Icon#1053:8": { - value: "2033:1462", - type: "INSTANCE_SWAP", - preferredValues: [], - }, - Alignment: { - value: "ImageLeft-Middle", - type: "VARIANT", - boundVariables: {}, - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664", - overriddenFields: [ - "componentProperties", - "primaryAxisSizingMode", - "layoutGrow", - "counterAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3056", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3052", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054", - overriddenFields: [ - "layoutAlign", - "sharedPluginData", - "pluginData", - "componentProperties", - "counterAxisSizingMode", - "layoutGrow", - "paddingRight", - "paddingLeft", - "primaryAxisSizingMode", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3055", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3053", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3050", - name: "Placeholder", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - mainComponent: "↳Swap Icon#1053:8", - visible: "Show Icon#1053:11", - }, - boundVariables: { - size: { - x: { - type: "VARIABLE_ALIAS", - id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", - }, - y: { - type: "VARIABLE_ALIAS", - id: "VariableID:1b137df291c4095c5e80c61d2c69a449cba2ccd9/3157:87", - }, - }, - }, - explicitVariableModes: { - "VariableCollectionId:e2fa2f7f8d460f250b4a1269b312f661b543be85/443:278": - "146:9", - }, - componentId: "2033:1462", - isExposedInstance: true, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3050;1856:4", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 37.5, - height: 37.5, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3051", - name: "Text", - type: "FRAME", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "Show Text#3122:8", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", - }, - }, - children: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3052", - name: "TitleBig", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Title Big#1053:9", - }, - componentId: "2033:2259", - isExposedInstance: true, - componentProperties: { - "Title Label#1366:0": { - value: "Title big text", - type: "TEXT", - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3052", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "HORIZONTAL", - primaryAxisSizingMode: "FIXED", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 321.5, - height: 32, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3053", - name: "TitleSmall", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Title Small#1053:12", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", - }, - }, - componentId: "2058:19206", - isExposedInstance: true, - componentProperties: { - "Title small Label#1366:1": { - value: "Title small text", - type: "TEXT", - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3053", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 18, - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 321.5, - height: 26, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054", - name: "Content", - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Content#1053:10", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", - }, - }, - componentId: "2058:19212", - isExposedInstance: true, - componentProperties: { - "Content Label#1366:2": { - value: - "You can send a fax to the dialed number or manually receive a fax by selecting one of these options.", - type: "TEXT", - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054", - overriddenFields: [ - "layoutAlign", - "sharedPluginData", - "pluginData", - "componentProperties", - "counterAxisSizingMode", - "layoutGrow", - "paddingRight", - "paddingLeft", - "primaryAxisSizingMode", - ], - }, - ], - children: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054;1053:1206", - name: "Content", - type: "TEXT", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - characters: "Content Label#1366:2", - }, - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - lineHeight: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", - }, - ], - fontFamily: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", - }, - ], - fontSize: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "OUTSIDE", - absoluteBoundingBox: { - x: -1710, - y: -2177.75, - width: 707.5, - height: 62, - }, - absoluteRenderBounds: { - x: -1709.5155029296875, - y: -2171.339599609375, - width: 681.0682373046875, - height: 53.128662109375, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - characters: - "You can send a fax to the dialed number or manually receive a fax by selecting one of these options.", - characterStyleOverrides: [], - styleOverrideTable: {}, - lineTypes: ["NONE"], - lineIndentations: [0], - style: { - fontFamily: "HP Simplified", - fontPostScriptName: - "HPSimplified-Regular", - fontStyle: "Regular", - fontWeight: 400, - textAutoResize: "HEIGHT", - fontSize: 25.5, - textAlignHorizontal: "LEFT", - textAlignVertical: "TOP", - letterSpacing: 0, - lineHeightPx: 30.600000381469727, - lineHeightPercent: 103.53753662109375, - lineHeightPercentFontSize: 120, - lineHeightUnit: "PIXELS", - }, - layoutVersion: 4, - styles: { - text: "2022:10407", - }, - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1256", - name: "Content", - visible: false, - type: "TEXT", - scrollBehavior: "SCROLLS", - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - lineHeight: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", - }, - ], - fontFamily: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", - }, - ], - fontSize: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "OUTSIDE", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 216, - height: 31, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - characters: "Content text", - characterStyleOverrides: [], - styleOverrideTable: {}, - lineTypes: ["NONE"], - lineIndentations: [0], - style: { - fontFamily: "HP Simplified", - fontPostScriptName: - "HPSimplified-Regular", - fontStyle: "Regular", - fontWeight: 400, - textAutoResize: "HEIGHT", - fontSize: 25.5, - textAlignHorizontal: "LEFT", - textAlignVertical: "TOP", - letterSpacing: 0, - lineHeightPx: 30.600000381469727, - lineHeightPercent: 103.53753662109375, - lineHeightPercentFontSize: 120, - lineHeightUnit: "PIXELS", - }, - layoutVersion: 4, - styles: { - text: "2022:10407", - }, - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1267", - name: "Content", - visible: false, - type: "TEXT", - scrollBehavior: "SCROLLS", - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - lineHeight: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", - }, - ], - fontFamily: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", - }, - ], - fontSize: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "OUTSIDE", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 216, - height: 31, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - characters: "Content text", - characterStyleOverrides: [], - styleOverrideTable: {}, - lineTypes: ["NONE"], - lineIndentations: [0], - style: { - fontFamily: "HP Simplified", - fontPostScriptName: - "HPSimplified-Regular", - fontStyle: "Regular", - fontWeight: 400, - textAutoResize: "HEIGHT", - fontSize: 25.5, - textAlignHorizontal: "LEFT", - textAlignVertical: "TOP", - letterSpacing: 0, - lineHeightPx: 30.600000381469727, - lineHeightPercent: 103.53753662109375, - lineHeightPercentFontSize: 120, - lineHeightUnit: "PIXELS", - }, - layoutVersion: 4, - styles: { - text: "2022:10407", - }, - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1283", - name: "Content", - visible: false, - type: "TEXT", - scrollBehavior: "SCROLLS", - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - lineHeight: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", - }, - ], - fontFamily: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", - }, - ], - fontSize: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "OUTSIDE", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 216, - height: 31, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - characters: "Content text", - characterStyleOverrides: [], - styleOverrideTable: {}, - lineTypes: ["NONE"], - lineIndentations: [0], - style: { - fontFamily: "HP Simplified", - fontPostScriptName: - "HPSimplified-Regular", - fontStyle: "Regular", - fontWeight: 400, - textAutoResize: "HEIGHT", - fontSize: 25.5, - textAlignHorizontal: "LEFT", - textAlignVertical: "TOP", - letterSpacing: 0, - lineHeightPx: 30.600000381469727, - lineHeightPercent: 103.53753662109375, - lineHeightPercentFontSize: 120, - lineHeightUnit: "PIXELS", - }, - layoutVersion: 4, - styles: { - text: "2022:10407", - }, - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3054;1202:1304", - name: "Content", - visible: false, - type: "TEXT", - scrollBehavior: "SCROLLS", - boundVariables: { - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - ], - lineHeight: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:7458686840ba6bbe67d34048518a37290df777ed/4411:711", - }, - ], - fontFamily: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:f698a4f032592c139aea25637f9791852d11c35b/1203:148", - }, - ], - fontSize: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:5926de5ea9f1df6a583324845256eec3e10058d8/3157:790", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 1, - g: 1, - b: 1, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e208fc12668fdb73b1d1a743ba7f06ef9dd690ba/2013:512", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "OUTSIDE", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 216, - height: 31, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - characters: "Content text", - characterStyleOverrides: [], - styleOverrideTable: {}, - lineTypes: ["NONE"], - lineIndentations: [0], - style: { - fontFamily: "HP Simplified", - fontPostScriptName: - "HPSimplified-Regular", - fontStyle: "Regular", - fontWeight: 400, - textAutoResize: "HEIGHT", - fontSize: 25.5, - textAlignHorizontal: "LEFT", - textAlignVertical: "TOP", - letterSpacing: 0, - lineHeightPx: 30.600000381469727, - lineHeightPercent: 103.53753662109375, - lineHeightPercentFontSize: 120, - lineHeightUnit: "PIXELS", - }, - layoutVersion: 4, - styles: { - text: "2022:10407", - }, - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 18, - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - paddingLeft: 10, - paddingRight: 10, - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - absoluteRenderBounds: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3055", - name: "ContentSmall", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Content Small#1053:13", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", - }, - }, - componentId: "2058:19218", - isExposedInstance: true, - componentProperties: { - "Content Small Label#1366:3": { - value: "Content small text", - type: "TEXT", - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3055", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 18, - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 321.5, - height: 20, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19664;1202:3056", - name: "Clarification", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Clarification#1053:7", - }, - boundVariables: { - itemSpacing: { - type: "VARIABLE_ALIAS", - id: "VariableID:61bc0ffe4bb813f939fb25252ec1ae428556cc4e/10419:0", - }, - }, - componentId: "2058:19224", - isExposedInstance: true, - componentProperties: { - "Clarification Label#1366:4": { - value: "Clarification text", - type: "TEXT", - }, - }, - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19664;1202:3056", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 18, - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2132.75, - width: 321.5, - height: 20, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 18, - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - absoluteRenderBounds: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 1, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "HORIZONTAL", - itemSpacing: 15, - primaryAxisSizingMode: "FIXED", - counterAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - absoluteRenderBounds: { - x: -1720, - y: -2177.75, - width: 727.5, - height: 62, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 1, - minWidth: 37.5, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19667", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19667", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19667;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1678.5, - y: -2177.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19670", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19670", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19670;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1564, - y: -2177.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19688", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19688", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19688;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1907.5, - y: -2122.25, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19691", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19691", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19691;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1793, - y: -2122.25, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19694", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19694;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19694", - overriddenFields: ["visible"], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1678.5, - y: -2122.25, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19673", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19673;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - { - id: "I2099:38616;1739:34917;2326:19673", - overriddenFields: ["visible"], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1564, - y: -2122.25, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19697", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19697", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19697;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1907.5, - y: -2066.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19700", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19700", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19700;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1793, - y: -2066.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19703", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19703", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19703;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1678.5, - y: -2066.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:34917;2326:19706", - name: "Slot", - visible: false, - type: "INSTANCE", - scrollBehavior: "SCROLLS", - componentId: "2060:58189", - overrides: [ - { - id: "I2099:38616;1739:34917;2326:19706", - overriddenFields: ["visible"], - }, - { - id: "I2099:38616;1739:34917;2326:19706;979:10619", - overriddenFields: [ - "sharedPluginData", - "pluginData", - ], - }, - ], - children: [], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - cornerRadius: 4, - cornerSmoothing: 0, - strokeWeight: 2, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "HORIZONTAL", - counterAxisAlignItems: "CENTER", - primaryAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - strokeDashes: [8, 4], - absoluteBoundingBox: { - x: -1564, - y: -2066.75, - width: 98, - height: 39, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "HORIZONTAL", - itemSpacing: 37.5, - primaryAxisSizingMode: "FIXED", - layoutWrap: "WRAP", - counterAxisSpacing: 37.5, - counterAxisAlignContent: "AUTO", - absoluteBoundingBox: { - x: -1907.5, - y: -2177.75, - width: 915, - height: 150, - }, - absoluteRenderBounds: { - x: -1907.5, - y: -2177.75, - width: 915, - height: 150, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - uniqueName: "Row", - width: 915, - height: 150, - x: 0, - y: 0, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - primaryAxisAlignItems: "MIN", - counterAxisAlignItems: "MIN", - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 37.5, - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1907.5, - y: -2177.75, - width: 915, - height: 150, - }, - absoluteRenderBounds: { - x: -1907.5, - y: -2177.75, - width: 915, - height: 150, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 0, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "HUG", - effects: [], - interactions: [], - uniqueName: "Grid", - width: 915, - height: 150, - x: 22.5, - y: 22.5, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - primaryAxisAlignItems: "MIN", - counterAxisAlignItems: "MIN", - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - overflowDirection: "VERTICAL_SCROLLING", - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 37.5, - primaryAxisSizingMode: "FIXED", - paddingLeft: 22.5, - paddingRight: 22.5, - paddingTop: 22.5, - paddingBottom: 22.5, - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1930, - y: -2200.25, - width: 960, - height: 536.25, - }, - absoluteRenderBounds: { - x: -1930, - y: -2200.25, - width: 960, - height: 536.25, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 1, - minHeight: 136.25, - maxHeight: 536.25, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "FILL", - effects: [], - interactions: [], - uniqueName: "Modal Container", - variantProperties: { - Type: "Fixed", - Allignment: "Default (L-R)", - }, - width: 960, - height: 536.25, - x: 0, - y: 0, - primaryAxisAlignItems: "MIN", - counterAxisAlignItems: "MIN", - }, - { - id: "I2099:38616;1739:61707", - name: "Scroll Container", - type: "INSTANCE", - locked: true, - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "↳Show Scroll Container#1739:42", - }, - componentId: "438:9274", - componentProperties: { - "Show Top Gradient#1272:20": { - value: false, - type: "BOOLEAN", - }, - "Show Bottom Gradient#1272:19": { - value: true, - type: "BOOLEAN", - }, - "Show Container Bullets#2778:4": { - value: true, - type: "BOOLEAN", - }, - "Show Left Gradient#1359:0": { - value: false, - type: "BOOLEAN", - }, - "Show Right Gradient#1359:4": { - value: true, - type: "BOOLEAN", - }, - "Show Scrollbar#1272:18": { - value: true, - type: "BOOLEAN", - }, - "Show V Scrollbar#1359:8": { - value: true, - type: "BOOLEAN", - }, - "Show H Scrollbar#1359:12": { - value: true, - type: "BOOLEAN", - }, - Variation: { - value: "Vertical", - type: "VARIANT", - boundVariables: {}, - }, - }, - overrides: [], - children: [ - { - id: "I2099:38616;1739:61707;548:24039", - name: "Gradient Top", - visible: false, - type: "RECTANGLE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "Show Top Gradient#1272:20", - }, - boundVariables: { - size: { - y: { - type: "VARIABLE_ALIAS", - id: "VariableID:7de17999810183e2464dbfd32573c3ecf15a9f2e/3157:462", - }, - }, - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", - }, - { - type: "VARIABLE_ALIAS", - id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "GRADIENT_LINEAR", - gradientHandlePositions: [ - { - x: 0.5, - y: -3.0616171314629196e-17, - }, - { - x: 0.5, - y: 0.9999999999999999, - }, - { - x: 0, - y: 0, - }, - ], - gradientStops: [ - { - color: { - r: 0.0784313753247261, - g: 0.0784313753247261, - b: 0.0784313753247261, - a: 1, - }, - position: 0, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", - }, - }, - }, - { - color: { - r: 0.0784313753247261, - g: 0.0784313753247261, - b: 0.0784313753247261, - a: 0, - }, - position: 1, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", - }, - }, - }, - ], - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - styles: { - fill: "438:9260", - }, - absoluteBoundingBox: { - x: -1930, - y: -2200, - width: 960, - height: 30, - }, - absoluteRenderBounds: null, - constraints: { - vertical: "TOP", - horizontal: "LEFT_RIGHT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutPositioning: "ABSOLUTE", - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:61707;548:24040", - name: "Gradient Bottom", - type: "RECTANGLE", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "Show Bottom Gradient#1272:19", - }, - boundVariables: { - size: { - y: { - type: "VARIABLE_ALIAS", - id: "VariableID:7de17999810183e2464dbfd32573c3ecf15a9f2e/3157:462", - }, - }, - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", - }, - { - type: "VARIABLE_ALIAS", - id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "GRADIENT_LINEAR", - gradientHandlePositions: [ - { - x: 0.5, - y: -3.0616171314629196e-17, - }, - { - x: 0.5, - y: 0.9999999999999999, - }, - { - x: 0, - y: 0, - }, - ], - gradientStops: [ - { - color: { - r: 0.0784313753247261, - g: 0.0784313753247261, - b: 0.0784313753247261, - a: 0, - }, - position: 0, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:26724dcbb1ea60587379f89ef3dc160ae7f7f7da/2022:241", - }, - }, - }, - { - color: { - r: 0.0784313753247261, - g: 0.0784313753247261, - b: 0.0784313753247261, - a: 1, - }, - position: 1, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:86e61e4b2a2fe9ab507a9302544920d38675a086/2036:2", - }, - }, - }, - ], - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - styles: { - fill: "438:9261", - }, - absoluteBoundingBox: { - x: -1930, - y: -1686, - width: 960, - height: 30, - }, - absoluteRenderBounds: { - x: -1930, - y: -1686, - width: 960, - height: 22, - }, - constraints: { - vertical: "BOTTOM", - horizontal: "LEFT_RIGHT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutPositioning: "ABSOLUTE", - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - effects: [], - interactions: [], - }, - { - id: "I2099:38616;1739:61707;548:24046", - name: "Scrollbar V", - type: "FRAME", - scrollBehavior: "SCROLLS", - componentPropertyReferences: { - visible: "Show Scrollbar#1272:18", - }, - boundVariables: { - paddingTop: { - type: "VARIABLE_ALIAS", - id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", - }, - paddingRight: { - type: "VARIABLE_ALIAS", - id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", - }, - paddingBottom: { - type: "VARIABLE_ALIAS", - id: "VariableID:eb2b818a81b47a755734677805c327446a7eaacc/3157:1078", - }, - }, - children: [ - { - id: "I2099:38616;1739:61707;548:24047", - name: "Bar", - type: "RECTANGLE", - scrollBehavior: "SCROLLS", - boundVariables: { - minHeight: { - type: "VARIABLE_ALIAS", - id: "VariableID:5fdc417718534e1862bd74d2b0cae46cbc3fdd93/3157:975", - }, - size: { - x: { - type: "VARIABLE_ALIAS", - id: "VariableID:4e7717a4a0ee45c0a9977c43d56cf4cb32848299/3157:257", - }, - }, - rectangleCornerRadii: { - RECTANGLE_TOP_LEFT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", - }, - RECTANGLE_TOP_RIGHT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", - }, - RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", - }, - RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS: { - type: "VARIABLE_ALIAS", - id: "VariableID:2044450eb10ae8e476766aeb5bf3550e3c2ba36e/3157:901", - }, - }, - fills: [ - { - type: "VARIABLE_ALIAS", - id: "VariableID:9b31d8ddc760d048c5d9e42dbce12aa5946a3041/2036:15", - }, - ], - }, - blendMode: "PASS_THROUGH", - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 0.658823549747467, - g: 0.658823549747467, - b: 0.658823549747467, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:9b31d8ddc760d048c5d9e42dbce12aa5946a3041/2036:15", - }, - }, - }, - ], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - cornerRadius: 1000, - cornerSmoothing: 0, - absoluteBoundingBox: { - x: -979, - y: -2195.5, - width: 4.5, - height: 31, - }, - absoluteRenderBounds: { - x: -979, - y: -2195.5, - width: 4.5, - height: 31, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 1, - minHeight: 15, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FILL", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: false, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - primaryAxisSizingMode: "FIXED", - counterAxisAlignItems: "MAX", - paddingRight: 4.5, - paddingTop: 4.5, - paddingBottom: 4.5, - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -979, - y: -2200, - width: 9, - height: 40, - }, - absoluteRenderBounds: { - x: -979, - y: -2200, - width: 9, - height: 40, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutSizingHorizontal: "HUG", - layoutSizingVertical: "FIXED", - effects: [], - interactions: [], - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - primaryAxisSizingMode: "FIXED", - counterAxisAlignItems: "MAX", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1930, - y: -2200, - width: 960, - height: 536, - }, - absoluteRenderBounds: { - x: -1930, - y: -2200, - width: 960, - height: 536, - }, - constraints: { - vertical: "TOP_BOTTOM", - horizontal: "LEFT_RIGHT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - layoutPositioning: "ABSOLUTE", - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - effects: [], - interactions: [], - uniqueName: "Scroll Container", - variantProperties: { - Variation: "Vertical", - }, - width: 960, - height: 536, - x: 0, - y: 0.25, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - primaryAxisAlignItems: "MIN", - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [], - fills: [], - strokes: [], - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0, - g: 0, - b: 0, - a: 0, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - itemSpacing: 10, - primaryAxisSizingMode: "FIXED", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1930, - y: -2200.25, - width: 960, - height: 536.25, - }, - absoluteRenderBounds: { - x: -1930, - y: -2200.25, - width: 960, - height: 536.25, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "STRETCH", - layoutGrow: 1, - layoutSizingHorizontal: "FILL", - layoutSizingVertical: "FILL", - effects: [], - interactions: [], - uniqueName: "Content", - width: 960, - height: 536.25, - x: 0, - y: 93.75, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - primaryAxisAlignItems: "MIN", - counterAxisAlignItems: "MIN", - isRelative: true, - }, - ], - blendMode: "PASS_THROUGH", - clipsContent: true, - background: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 0.1411764770746231, - g: 0.1411764770746231, - b: 0.1411764770746231, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", - }, - }, - }, - ], - fills: [ - { - blendMode: "NORMAL", - type: "SOLID", - color: { - r: 0.1411764770746231, - g: 0.1411764770746231, - b: 0.1411764770746231, - a: 1, - }, - boundVariables: { - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:8cbcd0032a7cac3b9799f16f6f48c35cab554a40/2243:10", - }, - }, - variableColorName: "Box-Style-box4", - }, - ], - strokes: [], - cornerRadius: 4.5, - cornerSmoothing: 0, - strokeWeight: 1, - strokeAlign: "INSIDE", - backgroundColor: { - r: 0.1411764770746231, - g: 0.1411764770746231, - b: 0.1411764770746231, - a: 1, - }, - layoutMode: "VERTICAL", - counterAxisSizingMode: "FIXED", - primaryAxisSizingMode: "FIXED", - counterAxisAlignItems: "CENTER", - layoutWrap: "NO_WRAP", - absoluteBoundingBox: { - x: -1930, - y: -2294, - width: 960, - height: 720, - }, - absoluteRenderBounds: { - x: -1969, - y: -2323.10009765625, - width: 1038, - height: 789.10009765625, - }, - constraints: { - vertical: "TOP", - horizontal: "LEFT", - }, - layoutAlign: "INHERIT", - layoutGrow: 0, - minHeight: 320, - maxHeight: 720, - layoutSizingHorizontal: "FIXED", - layoutSizingVertical: "FIXED", - effects: [ - { - type: "DROP_SHADOW", - visible: true, - color: { - r: 0, - g: 0, - b: 0, - a: 0.47999998927116394, - }, - blendMode: "NORMAL", - offset: { - x: 0, - y: 9.899999618530273, - }, - radius: 39, - showShadowBehindNode: true, - boundVariables: { - radius: { - type: "VARIABLE_ALIAS", - id: "VariableID:7b576d4f7cef936e728857b8d6f7952e2a6dd6fe/3157:78", - }, - color: { - type: "VARIABLE_ALIAS", - id: "VariableID:e7dccd708e8eb5f689ef26147d2d985b7af1bfd8/2013:336", - }, - offsetY: { - type: "VARIABLE_ALIAS", - id: "VariableID:55268df3aca26e8ed4182c6831670c631ab2e88b/4411:298", - }, - }, - variableColorName: "Elevation-elevation5", - }, - ], - styles: { - effect: "438:10166", - }, - interactions: [], - uniqueName: "Modal", - width: 960, - height: 720, - x: 160, - y: 40, - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - primaryAxisAlignItems: "MIN", - }, - ]; + console.log( `[benchmark] convertNodesToAltNodes: ${Date.now() - convertNodesStart}ms`, ); diff --git a/packages/backend/src/common/commonPosition.ts b/packages/backend/src/common/commonPosition.ts index 5001088c..53bf4ddb 100644 --- a/packages/backend/src/common/commonPosition.ts +++ b/packages/backend/src/common/commonPosition.ts @@ -1,6 +1,23 @@ export const getCommonPositionValue = ( node: SceneNode, ): { x: number; y: number } => { + if (node.parent && node.parent.absoluteBoundingBox) { + const x = node.absoluteBoundingBox.x - node.parent.absoluteBoundingBox.x; + const y = node.absoluteBoundingBox.y - node.parent.absoluteBoundingBox.y; + + const rect = calculateRectangleFromBoundingBox( + { + width: node.absoluteBoundingBox.width, + height: node.absoluteBoundingBox.height, + x: x, + y: y, + }, + node.cumulativeRotation || node.rotation || 0, + ); + + return { x: rect.left, y: rect.top }; + } + if (node.parent && node.parent.type === "GROUP") { return { x: node.x - node.parent.x, @@ -14,6 +31,66 @@ export const getCommonPositionValue = ( }; }; +interface BoundingBox { + width: number; // w_b + height: number; // h_b + x: number; // x_b + y: number; // y_b +} + +interface RectangleStyle { + width: number; // Original width (w) + height: number; // Original height (h) + left: number; // Final CSS left + top: number; // Final CSS top + rotation: number; // Rotation in degrees +} + +function calculateRectangleFromBoundingBox( + boundingBox: BoundingBox, + figmaRotationDegrees: number, +): RectangleStyle { + const cssRotationDegrees = -figmaRotationDegrees; // Direct CSS mapping + const theta = (cssRotationDegrees * Math.PI) / 180; + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + const absCosTheta = Math.abs(cosTheta); + const absSinTheta = Math.abs(sinTheta); + + const { width: w_b, height: h_b, x: x_b, y: y_b } = boundingBox; + + // For top-left origin, bounding box depends on rotation direction + const denominator = absCosTheta * absCosTheta - absSinTheta * absSinTheta; + const h = (w_b * absSinTheta - h_b * absCosTheta) / -denominator; + const w = (w_b - h * absSinTheta) / absCosTheta; + + // Rotate corners to find bounding box offsets + const corners = [ + { x: 0, y: 0 }, + { x: w, y: 0 }, + { x: w, y: h }, + { x: 0, y: h }, + ]; + const rotatedCorners = corners.map(({ x, y }) => ({ + x: x * cosTheta + y * sinTheta, + y: -x * sinTheta + y * cosTheta, + })); + + const minX = Math.min(...rotatedCorners.map((c) => c.x)); + const minY = Math.min(...rotatedCorners.map((c) => c.y)); + + const left = x_b - minX; + const top = y_b - minY; + + return { + width: parseFloat(w.toFixed(2)), + height: parseFloat(h.toFixed(2)), + left: parseFloat(left.toFixed(2)), + top: parseFloat(top.toFixed(2)), + rotation: cssRotationDegrees, + }; +} + export const commonIsAbsolutePosition = (node: SceneNode) => { if ("layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE") { return true; diff --git a/packages/backend/src/html/builderImpl/htmlBlend.ts b/packages/backend/src/html/builderImpl/htmlBlend.ts index 1a68039a..3a63197a 100644 --- a/packages/backend/src/html/builderImpl/htmlBlend.ts +++ b/packages/backend/src/html/builderImpl/htmlBlend.ts @@ -125,8 +125,9 @@ export const htmlRotation = (node: LayoutMixin, isJsx: boolean): string[] => { const parentRotation: number = parent && "rotation" in parent ? parent.rotation : 0; - const nodeRotation = node.rotation || 0; - const rotation = Math.round(parentRotation - nodeRotation) ?? 0; + const cssRotationDegrees = node.rotation || 0; + const nodeRotation = cssRotationDegrees; + const rotation = Math.round(nodeRotation) ?? 0; if ( roundToNearestHundreth(parentRotation) !== 0 && From 07d094e6f63cd05975667645af7c7b095181fb05 Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Tue, 11 Mar 2025 04:22:54 -0300 Subject: [PATCH 073/134] Debug --- packages/backend/src/html/builderImpl/htmlBlend.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/html/builderImpl/htmlBlend.ts b/packages/backend/src/html/builderImpl/htmlBlend.ts index 3a63197a..94c6bfd2 100644 --- a/packages/backend/src/html/builderImpl/htmlBlend.ts +++ b/packages/backend/src/html/builderImpl/htmlBlend.ts @@ -125,9 +125,8 @@ export const htmlRotation = (node: LayoutMixin, isJsx: boolean): string[] => { const parentRotation: number = parent && "rotation" in parent ? parent.rotation : 0; - const cssRotationDegrees = node.rotation || 0; - const nodeRotation = cssRotationDegrees; - const rotation = Math.round(nodeRotation) ?? 0; + const nodeRotation = -node.rotation || 0; + const rotation = Math.round(parentRotation - nodeRotation) ?? 0; if ( roundToNearestHundreth(parentRotation) !== 0 && From 958c34379468eaabde18175f3f945b00c7fef94a Mon Sep 17 00:00:00 2001 From: Bernardo Ferrari Date: Wed, 12 Mar 2025 02:59:22 -0300 Subject: [PATCH 074/134] New UI --- apps/plugin/plugin-src/code.ts | 6 +- apps/plugin/tailwind.config.js | 56 +- apps/plugin/ui-src/index.css | 67 +++ .../backend/src/altNodes/altConversion.ts | 87 --- .../src/altNodes/jsonNodeConversion.ts | 498 ++++++++++++++++++ packages/backend/src/code.ts | 447 +--------------- .../src/common/commonFormatAttributes.ts | 2 +- .../src/common/retrieveUI/retrieveColors.ts | 21 +- .../backend/src/html/builderImpl/htmlSize.ts | 12 +- packages/backend/src/index.ts | 1 - .../tailwind/builderImpl/tailwindBorder.ts | 4 +- .../src/tailwind/builderImpl/tailwindSize.ts | 12 +- packages/plugin-ui/src/PluginUI.tsx | 90 ++-- packages/plugin-ui/src/components/About.tsx | 14 +- .../plugin-ui/src/components/CodePanel.tsx | 3 +- .../plugin-ui/src/components/ColorsPanel.tsx | 11 +- .../plugin-ui/src/components/CopyButton.tsx | 34 +- .../plugin-ui/src/components/EmptyState.tsx | 2 +- .../src/components/FrameworkTabs.tsx | 6 +- .../src/components/GradientsPanel.tsx | 4 +- packages/plugin-ui/src/components/Preview.tsx | 10 +- .../src/components/SelectableToggle.tsx | 15 +- .../src/components/SettingsGroup.tsx | 4 +- packages/plugin-ui/tailwind.config.js | 56 ++ 24 files changed, 841 insertions(+), 621 deletions(-) delete mode 100644 packages/backend/src/altNodes/altConversion.ts create mode 100644 packages/backend/src/altNodes/jsonNodeConversion.ts diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 2b10d3a0..6cf5e016 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -4,11 +4,10 @@ import { flutterMain, tailwindMain, swiftuiMain, - convertIntoNodes, htmlMain, postSettingsChanged, } from "backend"; -import { nodesToJSON } from "backend/src/code"; +import { nodesToJSON } from "backend/src/altNodes/jsonNodeConversion"; import { retrieveGenericSolidUIColors } from "backend/src/common/retrieveUI/retrieveColors"; import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain"; import { htmlCodeGenTextStyles } from "backend/src/html/htmlMain"; @@ -140,7 +139,8 @@ const standardMode = async () => { }); // Listen for page changes - figma.on("currentpagechange", () => { + figma.loadAllPagesAsync(); + figma.on("documentchange", () => { console.log("[DEBUG] documentchange event triggered"); // Node: This was causing an infinite load when you try to export a background image from a group that contains children. // The reason for this is that the code will temporarily hide the children of the group in order to export a clean image diff --git a/apps/plugin/tailwind.config.js b/apps/plugin/tailwind.config.js index 0c74c637..a134a47d 100644 --- a/apps/plugin/tailwind.config.js +++ b/apps/plugin/tailwind.config.js @@ -6,5 +6,59 @@ module.exports = { "../../packages/plugin-ui/**/*.{js,ts,jsx,tsx}", ], darkMode: "class", - + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / )", + foreground: "hsl(var(--destructive-foreground) / )", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + }, + borderRadius: { + xl: "calc(var(--radius) + 4px)", + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + } + } }; diff --git a/apps/plugin/ui-src/index.css b/apps/plugin/ui-src/index.css index b5c61c95..5fbe3dcf 100644 --- a/apps/plugin/ui-src/index.css +++ b/apps/plugin/ui-src/index.css @@ -1,3 +1,70 @@ @tailwind base; @tailwind components; @tailwind utilities; + + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 220 14% 96%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 142 70% 45%; + --primary-foreground: 0 0% 100%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 13%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 142 70% 45%; + --primary-foreground: 0 0% 100%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } + } + + @layer base { + * { + @apply border-border outline-ring/50; + } + /* body { + @apply bg-background text-foreground; + } */ + } \ No newline at end of file diff --git a/packages/backend/src/altNodes/altConversion.ts b/packages/backend/src/altNodes/altConversion.ts deleted file mode 100644 index 9865d713..00000000 --- a/packages/backend/src/altNodes/altConversion.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ParentNode } from "types"; -import { - isNotEmpty, - assignRectangleType, - assignChildren, - isTypeOrGroupOfTypes, -} from "./altNodeUtils"; -import { addWarning } from "../common/commonConversionWarnings"; - -// List of types that can be flattened into SVG -const canBeFlattened = (node: SceneNode): boolean => { - // These node types should be directly flattened - const flattenableTypes: NodeType[] = [ - "VECTOR", - "STAR", - "POLYGON", - "BOOLEAN_OPERATION", - ]; - - // Handle special case for Rectangle nodes with zero or near-zero height - if (node.type === "RECTANGLE") { - // Check if the node is essentially a divider/line (near-zero height) - return false; // Rectangles should not be flattened by default - } - - return isTypeOrGroupOfTypes(flattenableTypes, node); -}; - -export const convertNodeToAltNode = - (parent: ParentNode | null) => - async (node: SceneNode): Promise => { - (node as any).canBeFlattened = canBeFlattened(node); - const type = node.type; - switch (type) { - // Standard nodes - case "RECTANGLE": - case "ELLIPSE": - case "LINE": - case "STAR": - case "POLYGON": - case "VECTOR": - case "BOOLEAN_OPERATION": - return node; - - // Group nodes - case "FRAME": - case "INSTANCE": - case "COMPONENT": - case "COMPONENT_SET": - // if the frame, instance etc. has no children, convert the frame to rectangle - if (node.children.length === 0) return cloneAsRectangleNode(node); - case "GROUP": - // if a Group is visible and has only one child, the Group should be ungrouped. - if (type === "GROUP" && node.children.length === 1 && node.visible) - return convertNodeToAltNode(parent)(node.children[0]); - case "SECTION": - const groupChildren = await convertNodesToAltNodes(node.children, node); - return assignChildren(groupChildren, node); - // Text Nodes - case "TEXT": - return node; - // Unsupported Nodes - case "SLICE": - return null; - default: - addWarning(`${node.type} node is not supported`); - return node; - // throw new Error( - // `Sorry, an unsupported node type was selected. Type:${node.type} id:${node.id}`, - // ); - } - }; - -export const convertNodesToAltNodes = async ( - sceneNode: ReadonlyArray, - parent: ParentNode | null, -): Promise> => - (await Promise.all(sceneNode.map(convertNodeToAltNode(parent)))).filter( - isNotEmpty, - ); - -// auto convert Frame to Rectangle when Frame has no Children -const cloneAsRectangleNode = (node: T): RectangleNode => { - assignRectangleType(node); - - return node as unknown as RectangleNode; -}; diff --git a/packages/backend/src/altNodes/jsonNodeConversion.ts b/packages/backend/src/altNodes/jsonNodeConversion.ts new file mode 100644 index 00000000..36b1272c --- /dev/null +++ b/packages/backend/src/altNodes/jsonNodeConversion.ts @@ -0,0 +1,498 @@ +import { addWarning } from "../common/commonConversionWarnings"; +import { PluginSettings } from "types"; +import { variableToColorName } from "../tailwind/conversionTables"; +import { HasGeometryTrait, Node, Paint } from "../api_types"; + +// Performance tracking counters +export let getNodeByIdAsyncTime = 0; +export let getNodeByIdAsyncCalls = 0; +export let getStyledTextSegmentsTime = 0; +export let getStyledTextSegmentsCalls = 0; +export let processColorVariablesTime = 0; +export let processColorVariablesCalls = 0; + +export const resetPerformanceCounters = () => { + getNodeByIdAsyncTime = 0; + getNodeByIdAsyncCalls = 0; + getStyledTextSegmentsTime = 0; + getStyledTextSegmentsCalls = 0; + processColorVariablesTime = 0; + processColorVariablesCalls = 0; +}; + +// Keep track of node names for sequential numbering +const nodeNameCounters: Map = new Map(); + +const variableCache = new Map(); + +const memoizedVariableToColorName = async ( + variableId: string, +): Promise => { + if (!variableCache.has(variableId)) { + const colorName = (await variableToColorName(variableId)).replaceAll( + ",", + "", + ); + variableCache.set(variableId, colorName); + return colorName; + } + return variableCache.get(variableId)!; +}; + +/** + * Process color variables in a paint style and add pre-computed variable names + * @param paint The paint style to process (fill or stroke) + */ +const processColorVariables = async (paint: Paint) => { + const start = Date.now(); + processColorVariablesCalls++; + + if ( + paint.type === "GRADIENT_ANGULAR" || + paint.type === "GRADIENT_DIAMOND" || + paint.type === "GRADIENT_LINEAR" || + paint.type === "GRADIENT_RADIAL" + ) { + // Filter stops with bound variables first to avoid unnecessary work + const stopsWithVariables = paint.gradientStops.filter( + (stop) => stop.boundVariables?.color, + ); + + // Process all gradient stops with variables in parallel + if (stopsWithVariables.length > 0) { + await Promise.all( + stopsWithVariables.map(async (stop) => { + (stop as any).variableColorName = await memoizedVariableToColorName( + stop.boundVariables!.color!.id, + ); + }), + ); + } + } else if (paint.type === "SOLID" && paint.boundVariables?.color) { + // Pre-compute and store the variable name + (paint as any).variableColorName = await memoizedVariableToColorName( + paint.boundVariables.color.id, + ); + } + + processColorVariablesTime += Date.now() - start; +}; + +const processEffectVariables = async ( + paint: DropShadowEffect | InnerShadowEffect, +) => { + const start = Date.now(); + processColorVariablesCalls++; + + if (paint.boundVariables?.color) { + // Pre-compute and store the variable name + (paint as any).variableColorName = await memoizedVariableToColorName( + paint.boundVariables.color.id, + ); + } + + processColorVariablesTime += Date.now() - start; +}; + +const getColorVariables = async ( + node: HasGeometryTrait, + settings: PluginSettings, +) => { + // This tries to be as fast as it can, using Promise.all so it can parallelize calls. + if (settings.useColorVariables) { + if (node.fills && Array.isArray(node.fills)) { + await Promise.all( + node.fills.map((fill: Paint) => processColorVariables(fill)), + ); + } + if (node.strokes && Array.isArray(node.strokes)) { + await Promise.all( + node.strokes.map((stroke: Paint) => processColorVariables(stroke)), + ); + } + if ("effects" in node && node.effects && Array.isArray(node.effects)) { + await Promise.all( + node.effects + .filter( + (effect: Effect) => + effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW", + ) + .map((effect: DropShadowEffect | InnerShadowEffect) => + processEffectVariables(effect), + ), + ); + } + } +}; + +function adjustChildrenOrder(node: any) { + if (!node.itemReverseZIndex || !node.children || node.layoutMode === "NONE") { + return; + } + + const children = node.children; + const absoluteChildren = []; + const fixedChildren = []; + + // Single pass to separate absolute and fixed children + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + if (child.layoutPositioning === "ABSOLUTE") { + absoluteChildren.push(child); + } else { + fixedChildren.unshift(child); // Add to beginning to maintain original order + } + } + + // Combine the arrays (reversed absolute children + original order fixed children) + node.children = [...absoluteChildren, ...fixedChildren]; +} + +/** + * Checks if a node can be flattened into SVG + */ +const canBeFlattened = (node: Node): boolean => { + // These node types should be directly flattened + const flattenableTypes: string[] = [ + "VECTOR", + "STAR", + "POLYGON", + "BOOLEAN_OPERATION", + ]; + + // Handle special case for Rectangle nodes with zero or near-zero height + if (node.type === "RECTANGLE") { + return false; // Rectangles should not be flattened by default + } + + return flattenableTypes.includes(node.type); +}; + +/** + * Recursively process both JSON node and Figma node to update with data not available in JSON + * This now includes the functionality from convertNodeToAltNode + * @param jsonNode The JSON node to process + * @param figmaNode The corresponding Figma node + * @param settings Plugin settings + * @param parentNode Optional parent node reference to set + * @param parentCumulativeRotation Optional parent cumulative rotation to inherit + * @returns Potentially modified jsonNode + */ +const processNodePair = async ( + jsonNode: Node, + figmaNode: SceneNode, + settings: PluginSettings, + parentNode?: Node, + parentCumulativeRotation: number = 0, +): Promise => { + if (!jsonNode.id) return null; + if (jsonNode.visible === false) return null; + + // Add canBeFlattened property + (jsonNode as any).canBeFlattened = canBeFlattened(jsonNode); + + // Handle node type-specific conversions (from convertNodeToAltNode) + const nodeType = jsonNode.type; + + // Handle empty frames and convert to rectangles + if ( + (nodeType === "FRAME" || + nodeType === "INSTANCE" || + nodeType === "COMPONENT" || + nodeType === "COMPONENT_SET") && + (!jsonNode.children || jsonNode.children.length === 0) + ) { + // Convert to rectangle + jsonNode.type = "RECTANGLE"; + return processNodePair( + jsonNode, + figmaNode, + settings, + parentNode, + parentCumulativeRotation, + ); + } + + // Handle single-child groups that should be ungrouped + if ( + nodeType === "GROUP" && + jsonNode.children && + jsonNode.children.length === 1 && + jsonNode.visible + ) { + // Process the child directly, but preserve parent reference + return processNodePair( + jsonNode.children[0], + (figmaNode as GroupNode).children[0], + settings, + parentNode, + parentCumulativeRotation, + ); + } + + // Return null for unsupported nodes + if (nodeType === "SLICE") { + return null; + } + + // Set parent reference if parent is provided + if (parentNode) { + (jsonNode as any).parent = parentNode; + } + + // Store the cumulative rotation (parent's cumulative + node's own) + if (parentNode?.type === "GROUP") { + jsonNode.cumulativeRotation = parentCumulativeRotation; + } + + // Ensure node has a unique name with simple numbering + const cleanName = jsonNode.name.trim(); + + // Track names with simple counter + const count = nodeNameCounters.get(cleanName) || 0; + nodeNameCounters.set(cleanName, count + 1); + + // For first occurrence, use original name; for duplicates, add sequential suffix + jsonNode.uniqueName = + count === 0 + ? cleanName + : `${cleanName}_${count.toString().padStart(2, "0")}`; + + // Handle text-specific properties + if (figmaNode.type === "TEXT") { + const getSegmentsStart = Date.now(); + getStyledTextSegmentsCalls++; + let styledTextSegments = figmaNode.getStyledTextSegments([ + "fontName", + "fills", + "fontSize", + "fontWeight", + "hyperlink", + "indentation", + "letterSpacing", + "lineHeight", + "listOptions", + "textCase", + "textDecoration", + "textStyleId", + "fillStyleId", + "openTypeFeatures", + ]); + getStyledTextSegmentsTime += Date.now() - getSegmentsStart; + + // Assign unique IDs to each segment + if (styledTextSegments.length > 0) { + const baseSegmentName = (jsonNode.uniqueName || jsonNode.name) + .replace(/[^a-zA-Z0-9_-]/g, "") + .toLowerCase(); + + // Add a uniqueId to each segment + styledTextSegments = await Promise.all( + styledTextSegments.map(async (segment, index) => { + const mutableSegment: any = Object.assign({}, segment); + + if (settings.useColorVariables && segment.fills) { + mutableSegment.fills = await Promise.all( + segment.fills.map(async (d) => { + if ( + d.blendMode !== "PASS_THROUGH" && + d.blendMode !== "NORMAL" + ) { + addWarning("BlendMode is not supported in Text colors"); + } + const fill = { ...d } as Paint; + await processColorVariables(fill); + return fill; + }), + ); + } + + // For single segments, don't add index suffix + if (styledTextSegments.length === 1) { + (mutableSegment as any).uniqueId = `${baseSegmentName}_span`; + } else { + // For multiple segments, add index suffix + (mutableSegment as any).uniqueId = + `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; + } + return mutableSegment; + }), + ); + + jsonNode.styledTextSegments = styledTextSegments; + } + + Object.assign(jsonNode, jsonNode.style); + if (!jsonNode.textAutoResize) { + jsonNode.textAutoResize = "NONE"; + } + } + + // Extract component metadata from instances + if ("variantProperties" in figmaNode && figmaNode.variantProperties) { + jsonNode.variantProperties = figmaNode.variantProperties; + } + + // Always copy size and position + if ("width" in figmaNode) { + jsonNode.width = figmaNode.width; + jsonNode.height = figmaNode.height; + jsonNode.x = figmaNode.x; + jsonNode.y = figmaNode.y; + } + + if ("rotation" in jsonNode) { + jsonNode.rotation = jsonNode.rotation * (180 / Math.PI); + } + + if ("individualStrokeWeights" in jsonNode) { + jsonNode.strokeTopWeight = jsonNode.individualStrokeWeights.top; + jsonNode.strokeBottomWeight = jsonNode.individualStrokeWeights.bottom; + jsonNode.strokeLeftWeight = jsonNode.individualStrokeWeights.left; + jsonNode.strokeRightWeight = jsonNode.individualStrokeWeights.right; + } + + await getColorVariables(jsonNode, settings); + + // Some places check if paddingLeft exists. This makes sure they all exist, even if 0. + if ("layoutMode" in jsonNode && jsonNode.layoutMode) { + if (jsonNode.paddingLeft === undefined) { + jsonNode.paddingLeft = 0; + } + if (jsonNode.paddingRight === undefined) { + jsonNode.paddingRight = 0; + } + if (jsonNode.paddingTop === undefined) { + jsonNode.paddingTop = 0; + } + if (jsonNode.paddingBottom === undefined) { + jsonNode.paddingBottom = 0; + } + } + + // Set default layout properties if missing + if (!jsonNode.layoutMode) jsonNode.layoutMode = "NONE"; + if (!jsonNode.layoutGrow) jsonNode.layoutGrow = 0; + if (!jsonNode.layoutSizingHorizontal) + jsonNode.layoutSizingHorizontal = "FIXED"; + if (!jsonNode.layoutSizingVertical) jsonNode.layoutSizingVertical = "FIXED"; + if (!jsonNode.primaryAxisAlignItems) { + jsonNode.primaryAxisAlignItems = "MIN"; + } + if (!jsonNode.counterAxisAlignItems) { + jsonNode.counterAxisAlignItems = "MIN"; + } + + // If layout sizing is HUG but there are no children, set it to FIXED + const hasChildren = + "children" in jsonNode && + jsonNode.children && + Array.isArray(jsonNode.children) && + jsonNode.children.length > 0; + + if (jsonNode.layoutSizingHorizontal === "HUG" && !hasChildren) { + jsonNode.layoutSizingHorizontal = "FIXED"; + } + if (jsonNode.layoutSizingVertical === "HUG" && !hasChildren) { + jsonNode.layoutSizingVertical = "FIXED"; + } + + // Process children recursively if both have children + if ( + "children" in jsonNode && + jsonNode.children && + Array.isArray(jsonNode.children) && + "children" in figmaNode && + figmaNode.children.length === jsonNode.children.length + ) { + console.log("cumulative", parentCumulativeRotation); + + const cumulative = + parentCumulativeRotation + + (jsonNode.type === "GROUP" ? jsonNode.rotation || 0 : 0); + + // Process children and handle potential null returns + const processedChildren = []; + for (let i = 0; i < jsonNode.children.length; i++) { + const processedChild = await processNodePair( + jsonNode.children[i], + figmaNode.children[i], + settings, + jsonNode, + cumulative, + ); + + if (processedChild !== null) { + processedChildren.push(processedChild); + } + } + + // Replace children array with processed children + jsonNode.children = processedChildren; + + if ( + jsonNode.layoutMode === "NONE" || + jsonNode.children.some( + (d: any) => + "layoutPositioning" in d && d.layoutPositioning === "ABSOLUTE", + ) + ) { + jsonNode.isRelative = true; + } + + adjustChildrenOrder(jsonNode); + } else if ( + "children" in figmaNode && + figmaNode.children.length !== jsonNode.children.length + ) { + addWarning( + "Error: JSON and Figma nodes have different child counts. Please report this issue.", + ); + } + + return jsonNode; +}; + +/** + * Convert Figma nodes to JSON format with parent references added + * @param nodes The Figma nodes to convert to JSON + * @param settings Plugin settings + * @returns JSON representation of the nodes with parent references + */ +export const nodesToJSON = async ( + nodes: ReadonlyArray, + settings: PluginSettings, +): Promise => { + // Reset name counters for each conversion + nodeNameCounters.clear(); + + const exportJsonStart = Date.now(); + // First get the JSON representation of nodes + const nodeJson = (await Promise.all( + nodes.map( + async (node) => + ( + (await node.exportAsync({ + format: "JSON_REST_V1", + })) as any + ).document, + ), + )) as Node[]; + + console.log("[debug] initial nodeJson", { ...nodeJson[0] }); + + console.log( + `[benchmark][inside nodesToJSON] JSON_REST_V1 export: ${Date.now() - exportJsonStart}ms`, + ); + + // Now process each top-level node pair (JSON node + Figma node) + const processNodesStart = Date.now(); + for (let i = 0; i < nodes.length; i++) { + await processNodePair(nodeJson[i], nodes[i], settings); + } + console.log( + `[benchmark][inside nodesToJSON] Process node pairs: ${Date.now() - processNodesStart}ms`, + ); + + return nodeJson; +}; diff --git a/packages/backend/src/code.ts b/packages/backend/src/code.ts index 479923a3..f4e0c784 100644 --- a/packages/backend/src/code.ts +++ b/packages/backend/src/code.ts @@ -1,6 +1,6 @@ import { + retrieveGenericLinearGradients, retrieveGenericSolidUIColors, - retrieveGenericLinearGradients as retrieveGenericGradients, } from "./common/retrieveUI/retrieveColors"; import { addWarning, @@ -11,429 +11,20 @@ import { postConversionComplete, postEmptyMessage } from "./messaging"; import { PluginSettings } from "types"; import { convertToCode } from "./common/retrieveUI/convertToCode"; import { generateHTMLPreview } from "./html/htmlMain"; -import { variableToColorName } from "./tailwind/conversionTables"; import { oldConvertNodesToAltNodes } from "./altNodes/oldAltConversion"; import { - convertNodesToAltNodes, - convertNodeToAltNode, -} from "./altNodes/altConversion"; -import { - HasGeometryTrait, - MinimalFillsTrait, - MinimalStrokesTrait, - Node, - Paint, -} from "./api_types"; - -// Performance tracking counters -let getNodeByIdAsyncTime = 0; -let getNodeByIdAsyncCalls = 0; -let getStyledTextSegmentsTime = 0; -let getStyledTextSegmentsCalls = 0; -let processColorVariablesTime = 0; -let processColorVariablesCalls = 0; - -// Keep track of node names for sequential numbering -const nodeNameCounters: Map = new Map(); - -const variableCache = new Map(); - -const memoizedVariableToColorName = async ( - variableId: string, -): Promise => { - if (!variableCache.has(variableId)) { - const colorName = (await variableToColorName(variableId)).replaceAll( - ",", - "", - ); - variableCache.set(variableId, colorName); - return colorName; - } - return variableCache.get(variableId)!; -}; - -// Define all property paths that might contain gradients -const GRADIENT_PROPERTIES = ["fills", "strokes"]; - -/** - * Process color variables in a paint style and add pre-computed variable names - * @param paint The paint style to process (fill or stroke) - */ -const processColorVariables = async (paint: Paint) => { - const start = Date.now(); - processColorVariablesCalls++; - - if ( - paint.type === "GRADIENT_ANGULAR" || - paint.type === "GRADIENT_DIAMOND" || - paint.type === "GRADIENT_LINEAR" || - paint.type === "GRADIENT_RADIAL" - ) { - // Filter stops with bound variables first to avoid unnecessary work - const stopsWithVariables = paint.gradientStops.filter( - (stop) => stop.boundVariables?.color, - ); - - // Process all gradient stops with variables in parallel - if (stopsWithVariables.length > 0) { - await Promise.all( - stopsWithVariables.map(async (stop) => { - (stop as any).variableColorName = await memoizedVariableToColorName( - stop.boundVariables!.color!.id, - ); - }), - ); - } - } else if (paint.type === "SOLID" && paint.boundVariables?.color) { - // Pre-compute and store the variable name - (paint as any).variableColorName = await memoizedVariableToColorName( - paint.boundVariables.color.id, - ); - } - - processColorVariablesTime += Date.now() - start; -}; - -const processEffectVariables = async ( - paint: DropShadowEffect | InnerShadowEffect, -) => { - const start = Date.now(); - processColorVariablesCalls++; - - if (paint.boundVariables?.color) { - // Pre-compute and store the variable name - (paint as any).variableColorName = await memoizedVariableToColorName( - paint.boundVariables.color.id, - ); - } - - processColorVariablesTime += Date.now() - start; -}; - -const getColorVariables = async ( - node: HasGeometryTrait, - settings: PluginSettings, -) => { - // This tries to be as fast as it can, using Promise.all so it can parallelize calls. - if (settings.useColorVariables) { - if (node.fills && Array.isArray(node.fills)) { - await Promise.all( - node.fills.map((fill: Paint) => processColorVariables(fill)), - ); - } - if (node.strokes && Array.isArray(node.strokes)) { - await Promise.all( - node.strokes.map((stroke: Paint) => processColorVariables(stroke)), - ); - } - if ("effects" in node && node.effects && Array.isArray(node.effects)) { - await Promise.all( - node.effects - .filter( - (effect: Effect) => - effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW", - ) - .map((effect: DropShadowEffect | InnerShadowEffect) => - processEffectVariables(effect), - ), - ); - } - } -}; - -function adjustChildrenOrder(node: any) { - if (!node.itemReverseZIndex || !node.children || node.layoutMode === "NONE") { - return; - } - - const children = node.children; - const absoluteChildren = []; - const fixedChildren = []; - - // Single pass to separate absolute and fixed children - for (let i = children.length - 1; i >= 0; i--) { - const child = children[i]; - if (child.layoutPositioning === "ABSOLUTE") { - absoluteChildren.push(child); - } else { - fixedChildren.unshift(child); // Add to beginning to maintain original order - } - } - - // Combine the arrays (reversed absolute children + original order fixed children) - node.children = [...absoluteChildren, ...fixedChildren]; -} - -/** - * Recursively process both JSON node and Figma node to update with data not available in JSON - * @param jsonNode The JSON node to process - * @param figmaNode The corresponding Figma node - * @param settings Plugin settings - * @param parentNode Optional parent node reference to set - */ -const processNodePair = async ( - jsonNode: Node, - figmaNode: SceneNode, - settings: PluginSettings, - parentNode?: Node, -) => { - if (!jsonNode.id) return; - - // Set parent reference if parent is provided - if (parentNode) { - (jsonNode as any).parent = parentNode; - } - - // Ensure node has a unique name with simple numbering - const cleanName = jsonNode.name.trim(); - - // Track names with simple counter - const count = nodeNameCounters.get(cleanName) || 0; - nodeNameCounters.set(cleanName, count + 1); - - // For first occurrence, use original name; for duplicates, add sequential suffix - jsonNode.uniqueName = - count === 0 - ? cleanName - : `${cleanName}_${count.toString().padStart(2, "0")}`; - - // Handle text-specific properties - if (figmaNode.type === "TEXT") { - const getSegmentsStart = Date.now(); - getStyledTextSegmentsCalls++; - let styledTextSegments = figmaNode.getStyledTextSegments([ - "fontName", - "fills", - "fontSize", - "fontWeight", - "hyperlink", - "indentation", - "letterSpacing", - "lineHeight", - "listOptions", - "textCase", - "textDecoration", - "textStyleId", - "fillStyleId", - "openTypeFeatures", - ]); - getStyledTextSegmentsTime += Date.now() - getSegmentsStart; - - // Assign unique IDs to each segment - if (styledTextSegments.length > 0) { - const baseSegmentName = (jsonNode.uniqueName || jsonNode.name) - .replace(/[^a-zA-Z0-9_-]/g, "") - .toLowerCase(); - - // Add a uniqueId to each segment - styledTextSegments = await Promise.all( - styledTextSegments.map(async (segment, index) => { - const mutableSegment: any = Object.assign({}, segment); - - if (settings.useColorVariables && segment.fills) { - mutableSegment.fills = await Promise.all( - segment.fills.map(async (d) => { - if ( - d.blendMode !== "PASS_THROUGH" && - d.blendMode !== "NORMAL" - ) { - addWarning("BlendMode is not supported in Text colors"); - } - const fill = { ...d } as Paint; - await processColorVariables(fill); - return fill; - }), - ); - } - - // For single segments, don't add index suffix - if (styledTextSegments.length === 1) { - (mutableSegment as any).uniqueId = `${baseSegmentName}_span`; - } else { - // For multiple segments, add index suffix - (mutableSegment as any).uniqueId = - `${baseSegmentName}_span_${(index + 1).toString().padStart(2, "0")}`; - } - return mutableSegment; - }), - ); - - jsonNode.styledTextSegments = styledTextSegments; - } - - Object.assign(jsonNode, jsonNode.style); - if (!jsonNode.textAutoResize) { - jsonNode.textAutoResize = "NONE"; - } - } - - // Extract component metadata from instances - if ("variantProperties" in figmaNode && figmaNode.variantProperties) { - jsonNode.variantProperties = figmaNode.variantProperties; - } - - // Always copy size and position - if ("width" in figmaNode) { - jsonNode.width = figmaNode.width; - jsonNode.height = figmaNode.height; - jsonNode.x = figmaNode.x; - jsonNode.y = figmaNode.y; - } - - if ("rotation" in jsonNode) { - jsonNode.rotation = jsonNode.rotation * (180 / Math.PI); - } - - if ("individualStrokeWeights" in jsonNode) { - jsonNode.strokeTopWeight = jsonNode.individualStrokeWeights.top; - jsonNode.strokeBottomWeight = jsonNode.individualStrokeWeights.bottom; - jsonNode.strokeLeftWeight = jsonNode.individualStrokeWeights.left; - jsonNode.strokeRightWeight = jsonNode.individualStrokeWeights.right; - } - - await getColorVariables(jsonNode, settings); - - // Some places check if paddingLeft exists. This makes sure they all exist, even if 0. - if ("layoutMode" in jsonNode && jsonNode.layoutMode) { - if (jsonNode.paddingLeft === undefined) { - jsonNode.paddingLeft = 0; - } - if (jsonNode.paddingRight === undefined) { - jsonNode.paddingRight = 0; - } - if (jsonNode.paddingTop === undefined) { - jsonNode.paddingTop = 0; - } - if (jsonNode.paddingBottom === undefined) { - jsonNode.paddingBottom = 0; - } - } - - // Set default layout properties if missing - if (!jsonNode.layoutMode) jsonNode.layoutMode = "NONE"; - if (!jsonNode.layoutGrow) jsonNode.layoutGrow = 0; - if (!jsonNode.layoutSizingHorizontal) - jsonNode.layoutSizingHorizontal = "FIXED"; - if (!jsonNode.layoutSizingVertical) jsonNode.layoutSizingVertical = "FIXED"; - if (!jsonNode.primaryAxisAlignItems) { - jsonNode.primaryAxisAlignItems = "MIN"; - } - if (!jsonNode.counterAxisAlignItems) { - jsonNode.counterAxisAlignItems = "MIN"; - } - - // If layout sizing is HUG but there are no children, set it to FIXED - const hasChildren = - "children" in jsonNode && - jsonNode.children && - Array.isArray(jsonNode.children) && - jsonNode.children.length > 0; - - if (jsonNode.layoutSizingHorizontal === "HUG" && !hasChildren) { - jsonNode.layoutSizingHorizontal = "FIXED"; - } - if (jsonNode.layoutSizingVertical === "HUG" && !hasChildren) { - jsonNode.layoutSizingVertical = "FIXED"; - } - - // Process children recursively if both have children - if ( - "children" in jsonNode && - jsonNode.children && - Array.isArray(jsonNode.children) && - "children" in figmaNode && - figmaNode.children.length === jsonNode.children.length - ) { - // Somehow this is slower than the for loop. - // await Promise.all( - // jsonNode.children.map((child: any, i: number) => - // processNodePair(child, figmaNode.children[i], settings), - // ), - // ); - - for (let i = 0; i < jsonNode.children.length; i++) { - await processNodePair( - jsonNode.children[i], - figmaNode.children[i], - settings, - jsonNode, - ); - } - - if ( - jsonNode.layoutMode === "NONE" || - jsonNode.children.some( - (d: any) => - "layoutPositioning" in d && d.layoutPositioning === "ABSOLUTE", - ) - ) { - jsonNode.isRelative = true; - } - - adjustChildrenOrder(jsonNode); - } else if ( - "children" in figmaNode && - figmaNode.children.length !== jsonNode.children.length - ) { - addWarning( - "Error: JSON and Figma nodes have different child counts. Please report this issue.", - ); - } -}; - -/** - * Convert Figma nodes to JSON format with parent references added - * @param nodes The Figma nodes to convert to JSON - * @param settings Plugin settings - * @returns JSON representation of the nodes with parent references - */ -export const nodesToJSON = async ( - nodes: ReadonlyArray, - settings: PluginSettings, -): Promise => { - // Reset name counters for each conversion - nodeNameCounters.clear(); - - const exportJsonStart = Date.now(); - // First get the JSON representation of nodes - const nodeJson = (await Promise.all( - nodes.map( - async (node) => - ( - (await node.exportAsync({ - format: "JSON_REST_V1", - })) as any - ).document, - ), - )) as Node[]; - - console.log("[debug] initial nodeJson", { ...nodeJson[0] }); - - console.log( - `[benchmark][inside nodesToJSON] JSON_REST_V1 export: ${Date.now() - exportJsonStart}ms`, - ); - - // Now process each top-level node pair (JSON node + Figma node) - const processNodesStart = Date.now(); - for (let i = 0; i < nodes.length; i++) { - await processNodePair(nodeJson[i], nodes[i], settings); - } - console.log( - `[benchmark][inside nodesToJSON] Process node pairs: ${Date.now() - processNodesStart}ms`, - ); - - return nodeJson; -}; + getNodeByIdAsyncCalls, + getNodeByIdAsyncTime, + getStyledTextSegmentsCalls, + getStyledTextSegmentsTime, + nodesToJSON, + processColorVariablesCalls, + processColorVariablesTime, + resetPerformanceCounters, +} from "./altNodes/jsonNodeConversion"; export const run = async (settings: PluginSettings) => { - // Reset performance counters at the beginning - getNodeByIdAsyncTime = 0; - getNodeByIdAsyncCalls = 0; - getStyledTextSegmentsTime = 0; - getStyledTextSegmentsCalls = 0; - processColorVariablesTime = 0; - processColorVariablesCalls = 0; - variableCache.clear(); + resetPerformanceCounters(); clearWarnings(); const { framework, useOldPluginVersion2025 } = settings; @@ -456,17 +47,9 @@ export const run = async (settings: PluginSettings) => { convertedSelection = oldConvertNodesToAltNodes(selection, null); console.log("convertedSelection", convertedSelection); } else { - const nodeJson = await nodesToJSON(selection, settings); + convertedSelection = await nodesToJSON(selection, settings); console.log(`[benchmark] nodesToJSON: ${Date.now() - nodeToJSONStart}ms`); - console.log("nodeJson", nodeJson); - - // Now we work directly with the JSON nodes - const convertNodesStart = Date.now(); - convertedSelection = await convertNodesToAltNodes(nodeJson, null); - - console.log( - `[benchmark] convertNodesToAltNodes: ${Date.now() - convertNodesStart}ms`, - ); + console.log("nodeJson", convertedSelection); } console.log("[debug] convertedSelection", { ...convertedSelection[0] }); @@ -492,7 +75,7 @@ export const run = async (settings: PluginSettings) => { const colorPanelStart = Date.now(); const colors = retrieveGenericSolidUIColors(framework); - // const gradients = retrieveGenericGradients(framework); + const gradients = retrieveGenericLinearGradients(framework); console.log( `[benchmark] color and gradient panel: ${Date.now() - colorPanelStart}ms`, ); @@ -523,7 +106,7 @@ export const run = async (settings: PluginSettings) => { code, htmlPreview, colors, - gradients: [], + gradients, settings, warnings: [...warnings], }); diff --git a/packages/backend/src/common/commonFormatAttributes.ts b/packages/backend/src/common/commonFormatAttributes.ts index 8adb6392..e71fd588 100644 --- a/packages/backend/src/common/commonFormatAttributes.ts +++ b/packages/backend/src/common/commonFormatAttributes.ts @@ -18,7 +18,7 @@ export const formatStyleAttribute = ( }; export const formatDataAttribute = (label: string, value?: string) => - ` data-${lowercaseFirstLetter(label)}${value === undefined ? `` : `="${value}"`}`; + ` data-${lowercaseFirstLetter(label).replace(" ", "-")}${value === undefined ? `` : `="${value}"`}`; export const formatClassAttribute = ( classes: string[], diff --git a/packages/backend/src/common/retrieveUI/retrieveColors.ts b/packages/backend/src/common/retrieveUI/retrieveColors.ts index b50a14ac..331e35cc 100644 --- a/packages/backend/src/common/retrieveUI/retrieveColors.ts +++ b/packages/backend/src/common/retrieveUI/retrieveColors.ts @@ -20,7 +20,6 @@ import { LinearGradientConversion, SolidColorConversion, Framework, - HTMLSettings, } from "types"; export const retrieveGenericSolidUIColors = ( @@ -80,25 +79,35 @@ export const retrieveGenericLinearGradients = ( const selectionColors = figma.getSelectionColors(); const colorStr: Array = []; + console.log("selectionColors", selectionColors); + selectionColors?.paints.forEach((paint) => { if (paint.type === "GRADIENT_LINEAR") { + let fill = { ...paint }; + const t = fill.gradientTransform; + fill.gradientHandlePositions = [ + { x: t[0][2], y: t[1][2] }, // Start: (e, f) + { x: t[0][0] + t[0][2], y: t[1][0] + t[1][2] }, // End: (a + e, b + f) + ]; + console.log("fill is", { ...fill }); + let exportValue = ""; switch (framework) { case "Flutter": - exportValue = flutterGradient(paint); + exportValue = flutterGradient(fill); break; case "HTML": - exportValue = htmlGradientFromFills(paint); + exportValue = htmlGradientFromFills(fill); break; case "Tailwind": - exportValue = tailwindGradient(paint); + exportValue = tailwindGradient(fill); break; case "SwiftUI": - exportValue = swiftuiGradient(paint); + exportValue = swiftuiGradient(fill); break; } colorStr.push({ - cssPreview: htmlGradientFromFills(paint), + cssPreview: htmlGradientFromFills(fill), exportValue, }); } diff --git a/packages/backend/src/html/builderImpl/htmlSize.ts b/packages/backend/src/html/builderImpl/htmlSize.ts index d6bbc280..ad30981c 100644 --- a/packages/backend/src/html/builderImpl/htmlSize.ts +++ b/packages/backend/src/html/builderImpl/htmlSize.ts @@ -28,7 +28,11 @@ export const htmlSizePartial = ( ) { w = formatWithJSX("flex", isJsx, "1 1 0"); } else { - w = formatWithJSX("align-self", isJsx, "stretch"); + if (node.maxWidth) { + w = formatWithJSX("width", isJsx, "100%"); + } else { + w = formatWithJSX("align-self", isJsx, "stretch"); + } } } @@ -43,7 +47,11 @@ export const htmlSizePartial = ( ) { h = formatWithJSX("flex", isJsx, "1 1 0"); } else { - h = formatWithJSX("align-self", isJsx, "stretch"); + if (node.maxHeight) { + h = formatWithJSX("height", isJsx, "100%"); + } else { + h = formatWithJSX("align-self", isJsx, "stretch"); + } } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 90f21d61..17d9ac79 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -3,5 +3,4 @@ export { htmlMain } from "./html/htmlMain"; export { tailwindMain } from "./tailwind/tailwindMain"; export { swiftuiMain } from "./swiftui/swiftuiMain"; export { run } from "./code"; -export { convertNodesToAltNodes as convertIntoNodes } from "./altNodes/altConversion"; export * from "./messaging"; diff --git a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts index 23459cc4..667e5ef7 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindBorder.ts @@ -23,9 +23,9 @@ const getBorder = ( if (useOutline) { const outlineWidth = pxToOutline(weight); if (outlineWidth === null) { - return `outline-[${numberToFixedString(weight)}px]`; + return `outline outline-[${numberToFixedString(weight)}px]`; } else { - return `outline-${outlineWidth}`; + return `outline outline-${outlineWidth}`; } } diff --git a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts index 43575681..22444dfc 100644 --- a/packages/backend/src/tailwind/builderImpl/tailwindSize.ts +++ b/packages/backend/src/tailwind/builderImpl/tailwindSize.ts @@ -46,7 +46,11 @@ export const tailwindSizePartial = ( ) { w = "flex-1"; } else { - w = "self-stretch"; + if (node.maxWidth) { + w = "w-full"; + } else { + w = "self-stretch"; + } } } @@ -61,7 +65,11 @@ export const tailwindSizePartial = ( ) { h = "flex-1"; } else { - h = "self-stretch"; + if (node.maxHeight) { + h = "h-full"; + } else { + h = "self-stretch"; + } } } diff --git a/packages/plugin-ui/src/PluginUI.tsx b/packages/plugin-ui/src/PluginUI.tsx index 912a74e7..928756d7 100644 --- a/packages/plugin-ui/src/PluginUI.tsx +++ b/packages/plugin-ui/src/PluginUI.tsx @@ -40,6 +40,43 @@ type PluginUIProps = { const frameworks: Framework[] = ["HTML", "Tailwind", "Flutter", "SwiftUI"]; +type FrameworkTabsProps = { + frameworks: Framework[]; + selectedFramework: Framework; + setSelectedFramework: (framework: Framework) => void; + showAbout: boolean; + setShowAbout: (show: boolean) => void; +}; + +const FrameworkTabs = ({ + frameworks, + selectedFramework, + setSelectedFramework, + showAbout, + setShowAbout, +}: FrameworkTabsProps) => { + return ( +
+ {frameworks.map((tab) => ( + + ))} +
+ ); +}; + export const PluginUI = (props: PluginUIProps) => { const [showAbout, setShowAbout] = useState(false); @@ -58,36 +95,27 @@ export const PluginUI = (props: PluginUIProps) => { return (
-
-
- {frameworks.map((tab) => ( - - ))} +
+
+ +
-
{ >
{showAbout ? ( - ) : ( diff --git a/packages/plugin-ui/src/components/About.tsx b/packages/plugin-ui/src/components/About.tsx index e39d3e33..c7bbc431 100644 --- a/packages/plugin-ui/src/components/About.tsx +++ b/packages/plugin-ui/src/components/About.tsx @@ -169,7 +169,7 @@ const About = ({
{/* Contact Card */} -
+
{/* Debug Helper Card */} -
+
@@ -219,11 +219,11 @@ const About = ({

- The new version is up to 100x faster, but might still cause some issues. - If you encounter problems, you can switch to the old version - (and please report issues so they can be fixed). + The new version is up to 100x faster, but might still cause some + issues. If you encounter problems, you can switch to the old + version (and please report issues so they can be fixed).

diff --git a/packages/plugin-ui/src/components/CodePanel.tsx b/packages/plugin-ui/src/components/CodePanel.tsx index 285f3dd2..9496e3e6 100644 --- a/packages/plugin-ui/src/components/CodePanel.tsx +++ b/packages/plugin-ui/src/components/CodePanel.tsx @@ -10,7 +10,6 @@ import { coldarkDark as theme } from "react-syntax-highlighter/dist/esm/styles/p import { CopyButton } from "./CopyButton"; import EmptyState from "./EmptyState"; import SettingsGroup from "./SettingsGroup"; -import CustomPrefixInput from "./CustomPrefixInput"; import FrameworkTabs from "./FrameworkTabs"; import { TailwindSettings } from "./TailwindSettings"; @@ -150,7 +149,7 @@ const CodePanel = (props: CodePanelProps) => {
{!isCodeEmpty && ( -
+
{/* Essential settings always shown */} -
+
+
-

- {/*
*/} +

Color Palette

- + {props.colors.length} color{props.colors.length > 1 ? "s" : ""}
@@ -33,7 +32,7 @@ const ColorsPanel = (props: { key={"button" + idx} className={`w-full h-16 rounded-lg text-sm font-semibold shadow-sm transition-all duration-300 ${ isPressed === idx - ? "ring-4 ring-green-300 ring-opacity-50 animate-pulse" + ? "ring-4 ring-primary ring-opacity-50 animate-pulse" : "ring-0" }`} style={{ backgroundColor: color.hex }} diff --git a/packages/plugin-ui/src/components/CopyButton.tsx b/packages/plugin-ui/src/components/CopyButton.tsx index 7c302b8c..8d209e02 100644 --- a/packages/plugin-ui/src/components/CopyButton.tsx +++ b/packages/plugin-ui/src/components/CopyButton.tsx @@ -1,8 +1,9 @@ "use client"; import { useState, useEffect } from "react"; -import { Copy, CheckCircle, Check } from "lucide-react"; +import { Copy, Check } from "lucide-react"; import copy from "copy-to-clipboard"; +import { cn } from "../lib/utils"; interface CopyButtonProps { value: string; @@ -47,11 +48,14 @@ export function CopyButton({ onClick={handleCopy} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - className={`inline-flex items-center justify-center px-3 py-1.5 text-sm font-medium border border-green-500 rounded-md shadow-sm hover:bg-green-500 dark:hover:bg-green-600 hover:text-white hover:border-transparent transition-all duration-300 ${ + className={cn( + `inline-flex items-center justify-center px-3 py-1.5 text-sm font-medium border rounded-md transition-all duration-300`, isCopied - ? "bg-green-600 text-white dark:text-white hover:bg-green-500" - : "bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-200 border-neutral-300 dark:border-neutral-600" - } ${className || ""} relative`} + ? "bg-primary border-primary text-primary-foreground" + : "bg-neutral-100 dark:bg-neutral-700 dark:hover:bg-muted-foreground/30 border-border text-foreground", + className, + `relative`, + )} aria-label={isCopied ? "Copied!" : "Copy to clipboard"} >
@@ -62,13 +66,7 @@ export function CopyButton({ : "opacity-100 scale-100 rotate-0" }`} > - + - +
{showLabel && ( - - {isCopied ? "Copied" : "Copy"} - + {isCopied ? "Copied" : "Copy"} )} {isCopied && (