├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── app ├── api │ └── completion │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── lib ├── figmaAPI.ts ├── getTextForSelection.ts ├── getTextOffset.ts └── types.ts ├── next.config.js ├── package-lock.json ├── package.json ├── plugin ├── .gitignore ├── code.ts ├── manifest.json └── tsconfig.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=xxxxxxxxx -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test-types: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out repository code 8 | uses: actions/checkout@v2 9 | - run: npm i 10 | - run: npm run prettier:check 11 | - run: npm run build 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/ai-plugin-template/fdb5e6b8073fd95cb4a697d9cf7977eda7a5375a/.prettierrc -------------------------------------------------------------------------------- /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 of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is an Figma AI plugin template that demonstrates streaming LLM responses inside of a Figma plugin. This template shows: 4 | 5 | - Securely storing OpenAI keys / prompts on a server 6 | - Streaming a GPT completion to an iframe 7 | - Streaming a GPT completion to a Figma document 8 | - A fully functional React iframe using tailwind and Next.js 9 | - Deploying your plugin to production 10 | - Accessing the Figma API directly from the iframe 11 | 12 | ![Gif of the plugin in action](https://static.figma.com/uploads/cd663ea9256a71040227bc4af94c614febc8fc56) 13 | 14 | ## Getting Started 15 | 16 | This plugin is set up to use [Next.js](https://nextjs.org/). 17 | 18 | First create this template using create-next-app: 19 | 20 | ```bash 21 | npx create-next-app@latest --example https://github.com/figma/ai-plugin-template/ 22 | ``` 23 | 24 | Next you need to store you OpenAI API key in the `.env.local` file. You can get an API key from the [API keys page](https://platform.openai.com/account/api-keys). Create a `.env.local` file in the root of this project and add your API key: 25 | 26 | ```bash 27 | OPENAI_API_KEY=*** 28 | ``` 29 | 30 | Then, run the development server: 31 | 32 | ```bash 33 | npm i 34 | npm run dev 35 | ``` 36 | 37 | You can then open up the Figma desktop app and import a plugin from the manifest file in this project. You can right click on the canvas and navigate to `Plugins > Development > Import plugin from manifest...` and select the `manifest.json` in `{path to this project}/plugin/manifest.json`. 38 | 39 | ![Image showing how to import from manifest](https://static.figma.com/uploads/dcfb742580ad1c70338f1f9670f70dfd1fd42596) 40 | 41 | ## Editing this template 42 | 43 | The main files you'll want to edit are: 44 | 45 | - `app/page.tsx`: will let you update the plugin `iframe`. The page auto-updates as you edit the file and will let you update the user interface of your plugin. 46 | - `app/completion/route.ts`: This is the "server" of the plugin and is what talks to OpenAI. This is where you can update the prompt that you are sending to GPT. 47 | - `plugin/manifest.json`: this is the [manifest file](https://www.figma.com/plugin-docs/manifest/) that will let you update the permissions and editor types of your plugin. 48 | 49 | ## Publishing your plugin 50 | 51 | In this example we will be publishing the Next.js app to [Vercel](https://vercel.com/). You can also publish to any other hosting provider that supports Next.js. 52 | 53 | 1. If you haven't already, push your code to a git repo on GitHub. 54 | 2. Create an account on Vercel and connect your GitHub account. 55 | 3. Deploy your app to Vercel. You can follow the guide [here](https://vercel.com/docs/concepts/deployments/git). 56 | 4. While deploying make sure to set the environment variable `OPENAI_API_KEY` to your OpenAI API key. 57 | ![Photo of environment variable editor](https://static.figma.com/uploads/e41166e6a4e0d9c9c90bf662a609396ab7fe33cc) 58 | 5. Once your app is deployed you can update the `siteURL` section of your `package.json` file to point to the deployed URL. It will look something like `https://your-site-here.vercel.app/` 59 | 60 | ```json 61 | "config": { 62 | "siteURL": "https://your-site-here.vercel.app/" 63 | } 64 | ``` 65 | 66 | 6. Run `npm run build` to create the production build of your plugin that points to your deployed URL. 67 | 7. Test your plugin locally and make sure that it works after pointing to vercel. 68 | 8. [Publish your plugin to community](https://help.figma.com/hc/en-us/articles/360042293394-Publish-plugins-to-the-Figma-Community) 69 | 9. After publishing to community your plugin will update automatically when you push to your git repo. 70 | 71 | ## figmaAPI 72 | 73 | This template includes a `figmaAPI` helper at `@/lib/figmaAPI` that lets you run plugin code from inside of the iframe. This is 74 | useful for avoiding the iframe <-> plugin postMessage API and reduces the amount of code you need to write. 75 | 76 | **Example:** 77 | 78 | ```ts 79 | import { figmaAPI } from "@/lib/figmaAPI"; 80 | 81 | const nodeId = "0:2"; 82 | 83 | const result = await figmaAPI.run( 84 | (figma, { nodeId }) => { 85 | return figma.getNodeById(nodeId)?.name; 86 | }, 87 | // Any variable you want to pass to the function must be passed as a parameter. 88 | { nodeId }, 89 | ); 90 | 91 | console.log(result); // "Page 1" 92 | ``` 93 | 94 | A few things to note about this helper: 95 | 96 | 1. The code cannot reference any variables outside of the function unless they are passed as a parameter to the second argument. This is 97 | because the code is stringified and sent to the plugin, and the plugin 98 | evals it. The plugin has no access to the variables in the iframe. 99 | 2. The return value of the function must be JSON serializable. This is 100 | because the result is sent back to the iframe via postMessage, which only 101 | supports JSON. 102 | 103 | ## Learn More 104 | 105 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 106 | - [Figma Plugin API](https://www.figma.com/plugin-docs/) - learn about the Figma plugin API. 107 | - [OpenAI API](https://platform.openai.com/docs/guides/gpt) - learn about GPT APIs. 108 | -------------------------------------------------------------------------------- /app/api/completion/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChatCompletionRequestMessage, 3 | Configuration, 4 | OpenAIApi, 5 | } from "openai-edge"; 6 | import { OpenAIStream, StreamingTextResponse } from "ai"; 7 | import { CompletionRequestBody } from "@/lib/types"; 8 | 9 | // Create an OpenAI API client 10 | const config = new Configuration({ 11 | apiKey: process.env.OPENAI_API_KEY, 12 | }); 13 | const openai = new OpenAIApi(config); 14 | 15 | export const runtime = "edge"; 16 | 17 | // This is the instructions that GPT-4 will use to know how to respond. For more information on 18 | // the difference between a system message and a user message, see: 19 | // https://platform.openai.com/docs/guides/gpt/chat-completions-api 20 | const systemMessage = { 21 | role: "system", 22 | content: `You are an expert poet, you will be given a list of bulleted strings and 23 | you will write a short and concise poem using some of the information in the list. 24 | Only respond with a poem, don't make the poem too long.`, 25 | } as const; 26 | 27 | // This is used to format the message that the user sends to the API. Note we should 28 | // never have the client create the prompt directly as this could mean that the client 29 | // could use your api for any general purpose completion and leak the "secret sauce" of 30 | // your prompt. 31 | async function buildUserMessage( 32 | req: Request, 33 | ): Promise { 34 | const body = await req.json(); 35 | 36 | // We use zod to validate the request body. To change the data that is sent to the API, 37 | // change the CompletionRequestBody type in lib/types.ts 38 | const { layers } = CompletionRequestBody.parse(body); 39 | 40 | const bulletedList = layers.map((layer) => `* ${layer}`).join("\n"); 41 | 42 | return { 43 | role: "user", 44 | content: bulletedList, 45 | }; 46 | } 47 | 48 | export async function POST(req: Request) { 49 | // Ask OpenAI for a streaming completion given the prompt 50 | const response = await openai.createChatCompletion({ 51 | model: "gpt-3.5-turbo", 52 | stream: true, 53 | temperature: 0, 54 | messages: [systemMessage, await buildUserMessage(req)], 55 | }); 56 | 57 | // Convert the response into a friendly text-stream 58 | const stream = OpenAIStream(response); 59 | // Respond with the stream 60 | const result = new StreamingTextResponse(stream); 61 | 62 | return result; 63 | } 64 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/ai-plugin-template/fdb5e6b8073fd95cb4a697d9cf7977eda7a5375a/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Figma Poet", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { figmaAPI } from "@/lib/figmaAPI"; 4 | import { getTextForSelection } from "@/lib/getTextForSelection"; 5 | import { getTextOffset } from "@/lib/getTextOffset"; 6 | import { CompletionRequestBody } from "@/lib/types"; 7 | import { useState } from "react"; 8 | import { z } from "zod"; 9 | 10 | // This function calls our API and lets you read each character as it comes in. 11 | // To change the prompt of our AI, go to `app/api/completion.ts`. 12 | async function streamAIResponse(body: z.infer) { 13 | const resp = await fetch("/api/completion", { 14 | method: "POST", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | body: JSON.stringify(body), 19 | }); 20 | 21 | const reader = resp.body?.pipeThrough(new TextDecoderStream()).getReader(); 22 | 23 | if (!reader) { 24 | throw new Error("Error reading response"); 25 | } 26 | 27 | return reader; 28 | } 29 | 30 | export default function Plugin() { 31 | const [completion, setCompletion] = useState(""); 32 | 33 | // This function calls our API and handles the streaming response. 34 | // This ends up building the text up and using React state to update the UI. 35 | const onStreamToIFrame = async () => { 36 | setCompletion(""); 37 | const layers = await getTextForSelection(); 38 | 39 | if (!layers.length) { 40 | figmaAPI.run(async (figma) => { 41 | figma.notify( 42 | "Please select a layer with text in it to generate a poem.", 43 | { error: true }, 44 | ); 45 | }); 46 | return; 47 | } 48 | 49 | const reader = await streamAIResponse({ 50 | layers, 51 | }); 52 | 53 | let text = ""; 54 | while (true) { 55 | const { done, value } = await reader.read(); 56 | if (done) { 57 | break; 58 | } 59 | text += value; 60 | setCompletion(text); 61 | } 62 | }; 63 | 64 | // This is the same as above, but instead of updating React state, it adds the 65 | // text to the Figma canvas. 66 | const onStreamToCanvas = async () => { 67 | const layers = await getTextForSelection(); 68 | 69 | if (!layers.length) { 70 | figmaAPI.run(async (figma) => { 71 | figma.notify( 72 | "Please select a layer with text in it to generate a poem.", 73 | { error: true }, 74 | ); 75 | }); 76 | return; 77 | } 78 | 79 | const reader = await streamAIResponse({ 80 | layers, 81 | }); 82 | 83 | let text = ""; 84 | let nodeID: string | null = null; 85 | const textPosition = await getTextOffset(); 86 | 87 | const createOrUpdateTextNode = async () => { 88 | // figmaAPI.run is a helper that lets us run code in the figma plugin sandbox directly 89 | // from the iframe without having to post messages back and forth. For more info, 90 | // see /lib/figmaAPI.ts 91 | // 92 | // It is important to note that any variables that this function closes over must be 93 | // specified in the second argument to figmaAPI.run. This is because the code is actually 94 | // run in the figma plugin sandbox, not in the iframe. 95 | nodeID = await figmaAPI.run( 96 | async (figma, { nodeID, text, textPosition }) => { 97 | let node = figma.getNodeById(nodeID ?? ""); 98 | 99 | // If the node doesn't exist, create it and position it to the right of the selection. 100 | if (!node) { 101 | node = figma.createText(); 102 | node.x = textPosition?.x ?? 0; 103 | node.y = textPosition?.y ?? 0; 104 | } 105 | 106 | if (node.type !== "TEXT") { 107 | return ""; 108 | } 109 | 110 | const oldHeight = node.height; 111 | 112 | await figma.loadFontAsync({ family: "Inter", style: "Medium" }); 113 | node.fontName = { family: "Inter", style: "Medium" }; 114 | 115 | node.characters = text; 116 | 117 | // Scroll and zoom to the node if it's height changed (ex we've added a new line). 118 | // We only do this when the height changes to reduce flickering. 119 | if (oldHeight !== node.height) { 120 | figma.viewport.scrollAndZoomIntoView([node]); 121 | } 122 | 123 | return node.id; 124 | }, 125 | { nodeID, text, textPosition }, 126 | ); 127 | }; 128 | 129 | while (true) { 130 | const { done, value } = await reader.read(); 131 | if (done) { 132 | break; 133 | } 134 | text += value; 135 | await createOrUpdateTextNode(); 136 | } 137 | }; 138 | 139 | return ( 140 |
141 |

Poem Generator

142 |
143 | Select a node to create a poem about the text inside of it. 144 |
145 |
146 | 152 | 158 |
159 | {completion && ( 160 |
161 |
162 |             

{completion}

163 |
164 |
165 | )} 166 |
167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /lib/figmaAPI.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a magic file that allows us to run code in the Figma plugin context 3 | * from the iframe. It does this by getting the code as a string, and sending it 4 | * to the plugin via postMessage. The plugin then evals the code and sends the 5 | * result back to the iframe. There are a few caveats: 6 | * 1. The code cannot reference any variables outside of the function. This is 7 | * because the code is stringified and sent to the plugin, and the plugin 8 | * evals it. The plugin has no access to the variables in the iframe. 9 | * 2. The return value of the function must be JSON serializable. This is 10 | * because the result is sent back to the iframe via postMessage, which only 11 | * supports JSON. 12 | * 13 | * You can get around these limitations by passing in the variables you need 14 | * as parameters to the function. 15 | * 16 | * @example 17 | * ```ts 18 | * const result = await figmaAPI.run((figma, {nodeId}) => { 19 | * return figma.getNodeById(nodeId)?.name; 20 | * }, {nodeId: "0:2"}); 21 | * 22 | * console.log(result); // "Page 1" 23 | * ``` 24 | */ 25 | class FigmaAPI { 26 | private id = 0; 27 | 28 | /** 29 | * Run a function in the Figma plugin context. The function cannot reference 30 | * any variables outside of itself, and the return value must be JSON 31 | * serializable. If you need to pass in variables, you can do so by passing 32 | * them as the second parameter. 33 | */ 34 | run( 35 | fn: (figma: PluginAPI, params: U) => Promise | T, 36 | params?: U, 37 | ): Promise { 38 | return new Promise((resolve, reject) => { 39 | const id = this.id++; 40 | const cb = (event: MessageEvent) => { 41 | if ( 42 | event.origin !== "https://www.figma.com" && 43 | event.origin !== "https://staging.figma.com" 44 | ) { 45 | return; 46 | } 47 | 48 | if (event.data.pluginMessage?.type === "EVAL_RESULT") { 49 | if (event.data.pluginMessage.id === id) { 50 | window.removeEventListener("message", cb); 51 | resolve(event.data.pluginMessage.result); 52 | } 53 | } 54 | 55 | if (event.data.pluginMessage?.type === "EVAL_REJECT") { 56 | if (event.data.pluginMessage.id === id) { 57 | window.removeEventListener("message", cb); 58 | const message = event.data.pluginMessage.error; 59 | reject( 60 | new Error( 61 | typeof message === "string" 62 | ? message 63 | : "An error occurred in FigmaAPI.run()", 64 | ), 65 | ); 66 | } 67 | } 68 | }; 69 | window.addEventListener("message", cb); 70 | 71 | const msg = { 72 | pluginMessage: { 73 | type: "EVAL", 74 | code: fn.toString(), 75 | id, 76 | params, 77 | }, 78 | pluginId: "*", 79 | }; 80 | 81 | ["https://www.figma.com", "https://staging.figma.com"].forEach( 82 | (origin) => { 83 | try { 84 | parent.postMessage(msg, origin); 85 | } catch {} 86 | }, 87 | ); 88 | }); 89 | } 90 | } 91 | 92 | export const figmaAPI = new FigmaAPI(); 93 | -------------------------------------------------------------------------------- /lib/getTextForSelection.ts: -------------------------------------------------------------------------------- 1 | import { figmaAPI } from "@/lib/figmaAPI"; 2 | 3 | export async function getTextForSelection() { 4 | return await figmaAPI.run((figma) => { 5 | const { selection } = figma.currentPage; 6 | 7 | const getTextForNode = (node: SceneNode) => { 8 | if (node.type === "TEXT") { 9 | return node.characters; 10 | } else if (node.type === "STICKY" || node.type === "SHAPE_WITH_TEXT") { 11 | return node.text.characters; 12 | } 13 | return null; 14 | }; 15 | 16 | const layers: string[] = []; 17 | 18 | for (const node of selection) { 19 | if ("findAllWithCriteria" in node) { 20 | const childText = node 21 | .findAllWithCriteria({ 22 | types: ["TEXT", "STICKY", "SHAPE_WITH_TEXT"], 23 | }) 24 | .map(getTextForNode) 25 | .filter((t): t is string => t !== null); 26 | layers.push(...childText); 27 | } 28 | const text = getTextForNode(node); 29 | if (text !== null) { 30 | layers.push(text); 31 | } 32 | } 33 | 34 | return layers; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/getTextOffset.ts: -------------------------------------------------------------------------------- 1 | import { figmaAPI } from "@/lib/figmaAPI"; 2 | 3 | export async function getTextOffset() { 4 | const bounding = await figmaAPI.run((figma) => { 5 | const { selection } = figma.currentPage; 6 | return selection.reduce( 7 | (acc, node) => { 8 | const boundingBox = node.absoluteBoundingBox; 9 | 10 | if (!boundingBox) { 11 | return acc; 12 | } 13 | 14 | return { 15 | top: Math.min(acc.top, boundingBox.y), 16 | right: Math.max(acc.right, boundingBox.x + boundingBox.width), 17 | }; 18 | }, 19 | { top: Infinity, right: -Infinity }, 20 | ); 21 | }); 22 | 23 | if (bounding.top === Infinity || bounding.right === -Infinity) { 24 | return null; 25 | } 26 | 27 | return { 28 | x: bounding.right + 48, 29 | y: bounding.top, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const CompletionRequestBody = z.object({ layers: z.array(z.string()) }); 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-plugin-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently -n plugin,next 'npm run build:plugin -- --watch --define:SITE_URL=\\\"http://localhost:3000\\\"' 'next dev'", 7 | "start": "next start", 8 | "build": "concurrently -n plugin,next 'npm run build:plugin -- --define:SITE_URL=\\\"$npm_package_config_siteURL\\\"' 'next build'", 9 | "lint": "next lint", 10 | "build:plugin": "esbuild plugin/code.ts --bundle --target=es6 --loader:.svg=text --outfile=plugin/dist/code.js", 11 | "build:next": "next build", 12 | "prettier:check": "prettier --check .", 13 | "prettier:write": "prettier --write ." 14 | }, 15 | "dependencies": { 16 | "@figma/plugin-typings": "*", 17 | "@types/node": "20.4.2", 18 | "@types/react": "18.2.15", 19 | "@types/react-dom": "18.2.7", 20 | "ai": "^2.1.20", 21 | "autoprefixer": "10.4.14", 22 | "esbuild": "^0.18.12", 23 | "eslint": "8.45.0", 24 | "eslint-config-next": "13.4.10", 25 | "next": "13.4.10", 26 | "openai-edge": "^1.2.0", 27 | "postcss": "8.4.26", 28 | "prettier": "^3.0.0", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "tailwindcss": "3.3.3", 32 | "typescript": "5.1.6", 33 | "zod": "^3.21.4" 34 | }, 35 | "devDependencies": { 36 | "concurrently": "^8.2.0" 37 | }, 38 | "config": { 39 | "siteURL": "https://test-ai-plugin-template.vercel.app/" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | *.log 3 | *.log.* 4 | node_modules 5 | 6 | out/ 7 | dist/ 8 | code.js 9 | -------------------------------------------------------------------------------- /plugin/code.ts: -------------------------------------------------------------------------------- 1 | declare const SITE_URL: string; 2 | 3 | figma.showUI(``, { 4 | width: 700, 5 | height: 700, 6 | }); 7 | 8 | figma.ui.onmessage = async (message, props) => { 9 | if (props.origin !== SITE_URL) { 10 | return; 11 | } 12 | 13 | switch (message.type) { 14 | case "EVAL": { 15 | const fn = eval.call(null, message.code); 16 | 17 | try { 18 | const result = await fn(figma, message.params); 19 | figma.ui.postMessage({ 20 | type: "EVAL_RESULT", 21 | result, 22 | id: message.id, 23 | }); 24 | } catch (e) { 25 | figma.ui.postMessage({ 26 | type: "EVAL_REJECT", 27 | error: 28 | typeof e === "string" 29 | ? e 30 | : e && typeof e === "object" && "message" in e 31 | ? e.message 32 | : null, 33 | id: message.id, 34 | }); 35 | } 36 | 37 | break; 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /plugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-plugin-template", 3 | "id": "ai-plugin-template", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "enableProposedApi": false, 7 | "editorType": ["figma", "figjam"], 8 | "networkAccess": { 9 | "allowedDomains": ["*"], 10 | "reasoning": "Internet access for local development." 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es6"], 5 | "strict": true, 6 | "typeRoots": ["../node_modules/@figma"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | }, 25 | "typeRoots": ["./node_modules/@figma"] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules", "plugin"] 29 | } 30 | --------------------------------------------------------------------------------