├── .gitignore
├── README.md
├── app
├── api
│ └── predictions
│ │ ├── [id]
│ │ └── route.js
│ │ └── route.js
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── Panel.tsx
└── ui
│ ├── button.tsx
│ ├── input.tsx
│ ├── select.tsx
│ └── textarea.tsx
├── eslint.config.mjs
├── lib
├── figmaAPI.ts
├── types.ts
└── utils.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── plugin
├── .gitignore
├── code.ts
├── manifest.json
└── tsconfig.json
├── postcss.config.mjs
├── public
└── vercel.svg
├── tailwind.config.ts
└── tsconfig.json
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Overview
2 |
3 | This is an Figma AI plugin template that [demonstrates AI responses](https://x.com/lichinlin/status/1892350882190098840) inside of a Figma plugin. This template shows:
4 |
5 | - Securely storing Replicate keys / prompts on a server
6 | - A fully functional React iframe using tailwind, shadcn, and Next.js
7 | - Deploying your plugin to production
8 | - Accessing the Figma API directly from the iframe
9 |
10 |
11 | ## Getting Started
12 |
13 | you need to store you Replicate API key in the `.env.local` file. You can get an API key from the [API token page](https://replicate.com/account/api-tokens). Create a `.env.local` file in the root of this project and add your API key:
14 |
15 | ```bash
16 | REPLICATE_API_TOKEN=***
17 | ```
18 |
19 | Then, run the development server:
20 |
21 | ```bash
22 | npm i
23 | npm run dev
24 | ```
25 |
26 | 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`.
27 |
28 | 
29 |
30 | ## Editing this template
31 |
32 | The main files you'll want to edit are:
33 |
34 | - `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.
35 | - `app/predictions/route.ts`: This is the "server" of the plugin and is what talks to Replicate AI. This is where you can update the prompt that you are sending to the server.
36 | - `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.
37 |
38 | ## Publishing your plugin
39 |
40 | 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.
41 |
42 | 1. If you haven't already, push your code to a git repo on GitHub.
43 | 2. Create an account on Vercel and connect your GitHub account.
44 | 3. Deploy your app to Vercel. You can follow the guide [here](https://vercel.com/docs/concepts/deployments/git).
45 | 4. While deploying make sure to set the environment variable `REPLICATE_API_TOKEN` to your Replicate Token.
46 | 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/`
47 |
48 | ```json
49 | "config": {
50 | "siteURL": "https://your-site-here.vercel.app/"
51 | }
52 | ```
53 |
54 | 6. Run `npm run build` to create the production build of your plugin that points to your deployed URL.
55 | 7. Test your plugin locally and make sure that it works after pointing to vercel.
56 | 8. [Publish your plugin to community](https://help.figma.com/hc/en-us/articles/360042293394-Publish-plugins-to-the-Figma-Community)
57 | 9. After publishing to community your plugin will update automatically when you push to your git repo.
58 |
59 | ## figmaAPI
60 |
61 | This template includes a `figmaAPI` helper at `@/lib/figmaAPI` that lets you run plugin code from inside of the iframe. This is
62 | useful for avoiding the iframe <-> plugin postMessage API and reduces the amount of code you need to write.
63 |
64 | **Example:**
65 |
66 | ```ts
67 | import { figmaAPI } from "@/lib/figmaAPI";
68 |
69 | const nodeId = "0:2";
70 |
71 | const result = await figmaAPI.run(
72 | (figma, { nodeId }) => {
73 | return figma.getNodeById(nodeId)?.name;
74 | },
75 | // Any variable you want to pass to the function must be passed as a parameter.
76 | { nodeId }
77 | );
78 |
79 | console.log(result); // "Page 1"
80 | ```
81 |
82 | A few things to note about this helper:
83 |
84 | 1. The code cannot reference any variables outside of the function unless they are passed as a parameter to the second argument. This is
85 | because the code is stringified and sent to the plugin, and the plugin
86 | evals it. The plugin has no access to the variables in the iframe.
87 | 2. The return value of the function must be JSON serializable. This is
88 | because the result is sent back to the iframe via postMessage, which only
89 | supports JSON.
90 |
91 | ## Learn More
92 |
93 | - [Figma plugin reference](https://github.com/figma/ai-plugin-template)
94 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
95 | - [Shadcn + Tailwind4](https://ui.shadcn.com/docs/tailwind-v4)
96 | - [Figma Plugin API](https://www.figma.com/plugin-docs/) - learn about the Figma plugin API.
97 | - [Replicate API](https://replicate.com/docs/topics/models/run-a-model) - learn about GPT APIs.
98 | - [Replicate web app example docs](https://replicate.com/docs/guides/nextjs)
99 |
--------------------------------------------------------------------------------
/app/api/predictions/[id]/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import Replicate from "replicate";
3 |
4 | const replicate = new Replicate({
5 | auth: process.env.REPLICATE_API_TOKEN,
6 | });
7 |
8 | export async function GET(request, context) {
9 | const { id } = await context.params;
10 | const prediction = await replicate.predictions.get(id);
11 |
12 | if (prediction?.error) {
13 | return NextResponse.json({ detail: prediction.error }, { status: 500 });
14 | }
15 |
16 | return NextResponse.json(prediction);
17 | }
18 |
--------------------------------------------------------------------------------
/app/api/predictions/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import Replicate from "replicate";
3 |
4 | const replicate = new Replicate({
5 | auth: process.env.REPLICATE_API_TOKEN,
6 | });
7 |
8 | // In production and preview deployments (on Vercel), the VERCEL_URL environment variable is set.
9 | // In development (on your local machine), the NGROK_HOST environment variable is set.
10 | const WEBHOOK_HOST = process.env.VERCEL_URL
11 | ? `https://${process.env.VERCEL_URL}`
12 | : process.env.NGROK_HOST;
13 |
14 | export async function POST(request) {
15 | if (!process.env.REPLICATE_API_TOKEN) {
16 | throw new Error(
17 | "The REPLICATE_API_TOKEN environment variable is not set. See README.md for instructions on how to set it."
18 | );
19 | }
20 |
21 | const { prompt, style } = await request.json();
22 |
23 | const options = {
24 | model: "recraft-ai/recraft-20b-svg",
25 | input: {
26 | prompt,
27 | size: "1024x1024",
28 | style,
29 | },
30 | };
31 |
32 | if (WEBHOOK_HOST) {
33 | options.webhook = `${WEBHOOK_HOST}/api/webhooks`;
34 | options.webhook_events_filter = ["start", "completed"];
35 | }
36 |
37 | // A prediction is the result you get when you run a model, including the input, output, and other details
38 | const prediction = await replicate.predictions.create(options);
39 |
40 | if (prediction?.error) {
41 | return NextResponse.json({ detail: prediction.error }, { status: 500 });
42 | }
43 |
44 | return NextResponse.json(prediction, { status: 201 });
45 | }
46 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lichin-lin/figma-text-to-icon/3da00969883c9aa512b7eec252293f2662606d6b/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "tailwindcss-animate";
3 | @custom-variant dark (&:is(.dark *));
4 |
5 | :root {
6 | --background: hsl(0 0% 100%);
7 | --foreground: hsl(0 0% 3.9%);
8 | --card: hsl(0 0% 100%);
9 | --card-foreground: hsl(0 0% 3.9%);
10 | --popover: hsl(0 0% 100%);
11 | --popover-foreground: hsl(0 0% 3.9%);
12 | --primary: hsl(0 0% 9%);
13 | --primary-foreground: hsl(0 0% 98%);
14 | --secondary: hsl(0 0% 96.1%);
15 | --secondary-foreground: hsl(0 0% 9%);
16 | --muted: hsl(0 0% 96.1%);
17 | --muted-foreground: hsl(0 0% 45.1%);
18 | --accent: hsl(0 0% 96.1%);
19 | --accent-foreground: hsl(0 0% 9%);
20 | --destructive: hsl(0 84.2% 60.2%);
21 | --destructive-foreground: hsl(0 0% 98%);
22 | --border: hsl(0 0% 89.8%);
23 | --input: hsl(0 0% 89.8%);
24 | --ring: hsl(0 0% 3.9%);
25 | --chart-1: hsl(12 76% 61%);
26 | --chart-2: hsl(173 58% 39%);
27 | --chart-3: hsl(197 37% 24%);
28 | --chart-4: hsl(43 74% 66%);
29 | --chart-5: hsl(27 87% 67%);
30 | --radius: 0.6rem;
31 | }
32 |
33 | .dark {
34 | --background: hsl(0 0% 3.9%);
35 | --foreground: hsl(0 0% 98%);
36 | --card: hsl(0 0% 3.9%);
37 | --card-foreground: hsl(0 0% 98%);
38 | --popover: hsl(0 0% 3.9%);
39 | --popover-foreground: hsl(0 0% 98%);
40 | --primary: hsl(0 0% 98%);
41 | --primary-foreground: hsl(0 0% 9%);
42 | --secondary: hsl(0 0% 14.9%);
43 | --secondary-foreground: hsl(0 0% 98%);
44 | --muted: hsl(0 0% 14.9%);
45 | --muted-foreground: hsl(0 0% 63.9%);
46 | --accent: hsl(0 0% 14.9%);
47 | --accent-foreground: hsl(0 0% 98%);
48 | --destructive: hsl(0 62.8% 30.6%);
49 | --destructive-foreground: hsl(0 0% 98%);
50 | --border: hsl(0 0% 14.9%);
51 | --input: hsl(0 0% 14.9%);
52 | --ring: hsl(0 0% 83.1%);
53 | --chart-1: hsl(220 70% 50%);
54 | --chart-2: hsl(160 60% 45%);
55 | --chart-3: hsl(30 80% 55%);
56 | --chart-4: hsl(280 65% 60%);
57 | --chart-5: hsl(340 75% 55%);
58 | }
59 |
60 | @theme inline {
61 | --color-background: var(--background);
62 | --color-foreground: var(--foreground);
63 | --color-card: var(--card);
64 | --color-card-foreground: var(--card-foreground);
65 | --color-popover: var(--popover);
66 | --color-popover-foreground: var(--popover-foreground);
67 | --color-primary: var(--primary);
68 | --color-primary-foreground: var(--primary-foreground);
69 | --color-secondary: var(--secondary);
70 | --color-secondary-foreground: var(--secondary-foreground);
71 | --color-muted: var(--muted);
72 | --color-muted-foreground: var(--muted-foreground);
73 | --color-accent: var(--accent);
74 | --color-accent-foreground: var(--accent-foreground);
75 | --color-destructive: var(--destructive);
76 | --color-destructive-foreground: var(--destructive-foreground);
77 | --color-border: var(--border);
78 | --color-input: var(--input);
79 | --color-ring: var(--ring);
80 | --color-chart-1: var(--chart-1);
81 | --color-chart-2: var(--chart-2);
82 | --color-chart-3: var(--chart-3);
83 | --color-chart-4: var(--chart-4);
84 | --color-chart-5: var(--chart-5);
85 | --radius-sm: calc(var(--radius) - 4px);
86 | --radius-md: calc(var(--radius) - 2px);
87 | --radius-lg: var(--radius);
88 | --radius-xl: calc(var(--radius) + 4px);
89 | }
90 |
91 | @layer base {
92 | * {
93 | @apply border-border outline-ring/50;
94 | }
95 | body {
96 | @apply bg-background text-foreground;
97 | }
98 | }
99 |
100 | html {
101 | width: 100vw;
102 | height: 100vh;
103 | background: hsl(0 0% 3.9%);
104 | }
105 |
106 | * {
107 | /* --font-geist-sans or --font-geist-mono */
108 | font-family: var(--font-geist-mono);
109 | }
110 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Create Next App",
17 | description: "Generated by create next app",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import Panel from "@/components/Panel";
5 |
6 | export default function Home() {
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/Panel.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Select,
3 | SelectValue,
4 | SelectTrigger,
5 | SelectContent,
6 | SelectItem,
7 | } from "./ui/select";
8 | import React from "react";
9 | import { Textarea } from "./ui/textarea";
10 | import { Button } from "./ui/button";
11 | import { figmaAPI } from "@/lib/figmaAPI";
12 |
13 | function Panel() {
14 | const [loading, setLoading] = React.useState(false);
15 | const [prompt, setPrompt] = React.useState("");
16 | const [style, setStyle] = React.useState("vector_illustration");
17 | const [output, setOutput] = React.useState(null);
18 | const [svgContent, setSvgContent] = React.useState("");
19 |
20 | // pass the result to figma
21 | const onPasteSVGIntoFigma = async () => {
22 | const result = await figmaAPI.run((figma, params) => {
23 | const { svg } = params;
24 | const newSVGNode = figma.createNodeFromSvg(svg);
25 | figma.currentPage.appendChild(newSVGNode);
26 | figma.currentPage.selection = [newSVGNode];
27 | return { nodeId: newSVGNode.id }
28 | }, { svg: svgContent });
29 |
30 | console.log(`nodeId: ${result.nodeId}`);
31 | };
32 |
33 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
34 | const getPrediction = async () => {
35 | setLoading(true);
36 | setOutput(null);
37 | const response = await fetch(`/api/predictions`, {
38 | method: "POST",
39 | headers: {
40 | "Content-Type": "application/json",
41 | },
42 | body: JSON.stringify({
43 | prompt,
44 | style,
45 | }),
46 | });
47 | let prediction = await response.json();
48 | if (response.status !== 201) {
49 | alert("Failed to create a prediction");
50 | return;
51 | }
52 |
53 | while (
54 | prediction.status !== "succeeded" &&
55 | prediction.status !== "failed"
56 | ) {
57 | await sleep(1000);
58 | const response = await fetch("/api/predictions/" + prediction.id);
59 | prediction = await response.json();
60 | if (response.status !== 200) {
61 | return;
62 | }
63 | console.log(prediction.logs);
64 |
65 | if (prediction.status === "succeeded") {
66 | setLoading(false);
67 | setOutput(prediction.output);
68 |
69 | const svgResponse = await fetch(prediction.output);
70 | const svgText = await svgResponse.text();
71 | setSvgContent(svgText);
72 | }
73 | }
74 | };
75 |
76 | const OPTIONS = [
77 | "vector_illustration",
78 | "vector_illustration/cartoon",
79 | "vector_illustration/doodle_line_art",
80 | "vector_illustration/engraving",
81 | "vector_illustration/flat_2",
82 | "vector_illustration/kawaii",
83 | "vector_illustration/line_art",
84 | "vector_illustration/line_circuit",
85 | "vector_illustration/linocut",
86 | "vector_illustration/seamless",
87 | "icon",
88 | "icon/broken_line",
89 | "icon/colored_outline",
90 | "icon/colored_shapes",
91 | "icon/colored_shapes_gradient",
92 | "icon/doodle_fill",
93 | "icon/doodle_offset_fill",
94 | "icon/offset_fill",
95 | "icon/outline",
96 | "icon/outline_gradient",
97 | "icon/uneven_fill",
98 | ];
99 |
100 | return (
101 |
102 | {/* result */}
103 |
104 | {!loading && !output && (
105 |
106 | 👇 Fill in the prompt first.
107 |
108 | )}
109 | {loading && !output && (
110 |
111 | ✨ Generating...
112 |
113 | )}
114 | {!loading && output && svgContent && (
115 | <>
116 |
120 |
121 |
128 |
129 | >
130 | )}
131 |
132 |
133 |
136 |
137 |
138 | {/* Prompt */}
139 |
140 | Prompt
141 |
148 |
149 | {/* Style */}
150 |
151 | Style
152 |
170 |
171 |
172 | {/* Submit */}
173 |
181 |
182 |
183 | );
184 | }
185 |
186 | export default Panel;
187 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
25 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27 | icon: "size-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | function Button({
38 | className,
39 | variant,
40 | size,
41 | asChild = false,
42 | ...props
43 | }: React.ComponentProps<"button"> &
44 | VariantProps & {
45 | asChild?: boolean
46 | }) {
47 | const Comp = asChild ? Slot : "button"
48 |
49 | return (
50 |
55 | )
56 | }
57 |
58 | export { Button, buttonVariants }
59 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
16 | )
17 | }
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | children,
30 | ...props
31 | }: React.ComponentProps) {
32 | return (
33 | span]:line-clamp-1",
37 | className
38 | )}
39 | {...props}
40 | >
41 | {children}
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | function SelectContent({
50 | className,
51 | children,
52 | position = "popper",
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
57 |
68 |
69 |
76 | {children}
77 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | function SelectLabel({
85 | className,
86 | ...props
87 | }: React.ComponentProps) {
88 | return (
89 |
94 | )
95 | }
96 |
97 | function SelectItem({
98 | className,
99 | children,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
111 |
112 |
113 |
114 |
115 |
116 | {children}
117 |
118 | )
119 | }
120 |
121 | function SelectSeparator({
122 | className,
123 | ...props
124 | }: React.ComponentProps) {
125 | return (
126 |
131 | )
132 | }
133 |
134 | function SelectScrollUpButton({
135 | className,
136 | ...props
137 | }: React.ComponentProps) {
138 | return (
139 |
147 |
148 |
149 | )
150 | }
151 |
152 | function SelectScrollDownButton({
153 | className,
154 | ...props
155 | }: React.ComponentProps) {
156 | return (
157 |
165 |
166 |
167 | )
168 | }
169 |
170 | export {
171 | Select,
172 | SelectContent,
173 | SelectGroup,
174 | SelectItem,
175 | SelectLabel,
176 | SelectScrollDownButton,
177 | SelectScrollUpButton,
178 | SelectSeparator,
179 | SelectTrigger,
180 | SelectValue,
181 | }
182 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/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/types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const CompletionRequestBody = z.object({ layers: z.array(z.string()) });
4 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | devIndicators: {
6 | appIsrStatus: false,
7 | },
8 | };
9 |
10 | export default nextConfig;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "figma-text-to-icon",
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 --turbopack'",
7 | "start": "next start",
8 | "lint": "next lint",
9 | "build": "concurrently -n plugin,next 'npm run build:plugin -- --define:SITE_URL=\\\"$npm_package_config_siteURL\\\"' 'next build'",
10 | "build:plugin": "esbuild plugin/code.ts --bundle --target=es6 --loader:.svg=text --outfile=plugin/dist/code.js",
11 | "build:next": "next build"
12 | },
13 | "dependencies": {
14 | "@figma/plugin-typings": "*",
15 | "@radix-ui/react-collapsible": "^1.1.3",
16 | "@radix-ui/react-dialog": "^1.1.6",
17 | "@radix-ui/react-popover": "^1.1.6",
18 | "@radix-ui/react-select": "^2.1.6",
19 | "@radix-ui/react-separator": "^1.1.2",
20 | "@radix-ui/react-slot": "^1.1.2",
21 | "@radix-ui/react-tooltip": "^1.1.8",
22 | "@tailwindcss/postcss": "^4.0.6",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "lucide-react": "^0.475.0",
26 | "next": "15.1.7",
27 | "react": "^19.0.0",
28 | "react-dom": "^19.0.0",
29 | "replicate": "^1.0.1",
30 | "shadcn-ui": "^0.9.4",
31 | "tailwind-merge": "^3.0.1",
32 | "tailwindcss-animate": "^1.0.7",
33 | "zod": "^3.21.4"
34 | },
35 | "devDependencies": {
36 | "@eslint/eslintrc": "^3",
37 | "@types/node": "^20",
38 | "@types/react": "^19",
39 | "@types/react-dom": "^19",
40 | "autoprefixer": "^10.4.20",
41 | "concurrently": "9.1.2",
42 | "esbuild": "0.25.0",
43 | "eslint": "^9",
44 | "eslint-config-next": "15.1.7",
45 | "postcss": "^8.5.2",
46 | "tailwindcss": "^4.0.6",
47 | "typescript": "^5"
48 | },
49 | "config": {
50 | "siteURL": "http://localhost:3000/"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/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: 300,
5 | height: 544,
6 | });
7 |
8 | figma.ui.onmessage = async (msg) => {
9 | if (msg.type === "EVAL") {
10 | const { code, id, params } = msg;
11 | try {
12 | const fn = eval(code);
13 | let result = fn(figma, params)
14 | if (result instanceof Promise) result = await result
15 | figma.ui.postMessage({
16 | type: "EVAL_RESULT",
17 | id,
18 | result,
19 | });
20 | } catch (error) {
21 | figma.ui.postMessage({
22 | type: "EVAL_REJECT",
23 | id,
24 | error: (error as Error).message,
25 | });
26 | }
27 | } else if (msg.type === "paste-svg") {
28 | const newSVGNode = figma.createNodeFromSvg(msg.svg);
29 | figma.currentPage.appendChild(newSVGNode);
30 | figma.currentPage.selection = [newSVGNode];
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/plugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "figma-text-to-icon",
3 | "id": "figma-text-to-icon",
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.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | "@tailwindcss/postcss": {},
4 | },
5 | };
6 | export default config;
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | },
24 | "typeRoots": [
25 | "./node_modules/@types",
26 | "./node_modules/@figma"
27 | ]
28 | },
29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/api/predictions/route.js"],
30 | "exclude": ["node_modules", "plugin"]
31 | }
32 |
--------------------------------------------------------------------------------