Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Figma project to Vite + React #7453

Merged
merged 8 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ path = [
"tools/slintpad/styles/**.css",
"tools/figma-inspector/**.json",
"tools/figma-inspector/**.html",
"tools/figma-inspector/**.css",
]
precedence = "aggregate"
SPDX-FileCopyrightText = "Copyright © SixtyFPS GmbH <[email protected]>"
Expand Down
2,755 changes: 1,534 additions & 1,221 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion tools/figma-inspector/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
*.js
*.js
dist
.tmp
!*.d.ts
120 changes: 120 additions & 0 deletions tools/figma-inspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,123 @@ 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.

## Sending Messages between the Frontend and Backend

Bolt Figma makes messaging between the frontend UI and backend code layers simple and type-safe. This can be done with `listenTS()` and `dispatchTS()`.

Using this method accounts for:

- Setting up a scoped event listener in the listening context
- Removing the listener when the event is called (if `once` is set to true)
- Ensuring End-to-End Type-Safety for the event

### 1. Declare the Event Type in EventTS in shared/universals.ts

```js
export type EventTS = {
myCustomEvent: {
oneValue: string,
anotherValue: number,
},
// [... other events]
};
```

### 2a. Send a Message from the Frontend to the Backend

**Backend Listener:** `src-code/code.ts`

```js
import { listenTS } from "./utils/code-utils";

listenTS("myCustomEvent", (data) => {
console.log("oneValue is", data.oneValue);
console.log("anotherValue is", data.anotherValue);
});
```

**Frontend Dispatcher:** `index.svelte` or `index.tsx` or `index.vue`

```js
import { dispatchTS } from "./utils/utils";

dispatchTS("myCustomEvent", { oneValue: "name", anotherValue: 20 });
```

### 2b. Send a Message from the Backend to the Frontend

**Frontend Listener:** `index.svelte` or `index.tsx` or `index.vue`

```js
import { listenTS } from "./utils/utils";

listenTS(
"myCustomEvent",
(data) => {
console.log("oneValue is", data.oneValue);
console.log("anotherValue is", data.anotherValue);
},
true,
);
```

_Note: `true` is passed as the 3rd argument which means the listener will only listen once and then be removed. Set this to true to avoid duplicate events if you only intend to recieve one reponse per function._

**Backend Dispatcher:** `src-code/code.ts`

```js
import { dispatchTS } from "./utils/code-utils";

dispatchTS("myCustomEvent", { oneValue: "name", anotherValue: 20 });
```

---

### Info on Build Process

Frontend code is built to the `.tmp` directory temporarily and then copied to the `dist` folder for final. This is done to avoid Figma throwing plugin errors with editing files directly in the `dist` folder.

The frontend code (JS, CSS, HTML) is bundled into a single `index.html` file and all assets are inlined.

The backend code is bundled into a single `code.js` file.

Finally the `manifest.json` is generated from the `figma.config.ts` file with type-safety. This is configured when running `yarn create bolt-figma`, but you can make additional modifications to the `figma.config.ts` file after initialization.

### Read if Dev or Production Mode

Use the built-in Vite env var MODE to determine this:

```js
const mode = import.meta.env.MODE; // 'dev' or 'production'
```

### Troubleshooting Assets

Figma requires the entire frontend code to be wrapped into a single HTML file. For this reason, bundling external images, svgs, and other assets is not possible.

The solution to this is to inline all assets. Vite is already setup to inline most asset types it understands such as JPG, PNG, SVG, and more, however if the file type you're trying to inline doesn't work, you may need to add it to the assetsInclude array in the vite config:

More Info: https://vitejs.dev/config/shared-options.html#assetsinclude

Additionally, you may be able to import the file as a raw string, and then use that data inline in your component using the `?raw` suffix.

For example:

```ts
import icon from "./assets/icon.svg?raw";
```

and then use that data inline in your component:

