mirror of
https://github.com/slint-ui/slint.git
synced 2025-07-07 13:15:23 +00:00
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:
parent
edbcab7d96
commit
de687b4093
10 changed files with 1042 additions and 0 deletions
|
@ -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
731
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -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
1
tools/figma-inspector/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.js
|
29
tools/figma-inspector/README.md
Normal file
29
tools/figma-inspector/README.md
Normal 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.
|
||||
|
94
tools/figma-inspector/code.ts
Normal file
94
tools/figma-inspector/code.ts
Normal 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");
|
||||
}
|
||||
};
|
18
tools/figma-inspector/manifest.json
Normal file
18
tools/figma-inspector/manifest.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
47
tools/figma-inspector/package.json
Normal file
47
tools/figma-inspector/package.json
Normal 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": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
11
tools/figma-inspector/tsconfig.json
Normal file
11
tools/figma-inspector/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["es2017"],
|
||||
"strict": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@figma"
|
||||
]
|
||||
}
|
||||
}
|
108
tools/figma-inspector/ui.html
Normal file
108
tools/figma-inspector/ui.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue