Skip to content

Commit f9d42d6

Browse files
authored
Export images for HTML (#181)
* Added simple image export using Base64 - groups with image bg not supported yet. * added a (flawed) way to export images from groups by exporting the paint objects * reworked background images in a much simpler way * store base64 in node so it's only done once * added some minor changes to messaging and re-drawing while trying to track down issues with the infinite image conversion issue * Fix missing prop in debug view * Prevent run from happening multiple times due to changing document during conversion * hopefully clarified the steps of exporting an image when there are child nodes. * Added toggle that allows you to disable exporting of images * embedImages is disabled for non-html languages.
1 parent b10cfab commit f9d42d6

File tree

13 files changed

+361
-194
lines changed

13 files changed

+361
-194
lines changed

apps/debug/pages/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default function Web() {
1919
<PluginFigmaToolbar variant="(Light)" />
2020
<PluginUI
2121
code={"code goes hereeeee"}
22+
isLoading={false}
2223
selectedFramework={selectedFramework}
2324
setSelectedFramework={setSelectedFramework}
2425
htmlPreview={null}
@@ -36,6 +37,7 @@ export default function Web() {
3637
<PluginFigmaToolbar variant="(Dark)" />
3738
<PluginUI
3839
code={"code goes hereeeee"}
40+
isLoading={false}
3941
selectedFramework={selectedFramework}
4042
setSelectedFramework={setSelectedFramework}
4143
htmlPreview={null}

apps/plugin/plugin-src/code.ts

Lines changed: 140 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const defaultPluginSettings: PluginSettings = {
2828
roundTailwindValues: false,
2929
roundTailwindColors: false,
3030
customTailwindColors: false,
31+
customTailwindPrefix: "",
32+
embedImages: false,
3133
};
3234

3335
// A helper type guard to ensure the key belongs to the PluginSettings type
@@ -63,14 +65,23 @@ const initSettings = async () => {
6365
safeRun(userPluginSettings);
6466
};
6567

66-
const safeRun = (settings: PluginSettings) => {
67-
try {
68-
run(settings);
69-
} catch (e) {
70-
if (e && typeof e === "object" && "message" in e) {
71-
const error = e as Error;
72-
console.log("error: ", error.stack);
73-
figma.ui.postMessage({ type: "error", error: error.message });
68+
// Used to prevent running from happening again.
69+
let isLoading = false;
70+
const safeRun = async (settings: PluginSettings) => {
71+
if (isLoading === false) {
72+
try {
73+
isLoading = true;
74+
await run(settings);
75+
// hack to make it not immediately set to false when complete. (executes on next frame)
76+
setTimeout(() => {
77+
isLoading = false;
78+
}, 1);
79+
} catch (e) {
80+
if (e && typeof e === "object" && "message" in e) {
81+
const error = e as Error;
82+
console.log("error: ", error.stack);
83+
figma.ui.postMessage({ type: "error", error: error.message });
84+
}
7485
}
7586
}
7687
};
@@ -86,6 +97,10 @@ const standardMode = async () => {
8697

8798
// Listen for document changes
8899
figma.on("documentchange", () => {
100+
// Node: This was causing an infinite load when you try to export a background image from a group that contains children.
101+
// The reason for this is that the code will temporarily hide the children of the group in order to export a clean image
102+
// then restores the visibility of the children. This constitutes a document change so it's restarting the whole conversion.
103+
// In order to stop this, we disable safeRun() when doing conversions (while isLoading === true).
89104
safeRun(userPluginSettings);
90105
});
91106

@@ -105,121 +120,124 @@ const codegenMode = async () => {
105120
// figma.showUI(__html__, { visible: false });
106121
await getUserSettings();
107122

108-
figma.codegen.on("generate", async ({ language, node }: CodegenEvent): Promise<CodegenResult[]> => {
109-
const convertedSelection = convertIntoNodes([node], null);
110-
111-
switch (language) {
112-
case "html":
113-
return [
114-
{
115-
title: "Code",
116-
code: await htmlMain(
117-
convertedSelection,
118-
{ ...userPluginSettings, jsx: false },
119-
true,
120-
),
121-
language: "HTML",
122-
},
123-
{
124-
title: "Text Styles",
125-
code: htmlCodeGenTextStyles(userPluginSettings),
126-
language: "HTML",
127-
},
128-
];
129-
case "html_jsx":
130-
return [
131-
{
132-
title: "Code",
133-
code: await htmlMain(
134-
convertedSelection,
135-
{ ...userPluginSettings, jsx: true },
136-
true,
137-
),
138-
language: "HTML",
139-
},
140-
{
141-
title: "Text Styles",
142-
code: htmlCodeGenTextStyles(userPluginSettings),
143-
language: "HTML",
144-
},
145-
];
146-
case "tailwind":
147-
case "tailwind_jsx":
148-
return [
149-
{
150-
title: "Code",
151-
code: await tailwindMain(convertedSelection, {
152-
...userPluginSettings,
153-
jsx: language === "tailwind_jsx",
154-
}),
155-
language: "HTML",
156-
},
157-
// {
158-
// title: "Style",
159-
// code: tailwindMain(convertedSelection, defaultPluginSettings),
160-
// language: "HTML",
161-
// },
162-
{
163-
title: "Tailwind Colors",
164-
code: retrieveGenericSolidUIColors("Tailwind")
165-
.map((d) => {
166-
let str = `${d.hex};`;
167-
if (d.colorName !== d.hex) {
168-
str += ` // ${d.colorName}`;
169-
}
170-
if (d.meta) {
171-
str += ` (${d.meta})`;
172-
}
173-
return str;
174-
})
175-
.join("\n"),
176-
language: "JAVASCRIPT",
177-
},
178-
{
179-
title: "Text Styles",
180-
code: tailwindCodeGenTextStyles(),
181-
language: "HTML",
182-
},
183-
];
184-
case "flutter":
185-
return [
186-
{
187-
title: "Code",
188-
code: flutterMain(convertedSelection, {
189-
...userPluginSettings,
190-
flutterGenerationMode: "snippet",
191-
}),
192-
language: "SWIFT",
193-
},
194-
{
195-
title: "Text Styles",
196-
code: flutterCodeGenTextStyles(),
197-
language: "SWIFT",
198-
},
199-
];
200-
case "swiftUI":
201-
return [
202-
{
203-
title: "SwiftUI",
204-
code: swiftuiMain(convertedSelection, {
205-
...userPluginSettings,
206-
swiftUIGenerationMode: "snippet",
207-
}),
208-
language: "SWIFT",
209-
},
210-
{
211-
title: "Text Styles",
212-
code: swiftUICodeGenTextStyles(),
213-
language: "SWIFT",
214-
},
215-
];
216-
default:
217-
break;
218-
}
123+
figma.codegen.on(
124+
"generate",
125+
async ({ language, node }: CodegenEvent): Promise<CodegenResult[]> => {
126+
const convertedSelection = convertIntoNodes([node], null);
127+
128+
switch (language) {
129+
case "html":
130+
return [
131+
{
132+
title: "Code",
133+
code: await htmlMain(
134+
convertedSelection,
135+
{ ...userPluginSettings, jsx: false },
136+
true,
137+
),
138+
language: "HTML",
139+
},
140+
{
141+
title: "Text Styles",
142+
code: htmlCodeGenTextStyles(userPluginSettings),
143+
language: "HTML",
144+
},
145+
];
146+
case "html_jsx":
147+
return [
148+
{
149+
title: "Code",
150+
code: await htmlMain(
151+
convertedSelection,
152+
{ ...userPluginSettings, jsx: true },
153+
true,
154+
),
155+
language: "HTML",
156+
},
157+
{
158+
title: "Text Styles",
159+
code: htmlCodeGenTextStyles(userPluginSettings),
160+
language: "HTML",
161+
},
162+
];
163+
case "tailwind":
164+
case "tailwind_jsx":
165+
return [
166+
{
167+
title: "Code",
168+
code: await tailwindMain(convertedSelection, {
169+
...userPluginSettings,
170+
jsx: language === "tailwind_jsx",
171+
}),
172+
language: "HTML",
173+
},
174+
// {
175+
// title: "Style",
176+
// code: tailwindMain(convertedSelection, defaultPluginSettings),
177+
// language: "HTML",
178+
// },
179+
{
180+
title: "Tailwind Colors",
181+
code: retrieveGenericSolidUIColors("Tailwind")
182+
.map((d) => {
183+
let str = `${d.hex};`;
184+
if (d.colorName !== d.hex) {
185+
str += ` // ${d.colorName}`;
186+
}
187+
if (d.meta) {
188+
str += ` (${d.meta})`;
189+
}
190+
return str;
191+
})
192+
.join("\n"),
193+
language: "JAVASCRIPT",
194+
},
195+
{
196+
title: "Text Styles",
197+
code: tailwindCodeGenTextStyles(),
198+
language: "HTML",
199+
},
200+
];
201+
case "flutter":
202+
return [
203+
{
204+
title: "Code",
205+
code: flutterMain(convertedSelection, {
206+
...userPluginSettings,
207+
flutterGenerationMode: "snippet",
208+
}),
209+
language: "SWIFT",
210+
},
211+
{
212+
title: "Text Styles",
213+
code: flutterCodeGenTextStyles(),
214+
language: "SWIFT",
215+
},
216+
];
217+
case "swiftUI":
218+
return [
219+
{
220+
title: "SwiftUI",
221+
code: swiftuiMain(convertedSelection, {
222+
...userPluginSettings,
223+
swiftUIGenerationMode: "snippet",
224+
}),
225+
language: "SWIFT",
226+
},
227+
{
228+
title: "Text Styles",
229+
code: swiftUICodeGenTextStyles(),
230+
language: "SWIFT",
231+
},
232+
];
233+
default:
234+
break;
235+
}
219236

220-
const blocks: CodegenResult[] = [];
221-
return blocks;
222-
});
237+
const blocks: CodegenResult[] = [];
238+
return blocks;
239+
},
240+
);
223241
};
224242

225243
switch (figma.mode) {

apps/plugin/ui-src/App.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,26 @@ export default function App() {
111111
}, []);
112112

113113
const handleFrameworkChange = (updatedFramework: Framework) => {
114-
setState((prevState) => ({
115-
...prevState,
116-
// code: "// Loading...",
117-
selectedFramework: updatedFramework,
118-
}));
119-
postUISettingsChangingMessage("framework", updatedFramework, {
120-
targetOrigin: "*",
121-
});
114+
if (updatedFramework !== state.selectedFramework) {
115+
setState((prevState) => ({
116+
...prevState,
117+
// code: "// Loading...",
118+
selectedFramework: updatedFramework,
119+
}));
120+
postUISettingsChangingMessage("framework", updatedFramework, {
121+
targetOrigin: "*",
122+
});
123+
}
124+
};
125+
const handlePreferencesChange = (
126+
key: keyof PluginSettings,
127+
value: boolean | string,
128+
) => {
129+
if (state.settings && state.settings[key] === value) {
130+
// do nothing
131+
} else {
132+
postUISettingsChangingMessage(key, value, { targetOrigin: "*" });
133+
}
122134
};
123135

124136
const darkMode = figmaColorBgValue !== "#ffffff";
@@ -131,11 +143,9 @@ export default function App() {
131143
warnings={state.warnings}
132144
selectedFramework={state.selectedFramework}
133145
setSelectedFramework={handleFrameworkChange}
146+
onPreferenceChanged={handlePreferencesChange}
134147
htmlPreview={state.htmlPreview}
135148
settings={state.settings}
136-
onPreferenceChanged={(key: string, value: boolean | string) =>
137-
postUISettingsChangingMessage(key, value, { targetOrigin: "*" })
138-
}
139149
colors={state.colors}
140150
gradients={state.gradients}
141151
/>

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@figma/plugin-typings": "^1.105.0",
17+
"js-base64": "^3.7.7",
1718
"react": "18.3.1",
1819
"react-dom": "18.3.1",
1920
"types": "workspace:*"

packages/backend/src/altNodes/altNodeUtils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export const isTypeOrGroupOfTypes = curry(
4242
},
4343
);
4444

45+
export const isSVGNode = (node: SceneNode) => {
46+
const altNode = node as AltNode<typeof node>;
47+
return altNode.canBeFlattened;
48+
};
49+
4550
export const renderNodeAsSVG = async (node: SceneNode) =>
4651
await node.exportAsync({ format: "SVG_STRING" });
4752

packages/backend/src/code.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { convertToCode } from "./common/retrieveUI/convertToCode";
1919

2020
export const run = async (settings: PluginSettings) => {
2121
clearWarnings();
22+
2223
const { framework } = settings;
2324
const selection = figma.currentPage.selection;
2425

0 commit comments

Comments
 (0)