[go: up one dir, main page]

Skip to content
Creating a Figma Plugin With Svelte

Created: – Last Updated:

JavaScript

Introduction

For me personally Figma (opens in a new tab) was certainly the tool that changed my webdesign workflow the most, and for the better. Coming from Photoshop it’s the much better tool for the job and it also enabled me way quicker to write plugins for it. If you know HTML, CSS, and JS you can write a Figma plugin! The Figma Developers documentation (opens in a new tab) explains the fundamentals about Figma plugins and also how you can e.g. create one with React. However, in this tutorial you’ll (potentially) step out of your comfort zone and learn how to use Svelte (opens in a new tab), TypeScript (opens in a new tab), and Rollup (opens in a new tab) to write a plugin for Figma.

The popularity of Svelte is rising, more and more people enjoy writing smaller to bigger apps with it. So it’s certainly good to have it in your toolset and to have some familiarity with it. Coming from React I most enjoyed the “simplicity” to some aspects of writing code in Svelte, as where React felt really verbose and clunky in comparison. Another thing you’ll learn to appreciate is the output and speed of Svelte apps. Lastly, why should you care about Figma? Well, it’s a super popular tool amongst individuals and larger teams – and super extensible with plugins.

Here’s what you’ll build. You can pull a random image from Unsplash (opens in a new tab) in the given size:

Showcasing the Figma plugin in action. After selecting valid inputs for width & height an image is added to the canvas. Invalid inputs throw an error.

The finished project is available on GitHub at figma-plugin-svelte-example (opens in a new tab). You can also use this as a template for your next project.

Conceptual Guide

Here’s what you’ll use and build explained on a higher level as a short summary:

  1. Inside Figma initialize a new plugin to create the necessary files and learn how to run a local plugin
  2. Add dependencies like Svelte, Rollup, TypeScript, and Figma Plugin DS Svelte (opens in a new tab) to build the app with hot-reloading support and modern output
  3. Use the Figma Plugin DS Svelte UI kit to build out the UI
  4. Leverage the browser fetch API to pull a random image from Unsplash and place it onto the current Figma frame using Figma’s plugin APIs

Setup Guide

Initializing a Plugin

Open the Figma desktop app and if you haven’t already log into your account. Create a new design file in your drafts as you’ll use this as a testground for the plugin. Once you have the design file open, go to Menu => Plugins => Development => New Plugin….

First, it’ll ask for a name (choose the name “SvelteTutorial”) and whether you want to create a “Figma design + Figjam” or “Figma design” plugin. Choose “Figma Design”. In the next step, choose the “Empty” template. The “Save As” dialogue asks you for a directory where you want to create the project. Figma will create a folder with the given name inside your provided folder. Once that’s done you should see a code.js and manifest.json file inside the newly created folder. Delete the code.js file, it’ll be autogenerated later.

The manifest.json will contain something like this:

manifest.json
json
{  "name": "SvelteTutorial",  "id": "1234567890123456789",  "api": "1.0.0",  "main": "code.js",  "editorType": ["figma"]}

You can learn more about the manifest.json file in the Figma Plugin Manifest documentation (opens in a new tab) but in a nutshell: This is the file with the plugin’s unique ID (assigned during the New Plugin… flow) and settings regarding the plugin. main points to the JavaScript code of our plugin. ui specifies the HTML file used in the iframe modal.

Add a new key called ui to manifest.json and change the location of main:

manifest.json
json
{  "name": "SvelteTutorial",  "id": "1234567890123456789",  "api": "1.0.0",  "main": "dist/code.js",  "ui": "dist/ui.html",  "editorType": ["figma"]}

Since you’ll be using TypeScript to write the source files and Rollup to compile the files into a dist folder, the manifest.json now points to the (soon to be) compiled files.

Adding Dependencies

Initialize a new package.json with npm init -y and add the following dependencies (use the “Copy” button for easier installation):

sh
npm install @figma/plugin-typings @rollup/plugin-commonjs @rollup/plugin-html @rollup/plugin-node-resolve @rollup/plugin-typescript cssnano figma-plugin-ds-svelte postcss rollup rollup-plugin-postcss rollup-plugin-svelte rollup-plugin-svg rollup-plugin-terser svelte tslib typescript

