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",
|
||||
"test": "vitest run",
|
||||
"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": {
|
||||
"@figma/plugin-typings": "^1.82.0",
|
||||
"@upstash/ratelimit": "1.0.0",
|
||||
"@upstash/redis": "1.25.2",
|
||||
"bits-ui": "0.11.8",
|
||||
@ -45,6 +49,8 @@
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@typescript-eslint/parser": "6.14.0",
|
||||
"autoprefixer": "10.4.16",
|
||||
"concurrently": "^8.2.2",
|
||||
"esbuild": "^0.19.10",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-svelte": "2.35.1",
|
||||
@ -61,5 +67,8 @@
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.10",
|
||||
"vitest": "1.0.4"
|
||||
},
|
||||
"config": {
|
||||
"siteURL": "https://svgl.vercel.app?figma=1"
|
||||
}
|
||||
}
|
||||
|
@ -10,15 +10,27 @@
|
||||
import { flyAndScale } from '@/utils/flyAndScale';
|
||||
|
||||
// Icons:
|
||||
import { CopyIcon, DownloadIcon, LinkIcon, PackageIcon, PaintBucket } from 'lucide-svelte';
|
||||
import { CopyIcon, DownloadIcon, LinkIcon, PackageIcon, PaintBucket, ChevronsRight } from 'lucide-svelte';
|
||||
|
||||
// Main Card:
|
||||
import CardSpotlight from './cardSpotlight.svelte';
|
||||
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:
|
||||
export let svgInfo: iSVG;
|
||||
|
||||
let isInFigma = false
|
||||
onMount(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
isInFigma = searchParams.get('figma') === '1';
|
||||
});
|
||||
|
||||
// Download SVG:
|
||||
const downloadSvg = (url?: string) => {
|
||||
download(url || '');
|
||||
@ -56,6 +68,11 @@
|
||||
const data = {
|
||||
[MIMETYPE]: getSvgContent(url, true)
|
||||
};
|
||||
|
||||
if(isInFigma) {
|
||||
const content = (await getSvgContent(url, false)) as string;
|
||||
figmaCopyToClipboard(content);
|
||||
} else {
|
||||
try {
|
||||
const clipboardItem = new ClipboardItem(data);
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
@ -63,11 +80,18 @@
|
||||
const content = (await getSvgContent(url, false)) as string;
|
||||
await navigator.clipboard.writeText(content);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Copied to clipboard!', {
|
||||
description: `${svgInfo.title} - ${svgInfo.category}`
|
||||
});
|
||||
};
|
||||
|
||||
const insertSVG = async (url?: string) => {
|
||||
const content = (await getSvgContent(url, false)) as string;
|
||||
figmaInsertSVG(content);
|
||||
}
|
||||
|
||||
// Icon Stroke & Size:
|
||||
let iconStroke = 1.8;
|
||||
let iconSize = 16;
|
||||
@ -102,6 +126,38 @@
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<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
|
||||
title="Copy to clipboard"
|
||||
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,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"types": ["@figma/plugin-typings"]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user