Simple Figma attributes inspector plugin (#7446)

Converts properties figma normally shows as CSS values, but converted to the equivalent Slint syntax.
This commit is contained in:
Nigel Breslaw 2025-01-24 13:10:00 +02:00 committed by GitHub
parent edbcab7d96
commit de687b4093
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1042 additions and 0 deletions

View file

@ -125,6 +125,8 @@ path = [
"tools/slintpad/**.html",
"tools/slintpad/**.json",
"tools/slintpad/styles/**.css",
"tools/figma-inspector/**.json",
"tools/figma-inspector/**.html",
]
precedence = "aggregate"
SPDX-FileCopyrightText = "Copyright © SixtyFPS GmbH <info@slint.dev>"

731
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@
packages:
- "api/node"
- "editors/vscode"
- "tools/figma-inspector"
- "tools/slintpad"
- "demos/printerdemo/node"
- "demos/home-automation/node"

1
tools/figma-inspector/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.js

View file

@ -0,0 +1,29 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -->
## Figma to Slint property inspector
### Features
- UI that follows Figma theme colors and light/dark mode.
- Only displays properties for a single selected item. Warns if nothing or multiple items are selected.
- Displayed properties are re-written to fit Slint syntax.
- Copy to clipboard button.
- Don't show copy if there are no properties.
### Future tweaks
- Set plugin up to build via Vite. Figma only works with a single *.js file. Using Vite means we
can have multiple files, import libraries, use react, etc and it will sort out a bundle that just becomes one *.js file.
- Can Shiki be used to syntax color the properties?
- Still some missing properties and thought needed for how to deal with Figma properties that don't exist in Slint e.g. you can have unlimited individual shadows and gradients. Slint only supports one of each.
- Grab the 'text' value for any text from the original data.
Below are the steps to get your plugin running. You can also find instructions at:
https://www.figma.com/plugin-docs/plugin-quickstart-guide/
Enusure you have the desktop version of Figma installed.
First install the dependencies with `pnpm i`.
Then build the project with `pnpm build`.
Then in Figma select Plugins > Plugins & Widgets > Import from manifest... and then chose the manifest.json from this folder.

View file

@ -0,0 +1,94 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
function getStatus(selectionCount: number) {
if (selectionCount === 0) {
return "Please select a layer";
}
if (selectionCount > 1) {
return "Please select only one layer";
}
return "Slint properties:";
}
type StyleObject = {
[key: string]: string;
};
const itemsToKeep = [
"color",
"font-family",
"font-size",
"font-weight",
"width",
"height",
"fill",
"opacity",
"border-radius",
"fill",
"stroke-width",
"stroke",
];
function transformStyle(styleObj: StyleObject): string {
const filteredEntries = Object.entries(styleObj)
.filter(([key]) => itemsToKeep.includes(key))
.map(([key, value]) => {
let finalKey = key;
let finalValue = value;
switch (key) {
case "fill":
finalKey = "background";
break;
case "stroke":
finalKey = "border-color";
break;
case "stroke-width":
finalKey = "border-width";
break;
case "font-family":
finalValue = `"${value}"`;
break;
}
if (value.includes("linear-gradient")) {
return `${finalKey}: @${finalValue}`;
}
return `${finalKey}: ${finalValue}`;
});
return filteredEntries.length > 0 ? `${filteredEntries.join(";\n")};` : "";
}
async function updateUI() {
const title = getStatus(figma.currentPage.selection.length);
let slintProperties = "";
if (figma.currentPage.selection.length === 1) {
const cssProperties =
await figma.currentPage.selection[0].getCSSAsync();
slintProperties = transformStyle(cssProperties);
console.log(cssProperties);
}
figma.ui.postMessage({ title, slintProperties });
}
// This shows the HTML page in "ui.html".
figma.showUI(__html__, { width: 400, height: 320, themeColors: true });
// init
updateUI();
figma.on("selectionchange", () => {
updateUI();
});
// Logic to react to UI events
figma.ui.onmessage = async (msg: { type: string; count: number }) => {
if (msg.type === "copy") {
figma.notify("Copied to clipboard");
}
};

View file

@ -0,0 +1,18 @@
{
"name": "Figma to Slint",
"id": "1464256128189783424",
"api": "1.0.0",
"main": "code.js",
"capabilities": [],
"enableProposedApi": false,
"documentAccess": "dynamic-page",
"editorType": [
"figma"
],
"ui": "ui.html",
"networkAccess": {
"allowedDomains": [
"none"
]
}
}

View file

@ -0,0 +1,47 @@
{
"name": "Figma to Slint (Beta)",
"version": "1.10.0",
"description": "Slint plugin for Figma",
"main": "code.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint --ext .ts,.tsx --ignore-pattern node_modules .",
"lint:fix": "eslint --ext .ts,.tsx --ignore-pattern node_modules --fix .",
"check": "biome check",
"format": "biome format",
"format:fix": "biome format --write",
"watch": "pnpm build --watch"
},
"author": "",
"license": "",
"devDependencies": {
"@figma/eslint-plugin-figma-plugins": "0.15.0",
"@figma/plugin-typings": "*",
"@typescript-eslint/eslint-plugin": "6.12.0",
"@typescript-eslint/parser": "6.12.0",
"eslint": "8.54.0",
"typescript": "5.3.2"
},
"eslintConfig": {
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@figma/figma-plugins/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"root": true,
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
}
}

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es2017"],
"strict": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@figma"
]
}
}

View file

@ -0,0 +1,108 @@
<style>
body {
margin: 0;
padding: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.container {
width: 100%;
height: 100%;
background-color: var(--figma-color-bg-secondary);
border-radius: 4px;
overflow: hidden
}
.title {
height: 30px;
font-size: 12px;
color: var(--figma-color-text-secondary);
border-bottom: 1px solid var(--figma-color-border);
display: flex;
align-items: center;
padding-left: 16px;
padding-right: 16px;
justify-content: space-between;
user-select: none;
}
.copy-icon {
cursor: pointer;
font-size: 14px;
}
.copy-icon:hover {
opacity: 0.7;
}
.content {
padding: 16px;
font-size: 12px;
color: var(--figma-color-text-secondary);
}
.hidden {
display: none;
}
</style>
<div class="container">
<div class="title">
<span id="copy-icon" class="copy-icon">📋</span>
</div>
<div class="content">
</div>
</div>
<script>
// Yes paste is achieved in a Figma plugin by temp creating a textarea
// hidden off screen and then using the deprecated execCommand API to
// copy the text to the clipboard.
function writeTextToClipboard(str) {
const prevActive = document.activeElement;
const textArea = document.createElement('textarea');
textArea.value = str;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return new Promise((res, rej) => {
document.execCommand('copy') ? res() : rej();
textArea.remove();
prevActive.focus();
});
}
document.getElementById("copy-icon").addEventListener('click', async () => {
const content = document.querySelector('.content').innerText;
writeTextToClipboard(content);
console.log(content);
parent.postMessage({ pluginMessage: { type: 'copy' } }, '*');
});
onmessage = (event) => {
if (event.data.pluginMessage && event.data.pluginMessage.title) {
document.querySelector('.title').firstChild.textContent = event.data.pluginMessage.title;
const content = event.data.pluginMessage.slintProperties;
const copyIcon = document.getElementById('copy-icon');
copyIcon.classList.toggle('hidden', !content);
if (content !== "") {
document.querySelector('.content').innerText = event.data.pluginMessage.slintProperties;
copyIcon.classList.toggle('hidden', false);
} else {
copyIcon.classList.toggle('hidden', true);
}
}
}
</script>