Also, create a .gitignore will the following contents:

.gitignore
text
node_modulesdist.cache

Since you’ll use TypeScript a tsconfig.json for the compiler is necessary. You can use this configuration:

tsconfig.json
json
{  "compilerOptions": {    "target": "es6",    "esModuleInterop": true,    "moduleResolution": "node",    "forceConsistentCasingInFileNames": true,    "importsNotUsedAsValues": "error",    "skipLibCheck": true,    "typeRoots": ["./node_modules/@types", "./node_modules/@figma"]  },  "include": ["./src/**/*"]}

Lastly, add scripts to your package.json to compile files on the fly in development mode and for production when you want to publish:

package.json
json
{  "scripts": {    "build": "rollup -c",    "dev": "rollup -c -w",    "clean": "rm -rf dist",    "typecheck": "tsc --noEmit"  }}

Compiling files

In these next steps you’ll create the source files that will be compiled by Rollup and the configuration for Rollup itself.

Start by creating a directory called src and create three files in this directory: code.ts, main.ts, and App.svelte.

The code.ts will be responsible for creating the iframe and listening for messages. For now it only needs to contain this:

src/code.ts
ts
figma.showUI(__html__, { width: 320, height: 220 })

From Figma’s API documentation (opens in a new tab) about showUI:

Enables you to render UI to interact with the user, or simply to access browser APIs. This function creates a modal dialog with an iframe containing the HTML markup in the html argument.

The __html__ is a global variable that is getting defined through the ui key in manifest.json.

In order to load App.svelte the main.ts file sets up the app onto the document.body:

src/main.ts
ts
import "svelte"import App from "./App.svelte"const app = new App({  target: document.body,})export default app

And now you can write Svelte code! Start with – of course 😉 – a “Hello World”:

src/App.svelte
svelte
<div>Hello World</div>

The last missing piece to this is rollup.config.js. It’ll define how these three source files are bundled into something that Figma can work with:

rollup.config.js
js
import resolve from "@rollup/plugin-node-resolve"import commonjs from "@rollup/plugin-commonjs"import typescript from "@rollup/plugin-typescript"import html from "@rollup/plugin-html"import svelte from "rollup-plugin-svelte"import { terser } from "rollup-plugin-terser"import svg from "rollup-plugin-svg"import postcss from "rollup-plugin-postcss"import cssnano from "cssnano"const production = !process.env.ROLLUP_WATCH/** * @type {import('rollup').RollupOptions} */const mainConfig = {  input: `src/main.ts`,  output: {    format: `iife`,    name: `ui`,    file: `dist/bundle.js`,  },  plugins: [    typescript(),    svelte({      compilerOptions: {        dev: !production,      },    }),    resolve({      browser: true,      dedupe: (importee) =>        importee === `svelte` || importee.startsWith(`svelte/`),    }),    commonjs(),    svg(),    postcss({      extensions: [`.css`],      plugins: [cssnano()],    }),    html({      fileName: `ui.html`,      template({ bundle }) {        return `<!doctype html><html lang="en">  <head>    <meta charset="utf-8">    <title>Your Name</title>  </head>  <body>    <script>${bundle[`bundle.js`].code}</script>  </body></html>        `      },    }),    production && terser(),  ],  watch: {    clearScreen: false,  },}/** * @type {import('rollup').RollupOptions} */const codeConfig = {  input: `src/code.ts`,  output: {    file: `dist/code.js`,    format: `cjs`,    name: `code`,  },  plugins: [    typescript(),    commonjs(),    resolve({      browser: true,    }),    production && terser(),  ],}const config = [mainConfig, codeConfig]export default config

Now it’s time you try out what you have so far in Figma. In your terminal, run:

sh
npm run dev

You should see some output in the terminal that signals that Rollup successfully compiled the files you created:

sh
$ rollup -c -wrollup v2.67.3bundles src/main.ts dist/bundle.js...created dist/bundle.js in 573msbundles src/code.ts dist/code.js...created dist/code.js in 357ms