```js
// Svelte
{@html icon}

// React
<div dangerouslySetInnerHTML={{ __html: icon }}></div>

// Vue
<div v-html="icon"></div>
```


28 changes: 28 additions & 0 deletions tools/figma-inspector/backend/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

import {
getStore,
setStore,
listenTS,
dispatchTS,
getStatus,
updateUI,
} from "./utils/code-utils";

figma.showUI(__html__, {
themeColors: true,
width: 400,
height: 320,
});

listenTS("copyToClipboard", () => {
figma.notify("Copied!");
});

figma.on("selectionchange", () => {
updateUI();
});

// init
updateUI();
11 changes: 11 additions & 0 deletions tools/figma-inspector/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"typeRoots": [
"../../../node_modules/@types",
"../../../node_modules/@figma",
"../src/globals.d.ts",
"../shared/universals.d.ts"
]
},
"include": ["./**/*.ts"]
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,50 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
// SPDX-License-Identifier: MIT

function getStatus(selectionCount: number) {
import type { Message, PluginMessageEvent } from "../../src/globals";
import type { EventTS } from "../../shared/universals";

export const dispatch = (data: any, origin = "*") => {
figma.ui.postMessage(data, {
origin,
});
};

export const dispatchTS = <Key extends keyof EventTS>(
event: Key,
data: EventTS[Key],
origin = "*",
) => {
dispatch({ event, data }, origin);
};

export const listenTS = <Key extends keyof EventTS>(
eventName: Key,
callback: (data: EventTS[Key]) => any,
listenOnce = false,
) => {
const func = (event: any) => {
if (event.event === eventName) {
callback(event);
if (listenOnce) {
figma.ui?.off("message", func); // Remove Listener so we only listen once
}
}
};

figma.ui.on("message", func);
};

export const getStore = async (key: string) => {
const value = await figma.clientStorage.getAsync(key);
return value;
};

export const setStore = async (key: string, value: string) => {
await figma.clientStorage.setAsync(key, value);
};

export function getStatus(selectionCount: number) {
if (selectionCount === 0) {
return "Please select a layer";
}
Expand All @@ -11,10 +54,6 @@ function getStatus(selectionCount: number) {
return "Slint properties:";
}

type StyleObject = {
[key: string]: string;
};

const itemsToKeep = [
"color",
"font-family",
Expand All @@ -30,6 +69,10 @@ const itemsToKeep = [
"stroke",
];

type StyleObject = {
[key: string]: string;
};

function transformStyle(styleObj: StyleObject): string {
const filteredEntries = Object.entries(styleObj)
.filter(([key]) => itemsToKeep.includes(key))
Expand Down Expand Up @@ -62,33 +105,15 @@ function transformStyle(styleObj: StyleObject): string {
return filteredEntries.length > 0 ? `${filteredEntries.join(";\n")};` : "";
}

async function updateUI() {
export 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 });
dispatchTS("updatePropertiesCallback", { 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");
}
};
29 changes: 29 additions & 0 deletions tools/figma-inspector/figma.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright © Hyper Brew LLC
// SPDX-License-Identifier: MIT

import type { FigmaConfig, PluginManifest } from "vite-figma-plugin/lib/types";
import { version } from "./package.json";

export const manifest: PluginManifest = {
name: "Figma to Slint",
id: "slint.figma.plugin",
api: "1.0.0",
main: "code.js",
ui: "index.html",
editorType: ["figma", "dev"],
documentAccess: "dynamic-page",
networkAccess: {
allowedDomains: ["*"],
reasoning: "For accessing remote assets",
},
};

const extraPrefs = {
copyZipAssets: ["public-zip/*"],
};

export const config: FigmaConfig = {
manifest,
version,
...extraPrefs,
};
17 changes: 17 additions & 0 deletions tools/figma-inspector/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app"></div>



<script type="module" src="/src/index-react.tsx"></script>

</body>
</html>
18 changes: 0 additions & 18 deletions tools/figma-inspector/manifest.json

This file was deleted.

Loading
Loading