mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
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:
commit
b5874b365b
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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: |-
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user