feat: add figma plugin

This commit is contained in:
quillzhou@gmail.com
2023-12-23 15:43:54 +08:00
parent f90966808b
commit 83a1d49af4
9 changed files with 306 additions and 8 deletions
+37
View File
@@ -0,0 +1,37 @@
declare const SITE_URL: string
figma.showUI(`<script>window.location.href = '${SITE_URL}'</script>`, {
width: 400,
height: 700,
})
figma.ui.onmessage = async (message, props) => {
if (!SITE_URL.includes(props.origin)) {
return
}
switch (message.type) {
case 'EVAL': {
const fn = eval.call(null, message.code)
try {
const result = await fn(figma, message.params)
figma.ui.postMessage({
type: 'EVAL_RESULT',
result,
id: message.id,
})
} catch (e) {
figma.ui.postMessage({
type: 'EVAL_REJECT',
error: typeof e === 'string' ? e : e && typeof e === 'object' && 'message' in e ? e.message : null,
id: message.id,
})
}
break
}
}
}
+26
View File
@@ -0,0 +1,26 @@
export function copyToClipboard(value: string) {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (window.copy) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.copy(value);
} else {
const area = document.createElement('textarea');
document.body.appendChild(area);
area.value = value;
// area.focus();
area.select();
const result = document.execCommand('copy');
document.body.removeChild(area);
if (!result) {
throw new Error();
}
}
} catch (e) {
console.error(`Unable to copy the value: ${value}`);
return false;
}
return true;
}
+54
View File
@@ -0,0 +1,54 @@
"use strict";
(() => {
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/figma/code.ts
figma.showUI(`<script>window.location.href = '${"http://localhost:5173?figma=1"}'<\/script>`, {
width: 400,
height: 700
});
figma.ui.onmessage = (message, props) => __async(void 0, null, function* () {
if (!"http://localhost:5173?figma=1".includes(props.origin)) {
return;
}
switch (message.type) {
case "EVAL": {
const fn = eval.call(null, message.code);
try {
const result = yield fn(figma, message.params);
figma.ui.postMessage({
type: "EVAL_RESULT",
result,
id: message.id
});
} catch (e) {
figma.ui.postMessage({
type: "EVAL_REJECT",
error: typeof e === "string" ? e : e && typeof e === "object" && "message" in e ? e.message : null,
id: message.id
});
}
break;
}
}
});
})();
+80
View File
@@ -0,0 +1,80 @@
/**
* This is a magic file that allows us to run code in the Figma plugin context
* from the iframe. It does this by getting the code as a string, and sending it
* to the plugin via postMessage. The plugin then evals the code and sends the
* result back to the iframe. There are a few caveats:
* 1. The code cannot reference any variables outside of the function. This is
* because the code is stringified and sent to the plugin, and the plugin
* evals it. The plugin has no access to the variables in the iframe.
* 2. The return value of the function must be JSON serializable. This is
* because the result is sent back to the iframe via postMessage, which only
* supports JSON.
*
* You can get around these limitations by passing in the variables you need
* as parameters to the function.
*
* @example
* ```ts
* const result = await figmaAPI.run((figma, {nodeId}) => {
* return figma.getNodeById(nodeId)?.name;
* }, {nodeId: "0:2"});
*
* console.log(result); // "Page 1"
* ```
*/
class FigmaAPI {
private id = 0
/**
* Run a function in the Figma plugin context. The function cannot reference
* any variables outside of itself, and the return value must be JSON
* serializable. If you need to pass in variables, you can do so by passing
* them as the second parameter.
*/
run<T, U>(fn: (figma: PluginAPI, params: U) => Promise<T> | T, params?: U): Promise<T> {
return new Promise((resolve, reject) => {
const id = this.id++
const cb = (event: MessageEvent) => {
if (event.origin !== 'https://www.figma.com' && event.origin !== 'https://staging.figma.com') {
return
}
if (event.data.pluginMessage?.type === 'EVAL_RESULT') {
if (event.data.pluginMessage.id === id) {
window.removeEventListener('message', cb)
resolve(event.data.pluginMessage.result)
}
}
if (event.data.pluginMessage?.type === 'EVAL_REJECT') {
if (event.data.pluginMessage.id === id) {
window.removeEventListener('message', cb)
const message = event.data.pluginMessage.error
reject(new Error(typeof message === 'string' ? message : 'An error occurred in FigmaAPI.run()'))
}
}
}
window.addEventListener('message', cb)
const msg = {
pluginMessage: {
type: 'EVAL',
code: fn.toString(),
id,
params,
},
pluginId: '*',
}
;['https://www.figma.com', 'https://staging.figma.com'].forEach((origin) => {
try {
parent.postMessage(msg, origin)
} catch (e) {
console.error(e)
}
})
})
}
}
export const figmaAPI = new FigmaAPI()
+22
View File
@@ -0,0 +1,22 @@
import { figmaAPI } from './figma-api'
export async function insertSVG(svgString: string) {
if (!svgString) return
figmaAPI.run(
async (figma, { svgString }: { svgString: string }) => {
const node = figma.createNodeFromSvg(svgString)
const selectedNode = figma.currentPage.selection[0]
if (selectedNode) {
node.x = selectedNode.x + selectedNode.width + 20
node.y = selectedNode.y
}
figma.currentPage.appendChild(node)
figma.currentPage.selection = [node]
figma.viewport.scrollAndZoomIntoView([node])
},
{ svgString },
)
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "SVGL",
"id": "svgl",
"api": "1.0.0",
"main": "dist/code.js",
"enableProposedApi": false,
"editorType": ["figma", "figjam"],
"permissions": ["currentuser"],
"networkAccess": {
"allowedDomains": ["*"],
"reasoning": "Internet access for local development."
}
}