Skip to content

Commit d412266

Browse files
[Feature] Implement twig component (#239)
* Add(tailwind): support for twig components * Support twig component content & embbed * Add(twig): handle content in component * Twig : simplify component name * Improvement(twig): handle twig attributes * Limit twig components attr to instances * Hide component properties starting with . or _ * Handle non twig components --------- Co-authored-by: Paule Herman <[email protected]>
1 parent 6450a8c commit d412266

File tree

5 files changed

+70
-3
lines changed

5 files changed

+70
-3
lines changed

packages/backend/src/common/commonFormatAttributes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const formatStyleAttribute = (
2020
export const formatDataAttribute = (label: string, value?: string) =>
2121
` data-${lowercaseFirstLetter(label).replace(" ", "-")}${value === undefined ? `` : `="${value}"`}`;
2222

23+
export const formatTwigAttribute = (label: string, value?: string) =>
24+
['.', '_'].includes(label.charAt(0)) ? '' : (` ${lowercaseFirstLetter(label).replace(" ", "-")}${value === undefined ? `` : `="${value}"`}`);
25+
2326
export const formatClassAttribute = (
2427
classes: string[],
2528
isJSX: boolean,

packages/backend/src/tailwind/tailwindDefaultBuilder.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import { pxToBlur } from "./conversionTables";
2828
import {
2929
formatDataAttribute,
30+
formatTwigAttribute,
3031
getClassLabel,
3132
} from "../common/commonFormatAttributes";
3233
import { TailwindColorType, TailwindSettings } from "types";
@@ -53,6 +54,10 @@ export class TailwindDefaultBuilder {
5354
return this.settings.tailwindGenerationMode === "jsx";
5455
}
5556

57+
get isTwigComponent() {
58+
return this.settings.tailwindGenerationMode === "twig" && this.node.type === "INSTANCE"
59+
}
60+
5661
constructor(node: SceneNode, settings: TailwindSettings) {
5762
this.node = node;
5863
this.settings = settings;
@@ -272,13 +277,15 @@ export class TailwindDefaultBuilder {
272277
if ("componentProperties" in this.node && this.node.componentProperties) {
273278
Object.entries(this.node.componentProperties)
274279
?.map((prop) => {
275-
if (prop[1].type === "VARIANT" || prop[1].type === "BOOLEAN") {
280+
if (prop[1].type === "VARIANT" || prop[1].type === "BOOLEAN" || (this.isTwigComponent && prop[1].type === "TEXT")) {
276281
const cleanName = prop[0]
277282
.split("#")[0]
278283
.replace(/\s+/g, "-")
279284
.toLowerCase();
280285

281-
return formatDataAttribute(cleanName, String(prop[1].value));
286+
return this.isTwigComponent
287+
? formatTwigAttribute(cleanName, String(prop[1].value))
288+
: formatDataAttribute(cleanName, String(prop[1].value));
282289
}
283290
return "";
284291
})

packages/backend/src/tailwind/tailwindMain.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ const tailwindFrame = async (
172172
node: FrameNode | InstanceNode | ComponentNode | ComponentSetNode,
173173
settings: TailwindSettings,
174174
): Promise<string> => {
175+
// Check if this is an instance and should be rendered as a Twig component
176+
if (node.type === "INSTANCE" && isTwigComponentNode(node)) {
177+
return tailwindTwigComponentInstance(node, settings);
178+
}
179+
175180
const childrenStr = await tailwindWidgetGenerator(node.children, settings);
176181

177182
const clipsContentClass =
@@ -192,6 +197,57 @@ const tailwindFrame = async (
192197
return tailwindContainer(node, childrenStr, combinedProps, settings);
193198
};
194199

200+
201+
// Helper function to generate Twig component syntax for component instances
202+
const tailwindTwigComponentInstance = async (
203+
node: InstanceNode,
204+
settings: TailwindSettings,
205+
): Promise<string> => {
206+
// Extract component name from the instance
207+
const componentName = extractComponentName(node);
208+
209+
// Get component properties if needed
210+
const builder = new TailwindDefaultBuilder(node, settings)
211+
// .commonPositionStyles()
212+
// .commonShapeStyles()
213+
;
214+
215+
const attributes = builder.build();
216+
217+
// If we have children, process them
218+
let childrenStr = "";
219+
220+
const embeddableChildren = node.children ? node.children.filter((n) => isTwigContentNode(n)) : [];
221+
222+
if (embeddableChildren.length > 0) {
223+
// We keep embedded components and Frame named "TwigContent"
224+
childrenStr = await tailwindWidgetGenerator(embeddableChildren, settings);
225+
return `\n<twig:${componentName}${attributes}>${indentString(childrenStr)}\n</twig:${componentName}>`;
226+
} else {
227+
// Self-closing tag if no children
228+
return `\n<twig:${componentName}${attributes} />`;
229+
}
230+
};
231+
232+
const isTwigComponentNode = (node: SceneNode): boolean => {
233+
return localTailwindSettings.tailwindGenerationMode === "twig" && node.type === "INSTANCE" && !extractComponentName(node).startsWith("HTML:") && !isTwigContentNode(node);
234+
}
235+
236+
const isTwigContentNode = (node: SceneNode): boolean => {
237+
return node.type === "INSTANCE" && node.name.startsWith("TwigContent");
238+
}
239+
240+
// Helper function to extract component name from an instance
241+
const extractComponentName = (node: InstanceNode): string => {
242+
// Try to get name from mainComponent if available
243+
if (node.mainComponent) {
244+
return node.mainComponent.name;
245+
}
246+
247+
// Fallback to node name if mainComponent is not available
248+
return node.name;
249+
};
250+
195251
export const tailwindContainer = (
196252
node: SceneNode &
197253
SceneNodeMixin &

packages/plugin-ui/src/codegenPreferenceOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const selectPreferenceOptions: SelectPreferenceOptions[] = [
8383
options: [
8484
{ label: "HTML", value: "html" },
8585
{ label: "React (JSX)", value: "jsx" },
86+
{ label: "Twig (Experimental)", value: "twig" },
8687
],
8788
includedLanguages: ["Tailwind"],
8889
},

packages/types/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface HTMLSettings {
99
htmlGenerationMode: "html" | "jsx" | "styled-components" | "svelte";
1010
}
1111
export interface TailwindSettings extends HTMLSettings {
12-
tailwindGenerationMode: "html" | "jsx";
12+
tailwindGenerationMode: "html" | "jsx" | "twig";
1313
roundTailwindValues: boolean;
1414
roundTailwindColors: boolean;
1515
useColorVariables: boolean;

0 commit comments

Comments
 (0)