Skip to content

Commit 6124a86

Browse files
authored
Shadow and blur effects support for text layers, Tailwind prefix support and new placeholder service (#166)
* Update placeholder image URLs to use placehold.co and enhance gradient warning messages * Add support for text shadow and layer blur effects across all text builders (HTML, Tailwind, Flutter and SwiftUI) * Add custom prefix support for Tailwind classes in code generation
1 parent 68e5fc0 commit 6124a86

File tree

14 files changed

+325
-36
lines changed

14 files changed

+325
-36
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,40 +105,40 @@ const codegenMode = async () => {
105105
// figma.showUI(__html__, { visible: false });
106106
await getUserSettings();
107107

108-
figma.codegen.on("generate", ({ language, node }) => {
108+
figma.codegen.on("generate", async ({ language, node }: CodegenEvent): Promise<CodegenResult[]> => {
109109
const convertedSelection = convertIntoNodes([node], null);
110110

111111
switch (language) {
112112
case "html":
113113
return [
114114
{
115-
title: `Code`,
116-
code: htmlMain(
115+
title: "Code",
116+
code: await htmlMain(
117117
convertedSelection,
118118
{ ...userPluginSettings, jsx: false },
119119
true,
120120
),
121121
language: "HTML",
122122
},
123123
{
124-
title: `Text Styles`,
124+
title: "Text Styles",
125125
code: htmlCodeGenTextStyles(userPluginSettings),
126126
language: "HTML",
127127
},
128128
];
129129
case "html_jsx":
130130
return [
131131
{
132-
title: `Code`,
133-
code: htmlMain(
132+
title: "Code",
133+
code: await htmlMain(
134134
convertedSelection,
135135
{ ...userPluginSettings, jsx: true },
136136
true,
137137
),
138138
language: "HTML",
139139
},
140140
{
141-
title: `Text Styles`,
141+
title: "Text Styles",
142142
code: htmlCodeGenTextStyles(userPluginSettings),
143143
language: "HTML",
144144
},
@@ -147,20 +147,20 @@ const codegenMode = async () => {
147147
case "tailwind_jsx":
148148
return [
149149
{
150-
title: `Code`,
151-
code: tailwindMain(convertedSelection, {
150+
title: "Code",
151+
code: await tailwindMain(convertedSelection, {
152152
...userPluginSettings,
153153
jsx: language === "tailwind_jsx",
154154
}),
155155
language: "HTML",
156156
},
157157
// {
158-
// title: `Style`,
158+
// title: "Style",
159159
// code: tailwindMain(convertedSelection, defaultPluginSettings),
160160
// language: "HTML",
161161
// },
162162
{
163-
title: `Tailwind Colors`,
163+
title: "Tailwind Colors",
164164
code: retrieveGenericSolidUIColors("Tailwind")
165165
.map((d) => {
166166
let str = `${d.hex};`;
@@ -176,39 +176,39 @@ const codegenMode = async () => {
176176
language: "JAVASCRIPT",
177177
},
178178
{
179-
title: `Text Styles`,
179+
title: "Text Styles",
180180
code: tailwindCodeGenTextStyles(),
181181
language: "HTML",
182182
},
183183
];
184184
case "flutter":
185185
return [
186186
{
187-
title: `Code`,
187+
title: "Code",
188188
code: flutterMain(convertedSelection, {
189189
...userPluginSettings,
190190
flutterGenerationMode: "snippet",
191191
}),
192192
language: "SWIFT",
193193
},
194194
{
195-
title: `Text Styles`,
195+
title: "Text Styles",
196196
code: flutterCodeGenTextStyles(),
197197
language: "SWIFT",
198198
},
199199
];
200200
case "swiftUI":
201201
return [
202202
{
203-
title: `SwiftUI`,
203+
title: "SwiftUI",
204204
code: swiftuiMain(convertedSelection, {
205205
...userPluginSettings,
206206
swiftUIGenerationMode: "snippet",
207207
}),
208208
language: "SWIFT",
209209
},
210210
{
211-
title: `Text Styles`,
211+
title: "Text Styles",
212212
code: swiftUICodeGenTextStyles(),
213213
language: "SWIFT",
214214
},

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"capabilities": ["inspect", "codegen", "vscode"],
99
"permissions": [],
1010
"networkAccess": {
11-
"allowedDomains": ["none"]
11+
"allowedDomains": ["https://placehold.co"]
1212
},
1313
"codegenLanguages": [
1414
{ "label": "HTML", "value": "html" },

packages/backend/src/flutter/builderImpl/flutterColor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const flutterBoxDecorationColor = (
5656
export const flutterDecorationImage = (node: SceneNode, fill: ImagePaint) => {
5757
addWarning("Image fills are replaced with placeholders");
5858
return generateWidgetCode("DecorationImage", {
59-
image: `NetworkImage("https://via.placeholder.com/${node.width.toFixed(
59+
image: `NetworkImage("https://placehold.co/${node.width.toFixed(
6060
0,
6161
)}x${node.height.toFixed(0)}")`,
6262
fit: fitToBoxFit(fill),

packages/backend/src/flutter/flutterMain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ const flutterContainer = (node: SceneNode, child: string): string => {
148148
let image = "";
149149
if ("fills" in node && retrieveTopFill(node.fills)?.type === "IMAGE") {
150150
addWarning("Image fills are replaced with placeholders");
151-
image = `Image.network("https://via.placeholder.com/${node.width.toFixed(
151+
image = `Image.network("https://placehold.co/${node.width.toFixed(
152152
0,
153153
)}x${node.height.toFixed(0)}")`;
154154
}

packages/backend/src/flutter/flutterTextBuilder.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
} from "../common/commonTextHeightSpacing";
1414

1515
export class FlutterTextBuilder extends FlutterDefaultBuilder {
16+
node?: TextNode;
17+
1618
constructor(optChild: string = "") {
1719
super(optChild);
1820
}
@@ -22,6 +24,7 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
2224
}
2325

2426
createText(node: TextNode): this {
27+
this.node = node;
2528
let alignHorizontal =
2629
node.textAlignHorizontal?.toString()?.toLowerCase() ?? "left";
2730
alignHorizontal =
@@ -106,6 +109,12 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
106109
styleProperties.fontFeatures = `[FontFeature.enable("sups")]`;
107110
}
108111

112+
// Add text-shadow if a drop shadow is applied
113+
const shadow = this.textShadow();
114+
if (shadow) {
115+
styleProperties.shadows = shadow;
116+
}
117+
109118
const style = generateWidgetCode("TextStyle", styleProperties);
110119

111120
let text = segment.characters;
@@ -155,6 +164,10 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
155164

156165
textAutoSize(node: TextNode): this {
157166
this.child = wrapTextAutoResize(node, this.child);
167+
// First wrap with SizedBox/Expanded as before, then apply layer blur if any.
168+
let wrapped = wrapTextAutoResize(node, this.child);
169+
wrapped = wrapTextWithLayerBlur(node, wrapped);
170+
this.child = wrapped;
158171
return this;
159172
}
160173

@@ -165,6 +178,39 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
165178
}
166179
return "";
167180
};
181+
182+
/**
183+
* New method to handle text shadow.
184+
* Checks if a drop shadow effect is applied to the node and
185+
* returns Flutter code for the TextStyle "shadows" property.
186+
*/
187+
textShadow(): string {
188+
if (this.node && (this.node as TextNode).effects) {
189+
const effects = (this.node as TextNode).effects;
190+
const dropShadow = effects.find(
191+
(effect) =>
192+
effect.type === "DROP_SHADOW" && effect.visible !== false,
193+
);
194+
if (dropShadow) {
195+
const ds = dropShadow as DropShadowEffect;
196+
const offsetX = Math.round(ds.offset.x);
197+
const offsetY = Math.round(ds.offset.y);
198+
const blurRadius = Math.round(ds.radius);
199+
const r = Math.round(ds.color.r * 255);
200+
const g = Math.round(ds.color.g * 255);
201+
const b = Math.round(ds.color.b * 255);
202+
// Convert to hex for Flutter Color (e.g., Color(0xFF112233))
203+
const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
204+
.toString(16)
205+
.slice(1)
206+
.toUpperCase();
207+
return `[Shadow(offset: Offset(${offsetX}, ${offsetY}), blurRadius: ${blurRadius}, color: Color(0xFF${hex}).withOpacity(${ds.color.a.toFixed(
208+
2,
209+
)}))]`;
210+
}
211+
}
212+
return "";
213+
}
168214
}
169215

170216
export const wrapTextAutoResize = (node: TextNode, child: string): string => {
@@ -201,5 +247,25 @@ export const wrapTextAutoResize = (node: TextNode, child: string): string => {
201247
return child;
202248
};
203249

250+
// New helper to wrap with layer blur using Flutter's ImageFiltered widget.
251+
export const wrapTextWithLayerBlur = (
252+
node: TextNode,
253+
child: string,
254+
): string => {
255+
if (node.effects) {
256+
const blurEffect = node.effects.find(
257+
(effect) =>
258+
effect.type === "LAYER_BLUR" && effect.visible !== false && effect.radius > 0,
259+
);
260+
if (blurEffect) {
261+
return generateWidgetCode("ImageFiltered", {
262+
imageFilter: `ImageFilter.blur(sigmaX: ${blurEffect.radius}, sigmaY: ${blurEffect.radius})`,
263+
child: child,
264+
});
265+
}
266+
}
267+
return child;
268+
};
269+
204270
export const parseTextAsCode = (originalText: string) =>
205271
originalText.replace(/\n/g, "\\n");

packages/backend/src/html/htmlMain.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ const htmlAsset = (node: SceneNode, settings: HTMLSettings): string => {
225225
if (retrieveTopFill(node.fills)?.type === "IMAGE") {
226226
addWarning("Image fills are replaced with placeholders");
227227
tag = "img";
228-
src = ` src="https://via.placeholder.com/${node.width.toFixed(
228+
src = ` src="https://placehold.co/$${node.width.toFixed(
229229
0,
230230
)}x${node.height.toFixed(0)}"`;
231231
}
@@ -268,15 +268,15 @@ const htmlContainer = (
268268
addWarning("Image fills are replaced with placeholders");
269269
if (!("children" in node) || node.children.length === 0) {
270270
tag = "img";
271-
src = ` src="https://via.placeholder.com/${node.width.toFixed(
271+
src = ` src="https://placehold.co/${node.width.toFixed(
272272
0,
273273
)}x${node.height.toFixed(0)}"`;
274274
} else {
275275
builder.addStyles(
276276
formatWithJSX(
277277
"background-image",
278278
settings.jsx,
279-
`url(https://via.placeholder.com/${node.width.toFixed(
279+
`url(https://placehold.co/${node.width.toFixed(
280280
0,
281281
)}x${node.height.toFixed(0)})`,
282282
),

packages/backend/src/html/htmlTextBuilder.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder {
2424
}
2525

2626
return segments.map((segment) => {
27+
// Prepare additional CSS properties from layer blur and drop shadow effects.
28+
const additionalStyles: { [key: string]: string } = {};
29+
30+
const layerBlurStyle = this.getLayerBlurStyle();
31+
if (layerBlurStyle) {
32+
additionalStyles.filter = layerBlurStyle;
33+
}
34+
const textShadowStyle = this.getTextShadowStyle();
35+
if (textShadowStyle) {
36+
additionalStyles["text-shadow"] = textShadowStyle;
37+
}
38+
2739
const styleAttributes = formatMultipleJSX(
2840
{
2941
color: htmlColorFromFills(segment.fills),
@@ -36,12 +48,13 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder {
3648
"line-height": this.lineHeight(segment.lineHeight, segment.fontSize),
3749
"letter-spacing": this.letterSpacing(
3850
segment.letterSpacing,
39-
segment.fontSize,
51+
segment.fontSize
4052
),
4153
// "text-indent": segment.indentation,
4254
"word-wrap": "break-word",
55+
...additionalStyles,
4356
},
44-
this.isJSX,
57+
this.isJSX
4558
);
4659

4760
const charsWithLineBreak = segment.characters.split("\n").join("<br/>");
@@ -139,4 +152,49 @@ export class HtmlTextBuilder extends HtmlDefaultBuilder {
139152
}
140153
return this;
141154
}
155+
156+
/**
157+
* Returns a CSS filter value for layer blur.
158+
*/
159+
private getLayerBlurStyle(): string {
160+
if (this.node && (this.node as TextNode).effects) {
161+
const effects = (this.node as TextNode).effects;
162+
const blurEffect = effects.find(
163+
(effect) =>
164+
effect.type === "LAYER_BLUR" &&
165+
effect.visible !== false &&
166+
effect.radius > 0
167+
);
168+
if (blurEffect && blurEffect.radius) {
169+
return `blur(${blurEffect.radius}px)`;
170+
}
171+
}
172+
return "";
173+
}
174+
175+
/**
176+
* Returns a CSS text-shadow value if a drop shadow effect is applied.
177+
*/
178+
private getTextShadowStyle(): string {
179+
if (this.node && (this.node as TextNode).effects) {
180+
const effects = (this.node as TextNode).effects;
181+
const dropShadow = effects.find(
182+
(effect) => effect.type === "DROP_SHADOW" && effect.visible !== false
183+
);
184+
if (dropShadow) {
185+
const ds = dropShadow as DropShadowEffect; // Type narrow the effect.
186+
const offsetX = Math.round(ds.offset.x);
187+
const offsetY = Math.round(ds.offset.y);
188+
const blurRadius = Math.round(ds.radius);
189+
const r = Math.round(ds.color.r * 255);
190+
const g = Math.round(ds.color.g * 255);
191+
const b = Math.round(ds.color.b * 255);
192+
const a = ds.color.a;
193+
return `${offsetX}px ${offsetY}px ${blurRadius}px rgba(${r}, ${g}, ${b}, ${a.toFixed(
194+
2
195+
)})`;
196+
}
197+
}
198+
return "";
199+
}
142200
}

packages/backend/src/swiftui/builderImpl/swiftuiColor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const swiftuiBackground = (
6060
return swiftuiGradient(fill);
6161
} else if (fill?.type === "IMAGE") {
6262
addWarning("Image fills are replaced with placeholders");
63-
return `AsyncImage(url: URL(string: "https://via.placeholder.com/${node.width.toFixed(
63+
return `AsyncImage(url: URL(string: "https://placehold.co/${node.width.toFixed(
6464
0,
6565
)}x${node.height.toFixed(0)}"))`;
6666
}

0 commit comments

Comments
 (0)