mirror of
https://github.com/penpot/penpot.git
synced 2026-02-12 14:42:56 +00:00
Merge pull request #8310 from oraios/mcp-tokens
✨ MCP improvements to enable UC2, design token handling
This commit is contained in:
@@ -90,32 +90,6 @@ This bootstrap command will:
|
|||||||
* build all components (`pnpm -r run build`)
|
* build all components (`pnpm -r run build`)
|
||||||
* start all components (`pnpm -r --parallel run start`)
|
* start all components (`pnpm -r --parallel run start`)
|
||||||
|
|
||||||
If you want to have types scrapped from a remote repository, the best
|
|
||||||
approach is executing the following:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
PENPOT_PLUGINS_API_DOC_URL=https://doc.plugins.penpot.app pnpm run build:types
|
|
||||||
pnpm run bootstrap
|
|
||||||
```
|
|
||||||
|
|
||||||
Or this, if you want skip build step bacause you have already have all
|
|
||||||
build artifacts ready (per example from previous `bootstrap` command):
|
|
||||||
|
|
||||||
```
|
|
||||||
PENPOT_PLUGINS_API_DOC_URL=https://doc.plugins.penpot.app pnpm run build:types
|
|
||||||
pnpm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want just to update the types definitions with the plugins api doc from the
|
|
||||||
current branch:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm run build:types
|
|
||||||
```
|
|
||||||
|
|
||||||
(That command will build plugins doc locally and will generate the types yaml from
|
|
||||||
the locally build documentation)
|
|
||||||
|
|
||||||
### 2. Load the Plugin in Penpot and Establish the Connection
|
### 2. Load the Plugin in Penpot and Establish the Connection
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm -r run build",
|
||||||
"build:multi-user": "pnpm -r run build:multi-user",
|
"build:multi-user": "pnpm -r run build:multi-user",
|
||||||
"build:types": "./scripts/build-types",
|
"build:types": "bash ./scripts/build-types",
|
||||||
"start": "pnpm -r --parallel run start",
|
"start": "pnpm -r --parallel run start",
|
||||||
"start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user",
|
"start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user",
|
||||||
"bootstrap": "pnpm -r install && pnpm run build && pnpm run start",
|
"bootstrap": "pnpm -r install && pnpm run build && pnpm run start",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class PenpotUtils {
|
|||||||
id: shape.id,
|
id: shape.id,
|
||||||
name: shape.name,
|
name: shape.name,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
children: children,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// add layout information if present
|
// add layout information if present
|
||||||
@@ -48,6 +47,23 @@ export class PenpotUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add component instance information if present
|
||||||
|
if (shape.isComponentInstance()) {
|
||||||
|
result.componentInstance = {};
|
||||||
|
const component = shape.component();
|
||||||
|
if (component) {
|
||||||
|
result.componentInstance.componentId = component.id;
|
||||||
|
result.componentInstance.componentName = component.name;
|
||||||
|
const mainInstance = component.mainInstance();
|
||||||
|
if (mainInstance) {
|
||||||
|
result.componentInstance.mainInstanceId = mainInstance.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, add children (last for more readable nesting order)
|
||||||
|
result.children = children;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +71,9 @@ export class PenpotUtils {
|
|||||||
* Finds all shapes that matches the given predicate in the given shape tree.
|
* Finds all shapes that matches the given predicate in the given shape tree.
|
||||||
*
|
*
|
||||||
* @param predicate - A function that takes a shape and returns true if it matches the criteria
|
* @param predicate - A function that takes a shape and returns true if it matches the criteria
|
||||||
* @param root - The root shape to start the search from (defaults to penpot.root)
|
* @param root - The root shape to start the search from (if null, searches all pages)
|
||||||
*/
|
*/
|
||||||
public static findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = penpot.root): Shape[] {
|
public static findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[] {
|
||||||
let result = new Array<Shape>();
|
let result = new Array<Shape>();
|
||||||
|
|
||||||
let find = function (shape: Shape | null) {
|
let find = function (shape: Shape | null) {
|
||||||
@@ -74,7 +90,16 @@ export class PenpotUtils {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
find(root);
|
if (root === null) {
|
||||||
|
const pages = penpot.currentFile?.pages;
|
||||||
|
if (pages) {
|
||||||
|
for (let page of pages) {
|
||||||
|
find(page.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
find(root);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,4 +447,94 @@ export class PenpotUtils {
|
|||||||
throw new Error(`Unsupported export mode: ${mode}`);
|
throw new Error(`Unsupported export mode: ${mode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all tokens that match the given name across all token sets.
|
||||||
|
*
|
||||||
|
* @param name - The name of the token to search for (case-sensitive exact match)
|
||||||
|
* @returns An array of all matching tokens (may be empty)
|
||||||
|
*/
|
||||||
|
public static findTokensByName(name: string): any[] {
|
||||||
|
const tokens: any[] = [];
|
||||||
|
// @ts-ignore
|
||||||
|
const tokenCatalog = penpot.library.local.tokens;
|
||||||
|
|
||||||
|
for (const set of tokenCatalog.sets) {
|
||||||
|
for (const token of set.tokens) {
|
||||||
|
if (token.name === name) {
|
||||||
|
tokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first token that matches the given name across all token sets.
|
||||||
|
*
|
||||||
|
* @param name - The name of the token to search for (case-sensitive exact match)
|
||||||
|
* @returns The first matching token, or null if not found
|
||||||
|
*/
|
||||||
|
public static findTokenByName(name: string): any | null {
|
||||||
|
// @ts-ignore
|
||||||
|
const tokenCatalog = penpot.library.local.tokens;
|
||||||
|
|
||||||
|
for (const set of tokenCatalog.sets) {
|
||||||
|
for (const token of set.tokens) {
|
||||||
|
if (token.name === name) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the token set that contains the given token.
|
||||||
|
*
|
||||||
|
* @param token - The token whose set to find
|
||||||
|
* @returns The TokenSet containing this token, or null if not found
|
||||||
|
*/
|
||||||
|
public static getTokenSet(token: any): any | null {
|
||||||
|
// @ts-ignore
|
||||||
|
const tokenCatalog = penpot.library.local.tokens;
|
||||||
|
|
||||||
|
for (const set of tokenCatalog.sets) {
|
||||||
|
if (set.tokens.includes(token)) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, ...]}}.
|
||||||
|
*
|
||||||
|
* @returns An object mapping token set names to objects that map token types to arrays of token names
|
||||||
|
*/
|
||||||
|
public static tokenOverview(): Record<string, Record<string, string[]>> {
|
||||||
|
const overview: Record<string, Record<string, string[]>> = {};
|
||||||
|
// @ts-ignore
|
||||||
|
const tokenCatalog = penpot.library.local.tokens;
|
||||||
|
|
||||||
|
for (const set of tokenCatalog.sets) {
|
||||||
|
const setOverview: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const token of set.tokens) {
|
||||||
|
const tokenType = token.type;
|
||||||
|
if (!setOverview[tokenType]) {
|
||||||
|
setOverview[tokenType] = [];
|
||||||
|
}
|
||||||
|
setOverview[tokenType].push(token.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
overview[set.name] = setOverview;
|
||||||
|
}
|
||||||
|
|
||||||
|
return overview;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
333
mcp/packages/server/data/initial_instructions.md
Normal file
333
mcp/packages/server/data/initial_instructions.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
You have access to Penpot tools in order to interact with a Penpot design project directly.
|
||||||
|
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
|
||||||
|
|
||||||
|
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
||||||
|
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
||||||
|
non-creative defaults such as white/black if you are lacking information).
|
||||||
|
|
||||||
|
# Executing Code
|
||||||
|
|
||||||
|
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
|
||||||
|
directly in the connected project.
|
||||||
|
|
||||||
|
VERY IMPORTANT: When writing code, NEVER LOG INFORMATION YOU ARE ALSO RETURNING. It would duplicate the information you receive!
|
||||||
|
|
||||||
|
To execute code correctly, you need to understand the Penpot Plugin API. You can retrieve API documentation via
|
||||||
|
the `penpot_api_info` tool.
|
||||||
|
|
||||||
|
This is the full list of types/interfaces in the Penpot API: $api_types
|
||||||
|
|
||||||
|
You use the `storage` object extensively to store data and utility functions you define across tool calls.
|
||||||
|
This allows you to inspect intermediate results while still being able to build on them in subsequent code executions.
|
||||||
|
|
||||||
|
# The Structure of Penpot Designs
|
||||||
|
|
||||||
|
A Penpot design ultimately consists of shapes.
|
||||||
|
The type `Shape` is a union type, which encompasses both containers and low-level shapes.
|
||||||
|
Shapes in a Penpot design are organized hierarchically.
|
||||||
|
At the top level, a design project contains one or more `Page` objects.
|
||||||
|
Each `Page` contains a tree of elements. For a given instance `page`, its root shape is `page.root`.
|
||||||
|
A Page is frequently structured into boards. A `Board` is a high-level grouping element.
|
||||||
|
A `Group` is a more low-level grouping element used to organize low-level shapes into a logical unit.
|
||||||
|
Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`, `Boolean`, and `SvgRaw`.
|
||||||
|
`ShapeBase` is a base type most shapes build upon.
|
||||||
|
|
||||||
|
# Core Shape Properties and Methods
|
||||||
|
|
||||||
|
**Type**:
|
||||||
|
Any given shape contains information on the concrete type via its `type` field.
|
||||||
|
|
||||||
|
**Position and Dimensions**:
|
||||||
|
* The location properties `x` and `y` refer to the top left corner of a shape's bounding box in the absolute (Page) coordinate system.
|
||||||
|
These are writable - set them directly to position shapes.
|
||||||
|
* `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.
|
||||||
|
|
||||||
|
**Other Writable Properties**:
|
||||||
|
* `name` - Shape name
|
||||||
|
* `fills`, `strokes` - Styling properties
|
||||||
|
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
**Modification Methods**:
|
||||||
|
* `resize(width, height)` - Change dimensions (required for width/height since they're read-only)
|
||||||
|
* `rotate(angle, center?)` - Rotate shape
|
||||||
|
* `remove()` - Permanently destroy the shape (use only for deletion, NOT for reparenting).
|
||||||
|
Exception: When the shape is a descendant of a board that is a component (asset), the shape will not be removed but instead be made invisible.
|
||||||
|
|
||||||
|
**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)
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
Cloning: Use `shape.clone(): Shape` to create an exact duplicate (including all properties and children) of a shape; same position as original.
|
||||||
|
|
||||||
|
# Images
|
||||||
|
|
||||||
|
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
|
||||||
|
`ImageData` object, i.e. the `fills` property of of a shape (e.g. a `Rectangle`) will contain a fill where `fillImage` is set.
|
||||||
|
Use the `export_shape` and `import_image` tools to export and import images.
|
||||||
|
|
||||||
|
# Layout Systems
|
||||||
|
|
||||||
|
Boards can have layout systems that automatically control the positioning and spacing of their children:
|
||||||
|
|
||||||
|
* If a board has a layout system, then child positions are controlled by the layout system.
|
||||||
|
For every child, key properties of the child within the layout are stored in `child.layoutChild: LayoutChildProperties`:
|
||||||
|
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
|
||||||
|
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
|
||||||
|
- sizing (`verticalSizing`, `horizontalSizing`: "fill" | "auto" | "fix")
|
||||||
|
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
|
||||||
|
- `zIndex: number` (higher numbers on top)
|
||||||
|
|
||||||
|
* **Flex Layout**: A flexbox-style layout system
|
||||||
|
- Properties: `dir`, `rowGap`, `columnGap`, `alignItems`, `justifyContent`;
|
||||||
|
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
|
||||||
|
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
||||||
|
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
||||||
|
Optionally, adjust indivudual child margins via `child.layoutChild`.
|
||||||
|
- 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 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"!
|
||||||
|
- 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".
|
||||||
|
- 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.
|
||||||
|
Otherwise, children will be arbitrarily reordered when the children order suddenly determines the display order.
|
||||||
|
- Check with: `if (board.flex) { ... }`
|
||||||
|
|
||||||
|
* **Grid Layout**: A CSS grid-style layout system
|
||||||
|
- Add to a board with `board.addGridLayout(): GridLayout`; instance then accessibly via `board.grid`;
|
||||||
|
Check with: `if (board.grid) { ... }`
|
||||||
|
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
|
||||||
|
- Children are positioned via 1-based row/column indices
|
||||||
|
- Add to grid via `board.flex.appendChild(shape, row, column)`
|
||||||
|
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
|
||||||
|
|
||||||
|
* When working with boards:
|
||||||
|
- ALWAYS check if the board has a layout system before attempting to reposition children
|
||||||
|
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
|
||||||
|
- Layout systems override manual positioning of children
|
||||||
|
|
||||||
|
# Text Elements
|
||||||
|
|
||||||
|
The rendered content of `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.
|
||||||
|
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!
|
||||||
|
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
|
||||||
|
|
||||||
|
A key object to use in your code is the `penpot` object (which is of type `Penpot`):
|
||||||
|
* `penpot.selection` provides the list of shapes the user has selected in the Penpot UI.
|
||||||
|
If it is unclear which elements to work on, you can ask the user to select them for you.
|
||||||
|
ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged.
|
||||||
|
* `penpot.root` provides the root shape of the currently active page.
|
||||||
|
* Generation of CSS content for elements via `penpot.generateStyle`
|
||||||
|
* Generation of HTML/SVG content for elements via `penpot.generateMarkup`
|
||||||
|
|
||||||
|
For example, to generate CSS for the currently selected elements, you can execute this:
|
||||||
|
return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true });
|
||||||
|
|
||||||
|
CRITICAL: The `penpotUtils` object provides essential utilities - USE THESE INSTEAD OF WRITING YOUR OWN:
|
||||||
|
* getPages(): { id: string; name: string }[]
|
||||||
|
* getPageById(id: string): Page | null
|
||||||
|
* getPageByName(name: string): Page | null
|
||||||
|
* shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): { id, name, type, children?, layout? }
|
||||||
|
Generates an overview structure of the given shape.
|
||||||
|
- children: recursive, limited by maxDepth
|
||||||
|
- layout: present if shape has flex/grid layout, contains { type: "flex" | "grid", ... }
|
||||||
|
* findShapeById(id: string): Shape | null
|
||||||
|
* findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null
|
||||||
|
If no root is provided, search globally (in all pages).
|
||||||
|
* findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[]
|
||||||
|
* isContainedIn(shape: Shape, container: Shape): boolean
|
||||||
|
Returns true iff shape is fully within the container's geometric bounds.
|
||||||
|
Note that a shape's bounds may not always reflect its actual visual content - descendants can overflow; check using analyzeDescendants (see below).
|
||||||
|
* setParentXY(shape: Shape, parentX: number, parentY: number): void
|
||||||
|
Sets shape position relative to its parent (since parentX/parentY are read-only)
|
||||||
|
* analyzeDescendants<T>(root: Shape, evaluator: (root: Shape, descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }>
|
||||||
|
General-purpose utility for analyzing/validating descendants
|
||||||
|
Calls evaluator on each descendant; collects non-null/undefined results
|
||||||
|
Powerful pattern: evaluator can return corrector functions or diagnostic data
|
||||||
|
* Further functions for specific tasks (described in the sections below)
|
||||||
|
|
||||||
|
General pointers for working with Penpot designs:
|
||||||
|
* Prefer `penpotUtils` helper functions — avoid reimplementing shape searching.
|
||||||
|
* To get an overview of a single page, use `penpotUtils.shapeStructure(page.root, 3)`.
|
||||||
|
Note that `penpot.root` refers to the current page only. When working across pages, first determine the relevant page(s).
|
||||||
|
* Use `penpotUtils.findShapes()` or `penpotUtils.findShape()` with predicates to locate elements efficiently.
|
||||||
|
|
||||||
|
Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
|
||||||
|
* Find all images:
|
||||||
|
const images = penpotUtils.findShapes(
|
||||||
|
shape => shape.type === 'image' || shape.fills?.some(fill => fill.fillImage),
|
||||||
|
penpot.root
|
||||||
|
);
|
||||||
|
* Find text elements:
|
||||||
|
const texts = penpotUtils.findShapes(shape => shape.type === 'text', penpot.root);
|
||||||
|
* Find (the first) shape with a given name:
|
||||||
|
const shape = penpotUtils.findShape(shape => shape.name === 'MyShape');
|
||||||
|
* Get structure of current selection:
|
||||||
|
const structure = penpotUtils.shapeStructure(penpot.selection[0]);
|
||||||
|
* Find shapes in current selection/board:
|
||||||
|
const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root);
|
||||||
|
* Validate/analyze descendants (returning corrector functions):
|
||||||
|
const fixes = penpotUtils.analyzeDescendants(board, (root, shape) => {
|
||||||
|
const xMod = shape.parentX % 4;
|
||||||
|
if (xMod !== 0) {
|
||||||
|
return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fixes.forEach(f => f.result()); // Apply all fixes
|
||||||
|
* Find containment violations:
|
||||||
|
const violations = penpotUtils.analyzeDescendants(board, (root, shape) => {
|
||||||
|
return !penpotUtils.isContainedIn(shape, root) ? 'outside-bounds' : null;
|
||||||
|
});
|
||||||
|
Always validate against the root container that is supposed to contain the shapes.
|
||||||
|
|
||||||
|
# Visual Inspection of Designs
|
||||||
|
|
||||||
|
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
||||||
|
|
||||||
|
# Revising Designs
|
||||||
|
|
||||||
|
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
||||||
|
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
||||||
|
Container sizes are usually intentional, check content first.
|
||||||
|
* Check for reasonable font sizes and typefaces
|
||||||
|
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
||||||
|
Consider converting boards to flex layout when appropriate.
|
||||||
|
|
||||||
|
# Asset Libraries
|
||||||
|
|
||||||
|
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
|
||||||
|
They enable design systems and consistent styling across projects.
|
||||||
|
Each Penpot file has its own local library and can connect to external shared libraries.
|
||||||
|
|
||||||
|
Accessing libraries: via `penpot.library` (type: `LibraryContext`):
|
||||||
|
* `penpot.library.local` (type: `Library`) - The current file's own library
|
||||||
|
* `penpot.library.connected` (type: `Library[]`) - Array of already-connected external libraries
|
||||||
|
* `penpot.library.availableLibraries()` (returns: `Promise<LibrarySummary[]>`) - Libraries available to connect
|
||||||
|
* `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise<Library>`) - Connect a new library
|
||||||
|
|
||||||
|
Each `Library` object has:
|
||||||
|
* `id: string`
|
||||||
|
* `name: string`
|
||||||
|
* `components: LibraryComponent[]` - Array of components
|
||||||
|
* `colors: LibraryColor[]` - Array of colors
|
||||||
|
* `typographies: LibraryTypography[]` - Array of typographies
|
||||||
|
|
||||||
|
Using library components:
|
||||||
|
* find a component in the library by name:
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
# Design Tokens
|
||||||
|
|
||||||
|
Design tokens are reusable design values (colors, dimensions, typography, etc.) for consistent styling.
|
||||||
|
|
||||||
|
The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
|
||||||
|
* `sets: TokenSet[]` - Token collections (order matters for precedence)
|
||||||
|
* `themes: TokenTheme[]` - Presets that activate specific sets
|
||||||
|
* `addSet(name: string): TokenSet` - Create new set
|
||||||
|
* `addTheme(group: string, name: string): TokenTheme` - Create new theme
|
||||||
|
|
||||||
|
`TokenSet` contains tokens with unique names:
|
||||||
|
* `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();`
|
||||||
|
* `tokens: Token[]` - All tokens in set
|
||||||
|
* `addToken(type: TokenType, name: string, value: TokenValueString): Token` - Creates a token, adding it to the set.
|
||||||
|
- `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase"
|
||||||
|
- Examples:
|
||||||
|
const token = set.addToken("color", "color.primary", "#0066FF"); // direct value
|
||||||
|
const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token
|
||||||
|
|
||||||
|
`Token`:
|
||||||
|
* `name: string` - Token name (may include group path like "color.base.white")
|
||||||
|
* `value: string | TokenValueString` - Raw value (may be direct value or reference to another token like "{color.primary}")
|
||||||
|
* `resolvedValue` - Computed final value (follows references)
|
||||||
|
* `type: TokenType`
|
||||||
|
|
||||||
|
Discovering tokens:
|
||||||
|
* `penpotUtils.tokenOverview()`: Maps from token set name to a mapping from token type to list of token names
|
||||||
|
* `penpotUtils.findTokenByName(name: string): Token | null`: Finds the first applicable token matching the given name
|
||||||
|
* `penpotUtils.findTokensByName(name: string): Token[]`: Finds all tokens that match the given name across all token sets
|
||||||
|
* `penpotUtils.getTokenSet(token: Token): TokenSet | null`: Gets the token set that contains the given token
|
||||||
|
|
||||||
|
Applying tokens:
|
||||||
|
* `shape.applyToken(token, properties: undefined | TokenProperty[])` - Apply a token to a shape for one or more properties
|
||||||
|
(if properties is undefined, use a default property based on the token type - not usually recommended).
|
||||||
|
`TokenProperty` is a union type; possible values are:
|
||||||
|
- "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"
|
||||||
|
- 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"
|
||||||
|
- TokenTypographyProps: "typography"
|
||||||
|
* `token.applyToShapes(shapes, properties)` - Apply from token
|
||||||
|
* Application is **asynchronous** (wait for ~100ms to see the effects)
|
||||||
|
* After application:
|
||||||
|
- `shape.tokens` returns a mapping `{ propertyName: "token.name" }` from `TokenProperty` to token name
|
||||||
|
- 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.
|
||||||
|
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
|
||||||
|
|
||||||
|
--
|
||||||
|
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
# Prompts configuration for Penpot MCP Server
|
|
||||||
# This file contains various prompts and instructions that can be used by the server
|
|
||||||
|
|
||||||
initial_instructions: |
|
|
||||||
You have access to Penpot tools in order to interact with a Penpot design project directly.
|
|
||||||
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
|
|
||||||
|
|
||||||
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
|
||||||
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
|
||||||
non-creative defaults such as white/black if you are lacking information).
|
|
||||||
|
|
||||||
# Executing Code
|
|
||||||
|
|
||||||
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
|
|
||||||
directly in the connected project.
|
|
||||||
|
|
||||||
VERY IMPORTANT: When writing code, NEVER LOG INFORMATION YOU ARE ALSO RETURNING. It would duplicate the information you receive!
|
|
||||||
|
|
||||||
To execute code correctly, you need to understand the Penpot Plugin API. You can retrieve API documentation via
|
|
||||||
the `penpot_api_info` tool.
|
|
||||||
|
|
||||||
This is the full list of types/interfaces in the Penpot API: $api_types
|
|
||||||
|
|
||||||
You use the `storage` object extensively to store data and utility functions you define across tool calls.
|
|
||||||
This allows you to inspect intermediate results while still being able to build on them in subsequent code executions.
|
|
||||||
|
|
||||||
# The Structure of Penpot Designs
|
|
||||||
|
|
||||||
A Penpot design ultimately consists of shapes.
|
|
||||||
The type `Shape` is a union type, which encompasses both containers and low-level shapes.
|
|
||||||
Shapes in a Penpot design are organized hierarchically.
|
|
||||||
At the top level, a design project contains one or more `Page` objects.
|
|
||||||
Each `Page` contains a tree of elements. For a given instance `page`, its root shape is `page.root`.
|
|
||||||
A Page is frequently structured into boards. A `Board` is a high-level grouping element.
|
|
||||||
A `Group` is a more low-level grouping element used to organize low-level shapes into a logical unit.
|
|
||||||
Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`, `Boolean`, and `SvgRaw`.
|
|
||||||
`ShapeBase` is a base type most shapes build upon.
|
|
||||||
|
|
||||||
# Core Shape Properties and Methods
|
|
||||||
|
|
||||||
**Type**:
|
|
||||||
Any given shape contains information on the concrete type via its `type` field.
|
|
||||||
|
|
||||||
**Position and Dimensions**:
|
|
||||||
* The location properties `x` and `y` refer to the top left corner of a shape's bounding box in the absolute (Page) coordinate system.
|
|
||||||
These are writable - set them directly to position shapes.
|
|
||||||
* `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.
|
|
||||||
|
|
||||||
**Other Writable Properties**:
|
|
||||||
* `name` - Shape name
|
|
||||||
* `fills`, `strokes` - Styling properties
|
|
||||||
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
|
|
||||||
|
|
||||||
**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).
|
|
||||||
|
|
||||||
**Modification Methods**:
|
|
||||||
* `resize(width, height)` - Change dimensions (required for width/height since they're read-only)
|
|
||||||
* `rotate(angle, center?)` - Rotate shape
|
|
||||||
* `remove()` - Permanently destroy the shape (use only for deletion, NOT for reparenting)
|
|
||||||
|
|
||||||
**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)
|
|
||||||
* 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)
|
|
||||||
|
|
||||||
# Images
|
|
||||||
|
|
||||||
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
|
|
||||||
`ImageData` object, i.e. the `fills` property of of a shape (e.g. a `Rectangle`) will contain a fill where `fillImage` is set.
|
|
||||||
Use the `export_shape` and `import_image` tools to export and import images.
|
|
||||||
|
|
||||||
# Layout Systems
|
|
||||||
|
|
||||||
Boards can have layout systems that automatically control the positioning and spacing of their children:
|
|
||||||
|
|
||||||
* If a board has a layout system, then child positions are controlled by the layout system.
|
|
||||||
For every child, key properties of the child within the layout are stored in `child.layoutChild: LayoutChildProperties`:
|
|
||||||
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
|
|
||||||
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
|
|
||||||
- sizing (`verticalSizing`, `horizontalSizing`: "fill" | "auto" | "fix")
|
|
||||||
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
|
|
||||||
- `zIndex: number` (higher numbers on top)
|
|
||||||
|
|
||||||
* **Flex Layout**: A flexbox-style layout system
|
|
||||||
- Properties: `dir`, `rowGap`, `columnGap`, `alignItems`, `justifyContent`;
|
|
||||||
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
|
|
||||||
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
|
||||||
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
|
||||||
Optionally, adjust indivudual child margins via `child.layoutChild`.
|
|
||||||
- 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 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"!
|
|
||||||
- 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".
|
|
||||||
- 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.
|
|
||||||
Otherwise, children will be arbitrarily reordered when the children order suddenly determines the display order.
|
|
||||||
- Check with: `if (board.flex) { ... }`
|
|
||||||
|
|
||||||
* **Grid Layout**: A CSS grid-style layout system
|
|
||||||
- Add to a board with `board.addGridLayout(): GridLayout`; instance then accessibly via `board.grid`;
|
|
||||||
Check with: `if (board.grid) { ... }`
|
|
||||||
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
|
|
||||||
- Children are positioned via 1-based row/column indices
|
|
||||||
- Add to grid via `board.flex.appendChild(shape, row, column)`
|
|
||||||
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
|
|
||||||
|
|
||||||
* When working with boards:
|
|
||||||
- ALWAYS check if the board has a layout system before attempting to reposition children
|
|
||||||
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
|
|
||||||
- Layout systems override manual positioning of children
|
|
||||||
|
|
||||||
# Text Elements
|
|
||||||
|
|
||||||
The rendered content of `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.
|
|
||||||
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!
|
|
||||||
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
|
|
||||||
|
|
||||||
A key object to use in your code is the `penpot` object (which is of type `Penpot`):
|
|
||||||
* `penpot.selection` provides the list of shapes the user has selected in the Penpot UI.
|
|
||||||
If it is unclear which elements to work on, you can ask the user to select them for you.
|
|
||||||
ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged.
|
|
||||||
* `penpot.root` provides the root shape of the currently active page.
|
|
||||||
* Generation of CSS content for elements via `penpot.generateStyle`
|
|
||||||
* Generation of HTML/SVG content for elements via `penpot.generateMarkup`
|
|
||||||
|
|
||||||
For example, to generate CSS for the currently selected elements, you can execute this:
|
|
||||||
return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true });
|
|
||||||
|
|
||||||
CRITICAL: The `penpotUtils` object provides essential utilities - USE THESE INSTEAD OF WRITING YOUR OWN:
|
|
||||||
* getPages(): { id: string; name: string }[]
|
|
||||||
* getPageById(id: string): Page | null
|
|
||||||
* getPageByName(name: string): Page | null
|
|
||||||
* shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): { id, name, type, children?, layout? }
|
|
||||||
Generates an overview structure of the given shape.
|
|
||||||
- children: recursive, limited by maxDepth
|
|
||||||
- layout: present if shape has flex/grid layout, contains { type: "flex" | "grid", ... }
|
|
||||||
* findShapeById(id: string): Shape | null
|
|
||||||
* findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null
|
|
||||||
If no root is provided, search globally (in all pages).
|
|
||||||
* findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[]
|
|
||||||
* isContainedIn(shape: Shape, container: Shape): boolean
|
|
||||||
Returns true iff shape is fully within the container's geometric bounds.
|
|
||||||
Note that a shape's bounds may not always reflect its actual visual content - descendants can overflow; check using analyzeDescendants (see below).
|
|
||||||
* setParentXY(shape: Shape, parentX: number, parentY: number): void
|
|
||||||
Sets shape position relative to its parent (since parentX/parentY are read-only)
|
|
||||||
* analyzeDescendants<T>(root: Shape, evaluator: (root: Shape, descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }>
|
|
||||||
General-purpose utility for analyzing/validating descendants
|
|
||||||
Calls evaluator on each descendant; collects non-null/undefined results
|
|
||||||
Powerful pattern: evaluator can return corrector functions or diagnostic data
|
|
||||||
|
|
||||||
General pointers for working with Penpot designs:
|
|
||||||
* Prefer `penpotUtils` helper functions — avoid reimplementing shape searching.
|
|
||||||
* To get an overview of a single page, use `penpotUtils.shapeStructure(page.root, 3)`.
|
|
||||||
Note that `penpot.root` refers to the current page only. When working across pages, first determine the relevant page(s).
|
|
||||||
* Use `penpotUtils.findShapes()` or `penpotUtils.findShape()` with predicates to locate elements efficiently.
|
|
||||||
|
|
||||||
Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
|
|
||||||
* Find all images:
|
|
||||||
const images = penpotUtils.findShapes(
|
|
||||||
shape => shape.type === 'image' || shape.fills?.some(fill => fill.fillImage),
|
|
||||||
penpot.root
|
|
||||||
);
|
|
||||||
* Find text elements:
|
|
||||||
const texts = penpotUtils.findShapes(shape => shape.type === 'text', penpot.root);
|
|
||||||
* Find (the first) shape with a given name:
|
|
||||||
const shape = penpotUtils.findShape(shape => shape.name === 'MyShape');
|
|
||||||
* Get structure of current selection:
|
|
||||||
const structure = penpotUtils.shapeStructure(penpot.selection[0]);
|
|
||||||
* Find shapes in current selection/board:
|
|
||||||
const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root);
|
|
||||||
* Validate/analyze descendants (returning corrector functions):
|
|
||||||
const fixes = penpotUtils.analyzeDescendants(board, (root, shape) => {
|
|
||||||
const xMod = shape.parentX % 4;
|
|
||||||
if (xMod !== 0) {
|
|
||||||
return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fixes.forEach(f => f.result()); // Apply all fixes
|
|
||||||
* Find containment violations:
|
|
||||||
const violations = penpotUtils.analyzeDescendants(board, (root, shape) => {
|
|
||||||
return !penpotUtils.isContainedIn(shape, root) ? 'outside-bounds' : null;
|
|
||||||
});
|
|
||||||
Always validate against the root container that is supposed to contain the shapes.
|
|
||||||
|
|
||||||
# Visual Inspection of Designs
|
|
||||||
|
|
||||||
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
|
||||||
|
|
||||||
# Revising Designs
|
|
||||||
|
|
||||||
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
|
||||||
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
|
||||||
Container sizes are usually intentional, check content first.
|
|
||||||
* Check for reasonable font sizes and typefaces
|
|
||||||
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
|
||||||
Consider converting boards to flex layout when appropriate.
|
|
||||||
|
|
||||||
# Asset Libraries
|
|
||||||
|
|
||||||
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
|
|
||||||
They enable design systems and consistent styling across projects.
|
|
||||||
Each Penpot file has its own local library and can connect to external shared libraries.
|
|
||||||
|
|
||||||
Accessing libraries: via `penpot.library` (type: `LibraryContext`):
|
|
||||||
* `penpot.library.local` (type: `Library`) - The current file's own library
|
|
||||||
* `penpot.library.connected` (type: `Library[]`) - Array of already-connected external libraries
|
|
||||||
* `penpot.library.availableLibraries()` (returns: `Promise<LibrarySummary[]>`) - Libraries available to connect
|
|
||||||
* `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise<Library>`) - Connect a new library
|
|
||||||
|
|
||||||
Each `Library` object has:
|
|
||||||
* `id: string`
|
|
||||||
* `name: string`
|
|
||||||
* `components: LibraryComponent[]` - Array of components
|
|
||||||
* `colors: LibraryColor[]` - Array of colors
|
|
||||||
* `typographies: LibraryTypography[]` - Array of typographies
|
|
||||||
|
|
||||||
Using library components:
|
|
||||||
* find a component in the library by name:
|
|
||||||
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();
|
|
||||||
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();
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
--
|
|
||||||
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.
|
|
||||||
@@ -1,18 +1,7 @@
|
|||||||
import { readFileSync, existsSync } from "fs";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import { join, dirname } from "path";
|
import { join } from "path";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
import { createLogger } from "./logger.js";
|
import { createLogger } from "./logger.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface defining the structure of the prompts configuration file.
|
|
||||||
*/
|
|
||||||
export interface PromptsConfig {
|
|
||||||
/** Initial instructions displayed when the server starts or connects to a client */
|
|
||||||
initial_instructions: string;
|
|
||||||
[key: string]: any; // Allow for future extension with additional prompt types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration loader for prompts and server settings.
|
* Configuration loader for prompts and server settings.
|
||||||
*
|
*
|
||||||
@@ -23,7 +12,7 @@ export interface PromptsConfig {
|
|||||||
export class ConfigurationLoader {
|
export class ConfigurationLoader {
|
||||||
private readonly logger = createLogger("ConfigurationLoader");
|
private readonly logger = createLogger("ConfigurationLoader");
|
||||||
private readonly baseDir: string;
|
private readonly baseDir: string;
|
||||||
private promptsConfig: PromptsConfig | null = null;
|
private initialInstructions: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new configuration loader instance.
|
* Creates a new configuration loader instance.
|
||||||
@@ -32,34 +21,14 @@ export class ConfigurationLoader {
|
|||||||
*/
|
*/
|
||||||
constructor(baseDir: string) {
|
constructor(baseDir: string) {
|
||||||
this.baseDir = baseDir;
|
this.baseDir = baseDir;
|
||||||
|
this.initialInstructions = this.loadFileContent(join(this.baseDir, "data", "initial_instructions.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private loadFileContent(filePath: string): string {
|
||||||
* Loads the prompts configuration from the YAML file.
|
if (!existsSync(filePath)) {
|
||||||
*
|
throw new Error(`Configuration file not found at ${filePath}`);
|
||||||
* Reads and parses the prompts.yml file, providing cached access
|
|
||||||
* to configuration values on subsequent calls.
|
|
||||||
*
|
|
||||||
* @returns The parsed prompts configuration object
|
|
||||||
*/
|
|
||||||
public getPromptsConfig(): PromptsConfig {
|
|
||||||
if (this.promptsConfig !== null) {
|
|
||||||
return this.promptsConfig;
|
|
||||||
}
|
}
|
||||||
|
return readFileSync(filePath, "utf8");
|
||||||
const promptsPath = join(this.baseDir, "data", "prompts.yml");
|
|
||||||
|
|
||||||
if (!existsSync(promptsPath)) {
|
|
||||||
throw new Error(`Prompts configuration file not found at ${promptsPath}, using defaults`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileContent = readFileSync(promptsPath, "utf8");
|
|
||||||
const parsedConfig = yaml.load(fileContent) as PromptsConfig;
|
|
||||||
|
|
||||||
this.promptsConfig = parsedConfig || {};
|
|
||||||
this.logger.info(`Loaded prompts configuration from ${promptsPath}`);
|
|
||||||
|
|
||||||
return this.promptsConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,18 +37,6 @@ export class ConfigurationLoader {
|
|||||||
* @returns The initial instructions string, or undefined if not configured
|
* @returns The initial instructions string, or undefined if not configured
|
||||||
*/
|
*/
|
||||||
public getInitialInstructions(): string {
|
public getInitialInstructions(): string {
|
||||||
const config = this.getPromptsConfig();
|
return this.initialInstructions;
|
||||||
return config.initial_instructions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reloads the configuration from disk.
|
|
||||||
*
|
|
||||||
* Forces a fresh read of the configuration file on the next access,
|
|
||||||
* useful for development or when configuration files are updated at runtime.
|
|
||||||
*/
|
|
||||||
public reloadConfiguration(): void {
|
|
||||||
this.promptsConfig = null;
|
|
||||||
this.logger.info("Configuration cache cleared, will reload on next access");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ fi
|
|||||||
if [[ "$URL" = "http://localhost:9090" ]]; then
|
if [[ "$URL" = "http://localhost:9090" ]]; then
|
||||||
pnpx concurrently --kill-others-on-fail -s last -k \
|
pnpx concurrently --kill-others-on-fail -s last -k \
|
||||||
"caddy file-server --root ../../plugins/dist/doc/ --listen :9090" \
|
"caddy file-server --root ../../plugins/dist/doc/ --listen :9090" \
|
||||||
"../types-generator/build $URL";
|
"bash ../types-generator/build $URL";
|
||||||
else
|
else
|
||||||
../types-generator/build $URL;
|
bash ../types-generator/build $URL;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Types Generator
|
# Types Generator
|
||||||
|
|
||||||
This subproject contains helper scripts used in the development of the
|
This subproject contains helper scripts used in the development of the
|
||||||
Penpot MCP server for generate the types yaml.
|
Penpot MCP server, specifically for the generation of a YAML file containing
|
||||||
|
Penpot plugin API types and their documentation.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -12,15 +13,41 @@ Install the environment via (optional, already handled by `build` script)
|
|||||||
|
|
||||||
pixi install
|
pixi install
|
||||||
|
|
||||||
|
## Running the API Documentation Preparation Script
|
||||||
|
|
||||||
### Buld API types
|
The script `prepare_api_docs.py` reads API documentation from a Web URL
|
||||||
|
and collects it in a single YAML file, which is then used by an MCP
|
||||||
The script `prepare_api_docs.py` reads API documentation from the Web
|
|
||||||
and collects it in a single yaml file, which is then used by an MCP
|
|
||||||
tool to provide API documentation to an LLM on demand.
|
tool to provide API documentation to an LLM on demand.
|
||||||
|
|
||||||
|
Successful execution will generate the output file `../packages/server/data/api_types.yml`.
|
||||||
|
|
||||||
|
### Generating the YAML File for a Given URL
|
||||||
|
|
||||||
Running the script:
|
Running the script:
|
||||||
|
|
||||||
./build <optional-url>
|
pixi run python prepare_api_docs.py <url>
|
||||||
|
|
||||||
|
You can alternatively run `./build <url>`, which additionally performs pixi environment installation.
|
||||||
|
|
||||||
|
For example, to generate the API documentation based on the current PROD Penpot API documentation,
|
||||||
|
use the URL
|
||||||
|
|
||||||
|
https://doc.plugins.penpot.app
|
||||||
|
|
||||||
|
### Generating the YAML File Based on the Current Documentation in the Repository
|
||||||
|
|
||||||
|
Requirement: [Caddy](https://caddyserver.com/download) must be installed and available in the system path.
|
||||||
|
|
||||||
|
To generate the API documentation based on the current documentation in the repository,
|
||||||
|
run the `build:types` script in the parent directory, i.e.
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
pnpm run build:types
|
||||||
|
|
||||||
|
This will spawn a local HTTP server on port 9090 and run the `prepare_api_docs.py` script with the
|
||||||
|
URL `http://localhost:9090`.
|
||||||
|
To run only the server without executing the script, run
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
caddy file-server --root ../plugins/dist/doc/ --listen 127.0.0.1:9090
|
||||||
|
|
||||||
This will generate `../packages/server/data/api_types.yml`.
|
|
||||||
|
|||||||
@@ -80,6 +80,25 @@ class PenpotAPIContentMarkdownConverter(MarkdownConverter):
|
|||||||
# return as code block
|
# return as code block
|
||||||
return f"\n```\n{soup.get_text()}\n```\n\n"
|
return f"\n```\n{soup.get_text()}\n```\n\n"
|
||||||
|
|
||||||
|
# check for <ul> tag with a single <li>: move the <li> content a <div> and process it as normal,
|
||||||
|
# to avoid single list items with superfluous bullet points and indentations.
|
||||||
|
# This happens frequently, especially in new versions of the docs generator, e.g. for methods:
|
||||||
|
# <ul class="tsd-signatures tsd-is-inherited">
|
||||||
|
# <li class="tsd-is-inherited">
|
||||||
|
# <div class="tsd-signature tsd-anchor-link" id="remove-1">...</div>
|
||||||
|
# </li>
|
||||||
|
# </ul>
|
||||||
|
if node.name == "ul" and "class" in node.attrs and "tsd-signatures" in node.attrs["class"]:
|
||||||
|
soup_ul = soup.find("ul")
|
||||||
|
if soup_ul is not None:
|
||||||
|
li_children = soup_ul.find_all("li", recursive=False)
|
||||||
|
if len(li_children) == 1:
|
||||||
|
# create a new div with the content of the single li
|
||||||
|
new_div = soup.new_tag("div")
|
||||||
|
for child in list(li_children[0].contents):
|
||||||
|
new_div.append(child)
|
||||||
|
return self.process_tag(new_div, parent_tags=parent_tags)
|
||||||
|
|
||||||
# other cases: use the default processing
|
# other cases: use the default processing
|
||||||
return super().process_tag(node, parent_tags=parent_tags)
|
return super().process_tag(node, parent_tags=parent_tags)
|
||||||
|
|
||||||
@@ -135,7 +154,7 @@ class YamlConverter:
|
|||||||
|
|
||||||
|
|
||||||
class PenpotAPIDocsProcessor:
|
class PenpotAPIDocsProcessor:
|
||||||
def __init__(self, url=None):
|
def __init__(self, url: str):
|
||||||
self.md_converter = PenpotAPIContentMarkdownConverter()
|
self.md_converter = PenpotAPIContentMarkdownConverter()
|
||||||
self.base_url = url
|
self.base_url = url
|
||||||
self.types: dict[str, TypeInfo] = {}
|
self.types: dict[str, TypeInfo] = {}
|
||||||
@@ -157,7 +176,7 @@ class PenpotAPIDocsProcessor:
|
|||||||
type_name = href.split("/")[-1].replace(".html", "")
|
type_name = href.split("/")[-1].replace(".html", "")
|
||||||
log.info("Processing page: %s", type_name)
|
log.info("Processing page: %s", type_name)
|
||||||
type_info = self.process_page(href, type_name)
|
type_info = self.process_page(href, type_name)
|
||||||
print(f"Adding '{type_name}' with {type_info}")
|
log.info(f"Adding '{type_name}' with {type_info}")
|
||||||
self.types[type_name] = type_info
|
self.types[type_name] = type_info
|
||||||
|
|
||||||
# add type reference information
|
# add type reference information
|
||||||
@@ -201,11 +220,21 @@ class PenpotAPIDocsProcessor:
|
|||||||
members_in_group = {}
|
members_in_group = {}
|
||||||
members[members_type] = members_in_group
|
members[members_type] = members_in_group
|
||||||
for member_tag in el.find_all(attrs={"class": "tsd-member"}):
|
for member_tag in el.find_all(attrs={"class": "tsd-member"}):
|
||||||
|
# determine member name
|
||||||
|
member_name = None
|
||||||
member_anchor = member_tag.find("a", attrs={"class": "tsd-anchor"}, recursive=False)
|
member_anchor = member_tag.find("a", attrs={"class": "tsd-anchor"}, recursive=False)
|
||||||
member_name = member_anchor.attrs["id"]
|
if member_anchor is not None:
|
||||||
member_heading = member_tag.find("h3")
|
member_name = member_anchor.attrs["id"]
|
||||||
|
else:
|
||||||
|
member_h3 = member_tag.find("h3", recursive=False)
|
||||||
|
if member_h3 is not None:
|
||||||
|
h3_span = member_h3.find("span", recursive=False)
|
||||||
|
if h3_span is not None:
|
||||||
|
member_name = h3_span.get_text().strip()
|
||||||
|
assert member_name is not None, f"Could not determine member name for\n{member_tag}"
|
||||||
# extract tsd-tag info (e.g., "Readonly") from the heading and reinsert it into the signature,
|
# extract tsd-tag info (e.g., "Readonly") from the heading and reinsert it into the signature,
|
||||||
# where we want to see it. The heading is removed, as it is redundant.
|
# where we want to see it. The heading is removed, as it is redundant.
|
||||||
|
member_heading = member_tag.find("h3")
|
||||||
if member_heading:
|
if member_heading:
|
||||||
tags_in_heading = member_heading.find_all(attrs={"class": "tsd-tag"})
|
tags_in_heading = member_heading.find_all(attrs={"class": "tsd-tag"})
|
||||||
if tags_in_heading:
|
if tags_in_heading:
|
||||||
@@ -237,25 +266,29 @@ class PenpotAPIDocsProcessor:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_API_DOCS_URL = "http://localhost:9090"
|
LOCAL_API_DOCS_URL = "http://localhost:9090"
|
||||||
|
PROD_API_DOCS_URL = "https://doc.plugins.penpot.app"
|
||||||
|
DEFAULT_API_DOCS_URL = LOCAL_API_DOCS_URL
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
target_dir = Path(__file__).parent.parent / "packages" / "server" / "data"
|
target_dir = Path(__file__).parent.parent / "packages" / "server" / "data"
|
||||||
url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_API_DOCS_URL
|
url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_API_DOCS_URL
|
||||||
|
|
||||||
print("Fetching plugin data from: {}".format(url))
|
log.info("Fetching plugin data from: {}".format(url))
|
||||||
PenpotAPIDocsProcessor(url).run(target_dir=str(target_dir))
|
PenpotAPIDocsProcessor(url).run(target_dir=str(target_dir))
|
||||||
|
|
||||||
|
|
||||||
def debug_type_conversion(rel_url: str):
|
def debug_type_conversion(rel_url: str, base_url: str):
|
||||||
"""
|
"""
|
||||||
This function is for debugging purposes only.
|
This function is for debugging purposes only.
|
||||||
It processes a single type page and prints the converted markdown to the console.
|
It processes a single type page and prints the converted markdown to the console.
|
||||||
|
|
||||||
|
:param base_url: base URL of the API docs (e.g., "http://localhost:9090")
|
||||||
:param rel_url: relative URL of the type page (e.g., "interfaces/ShapeBase")
|
:param rel_url: relative URL of the type page (e.g., "interfaces/ShapeBase")
|
||||||
"""
|
"""
|
||||||
type_name = rel_url.split("/")[-1]
|
type_name = rel_url.split("/")[-1].replace(".html", "")
|
||||||
processor = PenpotAPIDocsProcessor()
|
processor = PenpotAPIDocsProcessor(url=base_url)
|
||||||
type_info = processor.process_page(rel_url, type_name)
|
type_info = processor.process_page(rel_url, type_name)
|
||||||
print(f"--- overview ---\n{type_info.overview}\n")
|
print(f"--- overview ---\n{type_info.overview}\n")
|
||||||
for member_type, members in type_info.members.items():
|
for member_type, members in type_info.members.items():
|
||||||
@@ -265,5 +298,5 @@ def debug_type_conversion(rel_url: str):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# debug_type_conversion("interfaces/LayoutChildProperties")
|
# debug_type_conversion("interfaces/Path.html", LOCAL_API_DOCS_URL)
|
||||||
logging.run_main(main)
|
logging.run_main(main)
|
||||||
|
|||||||
Reference in New Issue
Block a user