diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f16fba9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Node +*.log +*.log.* +node_modules + +out/ +dist/ +code.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ba986f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ccdf26 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +GitHub Copilot: Sure, here's a possible repository description for this Figma plugin: + +# Figma Spacing and Typography Plugin + +This is a plugin for Figma that helps you update the spacing and typography of your designs. It replaces padding, gap, and font size values in selected frames and text nodes, using a set of predefined rules. The plugin also loads all the fonts used in the text nodes asynchronously, to ensure that the new font sizes are applied correctly. + +## Features + +- Replace padding, gap, and font size values in selected frames and text nodes +- Load all fonts used in the text nodes asynchronously +- Apply a set of predefined rules to calculate the new values +- Show an error message if no frame is selected or no text nodes are found +- Notify the user when the spacings and font sizes have been updated successfully + +## Usage + +To use the plugin, select a frame that contains nodes with padding, gap, or font size values that you want to update. Then, run the plugin from the Figma menu or using the keyboard shortcut. The plugin will replace the values in all the selected nodes, and notify you when the process is complete. + +## Installation + +To install the plugin, download the source code from this repository and open it in Figma. Then, go to the Plugins menu, select "Development" > "Create Plugin", and choose the "manifest.json" file from the source code folder. Finally, click "Create Plugin" and the plugin will be installed in your Figma account. + +## License + +This plugin is licensed under the MIT License. See the LICENSE file for more information. + diff --git a/code.ts b/code.ts new file mode 100644 index 0000000..847f5ad --- /dev/null +++ b/code.ts @@ -0,0 +1,199 @@ +// Function for loading all fonts used in a text node +const loadFonts = async (textNode: TextNode) => { + // Get an array of all the font names used in the text node + const fontNames = textNode.getRangeAllFontNames(0, textNode.characters.length); + + // Load all the fonts asynchronously + await Promise.all(fontNames.map(figma.loadFontAsync)); +}; + +// Define the new line heights as an array +const skipLineHeights = false; + +// Get the selected frame +const selectedFrame = figma.currentPage.selection[0] as FrameNode; + +if (selectedFrame) { + const nodesToReplace = []; + + // Find all nodes with padding, gap, or font size values to replace + function findNodesToReplace(node: BaseNode) { + if ('paddingLeft' in node || 'paddingRight' in node || 'paddingTop' in node || 'paddingBottom' in node || 'itemSpacing' in node || 'gridStyleId' in node) { + nodesToReplace.push(node); + } + + if ('children' in node) { + node.children.forEach(child => findNodesToReplace(child)); + } + } + + findNodesToReplace(selectedFrame); + + // Replace padding and gap values in all nodes + nodesToReplace.forEach(node => { + if ('paddingLeft' in node) { + node.paddingLeft = replacePaddingValue(node.paddingLeft); + } + + if ('paddingRight' in node) { + node.paddingRight = replacePaddingValue(node.paddingRight); + } + + if ('paddingTop' in node) { + node.paddingTop = replacePaddingValue(node.paddingTop); + } + + if ('paddingBottom' in node) { + node.paddingBottom = replacePaddingValue(node.paddingBottom); + } + + if ('itemSpacing' in node) { + node.itemSpacing = replacePaddingValue(node.itemSpacing); + } + + if ('gridStyleId' in node) { + node.gridStyleId = replacePaddingValue(node.gridStyleId); + } + }); + + // Replace font sizes and line heights in all text nodes + const textNodes = selectedFrame.findAll(node => node.type === "TEXT") as TextNode[]; + if (textNodes.length === 0) { + // Show an error message if no text nodes are found + figma.notify("Spacings updated successfully."); + figma.closePlugin(); + } else { + // Keep track of the number of fonts that are still loading + let numFontsLoading = 0; + + textNodes.forEach(async textNode => { + // Load all fonts used in the text node + numFontsLoading++; + await loadFonts(textNode); + + // Replace font size in the text node + const oldFontSize = textNode.fontSize; + const newFontSize = getNewFontSize(oldFontSize); + textNode.fontSize = newFontSize; + + // Replace line height in the text node + if (textNode.lineHeight.unit === "PIXELS") { + const lineHeight = textNode.lineHeight.value; + const newLineHeight = getNewLineHeight(lineHeight); + textNode.lineHeight = { unit: "PIXELS", value: newLineHeight }; + } + // Reset the line height of the text node to "auto" + if (skipLineHeights === true) { + textNode.lineHeight = { unit: "AUTO" }; + } + + // Decrement the count of fonts that are still loading + numFontsLoading--; + if (numFontsLoading === 0) { + // Close the plugin when all fonts have finished loading + figma.notify("Font sizes and spacings updated successfully."); + figma.closePlugin(); + } + }); + } +} else { + // Show an error message if no frame is selected + figma.notify("Please select a frame."); + figma.closePlugin(); +} + +function replacePaddingValue(value: number): number { + const paddingValuesToReplace = getPaddingValuesToReplace(); + const paddingNewValues = paddingValuesToReplace.map(getNewPaddingValue); + const index = paddingValuesToReplace.indexOf(value); + if (index !== -1) { + return paddingNewValues[index]; + } + return value; +} + +function getPaddingValuesToReplace(): number[] { + const paddingValuesToReplace = new Set(); + const nodesToCheck = [selectedFrame]; + while (nodesToCheck.length > 0) { + const node = nodesToCheck.pop(); + if ('paddingLeft' in node) { + paddingValuesToReplace.add(node.paddingLeft); + } + if ('paddingRight' in node) { + paddingValuesToReplace.add(node.paddingRight); + } + if ('paddingTop' in node) { + paddingValuesToReplace.add(node.paddingTop); + } + if ('paddingBottom' in node) { + paddingValuesToReplace.add(node.paddingBottom); + } + if ('itemSpacing' in node) { + paddingValuesToReplace.add(node.itemSpacing); + } + if ('gridStyleId' in node) { + paddingValuesToReplace.add(node.gridStyleId); + } + if ('children' in node) { + nodesToCheck.push(...node.children); + } + } + return Array.from(paddingValuesToReplace); +} + +function getNewPaddingValue(oldValue: number): number { + switch (true) { + case (oldValue <= 16): + return oldValue; + case (oldValue <= 24): + return Math.floor(oldValue / 16) * 14; + case (oldValue <= 32): + return Math.floor(oldValue / 16) * 12; + case (oldValue <= 48): + return Math.floor(oldValue / 16) * 10; + case (oldValue <= 80): + return Math.floor(oldValue / 16) * 8; + case (oldValue <= 128): + return Math.floor(oldValue / 16) * 6; + default: + return Math.floor(oldValue / 16) * 4; + } +} + +function getNewLineHeight(oldValue: number): number { + switch (true) { + case (oldValue <= 16): + return oldValue; + case (oldValue <= 24): + case (oldValue <= 32): + return Math.floor(oldValue / 16) * 14; + case (oldValue <= 48): + return Math.floor(oldValue / 16) * 12; + case (oldValue <= 80): + return Math.floor(oldValue / 16) * 10; + case (oldValue <= 128): + case (oldValue <= 256): + return Math.floor(oldValue / 16) * 8; + default: + return Math.floor(oldValue / 16) * 4; + } +} +function getNewFontSize(oldValue: number): number { + switch (true) { + case (oldValue <= 16): + return oldValue; + case (oldValue <= 24): + case (oldValue <= 32): + return Math.floor(oldValue / 16) * 14; + case (oldValue <= 48): + return Math.floor(oldValue / 16) * 12; + case (oldValue <= 80): + return Math.floor(oldValue / 16) * 10; + case (oldValue <= 128): + case (oldValue <= 256): + return Math.floor(oldValue / 16) * 8; + default: + return Math.floor(oldValue / 16) * 4; + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..de321f0 --- /dev/null +++ b/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "Responsive Replacer Dynamic", + "id": "1252632497510105166", + "api": "1.0.0", + "main": "code.js", + "capabilities": [], + "enableProposedApi": false, + "editorType": [ + "figma" + ], + "networkAccess": { + "allowedDomains": [ + "none" + ] + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2adb1f9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,35 @@ +{ + "name": "Responsive Replacer Dynamic", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "Responsive Replacer Dynamic", + "version": "1.0.0", + "devDependencies": { + "@figma/plugin-typings": "^1.65.0", + "typescript": "*" + } + }, + "node_modules/@figma/plugin-typings": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/@figma/plugin-typings/-/plugin-typings-1.65.0.tgz", + "integrity": "sha512-egLk8ygMKl4e/bs7PdeIFU0fuQuhGAjyX+pyxS/AZ8X1x0QLPaO6r64ekJt+gvmQTFdcaK2CLmF2zqLU+5Hhcg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b72955c --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "Responsive Replacer Dynamic", + "version": "1.0.0", + "description": "Your Figma Plugin", + "main": "code.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "watch": "npm run build -- --watch" + }, + "author": "", + "license": "", + "devDependencies": { + "@figma/plugin-typings": "^1.65.0", + "typescript": "*" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5b20a2d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es6"], + "strict": true, + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@figma" + ] + } +}