/**
* Map Figma auto-layout properties to CSS flexbox.
*/
export interface LayoutCSS {
display?: string;
flexDirection?: string;
justifyContent?: string;
alignItems?: string;
gap?: string;
padding?: string;
width?: string;
height?: string;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface LayoutNode {
layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE';
primaryAxisAlignItems?: string;
counterAxisAlignItems?: string;
itemSpacing?: number;
paddingTop?: number;
paddingRight?: number;
paddingBottom?: number;
paddingLeft?: number;
layoutSizingHorizontal?: string;
layoutSizingVertical?: string;
absoluteBoundingBox?: {width: number; height: number; x: number; y: number};
[key: string]: any;
}
const JUSTIFY_MAP: Record<string, string> = {
MIN: 'flex-start',
CENTER: 'center',
MAX: 'flex-end',
SPACE_BETWEEN: 'space-between',
};
const ALIGN_MAP: Record<string, string> = {
MIN: 'flex-start',
CENTER: 'center',
MAX: 'flex-end',
BASELINE: 'baseline',
};
function sizingToCSS(sizing: string | undefined, dimension: number | undefined): string | undefined {
if (sizing === 'FILL') return '100%';
if (sizing === 'HUG') return 'auto';
if (sizing === 'FIXED' && dimension !== undefined) return `${dimension}px`;
return undefined;
}
function formatPadding(top: number, right: number, bottom: number, left: number): string | undefined {
if (top === 0 && right === 0 && bottom === 0 && left === 0) return undefined;
if (top === right && right === bottom && bottom === left) return `${top}px`;
if (top === bottom && right === left) return `${top}px ${right}px`;
return `${top}px ${right}px ${bottom}px ${left}px`;
}
/**
* Extract CSS flexbox layout properties from a Figma node.
* Returns undefined if the node has no auto-layout.
*/
export function extractLayout(node: LayoutNode): LayoutCSS | undefined {
if (!node.layoutMode || node.layoutMode === 'NONE') return undefined;
const css: LayoutCSS = {
display: 'flex',
flexDirection: node.layoutMode === 'VERTICAL' ? 'column' : 'row',
};
if (node.primaryAxisAlignItems && JUSTIFY_MAP[node.primaryAxisAlignItems]) {
css.justifyContent = JUSTIFY_MAP[node.primaryAxisAlignItems];
}
if (node.counterAxisAlignItems && ALIGN_MAP[node.counterAxisAlignItems]) {
css.alignItems = ALIGN_MAP[node.counterAxisAlignItems];
}
if (node.itemSpacing && node.itemSpacing > 0) {
css.gap = `${node.itemSpacing}px`;
}
const padding = formatPadding(
node.paddingTop ?? 0,
node.paddingRight ?? 0,
node.paddingBottom ?? 0,
node.paddingLeft ?? 0,
);
if (padding) css.padding = padding;
const bounds = node.absoluteBoundingBox;
const width = sizingToCSS(node.layoutSizingHorizontal, bounds?.width);
if (width) css.width = width;
const height = sizingToCSS(node.layoutSizingVertical, bounds?.height);
if (height) css.height = height;
return css;
}