The manifest.json now points to these compiled files. Go back to Figma and open the draft design file that you created earlier. You now can run your plugin by doing a Right click => Plugins => Development => SvelteTutorial. You should a new window pop-up with the name of your plugin and a “Hello World” as text.

Svelte App

Now that you have a barebones project ready, it’s time for you to create the UI and add functionality to it. As a reminder: The goal of the plugin (and thus the UI) is to fetch a random image from Unsplash in a specific size (width & height) and place it onto the current Figma canvas. For this you’ll learn how to use Figma Plugin DS Svelte (opens in a new tab) as a UI kit, how to fetch images, and how to use Figma’s API to place images.

Creating the UI

The Figma Plugin DS Svelte will serve you in two ways:

  1. It has an extensive list (opens in a new tab) of CSS variables and utility classes (similar to Tailwind CSS) to help build UIs with your own set of components
  2. It features a bunch of components like Buttons, Menus, Switches, etc. to build UIs quicker

In order to use Figma Plugin DS Svelte you’ll need to add its GlobalCSS import to your App.svelte to have the CSS styles available.

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS } from "figma-plugin-ds-svelte"</script>

Then you have the utility classes (similar to Tailwind CSS) and CSS variables available. Create a wrapper component for the contents that will follow:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS } from "figma-plugin-ds-svelte"</script><div class="p-xsmall wrapper flex column justify-content-between">  <div>    Contents  </div>  <div class="mt-small footer pt-xxsmall">    Footer  </div></div><style>  .wrapper {    text-align: center;    height: 100%;  }  .footer {    border-top: 1px solid var(--silver);  }</style>

This is the syntax you should be familiar with when using Svelte. When you run the plugin inside Figma again, you should see “Contents” and “Footer” aligned and formatted in the pop-up box.

Time to add some inputs. For this, add the Type, Label, and Button component to your App.svelte:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"</script><div class="p-xsmall wrapper flex column justify-content-between">  <div>    <Type>      Get a Random Image from      <a href="https://unsplash.com/">Unsplash</a>    </Type>    <div class="flex mt-xsmall">      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Width:</Label>        Input      </div>      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Height:</Label>        Input      </div>    </div>    <div class="mt-xxsmall flex justify-content-center">      <Button>Add Image</Button>    </div>  </div>  <div class="mt-small footer pt-xxsmall">    <Type>      Read the tutorial      <a href="https://www.lekoarts.de">on lekoarts.de</a>    </Type>  </div></div>

The UI is nearly done! When you run the plugin again inside Figma you’ll see the two inputs for width and height horizontally aligned with the blue “Add Image” button below them. Now it’s time to add the proper Input to your UI.

At the time of writing this guide the Input component (opens in a new tab) from figma-plugin-ds-svelte didn’t offer the possibility to define the type on the <input /> (it defaults to type="input"). In order to be able to built-in validations for numbers the type has to be number and thus you need to “fork” the component. Or in other words: Create your own Input component by copying the contents and adapting it to use type="number".

Create a new file called Input.svelte inside src:

src/Input.svelte
svelte
<script lang="ts">  export let id = null  export let value = null  export let name = null  export let placeholder = "Placeholder"</script><input type="number" {name} required min="0" {id} bind:value {placeholder} /><style>  input {    font-size: var(--font-size-xsmall);    font-weight: var(--font-weight-normal);    letter-spacing: var(--font-letter-spacing-neg-xsmall);    line-height: var(--line-height);    position: relative;    display: flex;    overflow: visible;    align-items: center;    width: 100%;    height: 30px;    margin: 1px 0 1px 0;    padding: var(--size-xxsmall) var(--size-xxxsmall);    color: var(--black8);    border: 1px solid transparent;    border-radius: var(--border-radius-small);    outline: none;    background-color: var(--white);  }  input:hover,  input:placeholder-shown:hover {    color: var(--black8);    border: 1px solid var(--black1);    background-image: none;  }  input::selection {    color: var(--black);    background-color: var(--blue3);  }  input::placeholder {    color: var(--black3);    border: 1px solid transparent;  }  input:placeholder-shown {    color: var(--black8);    border: 1px solid var(--black1);    background-image: none;  }  input:focus:placeholder-shown {    border: 1px solid var(--blue);    outline: 1px solid var(--blue);    outline-offset: -2px;  }  input:active,  input:focus {    color: var(--black);    border: 1px solid var(--blue);    outline: 1px solid var(--blue);    outline-offset: -2px;  }</style>

