mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 12:58:55 +00:00
🐛 Add create variant util function
This commit is contained in:
parent
445d14293e
commit
823f2107cd
132
mcp/packages/plugin/src/PenpotUtils.test.ts
Normal file
132
mcp/packages/plugin/src/PenpotUtils.test.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { PenpotUtils } from "./PenpotUtils.ts";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal penpot global mock
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function makeVariantComponent(calls: string[]) {
|
||||||
|
return {
|
||||||
|
isVariant: () => true,
|
||||||
|
setVariantProperty: (pos: number, value: string) => {
|
||||||
|
calls.push(`setVariantProperty(${pos}, ${value})`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMockPenpot(variantComps: ReturnType<typeof makeVariantComponent>[]) {
|
||||||
|
const variants = {
|
||||||
|
renamePropertyCalls: [] as string[],
|
||||||
|
addPropertyCount: 0,
|
||||||
|
renameProperty(pos: number, name: string) {
|
||||||
|
this.renamePropertyCalls.push(`${pos}:${name}`);
|
||||||
|
},
|
||||||
|
addProperty() {
|
||||||
|
this.addPropertyCount++;
|
||||||
|
},
|
||||||
|
variantComponents() {
|
||||||
|
return variantComps;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = { variants };
|
||||||
|
|
||||||
|
return {
|
||||||
|
mock: {
|
||||||
|
utils: {
|
||||||
|
types: {
|
||||||
|
isVariantComponent: (c: any) => c.isVariant(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createVariantFromComponents: (_shapes: any[]) => container,
|
||||||
|
} as any,
|
||||||
|
variants,
|
||||||
|
container,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test("createVariant — single property: renames Property 1 and sets values", () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const comps = [makeVariantComponent(calls), makeVariantComponent(calls), makeVariantComponent(calls)];
|
||||||
|
const { mock, variants } = makeMockPenpot(comps);
|
||||||
|
|
||||||
|
(globalThis as any).penpot = mock;
|
||||||
|
|
||||||
|
PenpotUtils.createVariant([
|
||||||
|
{ shape: {} as any, properties: { Size: "Small" } },
|
||||||
|
{ shape: {} as any, properties: { Size: "Medium" } },
|
||||||
|
{ shape: {} as any, properties: { Size: "Large" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// renameProperty(0, "Size") — no addProperty since there's only one property
|
||||||
|
assert.deepEqual(variants.renamePropertyCalls, ["0:Size"]);
|
||||||
|
assert.equal(variants.addPropertyCount, 0);
|
||||||
|
|
||||||
|
// each component gets setVariantProperty(0, value)
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
"setVariantProperty(0, Small)",
|
||||||
|
"setVariantProperty(0, Medium)",
|
||||||
|
"setVariantProperty(0, Large)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createVariant — two properties: renames first, adds and renames second, sets all values", () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const comps = [makeVariantComponent(calls), makeVariantComponent(calls)];
|
||||||
|
const { mock, variants } = makeMockPenpot(comps);
|
||||||
|
|
||||||
|
(globalThis as any).penpot = mock;
|
||||||
|
|
||||||
|
PenpotUtils.createVariant([
|
||||||
|
{ shape: {} as any, properties: { Size: "Small", State: "Default" } },
|
||||||
|
{ shape: {} as any, properties: { Size: "Large", State: "Hover" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Property 1 renamed to Size; addProperty() called once; Property 2 renamed to State
|
||||||
|
assert.deepEqual(variants.renamePropertyCalls, ["0:Size", "1:State"]);
|
||||||
|
assert.equal(variants.addPropertyCount, 1);
|
||||||
|
|
||||||
|
// comp 0: pos 0 = Small, pos 1 = Default; comp 1: pos 0 = Large, pos 1 = Hover
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
"setVariantProperty(0, Small)",
|
||||||
|
"setVariantProperty(1, Default)",
|
||||||
|
"setVariantProperty(0, Large)",
|
||||||
|
"setVariantProperty(1, Hover)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createVariant — property name order follows first-seen order across components", () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const comps = [makeVariantComponent(calls), makeVariantComponent(calls)];
|
||||||
|
const { mock, variants } = makeMockPenpot(comps);
|
||||||
|
|
||||||
|
(globalThis as any).penpot = mock;
|
||||||
|
|
||||||
|
// "Color" appears first in comp[0]; "Size" appears first in comp[0] too
|
||||||
|
PenpotUtils.createVariant([
|
||||||
|
{ shape: {} as any, properties: { Color: "Red", Size: "Small" } },
|
||||||
|
{ shape: {} as any, properties: { Color: "Blue", Size: "Large" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(variants.renamePropertyCalls, ["0:Color", "1:Size"]);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
"setVariantProperty(0, Red)",
|
||||||
|
"setVariantProperty(1, Small)",
|
||||||
|
"setVariantProperty(0, Blue)",
|
||||||
|
"setVariantProperty(1, Large)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createVariant — returns the container from createVariantFromComponents", () => {
|
||||||
|
const { mock, container } = makeMockPenpot([]);
|
||||||
|
(globalThis as any).penpot = mock;
|
||||||
|
|
||||||
|
const result = PenpotUtils.createVariant([{ shape: {} as any, properties: { X: "1" } }]);
|
||||||
|
|
||||||
|
assert.equal(result, container);
|
||||||
|
});
|
||||||
@ -1,4 +1,15 @@
|
|||||||
import { Board, Bounds, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape, Text } from "@penpot/plugin-types";
|
import type {
|
||||||
|
Board,
|
||||||
|
Bounds,
|
||||||
|
Fill,
|
||||||
|
FlexLayout,
|
||||||
|
GridLayout,
|
||||||
|
Page,
|
||||||
|
Rectangle,
|
||||||
|
Shape,
|
||||||
|
Text,
|
||||||
|
VariantContainer,
|
||||||
|
} from "@penpot/plugin-types";
|
||||||
|
|
||||||
export class PenpotUtils {
|
export class PenpotUtils {
|
||||||
/**
|
/**
|
||||||
@ -512,6 +523,82 @@ export class PenpotUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a variant group from a list of main component instances and immediately
|
||||||
|
* configures its property names and values.
|
||||||
|
*
|
||||||
|
* Encapsulates the full multi-step workflow in a single call:
|
||||||
|
* 1. `penpot.createVariantFromComponents()` — groups the components
|
||||||
|
* 2. `variants.renameProperty()` / `variants.addProperty()` — sets property names
|
||||||
|
* 3. `comp.setVariantProperty()` — sets each component's property values
|
||||||
|
*
|
||||||
|
* @param components - Array of main-instance boards to combine. Each element carries
|
||||||
|
* a `shape` (the Board on the canvas) and a `properties` map of
|
||||||
|
* `{ propertyName: value }` for that component.
|
||||||
|
* @returns The newly created `VariantContainer`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Three button variants differing in Size
|
||||||
|
* const [s, m, l] = penpot.currentPage.findAllShapes(sh => sh.isMainComponent()).slice(0, 3) as Board[];
|
||||||
|
* const container = PenpotUtils.createVariant([
|
||||||
|
* { shape: s, properties: { Size: 'Small' } },
|
||||||
|
* { shape: m, properties: { Size: 'Medium' } },
|
||||||
|
* { shape: l, properties: { Size: 'Large' } },
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Two properties: Size × State
|
||||||
|
* const container = PenpotUtils.createVariant([
|
||||||
|
* { shape: s, properties: { Size: 'Small', State: 'Default' } },
|
||||||
|
* { shape: m, properties: { Size: 'Medium', State: 'Default' } },
|
||||||
|
* { shape: l, properties: { Size: 'Large', State: 'Hover' } },
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
public static createVariant(
|
||||||
|
components: Array<{ shape: Board; properties: Record<string, string> }>
|
||||||
|
): VariantContainer {
|
||||||
|
// Collect all unique property names (preserving first-seen order)
|
||||||
|
const propNames: string[] = [];
|
||||||
|
for (const { properties } of components) {
|
||||||
|
for (const name of Object.keys(properties)) {
|
||||||
|
if (!propNames.includes(name)) propNames.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create the variant container
|
||||||
|
// @ts-ignore — createVariantFromComponents was added after plugin-types@1.4.1
|
||||||
|
const container: VariantContainer = (penpot as any).createVariantFromComponents(components.map((c) => c.shape));
|
||||||
|
const variants = container.variants;
|
||||||
|
if (!variants) return container;
|
||||||
|
|
||||||
|
// 2. Rename / add properties
|
||||||
|
// createVariantFromComponents always creates exactly one property ("Property 1")
|
||||||
|
for (let i = 0; i < propNames.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
variants.renameProperty(0, propNames[0]);
|
||||||
|
} else {
|
||||||
|
variants.addProperty();
|
||||||
|
variants.renameProperty(i, propNames[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Set each component's property values
|
||||||
|
const variantComps = variants.variantComponents();
|
||||||
|
for (let compIdx = 0; compIdx < variantComps.length; compIdx++) {
|
||||||
|
const comp = variantComps[compIdx];
|
||||||
|
if (!penpot.utils.types.isVariantComponent(comp)) continue;
|
||||||
|
const props = components[compIdx]?.properties ?? {};
|
||||||
|
for (let propIdx = 0; propIdx < propNames.length; propIdx++) {
|
||||||
|
const value = props[propNames[propIdx]];
|
||||||
|
if (value !== undefined) {
|
||||||
|
comp.setVariantProperty(propIdx, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an overview of all tokens organized by token set name, token type, and token name.
|
* Generates an overview of all tokens organized by token set name, token type, and token name.
|
||||||
* The result is a nested object structure: {tokenSetName: {tokenType: [tokenName, ...]}}.
|
* The result is a nested object structure: {tokenSetName: {tokenType: [tokenName, ...]}}.
|
||||||
|
|||||||
@ -304,10 +304,29 @@ Variants are a system for grouping related component versions along named proper
|
|||||||
Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`.
|
Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`.
|
||||||
|
|
||||||
**Creating a variant group**:
|
**Creating a variant group**:
|
||||||
- `penpot.createVariantFromComponents(mainInstances: Board[]): VariantContainer`: Combines several main component instances into a new variant group.
|
|
||||||
All components end up inside a single new container on the canvas.
|
Use `penpotUtils.createVariant(components)` — it handles the full multi-step workflow in one call:
|
||||||
The container's `Variants` instance is initialised with one property `Property 1`, with the property values set to the respective component's name.
|
```js
|
||||||
- After creation, edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`.
|
const [s, m, l] = penpot.currentPage.findAllShapes(sh => sh.isMainComponent()).slice(0, 3);
|
||||||
|
// Single property:
|
||||||
|
const container = penpotUtils.createVariant([
|
||||||
|
{ shape: s, properties: { Size: 'Small' } },
|
||||||
|
{ shape: m, properties: { Size: 'Medium' } },
|
||||||
|
{ shape: l, properties: { Size: 'Large' } },
|
||||||
|
]);
|
||||||
|
// Multiple properties:
|
||||||
|
const container2 = penpotUtils.createVariant([
|
||||||
|
{ shape: s, properties: { Size: 'Small', State: 'Default' } },
|
||||||
|
{ shape: m, properties: { Size: 'Medium', State: 'Default' } },
|
||||||
|
{ shape: l, properties: { Size: 'Large', State: 'Hover' } },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
If you must use the lower-level API, follow this exact order — skipping or reordering steps leaves the variant broken:
|
||||||
|
1. `penpot.createVariantFromComponents(mainInstances)` — groups components; always creates one property called `"Property 1"`.
|
||||||
|
2. `container.variants.renameProperty(0, name)` — rename `Property 1`.
|
||||||
|
3. For each extra property: `variants.addProperty()` then `variants.renameProperty(pos, name)`.
|
||||||
|
4. For every component × every property: iterate `variants.variantComponents()` and call `comp.setVariantProperty(pos, value)`.
|
||||||
|
|
||||||
**Adding a variant to an existing group**:
|
**Adding a variant to an existing group**:
|
||||||
Use `variantContainer.appendChild(mainInstance)` to move a component's main instance into the container, then set its position manually and assign property values via `setVariantProperty`.
|
Use `variantContainer.appendChild(mainInstance)` to move a component's main instance into the container, then set its position manually and assign property values via `setVariantProperty`.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user