├── .gitignore ├── LICENSE.md ├── README.md ├── _screenshots ├── annotations.gif ├── barchart.png ├── capital.png ├── circletext.png ├── colored-f-26.6x40.svg ├── create-rects-shapes.png ├── create-shapes-connectors.png ├── goto.png ├── icon-drag-and-drop-hosted.png ├── icon-drag-and-drop.png ├── invert-image.png ├── metacards.gif ├── piechart.png ├── png-crop.png ├── resizer.png ├── sierpinski.png ├── stats.png ├── svg-inserter.png ├── text-review.png ├── text-search.png ├── trivia.png ├── vector-path.png ├── vote-tally.gif └── webpack.png ├── annotations ├── README.md ├── code.js ├── manifest.json └── ui.html ├── barchart ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── capital ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── circletext ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── codegen ├── .gitignore ├── manifest.json ├── package-lock.json ├── package.json ├── plugin-src │ └── code.js ├── ui-src │ ├── app.js │ └── index.html └── vite.config.ts ├── create-rects-shapes ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── create-shapes-connectors ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── dev-mode ├── code.js └── manifest.json ├── document-change ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── esbuild-react ├── .gitignore ├── manifest.json ├── package-lock.json ├── package.json ├── plugin-src │ ├── code.ts │ └── tsconfig.json ├── ui-src │ ├── App.css │ ├── App.tsx │ ├── Logo.tsx │ ├── index.html │ ├── logo.png │ ├── logo.svg │ ├── main.tsx │ ├── tsconfig.json │ └── vite-env.d.ts └── vite.config.ts ├── go-to ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── icon-drag-and-drop-hosted ├── .eslintrc.js ├── .gitignore ├── README.md ├── code.ts ├── index.html ├── manifest.json ├── tsconfig.json └── ui.html ├── icon-drag-and-drop ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── invert-image ├── .eslintrc.js ├── .gitignore ├── code.ts ├── decoder.html ├── manifest.json └── tsconfig.json ├── metacards ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json ├── package-lock.json ├── package.json ├── tsconfig.json └── ui.html ├── package-lock.json ├── package.json ├── payments ├── code.js └── manifest.json ├── piechart ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── png-crop ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── post-message ├── code.js ├── manifest.json └── ui.html ├── resizer ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── sierpinski ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── snippet-saver ├── code.js ├── manifest.json └── ui.html ├── stats ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── styles-to-variables ├── code.js └── manifest.json ├── svg-inserter ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── text-review ├── code.js └── manifest.json ├── text-search ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── trivia ├── .gitignore ├── code.ts ├── manifest.json ├── tsconfig.json └── ui.html ├── variables-import-export ├── README.md ├── code.js ├── export.html ├── import.html └── manifest.json ├── vector-path ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── vote-tally ├── .eslintrc.js ├── .gitignore ├── code.ts ├── manifest.json ├── package-lock.json ├── package.json └── tsconfig.json └── webpack-react ├── .gitignore ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── code.ts ├── logo.svg ├── ui.css ├── ui.html └── ui.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Figma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍱 Figma + FigJam Plugin Samples 2 | 3 | Sample plugins using the [Figma + FigJam Plugin API docs][docs]. 4 | 5 | To make a feature request, file a bug report, or ask a question about 6 | developing plugins, check out the available [resources][help]. 7 | 8 | ## DISCLAIMER: 9 | 10 | The resources you see here are example plugin samples meant for Figma plugin development and FIGMA PROVIDES THEM "AS IS", WITHOUT WARRANTY OF ANY KIND. We don't promise they're perfect or will always work as you expect. We're not responsible for any problems you might experience from using them. It's up to you to check these samples out thoroughly and make sure they're safe and suitable for your needs before you use them. If something goes wrong, Figma won't be held responsible. If you keep using these samples, it means you're okay with these terms. **FIGMA EXPLICITY DISCLAIMS ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT, AND NON-INFRINGEMENT AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE.** 11 | 12 | ## Getting Started 13 | 14 | These plugins are written using [TypeScript][ts] to take advantage of Figma's typed plugin API. Before installing these samples as development plugins, you'll need to compile the code using the TypeScript compiler. Typescript can also watch your code for changes as you're developing, making it easy to test new changes to your code in Figma. 15 | 16 | To install TypeScript, first [install Node.js][node]. Then: 17 | 18 | $ npm install -g typescript 19 | 20 | Next install the packages that the samples depend on. Currently, this will only install the lastest version of the Figma typings file. Most of the samples will reference this shared typings file in their `tsconfig.json`. 21 | 22 | $ npm install 23 | 24 | Now, to compile the Bar Chart sample plugin (for example): 25 | 26 | $ cd barchart 27 | $ tsc 28 | 29 | Now you can import the Bar Chart plugin from within the Figma desktop app (`Plugins > Development > Import plugin from manifest...` from the right-click menu)! 30 | 31 | The code for each plugin is in `code.ts` in that plugin's subdirectory. If a 32 | plugin shows some UI, the HTML will be in `ui.html`. 33 | 34 | For example, the code for the Bar Chart sample plugin is in 35 | [barchart/code.ts](barchart/code.ts), and the HTML for its UI is in 36 | [barchart/ui.html](barchart/ui.html). 37 | 38 | ### Styling your plugin UI 39 | 40 | [For plugins that have a UI](#examples-with-a-plugin-ui), we recommend matching the style and behavior of Figma. Many other plugins follow this convention and it helps create consistency in the plugin experience for users as they use different plugins. Here's a few approaches that can help when styling your UI: 41 | 42 | - [Figma Plugin DS](https://github.com/thomas-lowry/figma-plugin-ds) A lightweight UI library for styling Figma plugins. 43 | - [Create Figma Plugin UI](https://yuanqing.github.io/create-figma-plugin/#using-the-preact-component-library) - A library of production-grade [Preact](https://preactjs.com/) components that replicate the Figma editor’s UI design 44 | 45 | # FigJam Plugins 46 | 47 | The following sample plugins use the new FigJam node types ([stickies](https://www.figma.com/plugin-docs/api/StickyNode/), [shapes with text](https://www.figma.com/plugin-docs/api/ShapeWithTextNode/), [connectors](https://www.figma.com/plugin-docs/api/ConnectorNode/), and [stamps](https://www.figma.com/plugin-docs/api/StampNode/)) and so work best **in FigJam**, i.e. with an editorType of 'figjam' in your manifest.json file. 48 | 49 | ### Vote Tally 50 | 51 | 52 | 53 | This plugin will find all stamps close to a sticky and generate a tally of all the stamps (votes) next to a sticky on the page. 54 | 55 | [Check out the source code.](vote-tally/) 56 | 57 | ### Create Shapes + Connectors 58 | 59 | 60 | 61 | This plugin creates 5 `ROUNDED_RECTANGLE` Shapes with Text nodes and adds a Connector node in between each of them. 62 | 63 | [Check out the source code.](create-shapes-connectors/) 64 | 65 | # Additional Examples 66 | 67 | The following sample plugins work in both Figma and FigJam. 68 | 69 | ## Conditional Plugins 70 | 71 | You can create plugins that have conditional logic depending on whether they are run in Figma, or FigJam. 72 | 73 | 74 | 75 | When this plugin runs in Figma, it opens a window to prompt the user to enter a number, and it will then create that many rectangles on the screen. 76 | 77 | When this plugin runs in FigJam, it opens a window to prompt the user to enter a number, and it will then create that many `ROUNDED_RECTANGLE` shapes with text nodes, and also adds a connector node in between each shape. 78 | 79 | [Check out the source code.](create-rects-shapes/) 80 | 81 | ## Examples without a plugin UI 82 | 83 | ### Annotations 84 | 85 | 86 | 87 | Creates placeholder ALT text annotations for images using the name of the image. 88 | 89 | ### Circle Text 90 | 91 | 92 | 93 | Takes a single text node selected by the user and creates a copy with the 94 | characters arranged in a circle. 95 | 96 | [Check out the source code.](circletext/) 97 | 98 | ### Invert Image Color 99 | 100 | 101 | 102 | Takes image fills in the current selection and inverts their colors. 103 | 104 | This demonstrates: 105 | 106 | - how to read/write images stored in a Figma document, and 107 | - how to use `showUI` to access browser APIs. 108 | 109 | [Check out the source code.](invert-image/) 110 | 111 | ### Meta Cards 112 | 113 | 114 | 115 | This plugin will find links within a text node and create on canvas meta cards of an image, title, description and link based on the tags in the head of a webpage at the relative links. 116 | 117 | [Check out the source code.](metacards/) 118 | 119 | ### Sierpinski 120 | 121 | 122 | 123 | Generates a fractal using circles. 124 | 125 | [Check out the source code.](sierpinski/) 126 | 127 | ### Vector Path 128 | 129 | 130 | 131 | Generates a triangle using vector paths. 132 | 133 | [Check out the source code.](vector-path/) 134 | 135 | ## Examples with a plugin UI 136 | 137 | ### Bar Chart 138 | 139 | 140 | 141 | Generates a bar chart given user input in a modal. 142 | 143 | [Check out the source code.](barchart/) 144 | 145 | ### Document Statistics 146 | 147 | 148 | 149 | Computes a count of the nodes of each `NodeType` in the current document. 150 | 151 | [Check out the source code.](stats/) 152 | 153 | ### Pie Chart 154 | 155 | 156 | 157 | Generates a pie chart given user input in a modal. 158 | 159 | [Check out the source code.](piechart/) 160 | 161 | ### Text Search 162 | 163 | 164 | 165 | Searches for text in the document, given a query by the user in a modal. 166 | 167 | This demonstrates: 168 | 169 | - advanced message passing between the main code and the plugin UI, 170 | - how to keep Figma responsive during long-running operations, and 171 | - how to use the viewport API. 172 | 173 | [Check out the source code.](text-search/) 174 | 175 | ### Icon Drag-and-Drop 176 | 177 | 178 | 179 | Allows drag-and-drop of a simple icon library from a modal to the canvas. 180 | 181 | This demonstrates registering callbacks for drop events and communicating drop data from the plugin iframe. 182 | 183 | [Check out the source code.](icon-drag-and-drop/) 184 | 185 | ### Icon Drag-and-Drop Hosted 186 | 187 | 188 | 189 | Allows drag-and-drop of a simple icon library from a modal running an externally-hosted UI to the canvas. 190 | 191 | This demonstrates registering callbacks for drop events and embedding drop data using the `dataTransfer` object in the drop event. 192 | 193 | [Check out the source code.](icon-drag-and-drop-hosted/) 194 | 195 | ### PNG Crop 196 | 197 | 198 | 199 | Crops PNGs as they are dropped onto the canvas. 200 | 201 | This demonstrates registering callbacks for drop events and reading bytes from dropped files. 202 | 203 | [Check out the source code.](png-crop/) 204 | 205 | ## Examples of plugins for Dev Mode 206 | 207 | ### Snippet Saver 208 | 209 | An example of a plugin that allows you to author and save code snippets directly on nodes that will render in the inspect panel when the node is selected. 210 | 211 | [Check out the source code.](snippet-saver/) 212 | 213 | ### Codegen 214 | 215 | An example of a plugin for codegen 216 | 217 | [Check out the source code.](codegen/) 218 | 219 | ### Dev Mode 220 | 221 | An example of a plugin configured to work in Figma design, Dev Mode inspect, _and_ run codegen. 222 | 223 | [Check out the source code.](dev-mode/) 224 | 225 | ## Examples with variables 226 | 227 | ### Styles to Variables 228 | 229 | An example of a plugin that converts Figma styles to variables 230 | 231 | [Check out the source code.](styles-to-variables/) 232 | 233 | ### Variables Import / Export 234 | 235 | An example of a plugin that imports and exports variables 236 | 237 | [Check out the source code.](variables-import-export/) 238 | 239 | ## Examples with parameters 240 | 241 | ### Go To 242 | 243 | 244 | 245 | A plugin to quickly go to any layer or page in the Figma file. 246 | 247 | For more information on how to accept parameters as input to your plugin, take a look at [this guide](https://www.figma.com/plugin-docs/plugin-parameters). 248 | 249 | [Check out the source code.](go-to/) 250 | 251 | ### Resizer 252 | 253 | 254 | 255 | Resizes a selected shape. There are two submenus, allowing for absolute resizing and relative resizing. 256 | 257 | For more information on how to accept parameters as input to your plugin, take a look at [this guide](https://www.figma.com/plugin-docs/plugin-parameters). 258 | 259 | [Check out the source code.](resizer/) 260 | 261 | ### SVG Inserter 262 | 263 | 264 | 265 | Inserts an SVG icon into the canvas. 266 | 267 | For more information on how to accept parameters as input to your plugin, take a look at [this guide](https://www.figma.com/plugin-docs/plugin-parameters). 268 | 269 | [Check out the source code.](svg-inserter/) 270 | 271 | ### Text Review 272 | 273 | 274 | 275 | Example of how to use the text review API to suggest and flag changes while editing text nodes. 276 | 277 | [Check out the source code.](text-review/) 278 | 279 | ### Trivia 280 | 281 | 282 | 283 | Generates a series of trivia questions taken from an external trivia API. 284 | 285 | For more information on how to accept parameters as input to your plugin, take a look at [this guide](https://www.figma.com/plugin-docs/plugin-parameters). 286 | 287 | [Check out the source code.](trivia/) 288 | 289 | ### Post Message 290 | 291 | A very basic example of how to communicate between a UI and the Figma canvas using postMessage. 292 | 293 | [Check out the source code.](post-message/) 294 | 295 | ### Capital 296 | 297 | 298 | 299 | Finds the capital city of a country. This demonstrates: 300 | 301 | - How to make network requests to populate parameter suggestions 302 | 303 | For more information on how to accept parameters as input to your plugin, take a look at [this guide](https://www.figma.com/plugin-docs/plugin-parameters). 304 | 305 | [Check out the source code.](capital/) 306 | 307 | ## Examples with bundling 308 | 309 | ### React 310 | 311 | 312 | 313 | Create rectangles! This demonstrates: 314 | 315 | - Bundling plugin code using Webpack 316 | - Using React with TSX 317 | 318 | ``` 319 | $ npm install 320 | $ npm run build 321 | ``` 322 | 323 | [esbuild](esbuild-react/) and [Webpack](webpack-react/) examples are great places to start if you are interested in bundling. 324 | 325 | ## Other Figma Plugin Samples + Starters 326 | 327 | - [Create Figma Plugin](https://yuanqing.github.io/create-figma-plugin/) - A comprehensive toolkit for developing Figma plugins. 328 | - [Figma Plugin Boilerplate](https://github.com/thomas-lowry/figma-plugin-boilerplate) - A starter project for creating Figma Plugins with HTML, CSS (+ SCSS) and vanilla Javascript without any frameworks. 329 | - [Figsvelte](https://github.com/thomas-lowry/figsvelte) - A boilerplate for creating Figma plugins using Svelte. 330 | - [Figplug](https://rsms.me/figplug/) - A small program for building Figma plugins. It offers all the things you need for most projects: TypeScript, React/JSX, asset bundling, plugin manifest generation, etc. 331 | - [Plugma](https://github.com/gavinmcfarland/plugma) - A CLI for simplifying creating plugins. It uses a local dev server for faster development and better debugging. Built with Vite, so it supports most frameworks, with more being added. 332 | - [Figma Kit](https://github.com/tigranpetrossian/figma-kit) - A set of React components for building Figma plugins. 333 | - [Figma Plugin Starter](https://github.com/formaat-design/figma-plugin-starter) - A Figma plugin boilerplate with React, Vite and Reshaped 334 | 335 | If you're hoping to emulate the look and feel of Figma UI in your plugins, check out our [UI2 design system](https://www.figma.com/design/Gj9iMcTbFbHrFq1ZWbDBuyc9/UI2%3A-Figma's-Design-System?node-id=0-11724&t=vFlr9WowxY2AjuJo-0), or try [Tom's Figma Plugin DS](https://github.com/thomas-lowry/figma-plugin-ds), a community-provided set of CSS and JavaScript files. 336 | 337 | [docs]: https://www.figma.com/plugin-docs/ 338 | [help]: https://www.figma.com/plugin-docs/get-help 339 | [ts]: https://www.typescriptlang.org/ 340 | [node]: https://nodejs.org/en/download/ 341 | [webpack]: #webpack 342 | -------------------------------------------------------------------------------- /_screenshots/annotations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/annotations.gif -------------------------------------------------------------------------------- /_screenshots/barchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/barchart.png -------------------------------------------------------------------------------- /_screenshots/capital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/capital.png -------------------------------------------------------------------------------- /_screenshots/circletext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/circletext.png -------------------------------------------------------------------------------- /_screenshots/colored-f-26.6x40.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /_screenshots/create-rects-shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/create-rects-shapes.png -------------------------------------------------------------------------------- /_screenshots/create-shapes-connectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/create-shapes-connectors.png -------------------------------------------------------------------------------- /_screenshots/goto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/goto.png -------------------------------------------------------------------------------- /_screenshots/icon-drag-and-drop-hosted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/icon-drag-and-drop-hosted.png -------------------------------------------------------------------------------- /_screenshots/icon-drag-and-drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/icon-drag-and-drop.png -------------------------------------------------------------------------------- /_screenshots/invert-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/invert-image.png -------------------------------------------------------------------------------- /_screenshots/metacards.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/metacards.gif -------------------------------------------------------------------------------- /_screenshots/piechart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/piechart.png -------------------------------------------------------------------------------- /_screenshots/png-crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/png-crop.png -------------------------------------------------------------------------------- /_screenshots/resizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/resizer.png -------------------------------------------------------------------------------- /_screenshots/sierpinski.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/sierpinski.png -------------------------------------------------------------------------------- /_screenshots/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/stats.png -------------------------------------------------------------------------------- /_screenshots/svg-inserter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/svg-inserter.png -------------------------------------------------------------------------------- /_screenshots/text-review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/text-review.png -------------------------------------------------------------------------------- /_screenshots/text-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/text-search.png -------------------------------------------------------------------------------- /_screenshots/trivia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/trivia.png -------------------------------------------------------------------------------- /_screenshots/vector-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/vector-path.png -------------------------------------------------------------------------------- /_screenshots/vote-tally.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/vote-tally.gif -------------------------------------------------------------------------------- /_screenshots/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/_screenshots/webpack.png -------------------------------------------------------------------------------- /annotations/README.md: -------------------------------------------------------------------------------- 1 | # Annotations Sample 2 | 3 | ![Plugin Demo](../_screenshots/annotations.gif) 4 | 5 | This is a code sample that demonstrates how the Annotations API can be used to bulk create annotations on a specific type of node. 6 | 7 | This code is distributed strictly for educational purposes. It's purely a starting point and you should modify it as needed for your specific purposes. As such, there are some known limitations. 8 | 9 | ## Known Limitations 10 | 11 | This is a Dev Mode plugin that does not have a UI, it can be run via the Plugin tab in Dev Mode or via the Quick Actions Menu via the `CMD`+`/` (`CTRL`+`/` on Windows) keyboard shortcut 12 | -------------------------------------------------------------------------------- /annotations/code.js: -------------------------------------------------------------------------------- 1 | // Whether or not a node has a visible image fill on it. 2 | // Ignores "figma.mixed" fill values. 3 | function nodeHasImageFill(node) { 4 | return ( 5 | "fills" in node && 6 | Array.isArray(node.fills) && 7 | Boolean(node.fills.find((paint) => paint.visible && paint.type === "IMAGE")) 8 | ); 9 | } 10 | 11 | // Returns all Figma nodes and descendants with image fills for an array of nodes 12 | function getImageNodes(nodes) { 13 | const imageNodes = []; 14 | nodes.forEach((node) => { 15 | if (nodeHasImageFills(node)) { 16 | imageNodes.push(node); 17 | } 18 | 19 | // Checking node descendants 20 | if ("findAll" in node) { 21 | node.findAll((descendant) => { 22 | if (nodeHasImageFill(descendant)) { 23 | imageNodes.push(descendant); 24 | } 25 | }); 26 | } 27 | }); 28 | 29 | return imageNodes; 30 | } 31 | 32 | // helper function for creating an annotation 33 | function createImageAnnotation(node, customLabel) { 34 | let DEFAULT_TEMPLATE = `🔵 **ALT TEXT**\n${node.name}`; 35 | let markdown = customLabel || DEFAULT_TEMPLATE; 36 | node.annotations = [ 37 | { 38 | labelMarkdown: markdown, 39 | properties: [{ type: "fills" }], 40 | }, 41 | ]; 42 | } 43 | 44 | // helper function for notifying after annotations are created 45 | function showAnnotationNotification(count, skipped) { 46 | let msg = ""; 47 | if (count > 0) { 48 | msg += `Created ${count} annotation${count > 1 ? "s" : ""}.`; 49 | } 50 | if (skipped > 0) { 51 | msg += ` Skipped ${skipped} annotation${skipped > 1 ? "s" : ""}.`; 52 | } 53 | figma.notify(msg); 54 | } 55 | 56 | // function to create annotations in selection 57 | // selection => selection can be figma.currentPage.selection or figma.currentPage 58 | // label (optional) => custom label to be annotated, 59 | // if not provided will be labeled using image name 60 | function createAltTextAnnotations(selection, label) { 61 | let imageNodes = getImageNodes(selection); 62 | 63 | let count = 0; // number of annotations we'll create 64 | let skipped = 0; // number of annotations we'll skip 65 | 66 | imageNodes.forEach((node) => { 67 | // if an annotations already exists, we don't want to overwrite it 68 | if (node.annotations.length > 0) { 69 | skipped++; 70 | return; 71 | } 72 | 73 | createImageAnnotation(node, label); 74 | count++; 75 | }); 76 | 77 | showAnnotationNotification(count, skipped); 78 | } 79 | 80 | // runs plugin from menu commands 81 | figma.on("run", ({ command }) => { 82 | switch (command) { 83 | case "all-images": 84 | createAltTextAnnotations([figma.currentPage]); 85 | figma.closePlugin(); 86 | break; 87 | case "selection": 88 | createAltTextAnnotations(figma.currentPage.selection); 89 | figma.closePlugin(); 90 | default: 91 | // do nothing 92 | break; 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /annotations/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Annotations Sample Plugin", 3 | "id": "1442969948273040502", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "capabilities": ["inspect"], 7 | "enableProposedApi": true, 8 | "documentAccess": "dynamic-page", 9 | "editorType": ["dev"], 10 | "ui": "ui.html", 11 | "networkAccess": { 12 | "allowedDomains": ["none"] 13 | }, 14 | "menu": [ 15 | { "name": "Annotate images in selection", "command": "selection" }, 16 | { "name": "Annotate all images on page", "command": "all-images" } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /annotations/ui.html: -------------------------------------------------------------------------------- 1 | 3 | 103 | 104 | 105 |
106 |
107 | 108 | 109 |
110 |
111 |
112 | 113 | 116 |
117 | 120 | 121 |
122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /barchart/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /barchart/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__) 2 | figma.ui.onmessage = async (numbers) => { 3 | // Inter Regular is the font that objects will be created with by default in 4 | // Figma. We need to wait for fonts to load before creating text using them. 5 | await figma.loadFontAsync({ family: "Inter", style: "Regular" }) 6 | 7 | const frameWidth = 800 8 | const frameHeight = 600 9 | const chartX = 25 10 | const chartY = 50 11 | const chartWidth = frameWidth - 50 12 | const chartHeight = frameHeight - 50 13 | 14 | const frame = figma.createFrame() 15 | frame.resizeWithoutConstraints(frameWidth, frameHeight) 16 | 17 | // Center the frame in our current viewport so we can see it. 18 | frame.x = figma.viewport.center.x - frameWidth / 2 19 | frame.y = figma.viewport.center.y - frameHeight / 2 20 | 21 | // Border for the chart 22 | const border = figma.createRectangle() 23 | frame.appendChild(border) 24 | border.resizeWithoutConstraints(frameWidth, frameHeight) 25 | border.strokeAlign = 'INSIDE' 26 | border.strokeWeight = 3 27 | border.fills = [] 28 | border.strokes = [{ type: 'SOLID', color: {r: 0, g: 0, b: 0} }] 29 | border.constraints = {horizontal: 'STRETCH', vertical: 'STRETCH'} 30 | 31 | // Line at the bottom of the chart 32 | const line = figma.createRectangle() 33 | frame.appendChild(line) 34 | line.x = chartX 35 | line.y = chartY + chartHeight 36 | line.resizeWithoutConstraints(chartWidth, 3) 37 | line.fills = [{ type: 'SOLID', color: {r: 0, g: 0, b: 0} }] 38 | line.constraints = {horizontal: 'STRETCH', vertical: 'STRETCH'} 39 | 40 | const min = numbers.reduce((a, b) => Math.min(a, b), 0) 41 | const max = numbers.reduce((a, b) => Math.max(a, b), 0) 42 | 43 | for (let i = 0; i < numbers.length; i++) { 44 | const num = numbers[i]; 45 | const left = chartX + chartWidth * (i + 0.25) / numbers.length; 46 | const right = chartX + chartWidth * (i + 0.75) / numbers.length; 47 | const top = chartY + chartHeight - chartHeight * (Math.max(0, num) - min) / (max - min); 48 | const bottom = chartY + chartHeight - chartHeight * (Math.min(0, num) - min) / (max - min); 49 | 50 | // The column 51 | const column = figma.createRectangle() 52 | frame.appendChild(column) 53 | column.x = left 54 | column.y = top 55 | column.resizeWithoutConstraints(right - left, bottom - top) 56 | column.fills = [{ type: 'SOLID', color: {r: 1, g: 0, b: 0} }] 57 | column.constraints = {horizontal: 'STRETCH', vertical: 'STRETCH'} 58 | 59 | // The label 60 | const label = figma.createText() 61 | frame.appendChild(label) 62 | label.x = left - 50 63 | label.y = top - 50 64 | label.resizeWithoutConstraints(right - left + 100, 50) 65 | label.fills = [{ type: 'SOLID', color: {r: 0, g: 0, b: 0} }] 66 | label.characters = num.toString() 67 | label.fontSize = 30 68 | label.textAlignHorizontal = 'CENTER' 69 | label.textAlignVertical = 'BOTTOM' 70 | label.constraints = {horizontal: 'STRETCH', vertical: 'STRETCH'} 71 | } 72 | 73 | figma.closePlugin() 74 | } 75 | -------------------------------------------------------------------------------- /barchart/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bar-chart-sample", 3 | "name": "Bar Chart Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": [ 10 | "https://static.figma.com/api/figma-extension-api-0.0.1.css", 11 | "https://fonts.googleapis.com", 12 | "https://fonts.gstatic.com" 13 | ] 14 | }, 15 | "documentAccess": "dynamic-page" 16 | } 17 | -------------------------------------------------------------------------------- /barchart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /barchart/ui.html: -------------------------------------------------------------------------------- 1 | 2 |

Bar Chart Sample

3 |

4 | Values: 5 | 6 |

7 | 8 |

9 | 30 | -------------------------------------------------------------------------------- /capital/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | package.json 3 | package-lock.json -------------------------------------------------------------------------------- /capital/code.ts: -------------------------------------------------------------------------------- 1 | // This plugin fetches a list of countries from an external API and shows the capital of the selected country 2 | 3 | interface Country { 4 | name: string; 5 | data: { 6 | index: number; 7 | name: string; 8 | capital: string; 9 | }; 10 | } 11 | 12 | let resolveCountries = (countries: Country[]) => {}; 13 | const countriesPromise = new Promise((resolve) => { 14 | resolveCountries = resolve; 15 | }); 16 | 17 | // Create an invisible iframe UI to use network API's 18 | figma.showUI( 19 | ``, 26 | { visible: false } 27 | ); 28 | 29 | // Resolve the countries promise when a message is received from the iframe 30 | figma.ui.onmessage = (json) => { 31 | resolveCountries( 32 | json.map((country, index) => { 33 | return { 34 | name: country.name, 35 | data: { 36 | index: index, 37 | name: country.name, 38 | capital: country.capital, 39 | }, 40 | }; 41 | }) 42 | ); 43 | }; 44 | 45 | figma.parameters.on( 46 | 'input', 47 | async ({ key, query, result }: ParameterInputEvent) => { 48 | // When fetching data from an external source, it is recommended to show a relevant loading message 49 | result.setLoadingMessage('Loading countries...'); 50 | const countries = await countriesPromise; 51 | result.setSuggestions( 52 | // Filter suggestions based on the query entered 53 | countries.filter((country) => 54 | country.name.toLowerCase().includes(query.toLowerCase()) 55 | ) 56 | ); 57 | } 58 | ); 59 | 60 | figma.on('run', ({ parameters }: RunEvent) => { 61 | const countryName = parameters.country.name; 62 | const capital = parameters.country.capital; 63 | figma.closePlugin(`The capital of ${countryName} is ${capital}`); 64 | }); 65 | -------------------------------------------------------------------------------- /capital/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Capital", 3 | "id": "1025109963759603905", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "parameters": [ 8 | { 9 | "name": "Country", 10 | "key": "country", 11 | "description": "Select a country to find its capital city." 12 | } 13 | ], 14 | "networkAccess": { 15 | "allowedDomains": ["https://api.sampleapis.com/countries/countries"] 16 | }, 17 | "documentAccess": "dynamic-page" 18 | } 19 | -------------------------------------------------------------------------------- /capital/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /circletext/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /circletext/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /circletext/code.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | // Circular text sample code 3 | // Turns a selected text node into a set of letters on a circular arc. 4 | 5 | 6 | // Combines two transforms by doing a matrix multiplication. 7 | // The first transform applied is a, followed by b, which 8 | // is normally written b * a. 9 | function multiply(a, b) { 10 | return [ 11 | [ a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1], a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] ], 12 | [ a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1] + 0, a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] ] 13 | ] 14 | } 15 | 16 | // Creates a "move" transform. 17 | function move(x, y) { 18 | return [ 19 | [1, 0, x], 20 | [0, 1, y] 21 | ] 22 | } 23 | 24 | // Creates a "rotate" transform. 25 | function rotate(theta: number) { 26 | return [ 27 | [Math.cos(theta), Math.sin(theta), 0], 28 | [-Math.sin(theta), Math.cos(theta), 0] 29 | ] 30 | } 31 | 32 | // MAIN PLUGIN CODE 33 | 34 | async function main(): Promise { 35 | // Inter Regular is the font that objects will be created with by default in 36 | // Figma. We need to wait for fonts to load before creating text using them. 37 | await figma.loadFontAsync({ family: "Inter", style: "Regular" }) 38 | 39 | // Make sure the selection is a single piece of text before proceeding. 40 | if (figma.currentPage.selection.length !== 1) { 41 | return "Select a single node." 42 | } 43 | 44 | const node = figma.currentPage.selection[0] 45 | if (node.type !== 'TEXT') { 46 | return "Select a single text node." 47 | } 48 | 49 | // Replace spaces with nonbreaking spaces. 50 | const text = node.characters.replace(/ /g, " ") 51 | const gap = 5 52 | 53 | // Create a new text node for each character, and 54 | // measure the total width. 55 | const nodes = [] 56 | let width = 0 57 | for (let i = 0; i < text.length; i++) { 58 | const letterNode = figma.createText() 59 | letterNode.fontSize = node.fontSize 60 | letterNode.fontName = node.fontName 61 | 62 | letterNode.characters = text.charAt(i) 63 | width += letterNode.width 64 | if (i !== 0) { 65 | width += gap 66 | } 67 | node.parent.appendChild(letterNode) 68 | nodes.push(letterNode) 69 | } 70 | 71 | // Make the radius half the width of the original text, minus a bit. 72 | const r = node.width / 2 - 30 73 | const pi = 3.1415926 74 | 75 | // The arclength should be equal to the total desired width of the text, 76 | // => theta * r = width 77 | // => theta = width / r 78 | // 79 | // We define this angle such that 0 means pointing to the right, and pi/2 means 80 | // pointing straight up. 81 | // 82 | // Using these conventions, the starting angle for our curved text is 83 | // pi/2 + theta/2, and the ending angle is pi/2 - theta/2. 84 | 85 | let angle = pi / 2 + width / (2*r) 86 | const gapAngle = gap / r 87 | 88 | const centerX = node.x + node.width / 2 89 | const centerY = node.y + node.height / 2 90 | 91 | // Walk through each letter and position it on a circle of radius r. 92 | nodes.forEach(function (letterNode: TextNode) { 93 | const stepAngle = letterNode.width / r 94 | 95 | // Move forward in our arc half a letter width. 96 | angle -= stepAngle / 2 97 | 98 | const width = letterNode.width 99 | const height = letterNode.height 100 | 101 | // Move the letter so that the center of its baseline is on the origin. 102 | // (estimate the baseline as being 70% down from the top of the box). 103 | // 104 | // We accomplish this by moving the letter so its top left is at (0, 0), 105 | // then moving the letter to the left and up by the appopriate amount. 106 | letterNode.x = 0 107 | letterNode.y = 0 108 | letterNode.relativeTransform = multiply(move(-width/2, -0.7 * height), letterNode.relativeTransform) as Transform 109 | 110 | // Rotate the letter. Because we want to have the rotation angle be 0 at the top of the circle, 111 | // we need to subtract pi/2 before applying the rotation to the text. 112 | letterNode.relativeTransform = multiply(rotate(angle - pi/2), letterNode.relativeTransform) as Transform 113 | 114 | // Move the letter to its position on the arc. 115 | const desiredX = centerX + r * Math.cos(angle) 116 | const desiredY = centerY - r * Math.sin(angle) 117 | letterNode.relativeTransform = multiply(move(desiredX, desiredY), letterNode.relativeTransform) as Transform 118 | 119 | // Move forward in our arc half a letter width + the gap 120 | angle -= stepAngle / 2 + gapAngle 121 | }) 122 | 123 | // Put all nodes in a group! 124 | figma.group(nodes, node.parent) 125 | } 126 | 127 | void main().then((message: string | undefined) => { 128 | figma.closePlugin(message) 129 | }) 130 | -------------------------------------------------------------------------------- /circletext/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "circle-text-sample", 3 | "name": "Circle Text Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /circletext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /codegen/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /codegen/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Codegen Sample", 3 | "id": "180000000000000", 4 | "api": "1.0.0", 5 | "editorType": ["dev"], 6 | "capabilities": ["codegen"], 7 | "permissions": [], 8 | "codegenLanguages": [ 9 | { "label": "Everything", "value": "all" }, 10 | { "label": "HTML", "value": "html" }, 11 | { "label": "CSS", "value": "css" }, 12 | { "label": "JS", "value": "js" }, 13 | { "label": "JSON", "value": "json" }, 14 | { "label": "Weather 🌦", "value": "weather" } 15 | ], 16 | "codegenPreferences": [ 17 | { 18 | "itemType": "unit", 19 | "scaledUnit": "Sample Unit (su)", 20 | "defaultScaleFactor": 0.7, 21 | "default": false 22 | }, 23 | { 24 | "itemType": "select", 25 | "propertyName": "example", 26 | "label": "Yes or no", 27 | "options": [ 28 | { "label": "Yes", "value": "yes", "isDefault": true }, 29 | { "label": "No", "value": "no" } 30 | ] 31 | }, 32 | { 33 | "itemType": "action", 34 | "propertyName": "example", 35 | "label": "Example action" 36 | } 37 | ], 38 | "main": "dist/code.js", 39 | "ui": "dist/index.html", 40 | "documentAccess": "dynamic-page" 41 | } 42 | -------------------------------------------------------------------------------- /codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codegen-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run build:ui && npm run build:main -- --minify", 8 | "build:main": "esbuild plugin-src/code.js --bundle --outfile=dist/code.js", 9 | "build:ui": "npx vite build --minify esbuild --emptyOutDir=false --target=ES6", 10 | "build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"", 11 | "dev": "concurrently -n build,vite 'npm:build:watch' 'vite'" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@figma/plugin-typings": "^1.62.0", 17 | "concurrently": "^8.0.1", 18 | "esbuild": "^0.25.0", 19 | "prettier": "^2.8.7", 20 | "vite": "^4.5.9", 21 | "vite-plugin-singlefile": "^0.13.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /codegen/plugin-src/code.js: -------------------------------------------------------------------------------- 1 | if (figma.mode === "codegen") { 2 | figma.codegen.on("preferenceschange", (event) => { 3 | if (event.propertyName === "example") { 4 | figma.showUI( 5 | "

An iframe for external requests or custom settings!

", 6 | { 7 | width: 300, 8 | height: 300, 9 | } 10 | ); 11 | } 12 | }); 13 | 14 | figma.showUI(__html__, { visible: false }); 15 | 16 | figma.codegen.on("generate", (event) => { 17 | const { node, language } = event; 18 | const showAll = !language || language === "all"; 19 | return new Promise(async (resolve) => { 20 | const { unit, scaleFactor } = figma.codegen.preferences; 21 | const formatUnit = (number) => 22 | unit === "SCALED" 23 | ? `${(number * scaleFactor).toFixed(3)}su` 24 | : `${number}px`; 25 | const nodeObject = { 26 | type: node.type, 27 | name: node.name, 28 | width: formatUnit(node.width), 29 | height: formatUnit(node.height), 30 | }; 31 | 32 | const blocks = 33 | language === "weather" 34 | ? await weatherResult() 35 | : [ 36 | showAll || language === "html" 37 | ? { 38 | title: `Custom HTML`, 39 | code: `

${node.name} is a node! Isn't that great??? I really really think so. This is a long line.

`, 40 | language: "HTML", 41 | } 42 | : null, 43 | showAll || language === "css" 44 | ? { 45 | title: `Custom CSS`, 46 | code: `div { width: ${nodeObject.width}; height: ${nodeObject.height} }`, 47 | language: "CSS", 48 | } 49 | : null, 50 | showAll || language === "js" 51 | ? { 52 | title: `Custom JS`, 53 | code: `function log() { console.log(${JSON.stringify( 54 | nodeObject 55 | )}); }`, 56 | language: "JAVASCRIPT", 57 | } 58 | : null, 59 | showAll || language === "json" 60 | ? { 61 | title: `Custom JSON`, 62 | code: JSON.stringify(nodeObject), 63 | language: "JSON", 64 | } 65 | : null, 66 | ].filter(Boolean); 67 | 68 | async function weatherResult() { 69 | const weather = await ( 70 | await fetch( 71 | "https://api.open-meteo.com/v1/forecast?latitude=37.77&longitude=-122.42&daily=temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_probability_max¤t_weather=true&temperature_unit=fahrenheit&windspeed_unit=mph&precipitation_unit=inch&forecast_days=1&timezone=America%2FLos_Angeles" 72 | ) 73 | ).json(); 74 | return [ 75 | { 76 | title: "Weather 🌦", 77 | code: JSON.stringify(weather, null, 2), 78 | language: "JSON", 79 | }, 80 | ]; 81 | } 82 | 83 | blocks.forEach(({ language, code }, id) => { 84 | const message = { type: "FORMAT", code, language, id }; 85 | figma.ui.postMessage(message); 86 | }); 87 | 88 | let promiseCount = blocks.length; 89 | const results = []; 90 | figma.ui.onmessage = (message) => { 91 | if (message.type === "FORMAT_RESULT") { 92 | const item = blocks[message.id]; 93 | results[message.id] = Object.assign(item, { 94 | code: message.result, 95 | }); 96 | promiseCount--; 97 | if (promiseCount <= 0) { 98 | resolve(results); 99 | } 100 | } 101 | }; 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /codegen/ui-src/app.js: -------------------------------------------------------------------------------- 1 | import prettier from "prettier/esm/standalone.mjs"; 2 | import parserBabel from "prettier/esm/parser-babel.mjs"; 3 | import parserHTML from "prettier/esm/parser-html.mjs"; 4 | import parserCSS from "prettier/esm/parser-postcss.mjs"; 5 | 6 | const PRINT_WIDTH = 50; 7 | 8 | function formatCode({ language, code, printWidth = PRINT_WIDTH }) { 9 | switch (language) { 10 | case "HTML": 11 | return prettier.format(code, { 12 | printWidth, 13 | parser: "html", 14 | plugins: [parserHTML], 15 | htmlWhitespaceSensitivity: "ignore", 16 | bracketSameLine: false, 17 | }); 18 | case "CSS": 19 | return prettier.format(code, { 20 | printWidth, 21 | parser: "css", 22 | plugins: [parserCSS], 23 | }); 24 | case "JSON": 25 | return JSON.stringify(JSON.parse(code), null, 2); 26 | case "JAVASCRIPT": 27 | case "TYPESCRIPT": 28 | return prettier.format(code, { 29 | printWidth, 30 | parser: "babel-ts", 31 | plugins: [parserBabel], 32 | semi: true, 33 | }); 34 | } 35 | } 36 | 37 | window.onmessage = ({ data: { pluginMessage } }) => { 38 | if (pluginMessage.type === "FORMAT") { 39 | const result = formatCode(pluginMessage); 40 | parent.postMessage( 41 | { 42 | pluginMessage: { 43 | id: pluginMessage.id, 44 | result, 45 | type: "FORMAT_RESULT", 46 | }, 47 | }, 48 | "*" 49 | ); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /codegen/ui-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /codegen/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { viteSingleFile } from "vite-plugin-singlefile"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: "./ui-src", 7 | plugins: [viteSingleFile()], 8 | build: { 9 | target: "esnext", 10 | assetsInlineLimit: 100000000, 11 | chunkSizeWarningLimit: 100000000, 12 | cssCodeSplit: false, 13 | outDir: "../dist", 14 | rollupOptions: { 15 | output: {}, 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /create-rects-shapes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /create-rects-shapes/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /create-rects-shapes/code.ts: -------------------------------------------------------------------------------- 1 | // runs this code if the plugin is run in Figma 2 | 3 | if (figma.editorType === 'figma') { 4 | // This plugin creates 5 rectangles on the screen. 5 | const numberOfRectangles = 5 6 | 7 | // This file holds the main code for the plugins. It has access to the *document*. 8 | // You can access browser APIs such as the network by creating a UI which contains 9 | // a full browser environment (see documentation). 10 | 11 | const nodes: SceneNode[] = []; 12 | for (let i = 0; i < numberOfRectangles; i++) { 13 | const rect = figma.createRectangle(); 14 | rect.x = i * 150; 15 | rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}]; 16 | figma.currentPage.appendChild(rect); 17 | nodes.push(rect); 18 | } 19 | figma.currentPage.selection = nodes; 20 | figma.viewport.scrollAndZoomIntoView(nodes); 21 | 22 | // Make sure to close the plugin when you're done. Otherwise the plugin will 23 | // keep running, which shows the cancel button at the bottom of the screen. 24 | figma.closePlugin(); 25 | 26 | } 27 | 28 | // runs this code if the plugin is run in FigJam 29 | 30 | if (figma.editorType === 'figjam') { 31 | // This plugin creates 5 shapes with text on the screen, and adds a connector in between each one. 32 | const numberOfShapes = 5 33 | 34 | // This file holds the main code for the plugin. It has access to the *document*. 35 | // You can access browser APIs such as the network by creating a UI which contains 36 | // a full browser environment (see documentation). 37 | 38 | const nodes: SceneNode[] = []; 39 | for (let i = 0; i < numberOfShapes; i++) { 40 | const shape = figma.createShapeWithText(); 41 | shape.shapeType = 'ROUNDED_RECTANGLE' 42 | // You can set shapeType to one of: 'SQUARE' | 'ELLIPSE' | 'ROUNDED_RECTANGLE' | 'DIAMOND' | 'TRIANGLE_UP' | 'TRIANGLE_DOWN' | 'PARALLELOGRAM_RIGHT' | 'PARALLELOGRAM_LEFT' 43 | shape.x = i * 400; 44 | shape.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}]; 45 | figma.currentPage.appendChild(shape); 46 | nodes.push(shape); 47 | } 48 | 49 | for (let i = 0; i < (numberOfShapes - 1); i++) { 50 | const connector = figma.createConnector(); 51 | connector.strokeWeight = 8 52 | 53 | connector.connectorStart = { 54 | endpointNodeId: nodes[i].id, 55 | magnet: 'AUTO', 56 | }; 57 | 58 | connector.connectorEnd = { 59 | endpointNodeId: nodes[i+1].id, 60 | magnet: 'AUTO', 61 | }; 62 | } 63 | 64 | figma.currentPage.selection = nodes; 65 | figma.viewport.scrollAndZoomIntoView(nodes); 66 | 67 | // Make sure to close the plugin when you're done. Otherwise the plugin will 68 | // keep running, which shows the cancel button at the bottom of the screen. 69 | figma.closePlugin(); 70 | 71 | } 72 | -------------------------------------------------------------------------------- /create-rects-shapes/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Conditional Plugin", 3 | "id": "1009724408344532126", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /create-rects-shapes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /create-shapes-connectors/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /create-shapes-connectors/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /create-shapes-connectors/code.ts: -------------------------------------------------------------------------------- 1 | // This plugin creates 5 shapes on the screen. 2 | const numberOfShapes = 5 3 | 4 | // This file holds the main code for the plugin. It has access to the *document*. 5 | // You can access browser APIs such as the network by creating a UI which contains 6 | // a full browser environment (see documentation). 7 | 8 | const nodes: SceneNode[] = []; 9 | for (let i = 0; i < numberOfShapes; i++) { 10 | const shape = figma.createShapeWithText(); 11 | shape.shapeType = 'ROUNDED_RECTANGLE' 12 | // You can set shapeType to one of: 'SQUARE' | 'ELLIPSE' | 'ROUNDED_RECTANGLE' | 'DIAMOND' | 'TRIANGLE_UP' | 'TRIANGLE_DOWN' | 'PARALLELOGRAM_RIGHT' | 'PARALLELOGRAM_LEFT' 13 | shape.x = i * 400; 14 | shape.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}]; 15 | figma.currentPage.appendChild(shape); 16 | nodes.push(shape); 17 | } 18 | 19 | for (let i = 0; i < (numberOfShapes - 1); i++) { 20 | const connector = figma.createConnector(); 21 | connector.strokeWeight = 8 22 | 23 | connector.connectorStart = { 24 | endpointNodeId: nodes[i].id, 25 | magnet: 'AUTO', 26 | }; 27 | 28 | connector.connectorEnd = { 29 | endpointNodeId: nodes[i+1].id, 30 | magnet: 'AUTO', 31 | }; 32 | } 33 | 34 | figma.currentPage.selection = nodes; 35 | figma.viewport.scrollAndZoomIntoView(nodes); 36 | 37 | // Make sure to close the plugin when you're done. Otherwise the plugin will 38 | // keep running, which shows the cancel button at the bottom of the screen. 39 | figma.closePlugin(); 40 | -------------------------------------------------------------------------------- /create-shapes-connectors/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Shapes & Connectors", 3 | "id": "1009709756337344668", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /create-shapes-connectors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /dev-mode/code.js: -------------------------------------------------------------------------------- 1 | if (figma.editorType === "dev") { 2 | if (figma.mode === "inspect") { 3 | figma.showUI("

This is Dev Mode!

"); 4 | } else if (figma.mode === "codegen") { 5 | figma.codegen.on("generate", () => [ 6 | { title: "Codegen", code: "This is codegen!", language: "PLAINTEXT" }, 7 | ]); 8 | } 9 | } else if (figma.editorType === "figma") { 10 | figma.showUI("

This is Figma!

"); 11 | } 12 | -------------------------------------------------------------------------------- /dev-mode/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dev Mode Sample", 3 | "id": "180000000000000", 4 | "api": "1.0.0", 5 | "editorType": ["dev", "figma"], 6 | "capabilities": ["codegen", "inspect"], 7 | "main": "code.js", 8 | "permissions": [], 9 | "codegenLanguages": [{ "label": "Text", "value": "text" }], 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /document-change/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /document-change/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /document-change/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { height: 600, width: 600 }); 2 | 3 | void initialize(); 4 | 5 | async function initialize() { 6 | await figma.loadAllPagesAsync(); 7 | figma.on("documentchange", (event) => { 8 | const messages = event.documentChanges.map(documentChangeAsString); 9 | figma.ui.postMessage(messages, { origin: "*" }); 10 | }); 11 | } 12 | 13 | function documentChangeAsString(change: DocumentChange) { 14 | const { origin, type } = change; 15 | const list: string[] = [origin, type]; 16 | if (type === "PROPERTY_CHANGE") { 17 | list.push(change.node.type, change.properties.join(", ")); 18 | } else if (type === "STYLE_PROPERTY_CHANGE") { 19 | list.push(change.style?.name, change.properties.join(", ")); 20 | } else if (type === "STYLE_CREATE" || type === "STYLE_DELETE") { 21 | // noop 22 | } else { 23 | list.push(change.node.type); 24 | } 25 | return list.join(" "); 26 | } 27 | -------------------------------------------------------------------------------- /document-change/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "document-change-sample", 3 | "name": "Document Change Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /document-change/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /document-change/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document Change 8 | 24 | 25 | 26 | 27 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /esbuild-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /esbuild-react/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Sample", 3 | "id": "react-sample", 4 | "api": "1.0.0", 5 | "editorType": ["figma", "figjam"], 6 | "permissions": [], 7 | "main": "dist/code.js", 8 | "ui": "dist/index.html", 9 | "networkAccess": { 10 | "allowedDomains": ["none"] 11 | }, 12 | "documentAccess": "dynamic-page" 13 | } 14 | -------------------------------------------------------------------------------- /esbuild-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "1.0.0", 4 | "description": "react", 5 | "scripts": { 6 | "test": "npm run tsc && npm run build", 7 | "format": "prettier --write .", 8 | "tsc": "npm run tsc:main && npm run tsc:ui", 9 | "tsc:main": "tsc --noEmit -p plugin-src", 10 | "tsc:ui": "tsc --noEmit -p ui-src", 11 | "tsc:watch": "concurrently -n widget,iframe \"npm run tsc:main -- --watch --preserveWatchOutput\" \"npm run tsc:ui -- --watch --preserveWatchOutput\"", 12 | "build": "npm run build:ui && npm run build:main -- --minify", 13 | "build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js", 14 | "build:ui": "npx vite build --minify esbuild --emptyOutDir=false", 15 | "build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"", 16 | "dev": "concurrently -n tsc,build,vite 'npm:tsc:watch' 'npm:build:watch' 'vite'" 17 | }, 18 | "author": "Figma", 19 | "license": "MIT License", 20 | "dependencies": { 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0" 23 | }, 24 | "devDependencies": { 25 | "@figma/plugin-typings": "*", 26 | "@types/react": "^18.2.38", 27 | "@types/react-dom": "^18.2.16", 28 | "@vitejs/plugin-react-refresh": "^1.3.6", 29 | "concurrently": "^8.2.2", 30 | "esbuild": "^0.19.7", 31 | "prettier": "^3.1.0", 32 | "typescript": "^5.3.2", 33 | "vite": "^5.0.0", 34 | "vite-plugin-singlefile": "^0.13.5", 35 | "vite-svg-loader": "^5.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /esbuild-react/plugin-src/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { themeColors: true, height: 300 }); 2 | 3 | figma.ui.onmessage = (msg) => { 4 | if (msg.type === "create-rectangles") { 5 | const nodes = []; 6 | 7 | for (let i = 0; i < msg.count; i++) { 8 | const rect = figma.createRectangle(); 9 | rect.x = i * 150; 10 | rect.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }]; 11 | figma.currentPage.appendChild(rect); 12 | nodes.push(rect); 13 | } 14 | 15 | figma.currentPage.selection = nodes; 16 | figma.viewport.scrollAndZoomIntoView(nodes); 17 | } 18 | 19 | figma.closePlugin(); 20 | }; 21 | -------------------------------------------------------------------------------- /esbuild-react/plugin-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "strict": true, 6 | "typeRoots": ["../node_modules/@figma"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg: var(--figma-color-bg); 3 | --color-bg-hover: var(--figma-color-bg-hover); 4 | --color-bg-active: var(--figma-color-bg-pressed); 5 | --color-border: var(--figma-color-border); 6 | --color-border-focus: var(--figma-color-border-selected); 7 | --color-icon: var(--figma-color-icon); 8 | --color-text: var(--figma-color-text); 9 | --color-bg-brand: var(--figma-color-bg-brand); 10 | --color-bg-brand-hover: var(--figma-color-bg-brand-hover); 11 | --color-bg-brand-active: var(--figma-color-bg-brand-pressed); 12 | --color-border-brand: var(--figma-color-border-brand); 13 | --color-border-brand-focus: var(--figma-color-border-selected-strong); 14 | --color-text-brand: var(--figma-color-text-onbrand); 15 | } 16 | 17 | html, 18 | body, 19 | main { 20 | height: 100%; 21 | } 22 | 23 | body, 24 | input, 25 | button { 26 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 27 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 28 | font-size: 1rem; 29 | text-align: center; 30 | } 31 | 32 | body { 33 | background: var(--color-bg); 34 | color: var(--color-text); 35 | margin: 0; 36 | } 37 | 38 | button { 39 | border-radius: 0.25rem; 40 | background: var(--color-bg); 41 | color: var(--color-text); 42 | cursor: pointer; 43 | border: 1px solid var(--color-border); 44 | padding: 0.5rem 1rem; 45 | } 46 | button:hover { 47 | background-color: var(--color-bg-hover); 48 | } 49 | button:active { 50 | background-color: var(--color-bg-active); 51 | } 52 | button:focus-visible { 53 | border: none; 54 | outline-color: var(--color-border-focus); 55 | } 56 | button.brand { 57 | --color-bg: var(--color-bg-brand); 58 | --color-text: var(--color-text-brand); 59 | --color-bg-hover: var(--color-bg-brand-hover); 60 | --color-bg-active: var(--color-bg-brand-active); 61 | --color-border: transparent; 62 | --color-border-focus: var(--color-border-brand-focus); 63 | } 64 | 65 | input { 66 | background: 1px solid var(--color-bg); 67 | border: 1px solid var(--color-border); 68 | color: 1px solid var(--color-text); 69 | padding: 0.5rem; 70 | } 71 | 72 | input:focus-visible { 73 | border-color: var(--color-border-focus); 74 | outline-color: var(--color-border-focus); 75 | } 76 | 77 | svg { 78 | stroke: var(--color-icon, rgba(0, 0, 0, 0.9)); 79 | } 80 | 81 | main { 82 | align-items: center; 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: center; 86 | } 87 | 88 | section { 89 | align-items: center; 90 | display: flex; 91 | flex-direction: column; 92 | justify-content: center; 93 | margin-bottom: 1rem; 94 | } 95 | section > * + * { 96 | margin-top: 0.5rem; 97 | } 98 | footer > * + * { 99 | margin-left: 0.5rem; 100 | } 101 | 102 | img { 103 | height: auto; 104 | width: 2rem; 105 | } 106 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import logoPng from "./logo.png"; 3 | import logoSvg from "./logo.svg?raw"; 4 | import Logo from "./Logo"; 5 | import "./App.css"; 6 | 7 | function App() { 8 | const inputRef = useRef(null); 9 | 10 | const onCreate = () => { 11 | const count = Number(inputRef.current?.value || 0); 12 | parent.postMessage( 13 | { pluginMessage: { type: "create-rectangles", count } }, 14 | "*" 15 | ); 16 | }; 17 | 18 | const onCancel = () => { 19 | parent.postMessage({ pluginMessage: { type: "cancel" } }, "*"); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 26 |   27 | 28 |   29 | 30 |

Rectangle Creator

31 |
32 |
33 | 34 | 35 |
36 |
37 | 40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | const Logo: FC<{ className?: string }> = ({ className }) => ( 4 | 12 | 17 | 18 | ); 19 | 20 | export default Logo; 21 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Widget Template 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/plugin-samples/a88e5e4c814cc1c41cf98945b29599b4403b6bba/esbuild-react/ui-src/logo.png -------------------------------------------------------------------------------- /esbuild-react/ui-src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import App from "./App"; 3 | 4 | ReactDOM.render(, document.getElementById("root")); 5 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /esbuild-react/ui-src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /esbuild-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import reactRefresh from "@vitejs/plugin-react-refresh"; 3 | import { viteSingleFile } from "vite-plugin-singlefile"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | root: "./ui-src", 8 | plugins: [reactRefresh(), viteSingleFile()], 9 | build: { 10 | target: "esnext", 11 | assetsInlineLimit: 100000000, 12 | chunkSizeWarningLimit: 100000000, 13 | cssCodeSplit: false, 14 | outDir: "../dist", 15 | rollupOptions: { 16 | output: { 17 | inlineDynamicImports: true, 18 | }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /go-to/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: ["eslint:recommended", "plugin:@figma/figma-plugins/recommended"], 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | project: "./tsconfig.json", 7 | }, 8 | root: true, 9 | }; 10 | -------------------------------------------------------------------------------- /go-to/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | package.json 3 | package-lock.json 4 | node_modules/* 5 | -------------------------------------------------------------------------------- /go-to/code.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | 3 | // The 'input' event listens for text change in the Quick Actions box after a plugin is 'Tabbed' into. 4 | figma.parameters.on('input', ({ key, query, result }) => { 5 | switch (key) { 6 | case 'name': 7 | const filter = node => node.name.toLowerCase().startsWith(query.toLowerCase()); 8 | 9 | // Always suggest all of the pages in the file 10 | const pages = figma.root 11 | .findChildren(node => query.length === 0 ? true : filter(node)) 12 | .map(node => ({ name: `${node.name} (page)`, data: { name: node.name, id: node.id } })); 13 | 14 | // Only show layers when the user types a query 15 | const nodes = query.length > 0 ? figma.currentPage 16 | .findAll(node => filter(node)) : []; 17 | 18 | const formattedNodes = nodes.map((node) => { 19 | const name = `${node.name} [${node.id}]`; 20 | return ({ name, data: { name: node.name, id: node.id } }); 21 | }); 22 | const suggestions = [...pages, ...formattedNodes]; 23 | result.setSuggestions(suggestions); 24 | break; 25 | default: 26 | return; 27 | } 28 | }); 29 | 30 | // When the user presses Enter after inputting all parameters, the 'run' event is fired. 31 | figma.on('run', ({ parameters }) => { 32 | startPluginWithParameters(parameters); 33 | }); 34 | 35 | // Start the plugin with parameters 36 | async function startPluginWithParameters(parameters) { 37 | const { name, id } = parameters['name']; 38 | const node = await figma.getNodeByIdAsync(id); 39 | if (node) { 40 | // Node found, so we need to go to that node 41 | if (node.type === "PAGE") { 42 | await figma.setCurrentPageAsync(node); 43 | } else { 44 | // Figure out if the node is on the right page, 45 | // otherwise, we need to switch to that page before zooming into the view 46 | let currentParent = node.parent; 47 | while (currentParent.type !== "PAGE") { 48 | currentParent = currentParent.parent; 49 | } 50 | await figma.setCurrentPageAsync(currentParent); 51 | figma.viewport.scrollAndZoomIntoView([node]); 52 | figma.currentPage.selection = [node as SceneNode]; 53 | } 54 | } else { 55 | // Could not find node 56 | figma.notify(`Could not find node with name=${name}`); 57 | } 58 | figma.closePlugin(); 59 | } 60 | -------------------------------------------------------------------------------- /go-to/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Go to", 3 | "id": "1020470258681431214", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "parameters": [ 8 | { 9 | "name": "Page or Layer Name", 10 | "key": "name", 11 | "description": "Name of the location you want to go to" 12 | } 13 | ], 14 | "networkAccess": { 15 | "allowedDomains": ["none"] 16 | }, 17 | "documentAccess": "dynamic-page" 18 | } 19 | -------------------------------------------------------------------------------- /go-to/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/README.md: -------------------------------------------------------------------------------- 1 | # drag-and-drop-hosted 2 | 3 | This sample plugin demonstrates drag-and-drop from a hosted plugin UI. 4 | 5 | ## Development 6 | 7 | Serve up the `index.html` locally on port 4004, e.g. 8 | 9 | ``` 10 | python3 -m http.server 4004 11 | ``` 12 | 13 | Build the files: 14 | 15 | ``` 16 | tsc 17 | ``` 18 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { themeColors: true }); 2 | 3 | figma.on('drop', (event: DropEvent) => { 4 | console.log('[plugin] drop received!!', event); 5 | const { items } = event; 6 | 7 | if (items.length > 0 && items[0].type === 'image/svg+xml') { 8 | const data = items[0].data 9 | 10 | const newNode = figma.createNodeFromSvg(data); 11 | newNode.x = event.absoluteX; 12 | newNode.y = event.absoluteY; 13 | 14 | figma.currentPage.selection = [newNode]; 15 | } 16 | 17 | return false; 18 | }); -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 72 | 73 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "drag-and-drop-hosted", 3 | "name": "drag-and-drop-hosted", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "ui": "ui.html", 8 | "networkAccess": { 9 | "allowedDomains": ["*"], 10 | "reasoning": "localhost is used for development purposes only." 11 | }, 12 | "documentAccess": "dynamic-page" 13 | } 14 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "strict": true, 6 | "typeRoots": [ 7 | "../node_modules/@types", 8 | "../node_modules/@figma" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /icon-drag-and-drop-hosted/ui.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icon-drag-and-drop/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /icon-drag-and-drop/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__); 2 | 3 | // Receive the drop event from the UI 4 | figma.on('drop', (event) => { 5 | const { files, node, dropMetadata } = event; 6 | 7 | if (files.length > 0 && files[0].type === 'image/svg+xml') { 8 | files[0].getTextAsync().then((text) => { 9 | if (dropMetadata.parentingStrategy === 'page') { 10 | const newNode = figma.createNodeFromSvg(text); 11 | newNode.x = event.absoluteX; 12 | newNode.y = event.absoluteY; 13 | 14 | figma.currentPage.selection = [newNode]; 15 | } else if (dropMetadata.parentingStrategy === 'immediate') { 16 | const newNode = figma.createNodeFromSvg(text); 17 | 18 | // We can only append page nodes to documents 19 | if ('appendChild' in node && node.type !== 'DOCUMENT') { 20 | node.appendChild(newNode); 21 | } 22 | 23 | newNode.x = event.x; 24 | newNode.y = event.y; 25 | 26 | figma.currentPage.selection = [newNode]; 27 | } 28 | }); 29 | 30 | return false; 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /icon-drag-and-drop/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "icon-drag-and-drop-sample", 3 | "name": "Icon Drag-and-Drop Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": [ 10 | "https://static.figma.com/api/figma-extension-api-0.0.1.css", 11 | "https://fonts.googleapis.com", 12 | "https://fonts.gstatic.com" 13 | ] 14 | }, 15 | "documentAccess": "dynamic-page" 16 | } 17 | -------------------------------------------------------------------------------- /icon-drag-and-drop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /icon-drag-and-drop/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24 | 25 | 26 |

Drag any icon to the canvas:

27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 | Page 65 |
66 |
67 | In parentable node 68 |
69 |
70 | 71 | 102 | 103 | -------------------------------------------------------------------------------- /invert-image/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /invert-image/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /invert-image/code.ts: -------------------------------------------------------------------------------- 1 | async function invertPaint(paint: Paint) { 2 | // Only invert the color for images (but you could do it 3 | // for solid paints and gradients if you wanted) 4 | if (paint.type === 'IMAGE') { 5 | // Paints reference images by their hash. 6 | const image = figma.getImageByHash(paint.imageHash) 7 | 8 | // Get the bytes for this image. However, the "bytes" in this 9 | // context refers to the bytes of file stored in PNG format. It 10 | // needs to be decoded into RGBA so that we can easily operate 11 | // on it. 12 | const bytes = await image.getBytesAsync() 13 | 14 | // Decoding to RGBA requires browser APIs that are only available 15 | // within an iframe. So we create an invisible iframe to act as 16 | // a "worker" which will do the task of decoding and send us a 17 | // message when it's done. This worker lives in `decoder.html` 18 | figma.showUI(__html__, { visible: false }) 19 | 20 | // Send the raw bytes of the file to the worker 21 | figma.ui.postMessage(bytes) 22 | 23 | // Wait for the worker's response 24 | const newBytes: Uint8Array = await new Promise((resolve) => { 25 | figma.ui.onmessage = value => resolve(value as Uint8Array) 26 | }) 27 | 28 | // Create a new paint for the new image. Uploading the image will give us 29 | // an image hash. 30 | const newPaint = {...paint} 31 | newPaint.imageHash = figma.createImage(newBytes).hash 32 | return newPaint 33 | } 34 | return paint 35 | } 36 | 37 | async function invertIfApplicable(node: SceneNode) { 38 | // Look for fills on node types that have fills. 39 | // An alternative would be to do `if ('fills' in node) { ... } 40 | if ('fills' in node) { 41 | // Create a new array of fills, because we can't directly modify the old one 42 | const newFills = [] 43 | for (const paint of node.fills as Paint[]) { 44 | newFills.push(await invertPaint(paint)) 45 | } 46 | node.fills = newFills 47 | } 48 | } 49 | 50 | // This plugin looks at all the currently selected nodes and inverts the colors 51 | // in their image, if they use an image paint. 52 | Promise.all( 53 | figma.currentPage.selection.map(selected => invertIfApplicable(selected)) 54 | ).then(() => figma.closePlugin()).catch(() => {console.log("This operation failed")}); -------------------------------------------------------------------------------- /invert-image/decoder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /invert-image/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "invert-image-color-sample", 3 | "name": "Invert Image Color Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "decoder.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /invert-image/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /metacards/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /metacards/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /metacards/code.ts: -------------------------------------------------------------------------------- 1 | // This plugin will find links within a text node and create on canvas meta cards 2 | // of an image, title, description and link based on the tags in the head 3 | // of a webpage at the relative links 4 | 5 | // This file holds the main code for the plugins. It has access to the *document*. 6 | // You can access browser APIs in the 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-samples", 3 | "version": "1.0.0", 4 | "description": "Sample plugins using the [Figma Plugin API][docs].", 5 | "scripts": { 6 | "check": "for i in */tsconfig.json; do echo $i; (cd $(dirname $i) && tsc); done", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/figma/plugin-samples.git" 12 | }, 13 | "author": "Figma", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/figma/plugin-samples/issues" 17 | }, 18 | "homepage": "https://github.com/figma/plugin-samples#readme", 19 | "devDependencies": { 20 | "@figma/eslint-plugin-figma-plugins": "github:figma/eslint-plugin-figma-plugins", 21 | "@figma/plugin-typings": "https://github.com/figma/plugin-typings.git#dynamic-page-plugin-types", 22 | "@typescript-eslint/eslint-plugin": "^6.18.1", 23 | "@typescript-eslint/parser": "^6.18.1", 24 | "eslint": "^8.56.0", 25 | "typescript": "^5.3.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /payments/code.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | if (figma.payments.status.type === "NOT_SUPPORTED") { 3 | // FOR CREATORS MIGRATING FROM OFF-PLATFORM MONETIZATION TO ON-PLATFORM: 4 | // This means we are pre-GA, so you should not run any code related 5 | // to figma on-platform payments. You should also try to not include any 6 | // copy or references to on-platform payments in your resource. 7 | // Instead, run you existing off-platform payments code. 8 | figma.notify("RUN OFF-PLATFORM PAYMENTS CODE"); 9 | } else if (figma.payments.status.type === "PAID") { 10 | figma.notify("USER HAS PAID"); 11 | } else { 12 | const ONE_DAY_IN_SECONDS = 60 * 60 * 24; 13 | const secondsSinceFirstRun = figma.payments.getUserFirstRanSecondsAgo(); 14 | const daysSinceFirstRun = secondsSinceFirstRun / ONE_DAY_IN_SECONDS; 15 | if (daysSinceFirstRun > 3) { 16 | await figma.payments.initiateCheckoutAsync(); 17 | if (figma.payments.status.type === "UNPAID") { 18 | figma.notify("USER CANCELLED CHECKOUT"); 19 | } else { 20 | figma.notify("USER JUST SIGNED UP"); 21 | } 22 | } else { 23 | figma.notify("USER IS IN THREE DAY TRIAL PERIOD"); 24 | } 25 | } 26 | figma.closePlugin(); 27 | return figma.payments.status.type; 28 | } 29 | 30 | run(); 31 | -------------------------------------------------------------------------------- /payments/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Payments Demo", 3 | "id": "111111123", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "permissions": ["payments"], 8 | "enableProposedApi": true, 9 | "documentAccess": "dynamic-page" 10 | } 11 | -------------------------------------------------------------------------------- /piechart/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /piechart/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__) 2 | figma.ui.onmessage = numbers => { 3 | const width = 100 4 | const height = 100 5 | 6 | const frame = figma.createFrame() 7 | figma.currentPage.appendChild(frame) 8 | frame.resizeWithoutConstraints(width, height) 9 | 10 | numbers = numbers.map(x => Math.max(0, x)) 11 | const total = numbers.reduce((a, b) => a + b, 0) 12 | let start = 0; 13 | 14 | for (const num of numbers) { 15 | const c = Math.sqrt(start / total) 16 | const ellipse = figma.createEllipse() 17 | frame.appendChild(ellipse) 18 | ellipse.resizeWithoutConstraints(width, height) 19 | ellipse.fills = [{ type: 'SOLID', color: {r: c, g: c, b: c} }] 20 | ellipse.constraints = {horizontal: 'STRETCH', vertical: 'STRETCH'} 21 | ellipse.arcData = { 22 | startingAngle: (start / total - 0.25) * 2 * Math.PI, 23 | endingAngle: ((start + num) / total - 0.25) * 2 * Math.PI, 24 | innerRadius: 0, 25 | } 26 | start += num 27 | } 28 | 29 | figma.closePlugin() 30 | } 31 | -------------------------------------------------------------------------------- /piechart/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "pie-chart-sample", 3 | "name": "Pie Chart Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": [ 10 | "https://static.figma.com/api/figma-extension-api-0.0.1.css", 11 | "https://fonts.googleapis.com", 12 | "https://fonts.gstatic.com" 13 | ] 14 | }, 15 | "documentAccess": "dynamic-page" 16 | } 17 | -------------------------------------------------------------------------------- /piechart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /piechart/ui.html: -------------------------------------------------------------------------------- 1 | 2 |

Pie Chart Sample

3 |

4 | Values: 5 | 6 |

7 | 8 |

9 | 30 | -------------------------------------------------------------------------------- /png-crop/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /png-crop/code.ts: -------------------------------------------------------------------------------- 1 | figma.notify('Drop a PNG image', { timeout: Infinity }); 2 | 3 | figma.on('drop', (event) => { 4 | if (event.files && event.files.length > 0) { 5 | const file = event.files[0]; 6 | 7 | if (file.type === 'image/png') { 8 | file.getBytesAsync().then(bytes => { 9 | const image = figma.createImage(bytes) 10 | 11 | const ellipse = figma.createEllipse(); 12 | ellipse.x = event.x 13 | ellipse.y = event.y 14 | ellipse.resize(320, 320); 15 | ellipse.fills = [{ 16 | imageHash: image.hash, 17 | scaleMode: "FILL", 18 | scalingFactor: 1, 19 | type: "IMAGE", 20 | }]; 21 | }); 22 | 23 | return false; 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /png-crop/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "png-crop", 3 | "name": "PNG Crop", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /png-crop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /post-message/code.js: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { height: 200, width: 200 }); 2 | 3 | figma.ui.onmessage = (message) => { 4 | console.log("CODE LOG", message); 5 | // sending a message back to the ui in a half second... 6 | setTimeout(() => { 7 | figma.ui.postMessage(`code.js: ${Date.now()}`, { origin: "*" }); 8 | }, 500); 9 | }; 10 | -------------------------------------------------------------------------------- /post-message/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "post-message", 3 | "name": "Post Message", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "ui": "ui.html", 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /post-message/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Post Message 8 | 9 | 10 |

Open console and click this button

11 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resizer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /resizer/.gitignore: -------------------------------------------------------------------------------- 1 | code.js -------------------------------------------------------------------------------- /resizer/code.ts: -------------------------------------------------------------------------------- 1 | // Check that the input is a valid number 2 | function setSuggestionsForNumberInput(query: string, result: SuggestionResults, completions?: string[]) { 3 | if (query === '') { 4 | result.setSuggestions(completions ?? []) 5 | } else if (!Number.isFinite(Number(query))) { 6 | result.setError("Please enter a numeric value") 7 | } else if (Number(query) <= 0) { 8 | result.setError("Must be larger than 0") 9 | } else { 10 | const filteredCompletions = completions ? completions.filter(s => s.includes(query) && s !== query) : [] 11 | result.setSuggestions([query, ...filteredCompletions]) 12 | } 13 | } 14 | 15 | // The 'input' event listens for text change in the Quick Actions box after a plugin is 'Tabbed' into. 16 | figma.parameters.on('input', ({query, key, result}: ParameterInputEvent) => { 17 | if (figma.currentPage.selection.length === 0) { 18 | result.setError('Please select one or mode nodes first') 19 | return 20 | } 21 | 22 | switch (key) { 23 | case 'width': { 24 | const widthSizes = ['640', '800', '960', '1024', '1280'] 25 | setSuggestionsForNumberInput(query, result, widthSizes) 26 | break; 27 | } 28 | case 'height': { 29 | const heightSizes = ['480', '600', '720', '768', '960'] 30 | setSuggestionsForNumberInput(query, result, heightSizes) 31 | break; 32 | } 33 | case 'scale': 34 | setSuggestionsForNumberInput(query, result) 35 | break 36 | default: 37 | return 38 | } 39 | }) 40 | 41 | // When the user presses Enter after inputting all parameters, the 'run' event is fired. 42 | figma.on('run', ({command, parameters}: RunEvent) => { 43 | if (!parameters) { 44 | return; 45 | } 46 | if (command == 'relative') { 47 | resizeRelative(parameters) 48 | } else { 49 | resizeAbsolute(parameters) 50 | } 51 | figma.closePlugin() 52 | }) 53 | 54 | function resizeRelative(parameters: ParameterValues) { 55 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 56 | const scale = parseFloat(parameters.scale) 57 | 58 | for (const node of figma.currentPage.selection) { 59 | if ('rescale' in node) { 60 | node.rescale(scale) 61 | } 62 | } 63 | } 64 | 65 | function resizeAbsolute(parameters: ParameterValues) { 66 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 67 | const width = parseInt(parameters.width) 68 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 69 | const height = parseInt(parameters.height) 70 | 71 | for (const node of figma.currentPage.selection) { 72 | if ('resize' in node) { 73 | node.resize(width, height) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /resizer/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Resizer", 3 | "id": "994314547752599560", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "menu": [ 8 | { 9 | "name": "Absolute", 10 | "command": "absolute", 11 | "parameters": [ 12 | { 13 | "name": "Width", 14 | "key": "width", 15 | "description": "enter a width in pixels" 16 | }, 17 | { 18 | "name": "Height", 19 | "key": "height", 20 | "description": "enter a height in pixels" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "Relative", 26 | "command": "relative", 27 | "parameters": [ 28 | { 29 | "name": "Scale", 30 | "key": "scale", 31 | "description": "enter a relative scale factor" 32 | } 33 | ] 34 | } 35 | ], 36 | "networkAccess": { 37 | "allowedDomains": ["none"] 38 | }, 39 | "documentAccess": "dynamic-page" 40 | } 41 | -------------------------------------------------------------------------------- /resizer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"], 6 | "strictNullChecks": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sierpinski/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /sierpinski/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /sierpinski/code.ts: -------------------------------------------------------------------------------- 1 | const page = figma.currentPage; 2 | const depthLimit = 5; 3 | 4 | function visit(x, y, radius, depth, dir) { 5 | if (depth < depthLimit) { 6 | if (dir !== 'E') visit(x - radius * 3 / 2, y, radius / 2, depth + 1, 'W'); 7 | if (dir !== 'W') visit(x + radius * 3 / 2, y, radius / 2, depth + 1, 'E'); 8 | if (dir !== 'S') visit(x, y - radius * 3 / 2, radius / 2, depth + 1, 'N'); 9 | if (dir !== 'N') visit(x, y + radius * 3 / 2, radius / 2, depth + 1, 'S'); 10 | } 11 | const node = figma.createEllipse() 12 | node.x = x - radius 13 | node.y = y - radius 14 | node.resizeWithoutConstraints(2 * radius, 2 * radius) 15 | const fills: SolidPaint[] = [{ 16 | type: 'SOLID', 17 | color: { r: 0.5 + x / 600, g: 0.5 + y / 600, b: 1 - depth / depthLimit } 18 | }] 19 | 20 | node.fills = fills 21 | 22 | page.appendChild(node) 23 | } 24 | 25 | visit(0, 0, 100, 0, null); 26 | figma.closePlugin(); 27 | -------------------------------------------------------------------------------- /sierpinski/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sierpinski-sample", 3 | "name": "Sierpinski Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /sierpinski/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /snippet-saver/code.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_DATA_KEY = "snippets"; 2 | 3 | if (figma.mode === "codegen") { 4 | figma.codegen.on("preferenceschange", (event) => { 5 | if (event.propertyName === "editor") { 6 | figma.showUI(__html__, { width: 400, height: 450 }); 7 | } 8 | }); 9 | figma.ui.on("message", (event) => { 10 | if (event.type === "INITIALIZE") { 11 | handleCurrentSelection(); 12 | } else if (event.type === "SAVE") { 13 | figma.currentPage.selection[0].setPluginData(PLUGIN_DATA_KEY, event.data); 14 | } else { 15 | console.log("UNKNOWN EVENT", event); 16 | } 17 | }); 18 | figma.on("selectionchange", () => { 19 | handleCurrentSelection(); 20 | }); 21 | figma.codegen.on("generate", async () => { 22 | handleCurrentSelection(); 23 | const pluginDataArray = findPluginDataArrayForSelection(); 24 | const snippets = []; 25 | pluginDataArray.forEach((pluginData) => 26 | JSON.parse(pluginData).forEach((a) => snippets.push(a)) 27 | ); 28 | 29 | if (!snippets.length) { 30 | snippets.push({ 31 | title: "Snippets", 32 | code: "No snippets found. Add snippets with the Snippet Editor in the Plugin's Inspect settings!", 33 | language: "PLAINTEXT", 34 | }); 35 | } 36 | 37 | return snippets; 38 | }); 39 | } 40 | 41 | function findPluginDataArrayForSelection() { 42 | const data = []; 43 | function pluginDataForNode(node) { 44 | const pluginData = node.getPluginData(PLUGIN_DATA_KEY); 45 | // skipping duplicates. why? 46 | // component instances have same pluginData as mainComponent, unless they have override pluginData. 47 | if (pluginData && data.indexOf(pluginData) === -1) { 48 | data.push(pluginData); 49 | } 50 | } 51 | const currentNode = figma.currentPage.selection[0]; 52 | pluginDataForNode(currentNode); 53 | if (currentNode.type === "INSTANCE") { 54 | pluginDataForNode(currentNode.mainComponent); 55 | if (currentNode.mainComponent.parent.type === "COMPONENT_SET") { 56 | pluginDataForNode(currentNode.mainComponent.parent); 57 | } 58 | } else if (currentNode.type === "COMPONENT") { 59 | if (currentNode.parent.type === "COMPONENT_SET") { 60 | pluginDataForNode(currentNode.parent); 61 | } 62 | } 63 | return data; 64 | } 65 | 66 | function handleCurrentSelection() { 67 | const node = figma.currentPage.selection[0]; 68 | try { 69 | const nodePluginData = node ? node.getPluginData(PLUGIN_DATA_KEY) : null; 70 | const nodeId = node ? node.id : null; 71 | const nodeType = node ? node.type : null; 72 | figma.ui.postMessage({ 73 | type: "SELECTION", 74 | nodeId, 75 | nodeType, 76 | nodePluginData, 77 | }); 78 | return nodePluginData; 79 | } catch (e) { 80 | // no ui open. ignore this. 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /snippet-saver/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snippet Saver", 3 | "id": "239174719128739123", 4 | "api": "1.0.0", 5 | "editorType": ["dev"], 6 | "capabilities": ["codegen"], 7 | "permissions": [], 8 | "codegenLanguages": [{ "label": "Snippets", "value": "snippets" }], 9 | "codegenPreferences": [ 10 | { 11 | "itemType": "action", 12 | "propertyName": "editor", 13 | "label": "Snippet Editor" 14 | } 15 | ], 16 | "main": "code.js", 17 | "ui": "ui.html", 18 | "networkAccess": { "allowedDomains": ["none"] }, 19 | "documentAccess": "dynamic-page" 20 | } 21 | -------------------------------------------------------------------------------- /snippet-saver/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Snippet Editor 7 | 60 | 61 | 62 |
63 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /stats/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /stats/code.ts: -------------------------------------------------------------------------------- 1 | let nodeCount = 0 2 | const nodeTypeCounts = new Map() 3 | 4 | async function initialize() { 5 | await figma.loadAllPagesAsync() 6 | } 7 | 8 | function visit(node) { 9 | nodeTypeCounts.set(node.type, 1 + (nodeTypeCounts.get(node.type) | 0)) 10 | nodeCount++ 11 | if (node.children) node.children.forEach(visit) 12 | } 13 | 14 | initialize(); 15 | visit(figma.root) 16 | 17 | let text = `Node count: ${nodeCount}\n` 18 | const nodeTypes = Array.from(nodeTypeCounts.entries()) 19 | nodeTypes.sort((a, b) => b[1] - a[1]) 20 | text += `Node types:` + nodeTypes.map(([k,v]) => `\n ${k}: ${v}`).join('') 21 | 22 | figma.showUI(` 23 | ${text} 24 | `, {width: 500, height: 500}) 25 | -------------------------------------------------------------------------------- /stats/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "document-statistics-sample", 3 | "name": "Document Statistics Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /stats/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /styles-to-variables/code.js: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | 3 | initialize(); 4 | 5 | async function initialize() { 6 | const styles = await figma.getLocalPaintStylesAsync(); 7 | const tokenDataMap = styles.reduce(styleToTokenDataMap, {}); 8 | const tokenData = Object.values(tokenDataMap); 9 | createTokens(tokenData); 10 | figma.closePlugin(); 11 | } 12 | 13 | function createTokens(tokenData) { 14 | if (tokenData.length <= 0) { 15 | figma.notify("No convertible styles found. :("); 16 | return; 17 | } 18 | const collection = figma.variables.createVariableCollection(`Style Tokens`); 19 | let aliasCollection; 20 | const modeId = collection.modes[0].modeId; 21 | collection.renameMode(modeId, "Style"); 22 | console.log(tokenData); 23 | tokenData.forEach(({ color, hex, opacity, tokens }) => { 24 | if (tokens.length > 1) { 25 | aliasCollection = 26 | aliasCollection || 27 | figma.variables.createVariableCollection(`Style Tokens: Aliased`); 28 | aliasCollection.renameMode(aliasCollection.modes[0].modeId, "Style"); 29 | const opacityName = 30 | opacity === 1 ? "" : ` (${Math.round(opacity * 100)}%)`; 31 | const parentToken = figma.variables.createVariable( 32 | `${hex.toUpperCase()}${opacityName}`, 33 | aliasCollection, 34 | "COLOR" 35 | ); 36 | parentToken.setValueForMode(aliasCollection.modes[0].modeId, { 37 | r: color.r, 38 | g: color.g, 39 | b: color.b, 40 | a: opacity, 41 | }); 42 | tokens.forEach((name) => { 43 | const token = figma.variables.createVariable(name, collection, "COLOR"); 44 | token.setValueForMode(modeId, { 45 | type: "VARIABLE_ALIAS", 46 | id: parentToken.id, 47 | }); 48 | }); 49 | } else { 50 | const token = figma.variables.createVariable( 51 | tokens[0], 52 | collection, 53 | "COLOR" 54 | ); 55 | token.setValueForMode(modeId, { 56 | r: color.r, 57 | g: color.g, 58 | b: color.b, 59 | a: opacity, 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | function styleToTokenDataMap(into, current) { 66 | const paints = current.paints.filter( 67 | ({ visible, type }) => visible && type === "SOLID" 68 | ); 69 | if (paints.length === 1) { 70 | const { 71 | blendMode, 72 | color: { r, g, b }, 73 | opacity, 74 | type, 75 | } = paints[0]; 76 | const hex = rgbToHex({ r, g, b }); 77 | if (blendMode === "NORMAL") { 78 | const uniqueId = [hex, opacity].join("-"); 79 | into[uniqueId] = into[uniqueId] || { 80 | color: { r, g, b }, 81 | hex, 82 | opacity, 83 | tokens: [], 84 | }; 85 | into[uniqueId].tokens.push(current.name); 86 | } else { 87 | // do something different i guess 88 | } 89 | } 90 | return into; 91 | } 92 | 93 | function rgbToHex({ r, g, b }) { 94 | const toHex = (value) => { 95 | const hex = Math.round(value * 255).toString(16); 96 | return hex.length === 1 ? "0" + hex : hex; 97 | }; 98 | 99 | const hex = [toHex(r), toHex(g), toHex(b)].join(""); 100 | return `#${hex}`; 101 | } 102 | -------------------------------------------------------------------------------- /styles-to-variables/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Styles to Variables", 3 | "id": "1225497355831498032", 4 | "api": "1.0.0", 5 | "editorType": ["figma"], 6 | "permissions": [], 7 | "main": "code.js", 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /svg-inserter/.gitignore: -------------------------------------------------------------------------------- 1 | code.js -------------------------------------------------------------------------------- /svg-inserter/code.ts: -------------------------------------------------------------------------------- 1 | const icons = [ 2 | { name: "menu", 3 | svg: '' }, 4 | { name: "settings", 5 | svg: '' }, 6 | ] 7 | 8 | const cachedThumbnails = icons.reduce((a, icon) => { 9 | return { ...a, [icon.name]: processSVG({icon: icon.svg, color: 'black', size: '40'})} 10 | }, {}) 11 | 12 | function processSVG({icon, color, size}: {icon: string, color: string, size: string}): string { 13 | return icon.replace(/\$size\$/g, size).replace(/\$color\$/g, color) 14 | } 15 | 16 | // The 'input' event listens for text change in the Quick Actions box after a plugin is 'Tabbed' into. 17 | figma.parameters.on('input', ({key, query, parameters, result}: ParameterInputEvent) => { 18 | switch (key) { 19 | case 'icon': 20 | result.setSuggestions(icons 21 | .filter(s => s.name.includes(query)) 22 | .map(s => ({ name: s.name, data: s.svg, icon: cachedThumbnails[s.name] }))) 23 | break 24 | case 'size': 25 | const sizes = ['24', '48', '96', '192'] 26 | result.setSuggestions(sizes.filter(s => s.includes(query))) 27 | break 28 | case 'color': 29 | const colors = ['black', 'blue', 'red', 'green'] 30 | const icon = parameters.icon 31 | const suggestions = colors 32 | .filter(s => s.includes(query)) 33 | .map(s => ({ name: s, icon: processSVG({icon, size: '40', color: s}) })) 34 | 35 | // Add a custom freeform suggestion with a color preview. 36 | if (query) { 37 | suggestions.unshift({ name: query, icon: processSVG({icon, size: '40', color: query}) }) 38 | } 39 | 40 | result.setSuggestions(suggestions) 41 | break 42 | default: 43 | return 44 | } 45 | }) 46 | 47 | // When the user presses Enter after inputting all parameters, the 'run' event is fired. 48 | figma.on('run', ({parameters}: RunEvent) => { 49 | if (parameters) { 50 | startPluginWithParameters(parameters) 51 | } 52 | }) 53 | 54 | // Start the plugin with parameters 55 | function startPluginWithParameters(parameters: ParameterValues) { 56 | const icon = parameters.icon 57 | const size = parameters.size 58 | 59 | // Color is an optional parameter, so it is possibly undefined. 60 | const color = parameters.color ?? 'black' 61 | 62 | const processedSvg = icon.replace(/\$size\$/g, size).replace(/\$color\$/g, color) 63 | 64 | figma.createNodeFromSvg(processSVG({icon, size, color})) 65 | 66 | figma.closePlugin() 67 | } 68 | -------------------------------------------------------------------------------- /svg-inserter/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SVG Inserter", 3 | "id": "994353700568293385", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "parameters": [ 8 | { 9 | "name": "Icon name", 10 | "key": "icon", 11 | "description": "enter the name of the icon you want to insert" 12 | }, 13 | { 14 | "name": "Size", 15 | "key": "size", 16 | "description": "enter the size of the icon you want to insert", 17 | "allowFreeform": true 18 | }, 19 | { 20 | "name": "Color", 21 | "key": "color", 22 | "description": "enter the color of the icon you want to insert", 23 | "optional": true 24 | } 25 | ], 26 | "networkAccess": { 27 | "allowedDomains": ["none"] 28 | }, 29 | "documentAccess": "dynamic-page" 30 | } 31 | -------------------------------------------------------------------------------- /svg-inserter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /text-review/code.js: -------------------------------------------------------------------------------- 1 | if (figma.command === "textreview") { 2 | figma.on("textreview", ({ text }) => { 3 | // Replacing "✨Figma✨" with "xxxxxxx" so we don't highlight "✨Figma✨" 4 | const cleanText = text.replace(/✨Figma✨/g, "xxxxxxx"); 5 | const matches = Array.from(cleanText.matchAll(/figma/gi)); 6 | return matches.map((match) => ({ 7 | start: match.index, 8 | end: match.index + match[0].length, 9 | suggestions: ["✨Figma✨"], 10 | color: "GREEN", 11 | })); 12 | }); 13 | } else { 14 | figma.notify("I am running like any other plugin!"); 15 | figma.closePlugin(); 16 | } 17 | -------------------------------------------------------------------------------- /text-review/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Text Review", 3 | "id": "111111", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma"], 7 | "capabilities": ["textreview"], 8 | "enableProposedApi": true, 9 | "networkAccess": { 10 | "allowedDomains": ["none"] 11 | }, 12 | "documentAccess": "dynamic-page" 13 | } 14 | -------------------------------------------------------------------------------- /text-search/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /text-search/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /text-search/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__); 2 | 3 | let timer = undefined; 4 | 5 | figma.ui.onmessage = async message => { 6 | if (message.query !== undefined) { 7 | if (timer) { 8 | clearTimeout(timer); 9 | } 10 | if (message.query) { 11 | searchFor(message.query); 12 | } 13 | } else if (message.show) { 14 | const node = await figma.getNodeByIdAsync(message.show); 15 | if (node.type === 'DOCUMENT' || node.type === 'PAGE') { 16 | // DOCUMENTs and PAGEs can't be put into the selection. 17 | return; 18 | } 19 | figma.currentPage.selection = [node]; 20 | figma.viewport.scrollAndZoomIntoView([node]); 21 | } else if (message.quit) { 22 | figma.closePlugin(); 23 | } 24 | } 25 | 26 | // This is a generator that recursively produces all the nodes in subtree 27 | // starting at the given node 28 | function* walkTree(node) { 29 | yield node; 30 | const children = node.children; 31 | if (children) { 32 | for (const child of children) { 33 | yield* walkTree(child) 34 | } 35 | } 36 | } 37 | 38 | function searchFor(query) { 39 | query = query.toLowerCase() 40 | const walker = walkTree(figma.currentPage) 41 | 42 | function processOnce() { 43 | const results = []; 44 | let count = 0; 45 | let done = true; 46 | let res 47 | while (!(res = walker.next()).done) { 48 | const node = res.value 49 | if (node.type === 'TEXT') { 50 | const characters = node.characters.toLowerCase() 51 | if (characters.includes(query)) { 52 | results.push(node.id); 53 | } 54 | } 55 | if (++count === 1000) { 56 | done = false 57 | timer = setTimeout(processOnce, 20) 58 | break 59 | } 60 | } 61 | 62 | figma.ui.postMessage({ query, results, done }) 63 | } 64 | 65 | processOnce() 66 | } 67 | -------------------------------------------------------------------------------- /text-search/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "text-search-sample", 3 | "name": "Text Search Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /text-search/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /text-search/ui.html: -------------------------------------------------------------------------------- 1 |

Search Query:

2 |

3 |

4 |

5 |

6 | 90 | -------------------------------------------------------------------------------- /trivia/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | package.json 3 | package-lock.json -------------------------------------------------------------------------------- /trivia/code.ts: -------------------------------------------------------------------------------- 1 | interface Category { 2 | name: string; 3 | data: string; 4 | } 5 | interface TriviaParameters { 6 | number?: number; 7 | category?: number; 8 | difficulty?: string; 9 | type?: string; 10 | } 11 | 12 | interface TriviaResponse { 13 | responseCode: number; 14 | results: TriviaResult[]; 15 | } 16 | interface TriviaResult { 17 | category: string; 18 | type: string; 19 | difficulty: string; 20 | question: string; 21 | correctAnswer: string; 22 | incorrectAnswers: string[]; 23 | } 24 | 25 | const types = [ 26 | { name: "multiple choice", data: "multiple" }, 27 | { name: "true/false", data: "boolean" }, 28 | ]; 29 | const difficulties = ["easy", "medium", "hard"]; 30 | const numbers = ["5", "10", "15", "20", "25", "30"]; 31 | let categories: Category[] = []; 32 | 33 | startUI(); 34 | loadCategories(); 35 | 36 | // The 'input' event listens for text change in the Quick Actions box after a plugin is 'Tabbed' into. 37 | figma.parameters.on("input", ({ key, query, result }: ParameterInputEvent) => { 38 | switch (key) { 39 | case "number": 40 | result.setSuggestions(numbers.filter((s) => s.includes(query))); 41 | break; 42 | case "category": 43 | result.setLoadingMessage('Loading categories from API...') 44 | result.setSuggestions(categories.filter((s) => s.name.includes(query))) 45 | break; 46 | case "difficulty": 47 | result.setSuggestions(difficulties.filter((s) => s.includes(query))); 48 | break; 49 | case "type": 50 | result.setSuggestions(types.filter((s) => s.name.includes(query))); 51 | break; 52 | default: 53 | return; 54 | } 55 | }); 56 | 57 | // When the user presses Enter after inputting all parameters, the 'run' event is fired. 58 | figma.on("run", async ({ parameters }: RunEvent) => { 59 | await loadFonts() 60 | if (parameters) { 61 | await startPluginWithParameters(parameters); 62 | } 63 | }); 64 | 65 | // Start the plugin with parameters 66 | async function startPluginWithParameters(parameters: ParameterValues) { 67 | const validatedParameters = validateParameters(parameters); 68 | if (!validatedParameters) { 69 | figma.notify( 70 | "One of the parameters was not correctly specified. Please try again." 71 | ); 72 | figma.closePlugin(); 73 | } 74 | const url = createAPIUrl(validatedParameters); 75 | figma.ui.postMessage({ type: "questions", url }); 76 | } 77 | 78 | function loadCategories() { 79 | figma.ui.postMessage({ type: "category", url: "https://opentdb.com/api_category.php" }); 80 | } 81 | 82 | function validateParameters( 83 | parameters: ParameterValues 84 | ): TriviaParameters | null { 85 | const numberString = parameters.number; 86 | const number = validateNumber(numberString) 87 | if (number === null) { 88 | return null 89 | } 90 | 91 | const category = parameters.category; 92 | const difficulty = parameters.difficulty; 93 | const type = parameters.type; 94 | 95 | return { number, category, difficulty, type }; 96 | } 97 | 98 | function validateNumber(numberString: string) { 99 | let number: number; 100 | if (numberString) { 101 | number = Number(numberString); 102 | if (Number.isNaN(number)) { 103 | return null; 104 | } 105 | } 106 | return number 107 | } 108 | 109 | function createAPIUrl(parameters: TriviaParameters): string { 110 | const { number, category, difficulty, type } = parameters; 111 | let url = "https://opentdb.com/api.php?"; 112 | url += number ? `amount=${number}&` : `amount=10&`; 113 | url += category ? `category=${category}&` : ""; 114 | url += difficulty ? `difficulty=${difficulty}&` : ""; 115 | url += type ? `type=${type}` : ""; 116 | 117 | return url; 118 | } 119 | 120 | async function startUI() { 121 | figma.showUI(__html__, { visible: false }); 122 | figma.ui.onmessage = async (msg) => { 123 | if (msg.type === "category") { 124 | categories = msg.response.trivia_categories.map( 125 | (c: { name: string; id: number }) => ({ name: c.name, data: c.id }) 126 | ); 127 | } else if (msg.type === "questions") { 128 | const response = msg.response 129 | const triviaResponse: TriviaResponse = { 130 | responseCode: response.response_code, 131 | results: response.results.map(r => ({ 132 | category: r.category, 133 | type: r.type, 134 | difficulty: r.difficulty, 135 | question: r.question, 136 | correctAnswer: r.correct_answer, 137 | incorrectAnswers: r.incorrect_answers 138 | })) 139 | } 140 | displayQuestions(triviaResponse) 141 | figma.closePlugin(); 142 | } 143 | }; 144 | } 145 | 146 | function displayQuestions(triviaResponse: TriviaResponse) { 147 | const frame = figma.createFrame() 148 | frame.fills = [] 149 | for (const result of triviaResponse.results) { 150 | const resultFrame = displaySingleQuestion(result) 151 | frame.appendChild(resultFrame) 152 | frame.layoutGrow = 1 153 | } 154 | frame.layoutMode = "VERTICAL" 155 | frame.primaryAxisSizingMode = 'AUTO' 156 | frame.counterAxisSizingMode = 'AUTO' 157 | frame.itemSpacing = 50 158 | } 159 | 160 | function displaySingleQuestion(triviaResult: TriviaResult) { 161 | const frame = figma.createFrame() 162 | frame.fills = [{type: 'SOLID', color: {r: 1, g: 1, b: 1}}] 163 | frame.verticalPadding = 25 164 | frame.horizontalPadding = 25 165 | frame.primaryAxisSizingMode = 'AUTO' 166 | frame.counterAxisSizingMode = 'AUTO' 167 | frame.itemSpacing = 10 168 | frame.cornerRadius = 20 169 | 170 | const questionText = createText(triviaResult.question, 20) 171 | frame.appendChild(questionText) 172 | 173 | const optionsFrame = figma.createFrame() 174 | optionsFrame.itemSpacing = 10 175 | optionsFrame.layoutMode = "VERTICAL" 176 | optionsFrame.counterAxisSizingMode = "AUTO" 177 | 178 | const options = reorderOptions(triviaResult.correctAnswer, triviaResult.incorrectAnswers) 179 | for (const option of options) { 180 | const optionText = createText(option, 24) 181 | optionsFrame.appendChild(optionText) 182 | } 183 | frame.appendChild(optionsFrame) 184 | 185 | const correctAnswerFrame = figma.createFrame() 186 | const correctAnswer = createText(triviaResult.correctAnswer, 24) 187 | correctAnswerFrame.appendChild(correctAnswer) 188 | 189 | correctAnswerFrame.fills = [{type: 'SOLID', color: {r: .46, g: .86, b: .46} }] 190 | correctAnswerFrame.resize(correctAnswer.width, correctAnswer.height) 191 | 192 | const answerCover = figma.createFrame() 193 | answerCover.resize(correctAnswer.width, correctAnswer.height) 194 | answerCover.fills = [{type: 'SOLID', color: {r: 0, g: 0, b: 0}}] 195 | correctAnswerFrame.appendChild(answerCover) 196 | frame.appendChild(correctAnswerFrame) 197 | frame.layoutMode = "VERTICAL" 198 | return frame 199 | } 200 | 201 | function reorderOptions(correctAnswer: string, incorrectAnswers: string[]) { 202 | const options = [...incorrectAnswers, correctAnswer] 203 | 204 | // Reorder the questions 205 | options.sort(() => Math.random() > 0.5 ? 1 : -1) 206 | return options 207 | } 208 | 209 | function createText(characters: string, size: number) { 210 | const text = figma.createText() 211 | text.fontName = {family: 'Roboto', style: 'Regular'} 212 | text.characters = characters 213 | text.fontSize = size 214 | return text 215 | } 216 | 217 | async function loadFonts() { 218 | await figma.loadFontAsync({ family: "Roboto", style: "Regular" }) 219 | } -------------------------------------------------------------------------------- /trivia/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Trivia", 3 | "id": "1022628489809897477", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "ui": "ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "parameters": [ 9 | { 10 | "name": "Number", 11 | "key": "number", 12 | "description": "Number of questions (max 50)", 13 | "allowFreeform": true, 14 | "optional": true 15 | }, 16 | { 17 | "name": "Category", 18 | "key": "category", 19 | "description": "What knowledge do you want to put to the test?", 20 | "optional": true 21 | }, 22 | { 23 | "name": "Difficulty", 24 | "key": "difficulty", 25 | "description": "How difficult do you want these questions to be?", 26 | "optional": true 27 | }, 28 | { 29 | "name": "Type", 30 | "key": "type", 31 | "description": "Multiple choice or true/false", 32 | "optional": true 33 | } 34 | ], 35 | "networkAccess": { 36 | "allowedDomains": ["https://opentdb.com/"] 37 | }, 38 | "documentAccess": "dynamic-page" 39 | } 40 | -------------------------------------------------------------------------------- /trivia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /trivia/ui.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /variables-import-export/README.md: -------------------------------------------------------------------------------- 1 | # Variables Import / Export Sample 2 | 3 | This is a code sample to demonstrate how to use Variables APIs to import or export design tokens in a Figma plugin or widget. This sample imports and exports Variables formatted using the [W3C Design Tokens spec](https://design-tokens.github.io/community-group/format/#design-token). 4 | 5 | This code is distributed strictly for educational purposes. It's purely a starting point and you should modify it as needed for your specific purposes. As such, there are some [known limitations](#known-limitations). 6 | 7 | ## Usage 8 | 9 | Tokens should be defined in JSON following the W3C Design Tokens spec. An example below: 10 | 11 | ```json 12 | { 13 | "group name": { 14 | "token name": { 15 | "$value": 1234, 16 | "$type": "number" 17 | } 18 | }, 19 | "alias name": { 20 | "$value": "{group name.token name}" 21 | } 22 | } 23 | ``` 24 | 25 | ## Known Limitations 26 | 27 | - Import doesn't support multiple modes - as there is no concept of modes in the W3C Design Spec at this time 28 | - You can only import 1 collection and mode at a time 29 | - For importing/overwriting variables to a specific mode, see [Alternative Sophisticated UI](#alternative-sophisticated-ui) 30 | - Variables Import / Export only supports types that currently exist in the W3C Design Token Spec and in Figma. In other words, only `color`, `number`, and alias design tokens are currently supported 31 | 32 | ## Alternative Sophisticated UI 33 | 34 | Our Developer Advocates, [Jake Albaugh](https://github.com/jake-figma) and [Akbar Mirza](https://github.com/akbarbmirza/), have created a sample with a slightly more sophisticated UI. You can find that at https://github.com/jake-figma/variables-import-export 35 | -------------------------------------------------------------------------------- /variables-import-export/code.js: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | 3 | function createCollection(name) { 4 | const collection = figma.variables.createVariableCollection(name); 5 | const modeId = collection.modes[0].modeId; 6 | return { collection, modeId }; 7 | } 8 | 9 | function createToken(collection, modeId, type, name, value) { 10 | const token = figma.variables.createVariable(name, collection, type); 11 | token.setValueForMode(modeId, value); 12 | return token; 13 | } 14 | 15 | function createVariable(collection, modeId, key, valueKey, tokens) { 16 | const token = tokens[valueKey]; 17 | return createToken(collection, modeId, token.resolvedType, key, { 18 | type: "VARIABLE_ALIAS", 19 | id: `${token.id}`, 20 | }); 21 | } 22 | 23 | function importJSONFile({ fileName, body }) { 24 | const json = JSON.parse(body); 25 | const { collection, modeId } = createCollection(fileName); 26 | const aliases = {}; 27 | const tokens = {}; 28 | Object.entries(json).forEach(([key, object]) => { 29 | traverseToken({ 30 | collection, 31 | modeId, 32 | type: json.$type, 33 | key, 34 | object, 35 | tokens, 36 | aliases, 37 | }); 38 | }); 39 | processAliases({ collection, modeId, aliases, tokens }); 40 | } 41 | 42 | function processAliases({ collection, modeId, aliases, tokens }) { 43 | aliases = Object.values(aliases); 44 | let generations = aliases.length; 45 | while (aliases.length && generations > 0) { 46 | for (let i = 0; i < aliases.length; i++) { 47 | const { key, type, valueKey } = aliases[i]; 48 | const token = tokens[valueKey]; 49 | if (token) { 50 | aliases.splice(i, 1); 51 | tokens[key] = createVariable(collection, modeId, key, valueKey, tokens); 52 | } 53 | } 54 | generations--; 55 | } 56 | } 57 | 58 | function isAlias(value) { 59 | return value.toString().trim().charAt(0) === "{"; 60 | } 61 | 62 | function traverseToken({ 63 | collection, 64 | modeId, 65 | type, 66 | key, 67 | object, 68 | tokens, 69 | aliases, 70 | }) { 71 | type = type || object.$type; 72 | // if key is a meta field, move on 73 | if (key.charAt(0) === "$") { 74 | return; 75 | } 76 | if (object.$value !== undefined) { 77 | if (isAlias(object.$value)) { 78 | const valueKey = object.$value 79 | .trim() 80 | .replace(/\./g, "/") 81 | .replace(/[\{\}]/g, ""); 82 | if (tokens[valueKey]) { 83 | tokens[key] = createVariable(collection, modeId, key, valueKey, tokens); 84 | } else { 85 | aliases[key] = { 86 | key, 87 | type, 88 | valueKey, 89 | }; 90 | } 91 | } else if (type === "color") { 92 | tokens[key] = createToken( 93 | collection, 94 | modeId, 95 | "COLOR", 96 | key, 97 | parseColor(object.$value) 98 | ); 99 | } else if (type === "number") { 100 | tokens[key] = createToken( 101 | collection, 102 | modeId, 103 | "FLOAT", 104 | key, 105 | object.$value 106 | ); 107 | } else { 108 | console.log("unsupported type", type, object); 109 | } 110 | } else { 111 | Object.entries(object).forEach(([key2, object2]) => { 112 | if (key2.charAt(0) !== "$") { 113 | traverseToken({ 114 | collection, 115 | modeId, 116 | type, 117 | key: `${key}/${key2}`, 118 | object: object2, 119 | tokens, 120 | aliases, 121 | }); 122 | } 123 | }); 124 | } 125 | } 126 | 127 | async function exportToJSON() { 128 | const collections = await figma.variables.getLocalVariableCollectionsAsync(); 129 | const files = []; 130 | for (const collection of collections) { 131 | files.push(...(await processCollection(collection))); 132 | } 133 | figma.ui.postMessage({ type: "EXPORT_RESULT", files }); 134 | } 135 | 136 | async function processCollection({ name, modes, variableIds }) { 137 | const files = []; 138 | for (const mode of modes) { 139 | const file = { fileName: `${name}.${mode.name}.tokens.json`, body: {} }; 140 | for (const variableId of variableIds) { 141 | const { name, resolvedType, valuesByMode } = 142 | await figma.variables.getVariableByIdAsync(variableId); 143 | const value = valuesByMode[mode.modeId]; 144 | if (value !== undefined && ["COLOR", "FLOAT"].includes(resolvedType)) { 145 | let obj = file.body; 146 | name.split("/").forEach((groupName) => { 147 | obj[groupName] = obj[groupName] || {}; 148 | obj = obj[groupName]; 149 | }); 150 | obj.$type = resolvedType === "COLOR" ? "color" : "number"; 151 | if (value.type === "VARIABLE_ALIAS") { 152 | const currentVar = await figma.variables.getVariableByIdAsync( 153 | value.id 154 | ); 155 | obj.$value = `{${currentVar.name.replace(/\//g, ".")}}`; 156 | } else { 157 | obj.$value = resolvedType === "COLOR" ? rgbToHex(value) : value; 158 | } 159 | } 160 | } 161 | files.push(file); 162 | } 163 | return files; 164 | } 165 | 166 | figma.ui.onmessage = async (e) => { 167 | console.log("code received message", e); 168 | if (e.type === "IMPORT") { 169 | const { fileName, body } = e; 170 | importJSONFile({ fileName, body }); 171 | } else if (e.type === "EXPORT") { 172 | await exportToJSON(); 173 | } 174 | }; 175 | if (figma.command === "import") { 176 | figma.showUI(__uiFiles__["import"], { 177 | width: 500, 178 | height: 500, 179 | themeColors: true, 180 | }); 181 | } else if (figma.command === "export") { 182 | figma.showUI(__uiFiles__["export"], { 183 | width: 500, 184 | height: 500, 185 | themeColors: true, 186 | }); 187 | } 188 | 189 | function rgbToHex({ r, g, b, a }) { 190 | if (a !== 1) { 191 | return `rgba(${[r, g, b] 192 | .map((n) => Math.round(n * 255)) 193 | .join(", ")}, ${a.toFixed(4)})`; 194 | } 195 | const toHex = (value) => { 196 | const hex = Math.round(value * 255).toString(16); 197 | return hex.length === 1 ? "0" + hex : hex; 198 | }; 199 | 200 | const hex = [toHex(r), toHex(g), toHex(b)].join(""); 201 | return `#${hex}`; 202 | } 203 | 204 | function parseColor(color) { 205 | color = color.trim(); 206 | const rgbRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/; 207 | const rgbaRegex = 208 | /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$/; 209 | const hslRegex = /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/; 210 | const hslaRegex = 211 | /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*([\d.]+)\s*\)$/; 212 | const hexRegex = /^#([A-Fa-f0-9]{3}){1,2}$/; 213 | const floatRgbRegex = 214 | /^\{\s*r:\s*[\d\.]+,\s*g:\s*[\d\.]+,\s*b:\s*[\d\.]+(,\s*opacity:\s*[\d\.]+)?\s*\}$/; 215 | 216 | if (rgbRegex.test(color)) { 217 | const [, r, g, b] = color.match(rgbRegex); 218 | return { r: parseInt(r) / 255, g: parseInt(g) / 255, b: parseInt(b) / 255 }; 219 | } else if (rgbaRegex.test(color)) { 220 | const [, r, g, b, a] = color.match(rgbaRegex); 221 | return { 222 | r: parseInt(r) / 255, 223 | g: parseInt(g) / 255, 224 | b: parseInt(b) / 255, 225 | a: parseFloat(a), 226 | }; 227 | } else if (hslRegex.test(color)) { 228 | const [, h, s, l] = color.match(hslRegex); 229 | return hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100); 230 | } else if (hslaRegex.test(color)) { 231 | const [, h, s, l, a] = color.match(hslaRegex); 232 | return Object.assign( 233 | hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100), 234 | { a: parseFloat(a) } 235 | ); 236 | } else if (hexRegex.test(color)) { 237 | const hexValue = color.substring(1); 238 | const expandedHex = 239 | hexValue.length === 3 240 | ? hexValue 241 | .split("") 242 | .map((char) => char + char) 243 | .join("") 244 | : hexValue; 245 | return { 246 | r: parseInt(expandedHex.slice(0, 2), 16) / 255, 247 | g: parseInt(expandedHex.slice(2, 4), 16) / 255, 248 | b: parseInt(expandedHex.slice(4, 6), 16) / 255, 249 | }; 250 | } else if (floatRgbRegex.test(color)) { 251 | return JSON.parse(color); 252 | } else { 253 | throw new Error("Invalid color format"); 254 | } 255 | } 256 | 257 | function hslToRgbFloat(h, s, l) { 258 | const hue2rgb = (p, q, t) => { 259 | if (t < 0) t += 1; 260 | if (t > 1) t -= 1; 261 | if (t < 1 / 6) return p + (q - p) * 6 * t; 262 | if (t < 1 / 2) return q; 263 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 264 | return p; 265 | }; 266 | 267 | if (s === 0) { 268 | return { r: l, g: l, b: l }; 269 | } 270 | 271 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 272 | const p = 2 * l - q; 273 | const r = hue2rgb(p, q, (h + 1 / 3) % 1); 274 | const g = hue2rgb(p, q, h % 1); 275 | const b = hue2rgb(p, q, (h - 1 / 3) % 1); 276 | 277 | return { r, g, b }; 278 | } 279 | -------------------------------------------------------------------------------- /variables-import-export/export.html: -------------------------------------------------------------------------------- 1 | 71 |
72 | 73 | 77 |
78 | 93 | -------------------------------------------------------------------------------- /variables-import-export/import.html: -------------------------------------------------------------------------------- 1 | 64 |
65 | 71 | 106 | 107 |
108 | 109 | 133 | -------------------------------------------------------------------------------- /variables-import-export/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Variables Import Export", 3 | "id": "1225498390710809905", 4 | "api": "1.0.0", 5 | "editorType": ["figma"], 6 | "permissions": [], 7 | "main": "code.js", 8 | "menu": [ 9 | { "command": "import", "name": "Import Variables" }, 10 | { "command": "export", "name": "Export Variables" } 11 | ], 12 | "ui": { "import": "import.html", "export": "export.html" }, 13 | "documentAccess": "dynamic-page" 14 | } 15 | -------------------------------------------------------------------------------- /vector-path/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /vector-path/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /vector-path/code.ts: -------------------------------------------------------------------------------- 1 | const node = figma.createVector() 2 | 3 | // This creates a triangle 4 | node.vectorPaths = [{ 5 | windingRule: 'EVENODD', 6 | data: 'M 0 100 L 100 100 L 50 0 Z', 7 | }] 8 | 9 | // Put the node in the center of the viewport so we can see it 10 | node.x = figma.viewport.center.x - node.width / 2 11 | node.y = figma.viewport.center.y - node.height / 2 12 | 13 | figma.closePlugin() 14 | -------------------------------------------------------------------------------- /vector-path/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "vector-path-sample", 3 | "name": "Vector Path Sample", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figma", "figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /vector-path/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": [ 6 | "../node_modules/@types", 7 | "../node_modules/@figma" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /vote-tally/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:@figma/figma-plugins/recommended", 8 | ], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | }, 13 | root: true, 14 | }; 15 | -------------------------------------------------------------------------------- /vote-tally/.gitignore: -------------------------------------------------------------------------------- 1 | code.js 2 | -------------------------------------------------------------------------------- /vote-tally/code.ts: -------------------------------------------------------------------------------- 1 | // This plugin will find all stamps close to a sticky and generate a tally of 2 | // all the stamps (votes) next to a sticky on the page 3 | 4 | // This file holds the main code for the plugins. It has access to the *document*. 5 | // You can access browser APIs such as the network by creating a UI which contains 6 | // a full browser environment (see documentation). 7 | 8 | // Declare an array of stickies 9 | let stickies: SceneNode[] = []; 10 | let stamps: SceneNode[] = []; 11 | 12 | void initialize(); 13 | 14 | async function initialize() { 15 | // Find all stamps on the page 16 | await figma.loadAllPagesAsync(); 17 | stamps = figma.currentPage.findAll(node => node.type === "STAMP"); 18 | 19 | // If there is a selection, use that 20 | if(figma.currentPage.selection.length > 0){ 21 | 22 | stickies = figma.currentPage.selection.filter(node => node.type === "STICKY"); 23 | 24 | // If there is no selection, use all stickies 25 | } else { 26 | stickies = figma.currentPage.findChildren(node => node.type === "STICKY"); 27 | } 28 | 29 | if(stickies.length > 0){ 30 | 31 | // Tally up each sticky's votes 32 | Promise.all(stickies.map(sticky => tallyStickyVotes(sticky))).then(() => { 33 | // Notify the user 34 | figma.notify(`Tallied ${stickies.length} ${stickies.length > 1? 'votes' : 'vote' }.`); 35 | figma.closePlugin(); 36 | }).catch(() => { 37 | figma.closePlugin(); 38 | }); 39 | } else { 40 | figma.notify(`No votes found. Add some!`); 41 | // Make sure to close the plugin when you're done. Otherwise the plugin will 42 | // keep running, which shows the cancel button at the bottom of the screen. 43 | figma.closePlugin(); 44 | } 45 | } 46 | 47 | function isWithinProximity(node1, node2, tolerance = 40){ 48 | 49 | const node1Center = {x: node1.x + node1.width/2, y: node1.y + node1.height/2}; 50 | const node2Center = {x: node2.x + node2.width/2, y: node2.y + node2.height/2}; 51 | const proximity = node1.width/2 + tolerance; 52 | 53 | return Math.abs(node1Center.x - node2Center.x) <= proximity && Math.abs(node1Center.y - node2Center.y) <= proximity; 54 | 55 | } 56 | 57 | // Find all votes (stamps) attributed to a sticky (by proximity) 58 | function getStampsNearNode(sticky){ 59 | 60 | const stampGroups = {}; 61 | 62 | stamps.forEach(stamp => { 63 | if( isWithinProximity(sticky,stamp,60) ){ 64 | if(!stampGroups[stamp.name]){ 65 | stampGroups[stamp.name] = []; 66 | } 67 | stampGroups[stamp.name].push(stamp); 68 | } 69 | }); 70 | 71 | return stampGroups; 72 | } 73 | 74 | // Tally all the votes and arrange them in a consumable way 75 | async function tallyStickyVotes(sticky, removeStamps = false){ 76 | 77 | const stampVotes = getStampsNearNode(sticky); 78 | 79 | // Before setting the characters of the tally's text, we need to load the default font 80 | await figma.loadFontAsync({family: "Inter", style: "Medium"}); 81 | 82 | // Create an Auto Layout frame to hold the tally 83 | const tallyFrame = figma.createFrame(); 84 | tallyFrame.cornerRadius = 8; 85 | tallyFrame.resize(128,128); 86 | tallyFrame.layoutMode = "VERTICAL"; 87 | tallyFrame.paddingLeft = tallyFrame.paddingRight = tallyFrame.paddingTop = tallyFrame.paddingBottom = 16; 88 | tallyFrame.itemSpacing = 8; 89 | tallyFrame.primaryAxisSizingMode = tallyFrame.counterAxisSizingMode = "AUTO"; 90 | tallyFrame.fills = [{type : "SOLID", color: { r: 1, g: 1, b: 1 }}]; 91 | tallyFrame.x = sticky.x + sticky.width + 20; 92 | tallyFrame.y = sticky.y; 93 | tallyFrame.strokes = [{type : "SOLID", color: { r: 0, g: 0, b: 0 }}]; 94 | tallyFrame.strokeWeight = 2; 95 | tallyFrame.primaryAxisAlignItems = tallyFrame.counterAxisAlignItems = "CENTER"; 96 | 97 | const tallyTitle = figma.createText(); 98 | tallyFrame.appendChild(tallyTitle); 99 | tallyTitle.characters = "Votes"; 100 | tallyTitle.textAutoResize = "HEIGHT"; 101 | tallyTitle.layoutAlign = "INHERIT"; 102 | tallyTitle.textAlignHorizontal = "CENTER"; 103 | 104 | // Sort all votes to show the highest votes first 105 | const sortedVotes = Object.keys(stampVotes) 106 | .sort((a,b) => { 107 | return stampVotes[b].length - stampVotes[a].length; 108 | }); 109 | 110 | // Render each stamp with the number of times it was used 111 | sortedVotes.forEach(stampName => { 112 | 113 | const tallyLine = figma.createFrame(); 114 | tallyFrame.appendChild(tallyLine); 115 | tallyLine.layoutMode = "HORIZONTAL"; 116 | tallyLine.itemSpacing = 8; 117 | tallyLine.layoutAlign = "STRETCH"; 118 | tallyLine.primaryAxisAlignItems = "MIN"; 119 | tallyLine.counterAxisAlignItems = "CENTER"; 120 | tallyLine.counterAxisSizingMode = "AUTO"; 121 | tallyLine.primaryAxisSizingMode = "AUTO"; 122 | tallyLine.clipsContent = false; 123 | 124 | // If the vote was a profile, show each profile stamp...otherwise, just show a single stamp 125 | const stampsToShow = stampName === "Profile"? stampVotes[stampName] : [stampVotes[stampName][0]]; 126 | const stampFrame = figma.createFrame(); 127 | tallyLine.appendChild(stampFrame); 128 | stampFrame.layoutMode = "HORIZONTAL"; 129 | stampFrame.itemSpacing = 0; 130 | stampFrame.layoutAlign = "MIN"; 131 | stampFrame.primaryAxisAlignItems = "SPACE_BETWEEN"; 132 | stampFrame.counterAxisAlignItems = "CENTER"; 133 | stampFrame.primaryAxisSizingMode = "FIXED"; 134 | stampFrame.clipsContent = false; 135 | stampFrame.resize(stampsToShow.length * 32 * 0.66 ,32); 136 | stampsToShow.forEach(currentStamp => { 137 | const stamp = currentStamp.clone(); 138 | stamp.rotation = 0; 139 | stamp.resize(32,32); 140 | stampFrame.appendChild(stamp); 141 | }); 142 | 143 | const tally = figma.createText(); 144 | tallyLine.appendChild(tally); 145 | tally.characters = `${stampVotes[stampName].length}`; 146 | tally.textAutoResize = "HEIGHT"; 147 | tally.layoutAlign = "INHERIT"; 148 | 149 | // Clear the stamps if removeStamps is set to true 150 | if(removeStamps){ 151 | stampVotes[stampName].forEach(stamp => stamp.remove()) 152 | } 153 | 154 | }); 155 | 156 | } -------------------------------------------------------------------------------- /vote-tally/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vote Tally", 3 | "id": "1010229183389496680", 4 | "api": "1.0.0", 5 | "main": "code.js", 6 | "editorType": ["figjam"], 7 | "networkAccess": { 8 | "allowedDomains": ["none"] 9 | }, 10 | "documentAccess": "dynamic-page" 11 | } 12 | -------------------------------------------------------------------------------- /vote-tally/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vote-Tally", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@figma/plugin-typings": { 8 | "version": "1.31.0", 9 | "resolved": "https://registry.npmjs.org/@figma/plugin-typings/-/plugin-typings-1.31.0.tgz", 10 | "integrity": "sha512-9mOEn31yyta5QC3E3lpAaoHKeEP1/NH++nFOeXjQrfOEgVnX7za9+aG5biVxPS9DZpBcquIIHqyq31Ejnu8MgQ==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vote-tally/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vote-Tally", 3 | "version": "1.0.0", 4 | "description": "A FigJam plugin to count votes (stamps) near a sticky and show a tally on the page.", 5 | "main": "code.js", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.json" 8 | }, 9 | "author": "Figma", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/figma/plugin-samples/issues" 13 | }, 14 | "homepage": "https://github.com/figma/plugin-samples#readme", 15 | "devDependencies": { 16 | "@figma/plugin-typings": "^1.31.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vote-tally/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "typeRoots": ["../node_modules/@types", "../node_modules/@figma"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /webpack-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /webpack-react/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Webpack + React Sample", 3 | "id": "webpack-react-sample", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": ["none"] 10 | }, 11 | "documentAccess": "dynamic-page" 12 | } 13 | -------------------------------------------------------------------------------- /webpack-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "code.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack --mode=development --watch", 9 | "build": "webpack --mode=production" 10 | }, 11 | "author": "Figma", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@figma/plugin-typings": "*", 15 | "@types/node": "^16.7.1", 16 | "@types/react": "^18.2.6", 17 | "@types/react-dom": "^18.2.4", 18 | "css-loader": "^6.2.0", 19 | "html-inline-script-webpack-plugin": "^3.1.0", 20 | "html-webpack-plugin": "^5.3.2", 21 | "style-loader": "^3.2.1", 22 | "ts-loader": "^9.2.5", 23 | "typescript": "^4.3.5", 24 | "url-loader": "^4.1.1", 25 | "webpack": "^5.82.0", 26 | "webpack-cli": "^5.1.1" 27 | }, 28 | "dependencies": { 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webpack-react/src/code.ts: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { themeColors: true, height: 300 }); 2 | 3 | figma.ui.onmessage = (msg) => { 4 | if (msg.type === "create-rectangles") { 5 | const nodes = []; 6 | 7 | for (let i = 0; i < msg.count; i++) { 8 | const rect = figma.createRectangle(); 9 | rect.x = i * 150; 10 | rect.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }]; 11 | figma.currentPage.appendChild(rect); 12 | nodes.push(rect); 13 | } 14 | 15 | figma.currentPage.selection = nodes; 16 | figma.viewport.scrollAndZoomIntoView(nodes); 17 | } 18 | 19 | figma.closePlugin(); 20 | }; 21 | -------------------------------------------------------------------------------- /webpack-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webpack-react/src/ui.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg: var(--figma-color-bg); 3 | --color-bg-hover: var(--figma-color-bg-hover); 4 | --color-bg-active: var(--figma-color-bg-pressed); 5 | --color-border: var(--figma-color-border); 6 | --color-border-focus: var(--figma-color-border-selected); 7 | --color-icon: var(--figma-color-icon); 8 | --color-text: var(--figma-color-text); 9 | --color-bg-brand: var(--figma-color-bg-brand); 10 | --color-bg-brand-hover: var(--figma-color-bg-brand-hover); 11 | --color-bg-brand-active: var(--figma-color-bg-brand-pressed); 12 | --color-border-brand: var(--figma-color-border-brand); 13 | --color-border-brand-focus: var(--figma-color-border-selected-strong); 14 | --color-text-brand: var(--figma-color-text-onbrand); 15 | } 16 | 17 | html, 18 | body, 19 | main { 20 | height: 100%; 21 | } 22 | 23 | body, 24 | input, 25 | button { 26 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 27 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 28 | font-size: 1rem; 29 | text-align: center; 30 | } 31 | 32 | body { 33 | background: var(--color-bg); 34 | color: var(--color-text); 35 | margin: 0; 36 | } 37 | 38 | button { 39 | border-radius: 0.25rem; 40 | background: var(--color-bg); 41 | color: var(--color-text); 42 | cursor: pointer; 43 | border: 1px solid var(--color-border); 44 | padding: 0.5rem 1rem; 45 | } 46 | button:hover { 47 | background-color: var(--color-bg-hover); 48 | } 49 | button:active { 50 | background-color: var(--color-bg-active); 51 | } 52 | button:focus-visible { 53 | border: none; 54 | outline-color: var(--color-border-focus); 55 | } 56 | button.brand { 57 | --color-bg: var(--color-bg-brand); 58 | --color-text: var(--color-text-brand); 59 | --color-bg-hover: var(--color-bg-brand-hover); 60 | --color-bg-active: var(--color-bg-brand-active); 61 | --color-border: transparent; 62 | --color-border-focus: var(--color-border-brand-focus); 63 | } 64 | 65 | input { 66 | background: 1px solid var(--color-bg); 67 | border: 1px solid var(--color-border); 68 | color: 1px solid var(--color-text); 69 | padding: 0.5rem; 70 | } 71 | 72 | input:focus-visible { 73 | border-color: var(--color-border-focus); 74 | outline-color: var(--color-border-focus); 75 | } 76 | 77 | svg { 78 | stroke: var(--color-icon, rgba(0, 0, 0, 0.9)); 79 | } 80 | 81 | main { 82 | align-items: center; 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: center; 86 | } 87 | 88 | section { 89 | align-items: center; 90 | display: flex; 91 | flex-direction: column; 92 | justify-content: center; 93 | margin-bottom: 1rem; 94 | } 95 | section > * + * { 96 | margin-top: 0.5rem; 97 | } 98 | footer > * + * { 99 | margin-left: 0.5rem; 100 | } 101 | 102 | img { 103 | height: auto; 104 | width: 2rem; 105 | } 106 | -------------------------------------------------------------------------------- /webpack-react/src/ui.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /webpack-react/src/ui.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import "./ui.css"; 4 | 5 | declare function require(path: string): any; 6 | 7 | function App() { 8 | const inputRef = React.useRef(null); 9 | 10 | const onCreate = () => { 11 | const count = Number(inputRef.current?.value || 0); 12 | parent.postMessage( 13 | { pluginMessage: { type: "create-rectangles", count } }, 14 | "*" 15 | ); 16 | }; 17 | 18 | const onCancel = () => { 19 | parent.postMessage({ pluginMessage: { type: "cancel" } }, "*"); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 26 |

Rectangle Creator

27 |
28 |
29 | 30 | 31 |
32 |
33 | 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | ReactDOM.createRoot(document.getElementById("react-page")).render(); 43 | -------------------------------------------------------------------------------- /webpack-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react", 5 | "typeRoots": [ 6 | "./node_modules/@types", 7 | "./node_modules/@figma" 8 | ] 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /webpack-react/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | 7 | module.exports = (env, argv) => ({ 8 | mode: argv.mode === 'production' ? 'production' : 'development', 9 | 10 | // This is necessary because Figma's 'eval' works differently than normal eval 11 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 12 | 13 | entry: { 14 | ui: './src/ui.tsx', // The entry point for your UI code 15 | code: './src/code.ts', // The entry point for your plugin code 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | // Converts TypeScript code to JavaScript 21 | { 22 | test: /\.tsx?$/, 23 | use: 'ts-loader', 24 | exclude: /node_modules/, 25 | }, 26 | 27 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 28 | { 29 | test: /\.css$/, 30 | use: ['style-loader', 'css-loader'], 31 | }, 32 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 33 | // { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] } 34 | { 35 | test: /\.svg/, 36 | type: 'asset/inline', 37 | }, 38 | ], 39 | }, 40 | 41 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 42 | resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'] }, 43 | 44 | output: { 45 | filename: (pathData) => { 46 | return pathData.chunk.name === 'code' 47 | ? 'code.js' 48 | : '[name].[contenthash].js'; 49 | }, 50 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist" 51 | // Clean the output directory before emit. 52 | clean: true, 53 | }, 54 | 55 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 56 | plugins: [ 57 | new webpack.DefinePlugin({ 58 | global: {}, // Fix missing symbol error when running in developer VM 59 | }), 60 | new HtmlWebpackPlugin({ 61 | inject: 'body', 62 | template: './src/ui.html', 63 | filename: 'ui.html', 64 | chunks: ['ui'], 65 | }), 66 | new HtmlInlineScriptPlugin({ 67 | htmlMatchPattern: [/ui.html/], 68 | scriptMatchPattern: [/.js$/], 69 | }), 70 | ], 71 | }); 72 | --------------------------------------------------------------------------------