It’s not exactly the same as the <Input /> component from figma-plugin-ds-svelte as I removed some CSS styles that are not needed for this example. The CSS uses the custom properties defined globally.

Now it’s time to use the <Input /> component in your App.svelte:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"  import Input from "./Input.svelte"  let disabled = true  let width = 800  let height = 800  $: disabled = !(!!width && !!height)</script><div class="p-xsmall wrapper flex column justify-content-between">  <div>    <Type>      Get a Random Image from      <a href="https://unsplash.com/">Unsplash</a>    </Type>    <div class="flex mt-xsmall">      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Width:</Label>        <Input placeholder="800" bind:value={width} />      </div>      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Height:</Label>        <Input placeholder="800" bind:value={height} />      </div>    </div>    <div class="mt-xxsmall flex justify-content-center">      <Button {disabled}>Add Image</Button>    </div>  </div>  <div class="mt-small footer pt-xxsmall">    <Type>      Read the tutorial      <a href="https://www.lekoarts.de">on lekoarts.de</a>    </Type>  </div></div>

You’re binding the value that is coming from the inputs to variables called width and height. They are necessary when adding an image and thus the “Add Image” button is disabled if one of them or both are not defined. You can try this yourself by running the plugin. You should see that the “Add Image” button is faded out and only when you add both values it gets solid.

The disabled property to control this behavior is a reactive declaration (opens in a new tab). It tells Svelte to re-run this code whenever one of these values changes (so it coerces the boolean when width and height changes again and again).

UI Functionality

Now that the UI is in place, the last task is to wire up the business logic to add an image onto the canvas. For this you’ll use the Figma plugin API figma.ui.onmessage to listen for calls in the UI. The Svelte UI will call this handler after fetching data from Unsplash’s API. There are some interesting quirks when creating an image in Figma, you’ll learn those when creating the createImage function.

Fetch Unsplash’s API

To get started, create an async function called getImage and define a API_URL constant inside your App.svelte:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"  import Input from "./Input.svelte"  const API_URL = "https://source.unsplash.com/random/"  let disabled = true  let width = 800  let height = 800  $: disabled = !(!!width && !!height)  async function getImage() {}</script>

Here’s what the function will do:

  • Validate that the necessary input (width and height) is valid
  • Call the /random API endpoint from Unsplash to get redirected to a random image
  • Fetch the image that was chosen by the /random endpoint
  • Return the image as Uint8Array since the createImage plugin API (opens in a new tab) requires this as input

The complete function should look like this:

