Merge pull request #8414 from oraios/mcp-dev-latest

 Update MCP server to account for recent API changes & general improvements
This commit is contained in:
Luis de Dios 2026-02-26 13:18:19 +01:00 committed by GitHub
commit b5874b365b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 234 additions and 167 deletions

View File

@ -1,7 +1,10 @@
# Penpot MCP Project Overview - Updated
## Purpose
This project is a Model Context Protocol (MCP) server for Penpot integration. It provides a TypeScript-based server that can be used to extend Penpot's functionality through custom tools with bidirectional WebSocket communication.
This project is a Model Context Protocol (MCP) server for Penpot integration.
The MCP server communicates with a Penpot plugin via WebSockets, allowing
the MCP server to send tasks to the plugin and receive results,
enabling advanced AI-driven features in Penpot.
## Tech Stack
- **Language**: TypeScript
@ -13,21 +16,22 @@ This project is a Model Context Protocol (MCP) server for Penpot integration. It
## Project Structure
```
penpot-mcp/
├── common/ # Shared type definitions
/ (project root)
├── packages/common/ # Shared type definitions
│ ├── src/
│ │ ├── index.ts # Exports for shared types
│ │ └── types.ts # PluginTaskResult, request/response interfaces
│ └── package.json # @penpot-mcp/common package
├── mcp-server/ # Main MCP server implementation
├── packages/server/ # Main MCP server implementation
│ ├── src/
│ │ ├── index.ts # Main server entry point
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
│ │ ├── PluginTask.ts # Now supports result promises
│ │ ├── tasks/ # PluginTask implementations
│ │ └── tools/ # Tool implementations
| ├── data/ # Contains resources, such as API info and prompts
│ └── package.json # Includes @penpot-mcp/common dependency
├── penpot-plugin/ # Penpot plugin with response capability
├── packages/plugin/ # Penpot plugin with response capability
│ ├── src/
│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding
│ │ └── plugin.ts # Now sends task responses back to server
@ -37,55 +41,24 @@ penpot-mcp/
## Key Tasks
### Adjusting the System Prompt
The system prompt file is located in `packages/server/data/initial_instructions.md`.
### Adding a new Tool
1. Implement the tool class in `mcp-server/src/tools/` following the `Tool` interface.
1. Implement the tool class in `packages/server/src/tools/` following the `Tool` interface.
IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally.
2. Register the tool in `PenpotMcpServer`.
Look at `PrintTextTool` as an example.
Many tools are linked to tasks that are handled in the plugin, i.e. they have an associated `PluginTask` implementation in `mcp-server/src/tasks/`.
Tools can be associated with a `PluginTask` that is executed in the plugin.
Many tools build on `ExecuteCodePluginTask`, as many operations can be reduced to code execution.
### Adding a new PluginTask
1. Implement the input data interface for the task in `common/src/types.ts`.
2. Implement the `PluginTask` class in `mcp-server/src/tasks/`.
3. Implement the corresponding task handler class in the plugin (`penpot-plugin/src/task-handlers/`).
1. Implement the input data interface for the task in `packages/common/src/types.ts`.
2. Implement the `PluginTask` class in `packages/server/src/tasks/`.
3. Implement the corresponding task handler class in the plugin (`packages/plugin/src/task-handlers/`).
* In the success case, call `task.sendSuccess`.
* In the failure case, just throw an exception, which will be handled centrally!
* Look at `PrintTextTaskHandler` as an example.
4. Register the task handler in `penpot-plugin/src/plugin.ts` in the `taskHandlers` list.
## Key Components
### Enhanced WebSocket Protocol
- **Request Format**: `{id: string, task: string, params: any}`
- **Response Format**: `{id: string, result: {success: boolean, error?: string, data?: any}}`
- **Request/Response Correlation**: Using unique UUIDs for task tracking
- **Timeout Handling**: 30-second timeout with automatic cleanup
- **Type Safety**: Shared definitions via @penpot-mcp/common package
### Core Classes
- **PenpotMcpServer**: Enhanced with pending task tracking and response handling
- **PluginTask**: Now creates result promises that resolve when plugin responds
- **Tool implementations**: Now properly await task completion and report results
- **Plugin handlers**: Send structured responses back to server
### New Features
1. **Bidirectional Communication**: Plugin now responds with success/failure status
2. **Task Result Promises**: Every executePluginTask() sets and returns a promise
3. **Error Reporting**: Failed tasks properly report error messages to tools
4. **Shared Type Safety**: Common package ensures consistency across projects
5. **Timeout Protection**: Tasks don't hang indefinitely (30s limit)
6. **Request Correlation**: Unique IDs match requests to responses
## Task Flow
```
LLM Tool Call → MCP Server → WebSocket (Request) → Plugin → Penpot API
↑ ↓
Tool Response ← MCP Server ← WebSocket (Response) ← Plugin Result
```
4. Register the task handler in `packages/plugin/src/plugin.ts` in the `taskHandlers` list.

View File

@ -1,5 +1,3 @@
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
@ -19,7 +17,7 @@ read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
@ -62,15 +60,17 @@ excluded_tools: []
# (contrary to the memories, which are loaded on demand).
initial_prompt: |
IMPORTANT: You use an idiomatic, object-oriented style.
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
rather than mere functions (i.e. use the strategy pattern, for example).
Comments:
Always read the "project_overview" memory.
Comments:
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
clearly defines *what* it is. Any details then follow in subsequent sentences.
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
required for sentences).
# the name by which the project can be referenced within Serena
project_name: "penpot-mcp"
@ -128,3 +128,16 @@ encoding: utf-8
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:

View File

@ -1,4 +1,4 @@
import { Board, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape } from "@penpot/plugin-types";
import { Board, Bounds, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape, Text } from "@penpot/plugin-types";
export class PenpotUtils {
/**
@ -189,6 +189,24 @@ export class PenpotUtils {
return penpot.generateStyle([shape], { type: "css", includeChildren: true });
}
/**
* Gets the actual rendering bounds of a shape. For most shapes, this is simply the `bounds` property.
* However, for Text shapes, the `bounds` may not reflect the true size of the rendered text content,
* so we use the `textBounds` property instead.
*
* @param shape - The shape to get the bounds for
*/
public static getBounds(shape: Shape): Bounds {
if (shape.type === "text") {
const text = shape as Text;
// TODO: Remove ts-ignore once type definitions are updated
// @ts-ignore
return text.textBounds;
} else {
return shape.bounds;
}
}
/**
* Checks if a child shape is fully contained within its parent's bounds.
* Visual containment means all edges of the child are within the parent's bounding box.
@ -198,11 +216,13 @@ export class PenpotUtils {
* @returns true if child is fully contained within parent bounds, false otherwise
*/
public static isContainedIn(child: Shape, parent: Shape): boolean {
const childBounds = this.getBounds(child);
const parentBounds = this.getBounds(parent);
return (
child.x >= parent.x &&
child.y >= parent.y &&
child.x + child.width <= parent.x + parent.width &&
child.y + child.height <= parent.y + parent.height
childBounds.x >= parentBounds.x &&
childBounds.y >= parentBounds.y &&
childBounds.x + childBounds.width <= parentBounds.x + parentBounds.width &&
childBounds.y + childBounds.height <= parentBounds.y + parentBounds.height
);
}
@ -298,39 +318,16 @@ export class PenpotUtils {
/**
* Decodes a base64 string to a Uint8Array.
* This is required because the Penpot plugin environment does not provide the atob function.
*
* @param base64 - The base64-encoded string to decode
* @returns The decoded data as a Uint8Array
*/
public static atob(base64: string): Uint8Array {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const lookup = new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
public static base64ToByteArray(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
let bufferLength = base64.length * 0.75;
if (base64[base64.length - 1] === "=") {
bufferLength--;
if (base64[base64.length - 2] === "=") {
bufferLength--;
}
}
const bytes = new Uint8Array(bufferLength);
let p = 0;
for (let i = 0; i < base64.length; i += 4) {
const encoded1 = lookup[base64.charCodeAt(i)];
const encoded2 = lookup[base64.charCodeAt(i + 1)];
const encoded3 = lookup[base64.charCodeAt(i + 2)];
const encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return bytes;
}
@ -360,7 +357,7 @@ export class PenpotUtils {
height: number | undefined
): Promise<Rectangle> {
// convert base64 to Uint8Array
const bytes = PenpotUtils.atob(base64);
const bytes = PenpotUtils.base64ToByteArray(base64);
// upload the image data to Penpot
const imageData = await penpot.uploadMediaData(name, bytes, mimeType);

View File

@ -195,10 +195,39 @@ export class ExecuteCodeTaskHandler extends TaskHandler<ExecuteCodeTaskParams> {
const context = this.context;
const code = task.params.code;
let result: any = await (async (ctx) => {
const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`);
return fn(...Object.values(ctx));
})(context);
// set the penpot.flags.naturalChildOrdering to true during code execution.
// NOTE: This significantly simplifies API usage (see )
// TODO: Remove ts-ignore once Penpot types have been updated
let originalNaturalChildOrdering: any;
// @ts-ignore
if (penpot.flags) {
// @ts-ignore
originalNaturalChildOrdering = penpot.flags.naturalChildOrdering;
// @ts-ignore
penpot.flags.naturalChildOrdering = true;
} else {
// TODO: This can be removed once `flags` has been merged to PROD
throw new Error(
"You are using a version of the Penpot MCP server which is incompatible " +
"with the connected Penpot version. " +
"Read the documentation for instructions on which version to use: " +
"https://github.com/penpot/penpot/tree/develop/mcp\n" +
"If you are an LLM, tell the user about this!"
);
}
let result: any;
try {
// execute the code in an async function with the context variables as parameters
result = await (async (ctx) => {
const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`);
return fn(...Object.values(ctx));
})(context);
} finally {
// restore the original value of penpot.flags.naturalChildOrdering
// @ts-ignore
penpot.flags.naturalChildOrdering = originalNaturalChildOrdering;
}
console.log("Code execution result:", result);

View File

@ -11,7 +11,7 @@ Penpot:
open: (
name: string,
url: string,
options?: { width: number; height: number },
options?: { width: number; height: number; hidden: boolean },
) => void;
size: { width: number; height: number } | null;
resize: (width: number, height: number) => void;
@ -99,7 +99,7 @@ Penpot:
open: (
name: string,
url: string,
options?: { width: number; height: number },
options?: { width: number; height: number; hidden: boolean },
) => void;
size: { width: number; height: number } | null;
resize: (width: number, height: number) => void;
@ -110,7 +110,7 @@ Penpot:
Type Declaration
* open: (name: string, url: string, options?: { width: number; height: number }) => void
* open: ( name: string, url: string, options?: { width: number; height: number; hidden: boolean },) => void
Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`.
There is a minimum and maximum size for this modal and a default size but it's possible to customize it anyway with the options parameter.
@ -1062,7 +1062,7 @@ Board:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -1456,7 +1456,7 @@ Board:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -2171,7 +2171,7 @@ VariantContainer:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -2568,7 +2568,7 @@ VariantContainer:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -3270,7 +3270,7 @@ Boolean:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -3629,7 +3629,7 @@ Boolean:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -5850,7 +5850,7 @@ Ellipse:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -6179,7 +6179,7 @@ Ellipse:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -8279,7 +8279,7 @@ Group:
| "mixed";
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -8614,7 +8614,7 @@ Group:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -9523,7 +9523,7 @@ Image:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -9852,7 +9852,7 @@ Image:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -10444,6 +10444,8 @@ LayoutCellProperties:
position?: "area" | "auto" | "manual";
}
```
Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer
members:
Properties:
row: |-
@ -12986,7 +12988,7 @@ Path:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -13339,7 +13341,7 @@ Path:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -14313,7 +14315,7 @@ Rectangle:
rotation: number;
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -14644,7 +14646,7 @@ Rectangle:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -15349,7 +15351,7 @@ ShapeBase:
| "mixed";
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -15679,7 +15681,7 @@ ShapeBase:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -16273,7 +16275,7 @@ Stroke:
strokeColorRefFile?: string;
strokeColorRefId?: string;
strokeOpacity?: number;
strokeStyle?: "svg" | "none" | "mixed" | "solid" | "dotted" | "dashed";
strokeStyle?: "none" | "svg" | "mixed" | "solid" | "dotted" | "dashed";
strokeWidth?: number;
strokeAlignment?: "center" | "inner" | "outer";
strokeCapStart?: StrokeCap;
@ -16312,7 +16314,7 @@ Stroke:
Defaults to 1 if omitted.
strokeStyle: |-
```
strokeStyle?: "svg" | "none" | "mixed" | "solid" | "dotted" | "dashed"
strokeStyle?: "none" | "svg" | "mixed" | "solid" | "dotted" | "dashed"
```
The optional style of the stroke.
@ -16415,7 +16417,7 @@ SvgRaw:
| "mixed";
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -16739,7 +16741,7 @@ SvgRaw:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -17334,7 +17336,7 @@ Text:
| "mixed";
strokes: Stroke[];
layoutChild?: LayoutChildProperties;
layoutCell?: LayoutChildProperties;
layoutCell?: LayoutCellProperties;
setParentIndex(index: number): void;
tokens: {
width: string;
@ -17421,6 +17423,7 @@ Text:
direction: "mixed" | "ltr" | "rtl" | null;
align: "center" | "left" | "right" | "mixed" | "justify" | null;
verticalAlign: "center" | "top" | "bottom" | null;
textBounds: { x: number; y: number; width: number; height: number };
getRange(start: number, end: number): TextRange;
applyTypography(typography: LibraryTypography): void;
}
@ -17675,7 +17678,7 @@ Text:
Layout properties for children of the shape.
layoutCell: |-
```
readonly layoutCell?: LayoutChildProperties
readonly layoutCell?: LayoutCellProperties
```
Layout properties for cells in a grid layout.
@ -17835,6 +17838,13 @@ Text:
```
The vertical alignment of the text shape. It can be a specific alignment or 'mixed' if multiple alignments are used.
textBounds: |-
```
readonly textBounds: { x: number; y: number; width: number; height: number }
```
Return the bounding box for the text as a (x, y, width, height) rectangle
This is the box that covers the text even if it overflows its selection rectangle.
Methods:
getPluginData: |-
```

View File

@ -39,20 +39,28 @@ Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`
* `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board.
To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`.
* `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions.
* `bounds` is a READ-ONLY property. Use `x`, `y` with `resize()` to modify shape bounds.
* `bounds` is READ-ONLY (members: x, y, width, height). To modify the bounding box, change `x`, `y` or apply `resize()`.
**Other Writable Properties**:
* `name` - Shape name
* `fills`, `strokes` - Styling properties
IMPORTANT: The contents of the arrays are read-only. You cannot modify individual fills/strokes; you need to replace the entire array to change them, e.g.
`shape.fills = [{ fillColor: "#FF0000", fillOpacity: 1 }]` to set a single red fill.
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
* `fills: Fill[]`, `strokes: Stroke[]`, `shadows: Shadow[]` - Styling properties
- Setting fills: `shape.fills = [{ fillColor: "#FF0000", fillOpacity: 1 }]`; no fill (transparent): `shape.fills = []`;
- Colors: Use hex strings with caps only (e.g. '#FF5533')
- IMPORTANT: The contents of the arrays are read-only. You cannot modify individual fills/strokes; you need to replace the entire array to change them!
* `borderRadius` - Uniform border radius for all corners
* `borderRadiusTopLeft`, `borderRadiusTopRight`, `borderRadiusBottomRight`, `borderRadiusBottomLeft` - Individual corner radii.
* `blur: Blur` - Blur properties
* `blendMode` - Blend mode (e.g. `"normal"`, `"multiply"`, `"overlay"`, etc.)
* `rotation` (deg), `opacity`, `blocked`, `hidden`, `visible`
* `proportionLock` - Whether width and height are locked to the same ratio
* `constraintsHorizontal` - Horizontal resize constraint (`"left"`, `"right"`, `"center"`, `"leftright"`, `"scale"`)
* `constraintsVertical` - Vertical resize constraint (`"top"`, `"bottom"`, `"center"`, `"topbottom"`, `"scale"`)
* `flipX`, `flipY` - Horizontal/vertical flip
**Z-Order**:
* The z-order of shapes is determined by the order in the `children` array of the parent shape.
Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order
(i.e. add background shapes first, then foreground shapes later).
CRITICAL: NEVER use the broken function `appendChild` to achieve this, ALWAYS use `parent.insertChild(parent.children.length, shape)`
* To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`,
and, for precise control, `setParentIndex(index)` (0-based).
@ -65,9 +73,7 @@ Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`
**Hierarchical Structure**:
* `parent` - The parent shape (null for root shapes)
Note: Hierarchical nesting does not necessarily imply visual containment
* CRITICAL: To add children to a parent shape (e.g. a `Board`):
- ALWAYS use `parent.insertChild(index, shape)` to add a child, e.g. `parent.insertChild(parent.children.length, shape)` to append
- NEVER use `parent.appendChild(shape)` as it is BROKEN and will not insert in a predictable place (except in flex layout boards)
* To add children to a parent shape (e.g. a `Board`): `parent.appendChild(shape)` or `parent.insertChild(index, shape)`
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
- Automatically removes the shape from its old parent
- Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position)
@ -99,17 +105,11 @@ Boards can have layout systems that automatically control the positioning and sp
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
Optionally, adjust individual child margins via `child.layoutChild`.
- Sizing: `verticalSizing` and `horizontalSizing` are NOT functional. You need to size manually for the time being.
- When a board has flex layout,
- child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
appending or inserting children automatically positions them according to the layout rules.
- CRITICAL: For dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa.
ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"!
- When a board has flex layout, child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
appending or inserting children automatically positions them according to the layout rules.
- CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that
they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`; it will insert at the front
of the `children` array for dir="column" or dir="row", which is what you want. So call it in the order of visual appearance.
To insert at a specific index, use `board.insertChild(index, shape)`, bearing in mind the reversed order for dir="column"
or dir="row".
they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`. So call it in the order of visual appearance.
To insert at a specific index, use `board.insertChild(index, shape)`.
- Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessible via `board.flex`.
IMPORTANT: When adding a flex layout to a container that already has children,
use `penpotUtils.addFlexLayout(container, dir)` instead! This preserves the existing visual order of children.
@ -131,12 +131,12 @@ Boards can have layout systems that automatically control the positioning and sp
# Text Elements
The rendered content of `Text` element is given by the `characters` property.
The rendered content of a `Text` element is given by the `characters` property.
To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size,
it only changes the formal bounding box; if the text does not fit it, it will overflow.
it only changes the formal bounding box; if the text does not fit it, it will overflow; use `textBounds` for the actual bounding box of the rendered text.
The bounding box is sized automatically as long as the `growType` property is set to "auto-width" or "auto-height".
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing - otherwise the bounding box will be meaningless, with the text overflowing!
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing!
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
# The `penpot` and `penpotUtils` Objects, Exploring Designs
@ -228,31 +228,76 @@ Each `Library` object has:
* `colors: LibraryColor[]` - Array of colors
* `typographies: LibraryTypography[]` - Array of typographies
## Colors and Typographies
Adding a color:
```
const newColor: LibraryColor = penpot.library.local.createColor();
newColor.name = 'Brand Primary';
newColor.color = '#0066FF';
```
Adding a typography:
```
const newTypo: LibraryTypography = penpot.library.local.createTypography();
newTypo.name = 'Heading Large';
// Set typography properties...
```
## Components
Using library components:
* find a component in the library by name:
const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));
`const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));`
* create a new instance of the component on the current page:
const instance: Shape = component.instance();
`const instance: Shape = component.instance();`
This returns a `Shape` (often a `Board` containing child elements).
After instantiation, modify the instance's properties as desired.
* get the reference to the main component shape:
const mainShape: Shape = component.mainInstance();
`const mainShape: Shape = component.mainInstance();`
Adding assets to a library:
* const newColor: LibraryColor = penpot.library.local.createColor();
newColor.name = 'Brand Primary';
newColor.color = '#0066FF';
* const newTypo: LibraryTypography = penpot.library.local.createTypography();
newTypo.name = 'Heading Large';
// Set typography properties...
* const shapes: Shape[] = [shape1, shape2]; // shapes to include
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
newComponent.name = 'My Button';
Adding a component to a library:
```
const shapes: Shape[] = [shape1, shape2]; // shapes to include
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
newComponent.name = 'My Button';
```
Detaching:
* When creating new design elements based on a component instance/copy, use `shape.detach()` to break the link to the main component, allowing independent modification.
* Without detaching, some manipulations will have no effect; e.g. child/descendant removal will not work.
### Variants
Variants are a system for grouping related component versions along named property axes (e.g. Type, Style), powering a structured swap UI for designers using component instances.
* `VariantContainer` (extends `Board`): The board that physically groups all variant components together.
- check with `isVariantContainer()`
- property `variants: Variants`.
* `Variants`: Defines the combinations of property values for which component variants can exist and manages the concrete component variants.
- `properties: string[]` (ordered list of property names); `addProperty()`, `renameProperty(pos, name)`, `currentValues(property)`
- `variantComponents(): LibraryVariantComponent[]`
* `LibraryVariantComponent` (extends `LibraryComponent`): full library component with metadata, for which `isVariant()` returns true.
- `variantProps: { [property: string]: string }` (this component's value for each property)
- `variantError` (non-null if e.g. two variants share the same combination of property values)
- `setVariantProperty(pos, value)`
Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`.
**Creating a variant group**:
- `component.transformInVariant(): null`: Converts a standard component into a variant group, creating a `VariantContainer` and a second duplicate variant.
Both start with a default property `Property 1` with values `Value 1` / `Value 2`; there is no name-based auto-parsing.
- `board.combineAsVariants(ids: string[]): null`: Combines the board (a main component instance) with other main components (referenced via IDs) into a new variant group.
All components end up inside a single new `VariantContainer` on the canvas.
- In both cases, look for the created `VariantContainer` on the page, and then edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`.
**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`.
**Using Variants**:
- `compInstance.switchVariant(pos, value)`: On a component instance, switches to the nearest variant that has the given value at property position `pos`, keeping all other property values the same.
- To instantiate a specific variant, find the right `LibraryVariantComponent` by checking `variantProps`, then call `.instance()`.
# Design Tokens
Design tokens are reusable design values (colors, dimensions, typography, etc.) for consistent styling.
@ -276,7 +321,7 @@ The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
`Token`: union type encompassing various token types, with common properties:
* `name: string` - Token name (typically structured, e.g. "color.base.white")
* `value` - Raw value (direct value or reference to another token like "{color.primary}")
* `resolvedValue` - Computed final value (follows references) - currently NOT working, do not use!
* `resolvedValue` - Computed final value (follows references)
* `type: TokenType`
Discovering tokens:
@ -292,19 +337,19 @@ Applying tokens:
- "all": applies the token to all properties it can control
- TokenBorderRadiusProps: "r1", "r2", "r3", "r4"
- TokenShadowProps: "shadow"
- TokenColorProps: "fill", "stroke-color"
- TokenDimensionProps: "x", "y", "stroke-width"
- TokenFontFamiliesProps: "font-families"
- TokenFontSizesProps: "font-size"
- TokenFontWeightProps: "font-weight"
- TokenLetterSpacingProps: "letter-spacing"
- TokenNumberProps: "rotation", "line-height"
- TokenColorProps: "fill", "strokeColor"
- TokenDimensionProps: "x", "y", "strokeWidth"
- TokenFontFamiliesProps: "fontFamilies"
- TokenFontSizesProps: "fontSize"
- TokenFontWeightProps: "fontWeight"
- TokenLetterSpacingProps: "letterSpacing"
- TokenNumberProps: "rotation"
- TokenOpacityProps: "opacity"
- TokenSizingProps: "width", "height", "layout-item-min-w", "layout-item-max-w", "layout-item-min-h", "layout-item-max-h"
- TokenSpacingProps: "row-gap", "column-gap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
- TokenBorderWidthProps: "stroke-width"
- TokenTextCaseProps: "text-case"
- TokenTextDecorationProps: "text-decoration"
- TokenSizingProps: "width", "height", "layoutItemMinW", "layoutItemMaxW", "layoutItemMinH", "layoutItemMaxH"
- TokenSpacingProps: "rowGap", "columnGap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
- TokenBorderWidthProps: "strokeWidth"
- TokenTextCaseProps: "textCase"
- TokenTextDecorationProps: "textDecoration"
- TokenTypographyProps: "typography"
* `token.applyToShapes(shapes, properties)` - Apply from token
* Application is **asynchronous** (wait for ~100ms to see the effects)
@ -313,7 +358,7 @@ Applying tokens:
- The actual shape properties that the tokens control will reflect the token's resolved value.
Removing tokens:
Simply set the respective property directly - token binding is automatically removed, e.g.
Simply set the respective property directly - token binding is automatically removed, e.g.
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
# Visual Inspection of Designs