diff --git a/packages/backend/src/common/commonFormatAttributes.ts b/packages/backend/src/common/commonFormatAttributes.ts index e71fd588..3f858262 100644 --- a/packages/backend/src/common/commonFormatAttributes.ts +++ b/packages/backend/src/common/commonFormatAttributes.ts @@ -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, diff --git a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts index 8c5ef14e..25fc7d40 100644 --- a/packages/backend/src/tailwind/tailwindDefaultBuilder.ts +++ b/packages/backend/src/tailwind/tailwindDefaultBuilder.ts @@ -27,6 +27,7 @@ import { import { pxToBlur } from "./conversionTables"; import { formatDataAttribute, + formatTwigAttribute, getClassLabel, } from "../common/commonFormatAttributes"; import { TailwindColorType, TailwindSettings } from "types"; @@ -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; @@ -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 ""; }) diff --git a/packages/backend/src/tailwind/tailwindMain.ts b/packages/backend/src/tailwind/tailwindMain.ts index b668ef1a..e4580acc 100644 --- a/packages/backend/src/tailwind/tailwindMain.ts +++ b/packages/backend/src/tailwind/tailwindMain.ts @@ -172,6 +172,11 @@ const tailwindFrame = async ( node: FrameNode | InstanceNode | ComponentNode | ComponentSetNode, settings: TailwindSettings, ): Promise => { + // 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 = @@ -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 => { + // 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${indentString(childrenStr)}\n`; + } else { + // Self-closing tag if no children + return `\n`; + } +}; + +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 & diff --git a/packages/plugin-ui/src/codegenPreferenceOptions.ts b/packages/plugin-ui/src/codegenPreferenceOptions.ts index 4b6a98a9..c7c27d31 100644 --- a/packages/plugin-ui/src/codegenPreferenceOptions.ts +++ b/packages/plugin-ui/src/codegenPreferenceOptions.ts @@ -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"], }, diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index eb76dbd7..3f1bb0c6 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -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;