ts
async function getImage() {  if (Math.sign(width) === -1 || Math.sign(height) === -1) {    throw new Error("Enter positive values for height/width")  }  // It redirects to the URL of the actual image  const initialResponse = await self.fetch(`${API_URL}${width}x${height}`)  if (initialResponse.ok) {    // Fetch the actual image    const res = await self.fetch(initialResponse.url)    const buffer = await res.arrayBuffer()    return new Uint8Array(buffer)  } else {    throw new Error(initialResponse.statusText)  }}

You can now successfully fetch a random image from Unsplash 🎉 As a next step you have to add an addImage function that is called when you press the “Add Image” button in the UI.

Calling postMessage

Fetching data from remote APIs should always be handled gracefully since a lot of things can go wrong. User input might be wrong, your user is offline, or the remote API itself has problems. To not let the user waiting without any information it’s good UX to add loading and error states to your UI.

Add new variables called error, loading, and imageBytes to App.svelte. Also add an async function called addImage:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"  import Input from "./Input.svelte"  const API_URL = "https://source.unsplash.com/random/"  let disabled = true  let error = ""  let loading = false  let imageBytes = null  let width = 800  let height = 800  $: disabled = !(!!width && !!height)  async function getImage() {/* Function body */}  async function addImage() {}</script>

The addImage function will do the following:

  • Reset the error state to an empty array (so that subsequent errors are not merged when you press the “Add Image” multiple times)
  • Set the loading state to true before doing any actual work
  • In a try/catch block (to gracefully handle any errors) use the getImage() function to set the imageBytes variable
  • If an error occurs, the error is populated with an error message
  • Send the imageBytes and other metadata to the UI’s iframe window via postMessage (opens in a new tab)

The last item in this list is probably the most important piece of this function. The postMessage function is the interface to communicate any arbitrary information from your UI to Figma’s onmessage API.

The first argument of postMessage can be almost any data type or plain object, as long as it’s a serializable object (more fundamentals available in Creating User Interfaces (opens in a new tab)). I’ve chosen to model the message that is sent as a “Redux Action”-like format of an object with a type and payload. This way you can use multiple different postMessage in the future if you have a need for that.

Here’s the code for the addImage function:

ts
async function addImage() {  error = ""  loading = true  try {    const result = await getImage()    loading = false    imageBytes = result  } catch (errorMsg) {    loading = false    error = errorMsg    return  }  parent.postMessage(    {      pluginMessage: {        type: "ADD_IMAGE",        payload: {          imageBytes,          width,          height,        },      },    },    "*"  )}

Hang tight, only one more things is necessary to finish up the Svelte functionality. Call the addImage function when clicking the “Add Image” button and conditionally show loading or error states.

You can do so by adding an on:click handler to the button:

svelte
<Button {disabled} on:click={addImage}>Add Image</Button>

Below the button you can show the loading and error states:

svelte
<div class="mt-xxsmall flex justify-content-center">  <Button {disabled} on:click={addImage}>Add Image</Button></div>{#if loading}  <div class="mt-xxsmall"><Type weight="medium">Loading image...</Type></div>{/if}{#if error}  <div class="mt-xxsmall"><Type color="red" weight="medium">{error}</Type></div>{/if}

Your App.svelte is now complete and should look like this:

src/App.svelte
svelte
<script lang="ts">  import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"  import Input from "./Input.svelte"  const API_URL = "https://source.unsplash.com/random/"  let disabled = true  let error = ""  let loading = false  let imageBytes = null  let width = 800  let height = 800  $: disabled = !(!!width && !!height)  async function getImage() {    if (Math.sign(width) === -1 || Math.sign(height) === -1) {      throw new Error("Enter positive values for height/width")    }    // It redirects to the URL of the actual image    const initialResponse = await self.fetch(`${API_URL}${width}x${height}`)    if (initialResponse.ok) {      // Fetch the actual image      const res = await self.fetch(initialResponse.url)      const buffer = await res.arrayBuffer()      return new Uint8Array(buffer)    } else {      throw new Error(initialResponse.statusText)    }  }  async function addImage() {    error = ""    loading = true    try {      const result = await getImage()      loading = false      imageBytes = result    } catch (errorMsg) {      loading = false      error = errorMsg      return    }    parent.postMessage(      {        pluginMessage: {          type: "ADD_IMAGE",          payload: {            imageBytes,            width,            height,          },        },      },      "*"    )  }</script><div class="p-xsmall wrapper flex column justify-content-between">  <div>    <Type>      Get a Random Image from      <a href="https://unsplash.com/">Unsplash</a>    </Type>    <div class="flex mt-xsmall">      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Width:</Label>        <Input placeholder="800" bind:value={width} />      </div>      <div class="flex ml-xxsmall mr-xxsmall">        <Label>Height:</Label>        <Input placeholder="800" bind:value={height} />      </div>    </div>    <div class="mt-xxsmall flex justify-content-center">      <Button {disabled} on:click={addImage}>Add Image</Button>    </div>    {#if loading}      <div class="mt-xxsmall"><Type weight="medium">Loading image...</Type></div>    {/if}    {#if error}      <div class="mt-xxsmall"><Type color="red" weight="medium">{error}</Type></div>    {/if}  </div>  <div class="mt-small footer pt-xxsmall">    <Type>      Read the tutorial      <a href="https://www.lekoarts.de">on lekoarts.de</a>    </Type>  </div></div><style>  .wrapper {    text-align: center;    height: 100%;  }  .footer {    border-top: 1px solid var(--silver);  }</style>

Placing Images

Remember the code.ts file you created at the beginning? It’s now time to come back to this file. With the onmessage handler you’ll use the message that the UI sends via the postMessage function to place images onto the canvas. For this you learn how to create a reusable createImage helper function.

But let’s first verify, that the postMessage works successfully. In your code.ts file, add a handler for onmessage and log out the arguments:

src/code.ts
ts
figma.showUI(__html__, { width: 320, height: 220 })figma.ui.onmessage = (msg) => {  console.log(msg)  figma.closePlugin()}

The figma.closePlugin() function (as the name suggests) closes the plugin once this handler is called. You can comment this out for now for debugging purposes if you want.

Go to Figma and open the DevTools via Menu => Plugins => Development => Open console. Open your plugin and press the “Add Image” button. You should see something like this in the console now:

js
{  type: 'ADD_IMAGE',  payload: {    height: 800,    width: 800,    imageBytes: [/* Array of bytes */]  }}

If this didn’t work, restart rollup with npm run dev, close the plugin in Figma, and try again.

Instead of logging out the response, handle the ADD_IMAGE type:

ts
interface IPayload {  imageBytes: Uint8Array  width?: number  height?: number}interface IMessage {  type: "ADD_IMAGE"  payload: IPayload}figma.ui.onmessage = (msg: IMessage) => {  const { type, payload } = msg  if (type === `ADD_IMAGE`) {    if (payload.imageBytes) {      const { imageBytes, width, height } = payload      // createImage({ imageBytes, width, height })    }  }  figma.closePlugin()}

Your last task is to author the createImage function. Here’s what it’ll do:

  • Call Figma’s createImage API (opens in a new tab) to create an Image object. This quote from the API docs is quite important:

    Note that Image objects are not nodes. They are handles to images stored by Figma. Frame backgrounds, or fills of shapes (e.g. a rectangle) may contain images.

  • Create a fill object with the result of createImage. fill is used since images in Figma are not layers but fill types for a shape
  • Create a rectangle with the size of given width and height input. Mark the position as the center of the canvas
  • Apply the previously created fill type to the rectangle
  • Append the rectangle to the canvas and zoom + focus on it

The complete code.ts should look like this:

src/code.ts
ts
figma.showUI(__html__, { width: 320, height: 220 })interface IPayload {  imageBytes: Uint8Array  width?: number  height?: number}interface IMessage {  type: "ADD_IMAGE"  payload: IPayload}function createImage({  imageBytes,  width = 800,  height = 800,}: IPayload): void {  const image = figma.createImage(imageBytes)  const imageHash = image.hash  // An image in Figma is not a layer, but a fill type for a shape  const fill: ImagePaint = {    type: "IMAGE",    imageHash,    scaleMode: "FILL",  }  const rect = figma.createRectangle()  rect.resize(width, height)  rect.x = figma.viewport.center.x - Math.round(width / 2)  rect.y = figma.viewport.center.y - Math.round(height / 2)  rect.name = `Random Unsplash Image`  rect.fills = [fill]  figma.currentPage.appendChild(rect)  figma.currentPage.selection = [rect]  figma.viewport.scrollAndZoomIntoView([rect])}figma.ui.onmessage = (msg: IMessage) => {  const { type, payload } = msg  if (type === `ADD_IMAGE`) {    if (payload.imageBytes) {      const { imageBytes, width, height } = payload      createImage({ imageBytes, width, height })    }  }  figma.closePlugin()}

Try it out! You should get an image onto your canvas now when pressing the “Add Image” button 🎉

Publishing

This section is totally optional, you can continue to use your plugin locally without ever publishing it.

When you go to Menu => Plugins => Development => Manage plugins in development you can click on the three dots of your plugin and press Publish. Also see Figma’s guide on publishing (opens in a new tab).

Get Building!

You can use the result of this guide or my figma-plugin-svelte-example (opens in a new tab) project as a template for any future Svelte Figma plugins.