Skip to content

Commit d2c3f52

Browse files
authored
Add Tailwind support for Figma variables (#109)
* Modify Tailwind color functions to support Figma variables * Refactor function to arrow function * Fix fill opacity code * Add new Tailwind preferences * Prefer Tailwind color when color matches exactly * Update UI with shorter labels and tooltips --------- Co-authored-by: Dave Stewart <[email protected]>
1 parent 29dfb0f commit d2c3f52

File tree

6 files changed

+94
-45
lines changed

6 files changed

+94
-45
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ const defaultPluginSettings: PluginSettings = {
2424
responsiveRoot: false,
2525
flutterGenerationMode: "snippet",
2626
swiftUIGenerationMode: "snippet",
27-
roundTailwind: false,
27+
roundTailwindValues: false,
28+
roundTailwindColors: false,
29+
customTailwindColors: false,
2830
};
2931

3032
// A helper type guard to ensure the key belongs to the PluginSettings type

packages/backend/src/code.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export type PluginSettings = {
1919
responsiveRoot: boolean;
2020
flutterGenerationMode: string;
2121
swiftUIGenerationMode: string;
22-
roundTailwind: boolean;
22+
roundTailwindValues: boolean;
23+
roundTailwindColors: boolean,
24+
customTailwindColors: boolean,
2325
};
2426

2527
export const run = (settings: PluginSettings) => {

packages/backend/src/common/retrieveUI/retrieveColors.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
swiftuiGradient,
55
} from "../../swiftui/builderImpl/swiftuiColor";
66
import {
7+
getTailwindFromVariable,
78
tailwindColors,
89
tailwindGradient,
910
tailwindNearestColor,
10-
tailwindSolidColor,
11+
tailwindSolidColor
1112
} from "../../tailwind/builderImpl/tailwindColor";
1213
import {
1314
flutterColor,
@@ -68,8 +69,11 @@ const convertSolidColor = (
6869
const kind = "solid";
6970
const hex = rgbTo6hex(fill.color);
7071
const hexNearestColor = tailwindNearestColor(hex);
71-
exported = tailwindSolidColor(fill.color, fill.opacity, kind);
72-
colorName = tailwindColors[hexNearestColor];
72+
const colorVar = fill.boundVariables?.color
73+
exported = tailwindSolidColor(fill, kind);
74+
colorName = colorVar
75+
? getTailwindFromVariable(colorVar)
76+
: tailwindColors[hexNearestColor];
7377
} else if (framework === "SwiftUI") {
7478
exported = swiftuiColor(fill.color, opacity);
7579
}

packages/backend/src/tailwind/builderImpl/tailwindColor.ts

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { nearestColorFrom } from "../../nearest-color/nearestColor";
22
import { retrieveTopFill } from "../../common/retrieveFill";
3-
import { gradientAngle } from "../../common/color";
3+
import { gradientAngle, rgbTo6hex } from "../../common/color";
44
import { nearestOpacity, nearestValue } from "../conversionTables";
5+
import { localTailwindSettings } from "../tailwindMain";
56

67
// retrieve the SOLID color for tailwind
78
export const tailwindColorFromFills = (
@@ -13,7 +14,7 @@ export const tailwindColorFromFills = (
1314

1415
const fill = retrieveTopFill(fills);
1516
if (fill && fill.type === "SOLID") {
16-
return tailwindSolidColor(fill.color, fill.opacity, kind);
17+
return tailwindSolidColor(fill, kind);
1718
} else if (
1819
fill &&
1920
(fill.type === "GRADIENT_LINEAR" ||
@@ -22,31 +23,45 @@ export const tailwindColorFromFills = (
2223
fill.type === "GRADIENT_DIAMOND")
2324
) {
2425
if (fill.gradientStops.length > 0) {
25-
return tailwindSolidColor(
26-
fill.gradientStops[0].color,
27-
fill.opacity,
28-
kind
29-
);
26+
return tailwindSolidColor(fill.gradientStops[0], kind);
3027
}
3128
}
3229
return "";
3330
};
3431

35-
export const tailwindSolidColor = (
36-
color: RGB,
37-
opacity: number | undefined,
38-
kind: string
39-
): string => {
40-
// example: text-opacity-50
41-
// ignore the 100. If opacity was changed, let it be visible.
42-
const opacityProp =
43-
opacity !== 1.0 ? `opacity-${nearestOpacity(opacity ?? 1.0)}` : null;
32+
/**
33+
* Get the tailwind token name for a given color
34+
*
35+
* - variables: uses the tokenised variable name
36+
* - colors: uses the nearest Tailwind color name
37+
*/
38+
export const tailwindSolidColor = (fill: SolidPaint | ColorStop, kind?: string): string => {
39+
// example: stone-500 or custom-color-700
40+
let color: string;
41+
if (localTailwindSettings.customTailwindColors && fill.boundVariables?.color) {
42+
color = getTailwindFromVariable(fill.boundVariables.color);
43+
} else {
44+
const colorValue = "#" + rgbTo6hex(fill.color);
45+
const { name, value } = getTailwindFromFigmaRGB(fill.color);
46+
if (localTailwindSettings.roundTailwindColors || colorValue === value) {
47+
color = name;
48+
} else {
49+
color = `[#${rgbTo6hex(fill.color)}]`;
50+
}
51+
}
4452

45-
// example: text-red-500
46-
const colorProp = `${kind}-${getTailwindFromFigmaRGB(color)}${opacityProp ? `/${opacityProp}` : ""}`;
53+
// if no kind, it's a variable stop, so just return the name
54+
if (!kind) {
55+
return color;
56+
}
4757

48-
// if fill isn't visible, it shouldn't be painted.
49-
return colorProp;
58+
// grab opacity, or set it to full
59+
const opacity = "opacity" in fill && fill.opacity !== 1.0
60+
? `/${nearestOpacity(fill.opacity ?? 1.0)}`
61+
: "";
62+
63+
// example: text-red-500, text-custom-color-700/25
64+
return `${kind}-${color}${opacity}`;
5065
};
5166

5267
/**
@@ -71,24 +86,22 @@ export const tailwindGradient = (fill: GradientPaint): string => {
7186
const direction = gradientDirection(gradientAngle(fill));
7287

7388
if (fill.gradientStops.length === 1) {
74-
const fromColor = getTailwindFromFigmaRGB(fill.gradientStops[0].color);
89+
const fromColor = tailwindSolidColor(fill.gradientStops[0]);
7590

7691
return `${direction} from-${fromColor}`;
7792
} else if (fill.gradientStops.length === 2) {
78-
const fromColor = getTailwindFromFigmaRGB(fill.gradientStops[0].color);
79-
const toColor = getTailwindFromFigmaRGB(fill.gradientStops[1].color);
93+
const fromColor = tailwindSolidColor(fill.gradientStops[0]);
94+
const toColor = tailwindSolidColor(fill.gradientStops[1]);
8095

8196
return `${direction} from-${fromColor} to-${toColor}`;
8297
} else {
83-
const fromColor = getTailwindFromFigmaRGB(fill.gradientStops[0].color);
98+
const fromColor = tailwindSolidColor(fill.gradientStops[0]);
8499

85100
// middle (second color)
86-
const viaColor = getTailwindFromFigmaRGB(fill.gradientStops[1].color);
101+
const viaColor = tailwindSolidColor(fill.gradientStops[1]);
87102

88103
// last
89-
const toColor = getTailwindFromFigmaRGB(
90-
fill.gradientStops[fill.gradientStops.length - 1].color
91-
);
104+
const toColor = tailwindSolidColor(fill.gradientStops[fill.gradientStops.length - 1]);
92105

93106
return `${direction} from-${fromColor} via-${viaColor} to-${toColor}`;
94107
}
@@ -368,16 +381,17 @@ export const tailwindNearestColor = nearestColorFrom(
368381
);
369382

370383
// figma uses r,g,b in [0, 1], while nearestColor uses it in [0, 255]
371-
export const getTailwindFromFigmaRGB = (color: RGB): string => {
384+
export const getTailwindFromFigmaRGB = (color: RGB) => {
372385
const colorMultiplied = {
373386
r: color.r * 255,
374387
g: color.g * 255,
375388
b: color.b * 255,
376389
};
377-
378-
return tailwindColors[tailwindNearestColor(colorMultiplied)];
390+
const value = tailwindNearestColor(colorMultiplied);
391+
const name = tailwindColors[value];
392+
return { name, value };
379393
};
380394

381-
export const getTailwindColor = (color: string | RGB): string => {
382-
return tailwindColors[tailwindNearestColor(color)];
395+
export const getTailwindFromVariable = (alias: VariableAlias) => {
396+
return figma.variables.getVariableById(alias.id)?.name.replaceAll("/", "-") || alias.id;
383397
};

packages/backend/src/tailwind/conversionTables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const pxToRemToTailwind = (
3737

3838
if (convertedValue) {
3939
return conversionMap[convertedValue];
40-
} else if (localTailwindSettings.roundTailwind) {
40+
} else if (localTailwindSettings.roundTailwindValues) {
4141
return conversionMap[nearestValue(value / 16, keys)];
4242
}
4343

@@ -53,7 +53,7 @@ const pxToTailwind = (
5353

5454
if (convertedValue) {
5555
return conversionMap[convertedValue];
56-
} else if (localTailwindSettings.roundTailwind) {
56+
} else if (localTailwindSettings.roundTailwindValues) {
5757
return conversionMap[nearestValue(value, keys)];
5858
}
5959

packages/plugin-ui/src/PluginUI.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export type PluginSettings = {
1515
responsiveRoot: boolean;
1616
flutterGenerationMode: string;
1717
swiftUIGenerationMode: string;
18-
roundTailwind: boolean;
18+
roundTailwindValues: boolean;
19+
roundTailwindColors: boolean;
20+
customTailwindColors: boolean;
1921
};
2022

2123
type PluginUIProps = {
@@ -161,6 +163,7 @@ type LocalCodegenPreference =
161163
"framework" | "flutterGenerationMode" | "swiftUIGenerationMode"
162164
>;
163165
label: string;
166+
description: string;
164167
value?: boolean;
165168
isDefault?: boolean;
166169
includedLanguages?: FrameworkTypes[];
@@ -171,6 +174,7 @@ export const preferenceOptions: LocalCodegenPreference[] = [
171174
itemType: "individual_select",
172175
propertyName: "jsx",
173176
label: "React (JSX)",
177+
description: 'Render "class" attributes as "className"',
174178
isDefault: false,
175179
includedLanguages: ["HTML", "Tailwind"],
176180
},
@@ -191,21 +195,40 @@ export const preferenceOptions: LocalCodegenPreference[] = [
191195
{
192196
itemType: "individual_select",
193197
propertyName: "optimizeLayout",
194-
label: "Optimize Layout",
198+
label: "Optimize layout",
199+
description: 'Attempt to auto-layout suitable element groups',
195200
isDefault: true,
196201
includedLanguages: ["HTML", "Tailwind", "Flutter", "SwiftUI"],
197202
},
198203
{
199204
itemType: "individual_select",
200205
propertyName: "layerName",
201-
label: "Layer Names",
206+
label: "Layer names",
207+
description: 'Include layer names in classes',
202208
isDefault: false,
203209
includedLanguages: ["HTML", "Tailwind"],
204210
},
205211
{
206212
itemType: "individual_select",
207-
propertyName: "roundTailwind",
208-
label: "Round to Tailwind Values",
213+
propertyName: "roundTailwindValues",
214+
label: "Round values",
215+
description: 'Round pixel values to nearest Tailwind sizes',
216+
isDefault: false,
217+
includedLanguages: ["Tailwind"],
218+
},
219+
{
220+
itemType: "individual_select",
221+
propertyName: "roundTailwindColors",
222+
label: "Round colors",
223+
description: 'Round color values to nearest Tailwind colors',
224+
isDefault: false,
225+
includedLanguages: ["Tailwind"],
226+
},
227+
{
228+
itemType: "individual_select",
229+
propertyName: "customTailwindColors",
230+
label: "Custom colors",
231+
description: 'Use color variable names as custom color names',
209232
isDefault: false,
210233
includedLanguages: ["Tailwind"],
211234
},
@@ -318,6 +341,7 @@ export const CodePanel = (props: {
318341
<SelectableToggle
319342
key={preference.propertyName}
320343
title={preference.label}
344+
description={preference.description}
321345
isSelected={
322346
props.preferences?.[preference.propertyName] ??
323347
preference.isDefault
@@ -547,6 +571,7 @@ type SelectableToggleProps = {
547571
onSelect: (isSelected: boolean) => void;
548572
isSelected?: boolean;
549573
title: string;
574+
description?: string;
550575
buttonClass: string;
551576
checkClass: string;
552577
};
@@ -555,6 +580,7 @@ const SelectableToggle = ({
555580
onSelect,
556581
isSelected = false,
557582
title,
583+
description,
558584
buttonClass,
559585
checkClass,
560586
}: SelectableToggleProps) => {
@@ -565,6 +591,7 @@ const SelectableToggle = ({
565591
return (
566592
<button
567593
onClick={handleClick}
594+
title={description}
568595
className={`h-8 px-2 truncate flex items-center justify-center rounded-md cursor-pointer transition-all duration-300
569596
hover:bg-neutral-200 dark:hover:bg-neutral-700 gap-2 text-sm ring-1
570597
${

0 commit comments

Comments
 (0)