mirror of
https://github.com/pheralb/svgl.git
synced 2024-12-05 05:42:35 +08:00
feat: add figma plugin
This commit is contained in:
parent
f90966808b
commit
83a1d49af4
11
package.json
11
package.json
@ -22,9 +22,13 @@
|
|||||||
"check:size": "cd ./check-size && npm run start",
|
"check:size": "cd ./check-size && npm run start",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
|
"dev:figma": "concurrently -n plugin,svelte 'npm run build:plugin -- --watch --define:SITE_URL=\\\"http://localhost:5173?figma=1\\\"' 'npm run dev'",
|
||||||
|
"build:plugin": "esbuild src/figma/code.ts --bundle --target=es6 --loader:.svg=text --outfile=src/figma/dist/code.js",
|
||||||
|
"build:figma": "concurrently -n plugin,svelte 'npm run build:plugin -- --define:SITE_URL=\\\"$npm_package_config_siteURL\\\"' 'npm run build'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@figma/plugin-typings": "^1.82.0",
|
||||||
"@upstash/ratelimit": "1.0.0",
|
"@upstash/ratelimit": "1.0.0",
|
||||||
"@upstash/redis": "1.25.2",
|
"@upstash/redis": "1.25.2",
|
||||||
"bits-ui": "0.11.8",
|
"bits-ui": "0.11.8",
|
||||||
@ -45,6 +49,8 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||||
"@typescript-eslint/parser": "6.14.0",
|
"@typescript-eslint/parser": "6.14.0",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"esbuild": "^0.19.10",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-svelte": "2.35.1",
|
"eslint-plugin-svelte": "2.35.1",
|
||||||
@ -61,5 +67,8 @@
|
|||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"vite": "5.0.10",
|
"vite": "5.0.10",
|
||||||
"vitest": "1.0.4"
|
"vitest": "1.0.4"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"siteURL": "https://svgl.vercel.app?figma=1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,26 @@
|
|||||||
import { flyAndScale } from '@/utils/flyAndScale';
|
import { flyAndScale } from '@/utils/flyAndScale';
|
||||||
|
|
||||||
// Icons:
|
// Icons:
|
||||||
import { CopyIcon, DownloadIcon, LinkIcon, PackageIcon, PaintBucket } from 'lucide-svelte';
|
import { CopyIcon, DownloadIcon, LinkIcon, PackageIcon, PaintBucket, ChevronsRight } from 'lucide-svelte';
|
||||||
|
|
||||||
// Main Card:
|
// Main Card:
|
||||||
import CardSpotlight from './cardSpotlight.svelte';
|
import CardSpotlight from './cardSpotlight.svelte';
|
||||||
import { DropdownMenu } from 'bits-ui';
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
|
||||||
|
// Figma
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { copyToClipboard as figmaCopyToClipboard } from '@/figma/copy-to-clipboard';
|
||||||
|
import { insertSVG as figmaInsertSVG } from '@/figma/insert-svg';
|
||||||
|
|
||||||
|
|
||||||
// Props:
|
// Props:
|
||||||
export let svgInfo: iSVG;
|
export let svgInfo: iSVG;
|
||||||
|
|
||||||
|
let isInFigma = false
|
||||||
|
onMount(() => {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
isInFigma = searchParams.get('figma') === '1';
|
||||||
|
});
|
||||||
|
|
||||||
// Download SVG:
|
// Download SVG:
|
||||||
const downloadSvg = (url?: string) => {
|
const downloadSvg = (url?: string) => {
|
||||||
@ -56,18 +68,30 @@
|
|||||||
const data = {
|
const data = {
|
||||||
[MIMETYPE]: getSvgContent(url, true)
|
[MIMETYPE]: getSvgContent(url, true)
|
||||||
};
|
};
|
||||||
try {
|
|
||||||
const clipboardItem = new ClipboardItem(data);
|
if(isInFigma) {
|
||||||
await navigator.clipboard.write([clipboardItem]);
|
|
||||||
} catch (error) {
|
|
||||||
const content = (await getSvgContent(url, false)) as string;
|
const content = (await getSvgContent(url, false)) as string;
|
||||||
await navigator.clipboard.writeText(content);
|
figmaCopyToClipboard(content);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const clipboardItem = new ClipboardItem(data);
|
||||||
|
await navigator.clipboard.write([clipboardItem]);
|
||||||
|
} catch (error) {
|
||||||
|
const content = (await getSvgContent(url, false)) as string;
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Copied to clipboard!', {
|
toast.success('Copied to clipboard!', {
|
||||||
description: `${svgInfo.title} - ${svgInfo.category}`
|
description: `${svgInfo.title} - ${svgInfo.category}`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const insertSVG = async (url?: string) => {
|
||||||
|
const content = (await getSvgContent(url, false)) as string;
|
||||||
|
figmaInsertSVG(content);
|
||||||
|
}
|
||||||
|
|
||||||
// Icon Stroke & Size:
|
// Icon Stroke & Size:
|
||||||
let iconStroke = 1.8;
|
let iconStroke = 1.8;
|
||||||
let iconSize = 16;
|
let iconSize = 16;
|
||||||
@ -102,6 +126,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
|
{#if isInFigma}
|
||||||
|
<button
|
||||||
|
title="Insert to figma"
|
||||||
|
on:click={() => {
|
||||||
|
const svgHasTheme = typeof svgInfo.route !== 'string';
|
||||||
|
|
||||||
|
if (!svgHasTheme) {
|
||||||
|
insertSVG(
|
||||||
|
typeof svgInfo.route === 'string'
|
||||||
|
? svgInfo.route
|
||||||
|
: "Something went wrong. Couldn't copy the SVG."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
insertSVG(
|
||||||
|
typeof svgInfo.route !== 'string'
|
||||||
|
? dark
|
||||||
|
? svgInfo.route.dark
|
||||||
|
: svgInfo.route.light
|
||||||
|
: svgInfo.route
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
class="flex items-center space-x-2 rounded-md p-2 duration-100 hover:bg-neutral-200 dark:hover:bg-neutral-700/40"
|
||||||
|
>
|
||||||
|
<ChevronsRight size={iconSize} strokeWidth={iconStroke} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
37
src/figma/code.ts
Normal file
37
src/figma/code.ts
Normal 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
src/figma/copy-to-clipboard.ts
Normal file
26
src/figma/copy-to-clipboard.ts
Normal 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
src/figma/dist/code.js
vendored
Normal file
54
src/figma/dist/code.js
vendored
Normal 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
src/figma/figma-api.ts
Normal file
80
src/figma/figma-api.ts
Normal 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
src/figma/insert-svg.ts
Normal file
22
src/figma/insert-svg.ts
Normal 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
src/figma/manifest.json
Normal file
13
src/figma/manifest.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": ["@figma/plugin-typings"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user