├── .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 | 
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 |
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 | Create Bar Chart
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 |
36 |
37 |
38 | Create
39 |
40 | Cancel
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 | Parenting strategy:
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 | Create Pie Chart
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 | send a message!
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 | Show previous
4 | Show next
5 | Close
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 | Export Variables
73 |
77 |
78 |
93 |
--------------------------------------------------------------------------------
/variables-import-export/import.html:
--------------------------------------------------------------------------------
1 |
64 |
65 |
71 |
72 | {
73 | "color": {
74 | "grouptyped": {
75 | "$type": "color",
76 | "brown": { "$value": "#a2845e" },
77 | "danger-deep": { "$value": "{color.valuetyped.danger}" }
78 | },
79 | "valuetyped": {
80 | "red": {
81 | "$value": "{color.deep.deep.deep.deep.deep}"
82 | },
83 | "danger": { "$value": "{color.valuetyped.red}" }
84 | },
85 | "deep": {"deep": {"deep": {"deep": {"deep": {"$type": "color", "$value": "#FF0000" }}}}}
86 | },
87 | "spacing": {
88 | "$type": "number",
89 | "some numbers": {
90 | "spacer0": {"$value": 0},
91 | "spacerXs": {"$value": 4},
92 | "spacerS": {"$value": 8},
93 | "spacerM": {"$value": 16},
94 | "spacerX": {"$value": 24},
95 | "spacerXl": {"$value": 32},
96 | "spacerXxl": {"$value": 40},
97 | "spacex": {
98 | "funniness": {"$value": 0},
99 | "cleverness": {"$value": 1}
100 | }
101 | }
102 | }
103 | }
104 |
106 | Import Variables
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 |
32 |
33 |
34 | Create
35 |
36 | Cancel
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 |
--------------------------------------------------------------------------------