/** * createComponentWithVariants * * Creates a component set by generating all combinations of `variantAxes`, * building one Figma component per combination, then calling * `figma.combineAsVariants` to produce the component set. After combining, * the variants are repositioned into a grid so they don't all stack at (0, 0). * * @param {{ * name: string, * variantAxes: Record, * baseProps: { * width: number, * height: number, * fills?: Paint[], * padding?: {top?: number, bottom?: number, left?: number, right?: number}, * radius?: number, * layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE', * itemSpacing?: number * }, * page: PageNode * }} config * - `name`: Component set name (e.g. "Button"). * - `variantAxes`: Each key is a variant property name; each value is an array of * allowed values. All combinations are generated (Cartesian product). * Example: { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Ghost'] } * produces 6 variants. * - `baseProps`: Visual properties applied to every variant. * - `page`: The PageNode to create components on (must be set as current page by caller). * @param {string} [runId] - Optional dsb_run_id to tag every node. * @returns {Promise<{ * componentSet: ComponentSetNode, * variants: ComponentNode[] * }>} */ async function createComponentWithVariants(config, runId) { const { name, variantAxes, baseProps, page } = config // Ensure we are on the correct page await figma.setCurrentPageAsync(page) // Compute Cartesian product of variant axes const axisNames = Object.keys(variantAxes) const axisValues = axisNames.map((k) => variantAxes[k]) const combinations = cartesianProduct(axisValues) // Build one component per combination const components = [] for (const combo of combinations) { const comp = figma.createComponent() // Name: "Property=Value, Property=Value, ..." comp.name = axisNames.map((ax, i) => `${ax}=${combo[i]}`).join(', ') // Base geometry comp.resize(baseProps.width, baseProps.height) // Fills if (baseProps.fills !== undefined) { comp.fills = baseProps.fills } else { comp.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }] } // Corner radius if (baseProps.radius !== undefined) { comp.cornerRadius = baseProps.radius } // Auto-layout if (baseProps.layoutMode && baseProps.layoutMode !== 'NONE') { comp.layoutMode = baseProps.layoutMode comp.primaryAxisAlignItems = 'CENTER' comp.counterAxisAlignItems = 'CENTER' if (baseProps.itemSpacing !== undefined) { comp.itemSpacing = baseProps.itemSpacing } } // Padding if (baseProps.padding) { comp.paddingTop = baseProps.padding.top ?? 0 comp.paddingBottom = baseProps.padding.bottom ?? 0 comp.paddingLeft = baseProps.padding.left ?? 0 comp.paddingRight = baseProps.padding.right ?? 0 } // Plugin data const variantKey = axisNames.map((ax, i) => `${ax}:${combo[i]}`).join('|') comp.setPluginData('dsb_key', `component/${name}/${variantKey}`) if (runId) { comp.setPluginData('dsb_run_id', runId) } page.appendChild(comp) components.push(comp) } // Combine into a component set const componentSet = figma.combineAsVariants(components, page) componentSet.name = name componentSet.setPluginData('dsb_key', `componentSet/${name}`) if (runId) { componentSet.setPluginData('dsb_run_id', runId) } // Grid layout — variants stack at (0, 0) after combineAsVariants; reposition them. const GRID_GAP = 16 const cols = Math.max(1, axisValues[axisValues.length - 1]?.length ?? 1) const variantWidth = baseProps.width const variantHeight = baseProps.height componentSet.children.forEach((variant, idx) => { const col = idx % cols const row = Math.floor(idx / cols) variant.x = col * (variantWidth + GRID_GAP) variant.y = row * (variantHeight + GRID_GAP) }) // Resize component set to wrap its children with padding const totalCols = Math.min(cols, combinations.length) const totalRows = Math.ceil(combinations.length / cols) const PADDING = 40 componentSet.resize( totalCols * variantWidth + (totalCols - 1) * GRID_GAP + PADDING * 2, totalRows * variantHeight + (totalRows - 1) * GRID_GAP + PADDING * 2, ) // Position component set at a safe canvas location componentSet.x = 480 componentSet.y = 80 return { componentSet, variants: componentSet.children } } /** * Computes the Cartesian product of multiple arrays. * cartesianProduct([[A, B], [1, 2]]) → [[A,1], [A,2], [B,1], [B,2]] * * @param {Array} arrays * @returns {string[][]} */ function cartesianProduct(arrays) { return arrays.reduce( (acc, curr) => acc.flatMap((combo) => curr.map((val) => [...combo, val])), [[]], ) }