149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
/**
|
|
* 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<string, string[]>,
|
|
* 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<string[]>} arrays
|
|
* @returns {string[][]}
|
|
*/
|
|
function cartesianProduct(arrays) {
|
|
return arrays.reduce(
|
|
(acc, curr) => acc.flatMap((combo) => curr.map((val) => [...combo, val])),
|
|
[[]],
|
|
)
|
|
}
|