Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/backend/src/common/commonFormatAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const formatStyleAttribute = (
export const formatDataAttribute = (label: string, value?: string) =>
` data-${lowercaseFirstLetter(label).replace(" ", "-")}${value === undefined ? `` : `="${value}"`}`;

export const formatTwigAttribute = (label: string, value?: string) =>
['.', '_'].includes(label.charAt(0)) ? '' : (` ${lowercaseFirstLetter(label).replace(" ", "-")}${value === undefined ? `` : `="${value}"`}`);

export const formatClassAttribute = (
classes: string[],
isJSX: boolean,
Expand Down
11 changes: 9 additions & 2 deletions packages/backend/src/tailwind/tailwindDefaultBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { pxToBlur } from "./conversionTables";
import {
formatDataAttribute,
formatTwigAttribute,
getClassLabel,
} from "../common/commonFormatAttributes";
import { TailwindColorType, TailwindSettings } from "types";
Expand All @@ -53,6 +54,10 @@ export class TailwindDefaultBuilder {
return this.settings.tailwindGenerationMode === "jsx";
}

get isTwigComponent() {
return this.settings.tailwindGenerationMode === "twig" && this.node.type === "INSTANCE"
}

constructor(node: SceneNode, settings: TailwindSettings) {
this.node = node;
this.settings = settings;
Expand Down Expand Up @@ -272,13 +277,15 @@ export class TailwindDefaultBuilder {
if ("componentProperties" in this.node && this.node.componentProperties) {
Object.entries(this.node.componentProperties)
?.map((prop) => {
if (prop[1].type === "VARIANT" || prop[1].type === "BOOLEAN") {
if (prop[1].type === "VARIANT" || prop[1].type === "BOOLEAN" || (this.isTwigComponent && prop[1].type === "TEXT")) {
const cleanName = prop[0]
.split("#")[0]
.replace(/\s+/g, "-")
.toLowerCase();

return formatDataAttribute(cleanName, String(prop[1].value));
return this.isTwigComponent
? formatTwigAttribute(cleanName, String(prop[1].value))
: formatDataAttribute(cleanName, String(prop[1].value));
}
return "";
})
Expand Down
56 changes: 56 additions & 0 deletions packages/backend/src/tailwind/tailwindMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ const tailwindFrame = async (
node: FrameNode | InstanceNode | ComponentNode | ComponentSetNode,
settings: TailwindSettings,
): Promise<string> => {
// Check if this is an instance and should be rendered as a Twig component
if (node.type === "INSTANCE" && isTwigComponentNode(node)) {
return tailwindTwigComponentInstance(node, settings);
}

const childrenStr = await tailwindWidgetGenerator(node.children, settings);

const clipsContentClass =
Expand All @@ -192,6 +197,57 @@ const tailwindFrame = async (
return tailwindContainer(node, childrenStr, combinedProps, settings);
};


// Helper function to generate Twig component syntax for component instances
const tailwindTwigComponentInstance = async (
node: InstanceNode,
settings: TailwindSettings,
): Promise<string> => {
// Extract component name from the instance
const componentName = extractComponentName(node);

// Get component properties if needed
const builder = new TailwindDefaultBuilder(node, settings)
// .commonPositionStyles()
// .commonShapeStyles()
;

const attributes = builder.build();

// If we have children, process them
let childrenStr = "";

const embeddableChildren = node.children ? node.children.filter((n) => isTwigContentNode(n)) : [];

if (embeddableChildren.length > 0) {
// We keep embedded components and Frame named "TwigContent"
childrenStr = await tailwindWidgetGenerator(embeddableChildren, settings);
return `\n<twig:${componentName}${attributes}>${indentString(childrenStr)}\n</twig:${componentName}>`;
} else {
// Self-closing tag if no children
return `\n<twig:${componentName}${attributes} />`;
}
};

const isTwigComponentNode = (node: SceneNode): boolean => {
return localTailwindSettings.tailwindGenerationMode === "twig" && node.type === "INSTANCE" && !extractComponentName(node).startsWith("HTML:") && !isTwigContentNode(node);
}

const isTwigContentNode = (node: SceneNode): boolean => {
return node.type === "INSTANCE" && node.name.startsWith("TwigContent");
}

// Helper function to extract component name from an instance
const extractComponentName = (node: InstanceNode): string => {
// Try to get name from mainComponent if available
if (node.mainComponent) {
return node.mainComponent.name;
}

// Fallback to node name if mainComponent is not available
return node.name;
};

export const tailwindContainer = (
node: SceneNode &
SceneNodeMixin &
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ui/src/codegenPreferenceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const selectPreferenceOptions: SelectPreferenceOptions[] = [
options: [
{ label: "HTML", value: "html" },
{ label: "React (JSX)", value: "jsx" },
{ label: "Twig (Experimental)", value: "twig" },
],
includedLanguages: ["Tailwind"],
},
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface HTMLSettings {
htmlGenerationMode: "html" | "jsx" | "styled-components" | "svelte";
}
export interface TailwindSettings extends HTMLSettings {
tailwindGenerationMode: "html" | "jsx";
tailwindGenerationMode: "html" | "jsx" | "twig";
roundTailwindValues: boolean;
roundTailwindColors: boolean;
useColorVariables: boolean;
Expand Down