css-generator.ts•4.89 kB
import type { FlatElement, PseudoCodeOptions } from "../types/generator.js";
/**
* Generates CSS classes for all elements in the design.
* Creates class definitions based on element properties and appearance.
*/
export const generateCSS = ({
elements,
options,
}: {
elements: FlatElement[];
options: PseudoCodeOptions;
}): string => {
const cssBlocks: string[] = [];
elements.forEach((element) => {
if (element.type === "Component") return;
const className = generateClassName(element);
const cssProperties = generateCSSProperties(element);
if (Object.keys(cssProperties).length > 0) {
const cssBlock = formatCSSBlock({
className,
properties: cssProperties,
options,
});
cssBlocks.push(cssBlock);
}
});
return cssBlocks.join("\n\n");
};
/**
* Generates a CSS class name from an element's properties.
* Creates a unique, clean, and descriptive class name based on element type and name.
*/
export const generateClassName = (element: FlatElement): string => {
return element.name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.replace(/\s+/g, "-")
.replace(/^-+|-+$/g, "").slice(0, 50);
};
/**
* Extracts and formats CSS properties from an element's attributes.
* Converts Figma style properties into valid CSS properties.
*/
export const generateCSSProperties = (
element: FlatElement
): Record<string, string> => {
const properties: Record<string, string> = {};
if (element.appearance) {
const { appearance } = element;
if (appearance.backgroundColor)
properties["background-color"] = appearance.backgroundColor;
if (appearance.borderColor)
properties["border-color"] = appearance.borderColor;
if (appearance.borderWidth)
properties["border-width"] = appearance.borderWidth;
if (appearance.borderRadius)
properties["border-radius"] = appearance.borderRadius;
if (appearance.opacity !== undefined)
properties["opacity"] = String(appearance.opacity);
if (appearance.shadow) properties["box-shadow"] = appearance.shadow;
}
if (element.text) {
const { text } = element;
if (text.fontFamily) properties["font-family"] = `'${text.fontFamily}'`;
if (text.fontSize) properties["font-size"] = `${text.fontSize}px`;
if (text.fontWeight) properties["font-weight"] = String(text.fontWeight);
if (text.color) properties["color"] = text.color;
if (text.alignment) properties["text-align"] = text.alignment.toLowerCase();
properties["margin"] = "0";
}
if (element.layout) {
const { layout } = element;
if (layout.direction) {
properties["display"] = "flex";
properties["flex-direction"] =
layout.direction === "column" ? "column" : "row";
}
if (layout.justifyContent)
properties["justify-content"] = convertJustifyContent(
layout.justifyContent
);
if (layout.alignItems)
properties["align-items"] = convertAlignItems(layout.alignItems);
if (layout.gap) properties["gap"] = `${layout.gap}px`;
if (layout.padding) properties["padding"] = layout.padding;
if (layout.width) properties["width"] = layout.width;
if (layout.height) properties["height"] = layout.height;
}
return properties;
};
/**
* Converts Figma justify content values to standard CSS values
* Maps between Figma's property names and CSS flexbox justify-content values.
*/
export const convertJustifyContent = (figmaValue: string): string => {
const mapping: Record<string, string> = {
"space-between": "space-between",
"space-around": "space-around",
"space-evenly": "space-evenly",
center: "center",
"flex-start": "flex-start",
"flex-end": "flex-end",
start: "flex-start",
end: "flex-end",
};
return mapping[figmaValue] || figmaValue;
};
/**
* Converts Figma alignment values to standard CSS values
* Maps between Figma's property names and CSS flexbox align-items values.
*/
export const convertAlignItems = (figmaValue: string): string => {
const mapping: Record<string, string> = {
center: "center",
"flex-start": "flex-start",
"flex-end": "flex-end",
stretch: "stretch",
baseline: "baseline",
start: "flex-start",
end: "flex-end",
};
return mapping[figmaValue] || figmaValue;
};
/**
* Formats CSS properties into a properly indented CSS class block
* Creates a formatted CSS class with its properties.
*/
export const formatCSSBlock = ({
className,
properties,
options,
}: {
className: string;
properties: Record<string, string>;
options: PseudoCodeOptions;
}): string => {
const propertyLines = Object.entries(properties)
.map(([key, value]) => ` ${key}: ${value};`)
.join("\n");
let cssBlock = `.${className} {\n${propertyLines}\n}`;
if (options.includeComments) {
cssBlock = `\n${cssBlock}`;
}
return cssBlock;
};