From 7d09d930fe00cb94987a7d622a668ee81984ba02 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 3 Feb 2026 11:13:46 +0100 Subject: [PATCH 1/6] :books: Update changelog --- CHANGES.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ba5edaf58b..7ba6b64e95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,6 @@ # CHANGELOG -## 2.13.0 (Unreleased) - -### :boom: Breaking changes & Deprecations - -### :rocket: Epics and highlights +## 2.13.0 ### :heart: Community contributions (Thank you!) From c14ccc18b8c50f21bd581fc662503ac5444df0cf Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 4 Feb 2026 13:37:01 +0100 Subject: [PATCH 2/6] :sparkles: Import mcp from develop --- docker/devenv/Dockerfile | 26 +- docker/devenv/docker-compose.yaml | 5 + docker/devenv/files/Caddyfile | 11 +- docker/devenv/files/bashrc | 3 +- docker/devenv/files/nginx.conf | 32 +- docker/devenv/files/tmux.conf | 1 + mcp/.gitignore | 11 + mcp/.prettierignore | 7 + mcp/.prettierrc | 20 + mcp/.serena/.gitignore | 1 + .../memories/code_style_conventions.md | 25 + mcp/.serena/memories/project_overview.md | 91 + mcp/.serena/memories/suggested_commands.md | 70 + .../memories/task_completion_guidelines.md | 56 + mcp/.serena/project.yml | 130 + mcp/README.md | 291 + mcp/docs/multi-user-mode.md | 41 + mcp/package.json | 26 + mcp/packages/common/package.json | 20 + mcp/packages/common/src/index.ts | 1 + mcp/packages/common/src/types.ts | 85 + mcp/packages/common/tsconfig.json | 19 + mcp/packages/plugin/.gitignore | 24 + mcp/packages/plugin/README.md | 21 + mcp/packages/plugin/index.html | 15 + mcp/packages/plugin/package.json | 24 + mcp/packages/plugin/public/manifest.json | 6 + mcp/packages/plugin/src/PenpotUtils.ts | 425 + mcp/packages/plugin/src/TaskHandler.ts | 77 + mcp/packages/plugin/src/main.ts | 110 + mcp/packages/plugin/src/plugin.ts | 69 + mcp/packages/plugin/src/style.css | 10 + .../task-handlers/ExecuteCodeTaskHandler.ts | 212 + mcp/packages/plugin/src/vite-env.d.ts | 4 + mcp/packages/plugin/tsconfig.json | 24 + mcp/packages/plugin/vite.config.ts | 43 + mcp/packages/plugin/vite.release.config.ts | 10 + mcp/packages/server/.gitignore | 0 mcp/packages/server/README.md | 24 + mcp/packages/server/data/api_types.yml | 18268 ++++++++++++++++ mcp/packages/server/data/prompts.yml | 267 + mcp/packages/server/package.json | 54 + mcp/packages/server/pnpm-lock.yaml | 2840 +++ mcp/packages/server/src/ApiDocs.ts | 128 + .../server/src/ConfigurationLoader.ts | 85 + mcp/packages/server/src/PenpotMcpServer.ts | 262 + mcp/packages/server/src/PluginBridge.ts | 227 + mcp/packages/server/src/PluginTask.ts | 122 + mcp/packages/server/src/ReplServer.ts | 112 + mcp/packages/server/src/Tool.ts | 121 + mcp/packages/server/src/ToolResponse.ts | 97 + mcp/packages/server/src/index.ts | 67 + mcp/packages/server/src/logger.ts | 80 + mcp/packages/server/src/static/repl.html | 554 + .../server/src/tasks/ExecuteCodePluginTask.ts | 22 + .../server/src/tools/ExecuteCodeTool.ts | 77 + .../server/src/tools/ExportShapeTool.ts | 147 + .../server/src/tools/HighLevelOverviewTool.ts | 26 + .../server/src/tools/ImportImageTool.ts | 123 + .../server/src/tools/PenpotApiInfoTool.ts | 88 + mcp/packages/server/src/utils/FileUtils.ts | 44 + mcp/packages/server/tsconfig.json | 22 + mcp/pnpm-lock.yaml | 2851 +++ mcp/pnpm-workspace.yaml | 6 + mcp/resources/architecture.png | Bin 0 -> 66448 bytes mcp/scripts/build | 45 + mcp/scripts/build-types | 23 + mcp/scripts/check | 7 + mcp/scripts/fmt | 4 + mcp/scripts/setup | 7 + mcp/types-generator/.gitattributes | 2 + mcp/types-generator/.gitignore | 4 + mcp/types-generator/README.md | 26 + mcp/types-generator/build | 11 + mcp/types-generator/pixi.lock | 1090 + mcp/types-generator/pixi.toml | 20 + mcp/types-generator/prepare_api_docs.py | 269 + 77 files changed, 30257 insertions(+), 11 deletions(-) create mode 100644 mcp/.gitignore create mode 100644 mcp/.prettierignore create mode 100644 mcp/.prettierrc create mode 100644 mcp/.serena/.gitignore create mode 100644 mcp/.serena/memories/code_style_conventions.md create mode 100644 mcp/.serena/memories/project_overview.md create mode 100644 mcp/.serena/memories/suggested_commands.md create mode 100644 mcp/.serena/memories/task_completion_guidelines.md create mode 100644 mcp/.serena/project.yml create mode 100644 mcp/README.md create mode 100644 mcp/docs/multi-user-mode.md create mode 100644 mcp/package.json create mode 100644 mcp/packages/common/package.json create mode 100644 mcp/packages/common/src/index.ts create mode 100644 mcp/packages/common/src/types.ts create mode 100644 mcp/packages/common/tsconfig.json create mode 100644 mcp/packages/plugin/.gitignore create mode 100644 mcp/packages/plugin/README.md create mode 100644 mcp/packages/plugin/index.html create mode 100644 mcp/packages/plugin/package.json create mode 100644 mcp/packages/plugin/public/manifest.json create mode 100644 mcp/packages/plugin/src/PenpotUtils.ts create mode 100644 mcp/packages/plugin/src/TaskHandler.ts create mode 100644 mcp/packages/plugin/src/main.ts create mode 100644 mcp/packages/plugin/src/plugin.ts create mode 100644 mcp/packages/plugin/src/style.css create mode 100644 mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts create mode 100644 mcp/packages/plugin/src/vite-env.d.ts create mode 100644 mcp/packages/plugin/tsconfig.json create mode 100644 mcp/packages/plugin/vite.config.ts create mode 100644 mcp/packages/plugin/vite.release.config.ts create mode 100644 mcp/packages/server/.gitignore create mode 100644 mcp/packages/server/README.md create mode 100644 mcp/packages/server/data/api_types.yml create mode 100644 mcp/packages/server/data/prompts.yml create mode 100644 mcp/packages/server/package.json create mode 100644 mcp/packages/server/pnpm-lock.yaml create mode 100644 mcp/packages/server/src/ApiDocs.ts create mode 100644 mcp/packages/server/src/ConfigurationLoader.ts create mode 100644 mcp/packages/server/src/PenpotMcpServer.ts create mode 100644 mcp/packages/server/src/PluginBridge.ts create mode 100644 mcp/packages/server/src/PluginTask.ts create mode 100644 mcp/packages/server/src/ReplServer.ts create mode 100644 mcp/packages/server/src/Tool.ts create mode 100644 mcp/packages/server/src/ToolResponse.ts create mode 100644 mcp/packages/server/src/index.ts create mode 100644 mcp/packages/server/src/logger.ts create mode 100644 mcp/packages/server/src/static/repl.html create mode 100644 mcp/packages/server/src/tasks/ExecuteCodePluginTask.ts create mode 100644 mcp/packages/server/src/tools/ExecuteCodeTool.ts create mode 100644 mcp/packages/server/src/tools/ExportShapeTool.ts create mode 100644 mcp/packages/server/src/tools/HighLevelOverviewTool.ts create mode 100644 mcp/packages/server/src/tools/ImportImageTool.ts create mode 100644 mcp/packages/server/src/tools/PenpotApiInfoTool.ts create mode 100644 mcp/packages/server/src/utils/FileUtils.ts create mode 100644 mcp/packages/server/tsconfig.json create mode 100644 mcp/pnpm-lock.yaml create mode 100644 mcp/pnpm-workspace.yaml create mode 100644 mcp/resources/architecture.png create mode 100755 mcp/scripts/build create mode 100755 mcp/scripts/build-types create mode 100755 mcp/scripts/check create mode 100755 mcp/scripts/fmt create mode 100755 mcp/scripts/setup create mode 100644 mcp/types-generator/.gitattributes create mode 100644 mcp/types-generator/.gitignore create mode 100644 mcp/types-generator/README.md create mode 100755 mcp/types-generator/build create mode 100644 mcp/types-generator/pixi.lock create mode 100644 mcp/types-generator/pixi.toml create mode 100644 mcp/types-generator/prepare_api_docs.py diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 04eb0136ea..fa01edf221 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -179,9 +179,10 @@ RUN set -eux; \ FROM base AS setup-utils -ENV CLJKONDO_VERSION=2025.07.28 \ +ENV CLJKONDO_VERSION=2026.01.19 \ BABASHKA_VERSION=1.12.208 \ - CLJFMT_VERSION=0.13.1 + CLJFMT_VERSION=0.15.6 \ + PIXI_VERSION=0.63.2 RUN set -ex; \ ARCH="$(dpkg --print-architecture)"; \ @@ -224,6 +225,26 @@ RUN set -ex; \ tar -xf /tmp/babashka.tar.gz; \ rm -rf /tmp/babashka.tar.gz; +RUN set -ex; \ + ARCH="$(dpkg --print-architecture)"; \ + case "${ARCH}" in \ + aarch64|arm64) \ + BINARY_URL="https://github.com/prefix-dev/pixi/releases/download/v$PIXI_VERSION/pixi-aarch64-unknown-linux-musl.tar.gz"; \ + ;; \ + amd64|x86_64) \ + BINARY_URL="https://github.com/prefix-dev/pixi/releases/download/v$PIXI_VERSION/pixi-x86_64-unknown-linux-musl.tar.gz"; \ + ;; \ + *) \ + echo "Unsupported arch: ${ARCH}"; \ + exit 1; \ + ;; \ + esac; \ + cd /tmp; \ + curl -LfsSo /tmp/pixi.tar.gz ${BINARY_URL}; \ + cd /opt/utils/bin; \ + tar -xf /tmp/pixi.tar.gz; \ + rm -rf /tmp/pixi.tar.gz; + RUN set -ex; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ @@ -398,7 +419,6 @@ COPY files/Caddyfile /home/ COPY files/selfsigned.crt /home/ COPY files/selfsigned.key /home/ COPY files/start-tmux.sh /home/start-tmux.sh -COPY files/start-tmux-back.sh /home/start-tmux-back.sh COPY files/entrypoint.sh /home/entrypoint.sh COPY files/init.sh /home/init.sh diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index 3973eea3b4..322a68985d 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -46,6 +46,11 @@ services: - 9090:9090 - 9091:9091 + # MCP + - 4400:4400 + - 4401:4401 + - 4402:4402 + environment: - EXTERNAL_UID=${CURRENT_USER_ID} # SMTP setup diff --git a/docker/devenv/files/Caddyfile b/docker/devenv/files/Caddyfile index 719ed142ae..eb822a91c3 100644 --- a/docker/devenv/files/Caddyfile +++ b/docker/devenv/files/Caddyfile @@ -1,16 +1,17 @@ { - auto_https off + auto_https off } localhost:3449 { - reverse_proxy localhost:4449 - tls /home/selfsigned.crt /home/selfsigned.key + reverse_proxy localhost:4449 + tls /home/selfsigned.crt /home/selfsigned.key + header -Strict-Transport-Security } http://localhost:3450 { - reverse_proxy localhost:4449 + reverse_proxy localhost:4449 } http://penpot-devenv-main:3450 { - reverse_proxy localhost:4449 + reverse_proxy localhost:4449 } diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index 8bcf7b67b1..98fc4a96dc 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -6,7 +6,8 @@ export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bi export CARGO_HOME="/home/penpot/.cargo" alias l='ls --color -GFlh' -alias rm='rm -r' +alias ll='ls --color -GFlh' +alias rm='rm -rf' alias ls='ls --color -F' alias lsd='ls -d *(/)' alias lsf='ls -h *(.)' diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 0c227e244d..f7926c42ef 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -121,6 +121,28 @@ http { proxy_http_version 1.1; } + location /mcp { + alias /home/penpot/penpot/mcp/packages/plugin/dist; + proxy_http_version 1.1; + } + + location /mcp/ws { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://127.0.0.1:4402; + proxy_http_version 1.1; + } + + location /mcp/stream { + proxy_pass http://127.0.0.1:4401/mcp; + proxy_http_version 1.1; + } + + location /mcp/sse { + proxy_pass http://127.0.0.1:4401/sse; + proxy_http_version 1.1; + } + location /admin { proxy_pass http://127.0.0.1:6063/admin; } @@ -141,8 +163,14 @@ http { proxy_pass http://127.0.0.1:5000; } - location /nitrate/ { - proxy_pass http://127.0.0.1:3000/; + location /control-center { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } location /wasm-playground { diff --git a/docker/devenv/files/tmux.conf b/docker/devenv/files/tmux.conf index 9498c79318..2044d56556 100644 --- a/docker/devenv/files/tmux.conf +++ b/docker/devenv/files/tmux.conf @@ -1,3 +1,4 @@ +set -g default-command "${SHELL}" set -g mouse off set -g history-limit 50000 setw -g mode-keys emacs diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000000..8a245a5dca --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,11 @@ +.idea +node_modules +dist +*.bak +*.orig +temp +*.tsbuildinfo + +# Log files +logs/ +*.log diff --git a/mcp/.prettierignore b/mcp/.prettierignore new file mode 100644 index 0000000000..00e05b35e2 --- /dev/null +++ b/mcp/.prettierignore @@ -0,0 +1,7 @@ +*.md +*.json +python-scripts/ +.serena/ + +# auto-generated files +mcp-server/data/api_types.yml diff --git a/mcp/.prettierrc b/mcp/.prettierrc new file mode 100644 index 0000000000..3d58be61d9 --- /dev/null +++ b/mcp/.prettierrc @@ -0,0 +1,20 @@ +{ + "tabWidth": 4, + "overrides": [ + { + "files": "*.yml", + "options": { + "tabWidth": 2 + } + } + ], + "useTabs": false, + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "printWidth": 120, + "endOfLine": "auto" +} diff --git a/mcp/.serena/.gitignore b/mcp/.serena/.gitignore new file mode 100644 index 0000000000..14d86ad623 --- /dev/null +++ b/mcp/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/mcp/.serena/memories/code_style_conventions.md b/mcp/.serena/memories/code_style_conventions.md new file mode 100644 index 0000000000..cc9b1c07ae --- /dev/null +++ b/mcp/.serena/memories/code_style_conventions.md @@ -0,0 +1,25 @@ +# Code Style and Conventions + +## General Principles +- **Object-Oriented Design**: VERY IMPORTANT: Use idiomatic, object-oriented style with explicit abstractions +- **Strategy Pattern**: Prefer explicitly typed interfaces over bare functions for non-trivial functionality +- **Clean Architecture**: Tools implement a common interface for consistent registration and execution + +## TypeScript Configuration +- **Strict Mode**: All strict TypeScript options enabled +- **Target**: ES2022 +- **Module System**: CommonJS +- **Declaration Files**: Generated with source maps + +## Naming Conventions +- **Classes**: PascalCase (e.g., `ExeceuteCodeTool`, `PenpotMcpServer`) +- **Interfaces**: PascalCase (e.g., `Tool`) +- **Methods**: camelCase (e.g., `execute`, `registerTools`) +- **Constants**: camelCase for readonly properties (e.g., `definition`) +- **Files**: PascalCase for classes (e.g., `ExecuteCodeTool.ts`) + +## Documentation Style +- **JSDoc**: Use comprehensive JSDoc comments for classes, methods, and interfaces +- **Description Format**: Initial elliptical phrase that defines *what* it is, followed by details +- **Comment Style**: VERY IMPORTANT: Start with lowercase for comments of code blocks (unless lengthy explanation with multiple sentences) + diff --git a/mcp/.serena/memories/project_overview.md b/mcp/.serena/memories/project_overview.md new file mode 100644 index 0000000000..528976b077 --- /dev/null +++ b/mcp/.serena/memories/project_overview.md @@ -0,0 +1,91 @@ +# 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. + +## Tech Stack +- **Language**: TypeScript +- **Runtime**: Node.js +- **Framework**: MCP SDK (@modelcontextprotocol/sdk) +- **Build Tool**: TypeScript Compiler (tsc) + esbuild +- **Package Manager**: pnpm +- **WebSocket**: ws library for real-time communication + +## Project Structure +``` +penpot-mcp/ +├── 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 +│ ├── 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 +│ └── package.json # Includes @penpot-mcp/common dependency +├── penpot-plugin/ # Penpot plugin with response capability +│ ├── src/ +│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding +│ │ └── plugin.ts # Now sends task responses back to server +│ └── package.json # Includes @penpot-mcp/common dependency +└── prepare-api-docs # Python project for the generation of API docs +``` + +## Key Tasks + +### Adding a new Tool + +1. Implement the tool class in `mcp-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/`. + +### 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/`). + * 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 +``` + diff --git a/mcp/.serena/memories/suggested_commands.md b/mcp/.serena/memories/suggested_commands.md new file mode 100644 index 0000000000..34f92fbae7 --- /dev/null +++ b/mcp/.serena/memories/suggested_commands.md @@ -0,0 +1,70 @@ +# Suggested Commands + +## Development Commands +```bash +# Navigate to MCP server directory +cd penpot/mcp/server + +# Install dependencies +pnpm install + +# Build the TypeScript project +pnpm run build + +# Start the server (production) +pnpm run start + +# Start the server in development mode +npm run start:dev +``` + +## Testing and Development +```bash +# Run TypeScript compiler in watch mode +pnpx tsc --watch + +# Check TypeScript compilation without emitting files +pnpx tsc --noEmit +``` + +## Windows-Specific Commands +```cmd +# Directory navigation +cd penpot/mcp/server +dir # List directory contents +type package.json # Display file contents + +# Git operations +git status +git add . +git commit -m "message" +git push + +# File operations +copy src\file.ts backup\file.ts # Copy files +del dist\* # Delete files +mkdir new-directory # Create directory +rmdir /s directory # Remove directory recursively +``` + +## Project Structure Navigation +```bash +# Key directories +cd penpot/mcp/server/src # Source code +cd penpot/mcp/server/src/tools # Tool implementations +cd penpot/mcp/server/src/interfaces # Type definitions +cd penpot/mcp/server/dist # Compiled output +``` + +## Common Utilities +```cmd +# Search for text in files +findstr /s /i "HelloWorld" *.ts + +# Find files by name +dir /s /b *Tool.ts + +# Process management +tasklist | findstr node # Find Node.js processes +taskkill /f /im node.exe # Kill Node.js processes +``` diff --git a/mcp/.serena/memories/task_completion_guidelines.md b/mcp/.serena/memories/task_completion_guidelines.md new file mode 100644 index 0000000000..678fd83e55 --- /dev/null +++ b/mcp/.serena/memories/task_completion_guidelines.md @@ -0,0 +1,56 @@ +# Task Completion Guidelines + +## After Making Code Changes + +### 1. Build and Test +```bash +cd mcp-server +npm run build:full # or npm run build for faster bundling only +``` + +### 2. Verify TypeScript Compilation +```bash +npx tsc --noEmit +``` + +### 3. Test the Server +```bash +# Start in development mode to test changes +npm run dev +``` + +### 4. Code Quality Checks +- Ensure all code follows the established conventions +- Verify JSDoc comments are complete and accurate +- Check that error handling is appropriate +- Use clean imports WITHOUT file extensions (esbuild handles resolution) +- Validate that tool interfaces are properly implemented + +### 5. Integration Testing +- Test tool registration in the main server +- Verify MCP protocol compliance +- Ensure tool definitions match implementation + +## Before Committing Changes +1. **Build Successfully**: `npm run build:full` completes without errors +2. **No TypeScript Errors**: `npx tsc --noEmit` passes +3. **Documentation Updated**: JSDoc comments reflect changes +4. **Tool Registry Updated**: New tools added to `registerTools()` method +5. **Interface Compliance**: All tools implement the `Tool` interface correctly + +## File Organization +- Place new tools in `src/tools/` directory +- Update main server registration in `src/index.ts` +- Follow existing naming conventions + +## Common Patterns +- All tools must implement the `Tool` interface +- Use readonly properties for tool definitions +- Include comprehensive error handling +- Follow the established documentation style +- Import WITHOUT file extensions (esbuild resolves them automatically) + +## Build System +- Uses esbuild for fast bundling and TypeScript for declarations +- Import statements should omit file extensions entirely +- IDE refactoring is safe - no extension-related build failures \ No newline at end of file diff --git a/mcp/.serena/project.yml b/mcp/.serena/project.yml new file mode 100644 index 0000000000..c9ed0f7330 --- /dev/null +++ b/mcp/.serena/project.yml @@ -0,0 +1,130 @@ + + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +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, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (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 + rather than mere functions (i.e. use the strategy pattern, for example). + + 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 + required for sentences). +# the name by which the project can be referenced within Serena +project_name: "penpot-mcp" + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: utf-8 + + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- typescript diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000000..c8d5a94e0e --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,291 @@ +![mcp-server-cover-github-1](https://github.com/user-attachments/assets/dcd14e63-fecd-424f-9a50-c1b1eafe2a4f) + +# Penpot's Official MCP Server + +Penpot integrates a LLM layer built on the Model Context Protocol +(MCP) via Penpot's Plugin API to interact with a Penpot design +file. Penpot's MCP server enables LLMs to perfom data queries, +transformation and creation operations. + +Penpot's MCP Server is unlike any other you've seen. You get +design-to- design, code-to-design and design-code supercharged +workflows. + + +[![Penpot MCP video playlist](https://github.com/user-attachments/assets/204f1d99-ce51-41dd-a5dd-1ef739f8f089)](https://www.youtube.com/playlist?list=PLgcCPfOv5v57SKMuw1NmS0-lkAXevpn10) + + +## Architecture + +The **Penpot MCP Server** exposes tools to AI clients (LLMs), which +support the retrieval of design data as well as the modification and +creation of design elements. The MCP server communicates with Penpot +via the dedicated **Penpot MCP Plugin**, +which connects to the MCP server via WebSocket. +This enables the LLM to carry out tasks in the context of a design file by +executing code that leverages the Penpot Plugin API. +The LLM is free to write and execute arbitrary code snippets +within the Penpot Plugin environment to accomplish its tasks. + +![Architecture](resources/architecture.png) + +This repository thus contains not only the MCP server implementation itself +but also the supporting Penpot MCP Plugin +(see section [Repository Structure](#repository-structure) below). + +## Demonstration + +[![Video](https://v32155.1blu.de/penpot/PenpotFest2025_thumbnail.png)](https://v32155.1blu.de/penpot/PenpotFest2025.mp4) + + +## Usage + +To use the Penpot MCP server, you must + * run the MCP server and connect your AI client to it, + * run the web server providing the Penpot MCP plugin, and + * open the Penpot MCP plugin in Penpot and connect it to the MCP server. + +Follow the steps below to enable the integration. + + +### Prerequisites + +The project requires [Node.js](https://nodejs.org/) (tested with v22.x +with corepack). + +Following the installation of Node.js, the tools `pnpm` and `npx` +should be available in your terminal. For ensure corepack installed +and enabled correctly, just execute the `./scripts/setup`. + +It is also required to have `caddy` executeable in the path, it is +used for start a local server for generate types documentation from +the current branch. If you want to run it outside devenv where all +dependencies are already provided, please download caddy from +[here](https://caddyserver.com/download). + +You should probably be using penpot devenv, where all this +dependencies are already present and correctly setup. But nothing +prevents you execute this outside of devenv if you satisfy the +specified dependencies. + + +### 1. Build & Launch the MCP Server and the Plugin Server + +If it's your first execution, install the required dependencies: + +```shell +cd mcp/ +./scripts/setup +``` + +Then build all components and start the two servers: + +```shell +pnpm run bootstrap +``` + +This bootstrap command will: + + * install dependencies for all components (`pnpm -r run install`) + * build all components (`pnpm -r run build`) + * 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 + +> [!NOTE] +> **Browser Connectivity Restrictions** +> +> Starting with Chromium version 142, the private network access (PNA) restrictions have been hardened, +> and when connecting to `localhost` from a web application served from a different origin +> (such as https://design.penpot.app), the connection must explicitly be allowed. +> +> Most Chromium-based browsers (e.g. Chrome, Vivaldi) will display a popup requesting permission +> to access the local network. Be sure to approve the request to allow the connection. +> +> Some browsers take additional security measures, and you may need to disable them. +> For example, in Brave, disable the "Shield" for the Penpot website to allow local network access. +> +> If your browser refuses to connect to the locally served plugin, check its configuration or +> try a different browser (e.g. Firefox) that does not enforce these restrictions. + +1. Open Penpot in your browser +2. Navigate to a design file +3. Open the Plugins menu +4. Load the plugin using the development URL (`http://localhost:4400/manifest.json` by default) +5. Open the plugin UI +6. In the plugin UI, click "Connect to MCP server". + The connection status should change from "Not connected" to "Connected to MCP server". + (Check the browser's developer console for WebSocket connection logs. + Check the MCP server terminal for WebSocket connection messages.) + +> [!IMPORTANT] +> Do not close the plugin's UI while using the MCP server, as this will close the connection. + +### 3. Connect an MCP Client + +By default, the server runs on port 4401 and provides: + +- **Modern Streamable HTTP endpoint**: `http://localhost:4401/mcp` +- **Legacy SSE endpoint**: `http://localhost:4401/sse` + +These endpoints can be used directly by MCP clients that support them. +Simply configure the client to connect the MCP server by providing the respective URL. + +When using a client that only supports stdio transport, +a proxy like `mcp-remote` is required. + +#### Using a Proxy for stdio Transport + +NOTE: only relevant if you are executing this outside of devenv + +The `mcp-remote` package can proxy stdio transport to HTTP/SSE, +allowing clients that support only stdio to connect to the MCP server indirectly. + +1. Install `mcp-remote` globally if you haven't already: + + npm install -g mcp-remote + +2. Use `mcp-remote` to provide the launch command for your MCP client: + + npx -y mcp-remote http://localhost:4401/sse --allow-http + +#### Example: Claude Desktop + +For Windows and macOS, there is the official [Claude Desktop app](https://claude.ai/download), which you can use as an MCP client. +For Linux, there is an [unofficial community version](https://github.com/aaddrick/claude-desktop-debian). + +Since Claude Desktop natively supports only stdio transport, you will need to use a proxy like `mcp-remote`. +Install it as described above. + +To add the server to Claude Desktop's configuration, locate the configuration file (or find it via Menu / File / Settings / Developer): + +- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Linux**: `~/.config/Claude/claude_desktop_config.json` + +Add a `penpot` entry under `mcpServers` with the following content: + +```json +{ + "mcpServers": { + "penpot": { + "command": "npx", + "args": ["-y", "mcp-remote", "http://localhost:4401/sse", "--allow-http"] + } + } +} +``` + +After updating the configuration file, restart Claude Desktop completely for the changes to take effect. + +> [!IMPORTANT] +> Be sure to fully quit the app for the changes to take effect; closing the window is *not* sufficient. +> To fully terminate the app, choose Menu / File / Quit. + +After the restart, you should see the MCP server listed when clicking on the "Search and tools" icon at the bottom +of the prompt input area. + +#### Example: Claude Code + +To add the Penpot MCP server to a Claude Code project, issue the command + + claude mcp add penpot -t http http://localhost:4401/mcp + +## Repository Structure + +This repository is a monorepo containing four main components: + +1. **Common Types** (`common/`): + - Shared TypeScript definitions for request/response protocol + - Ensures type safety across server and plugin components + +2. **Penpot MCP Server** (`mcp-server/`): + - Provides MCP tools to LLMs for Penpot interaction + - Runs a WebSocket server accepting connections from the Penpot MCP plugin + - Implements request/response correlation with unique task IDs + - Handles task timeouts and proper error reporting + +3. **Penpot MCP Plugin** (`penpot-plugin/`): + - Connects to the MCP server via WebSocket + - Executes tasks in Penpot using the Plugin API + - Sends structured responses back to the server# + +4. **Helper Scripts** (`python-scripts/`): + - Python scripts that prepare data for the MCP server (development use) + +The core components are written in TypeScript, rendering interactions with the +Penpot Plugin API both natural and type-safe. + +## Configuration + +The Penpot MCP server can be configured using environment variables. All configuration +options use the `PENPOT_MCP_` prefix for consistency. + +### Server Configuration + +| Environment Variable | Description | Default | +|------------------------------------|----------------------------------------------------------------------------|--------------| +| `PENPOT_MCP_SERVER_LISTEN_ADDRESS` | Address on which the MCP server listens (binds to) | `localhost` | +| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` | +| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` | +| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` | +| `PENPOT_MCP_SERVER_ADDRESS` | Hostname or IP address via which clients can reach the MCP server | `localhost` | +| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` | + +### Logging Configuration + +| Environment Variable | Description | Default | +|------------------------|------------------------------------------------------|----------| +| `PENPOT_MCP_LOG_LEVEL` | Log level: `trace`, `debug`, `info`, `warn`, `error` | `info` | +| `PENPOT_MCP_LOG_DIR` | Directory for log files | `logs` | + +### Plugin Server Configuration + +| Environment Variable | Description | Default | +|-------------------------------------------|-----------------------------------------------------------------------------------------|--------------| +| `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS` | Address on which the plugin web server listens (single address or comma-separated list) | (local only) | + +## Beyond Local Execution + +The above instructions describe how to run the MCP server and plugin server locally. +We are working on enabling remote deployments of the MCP server, particularly +in [multi-user mode](docs/multi-user-mode.md), where multiple Penpot users will +be able to connect to the same MCP server instance. + +To run the server remotely (even for a single user), +you may set the following environment variables to configure the two servers +(MCP server & plugin server) appropriately: + * `PENPOT_MCP_REMOTE_MODE=true`: This ensures that the MCP server is operating + in remote mode, with local file system access disabled. + * `PENPOT_MCP_SERVER_LISTEN_ADDRESS` and `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS`: + Set these according to your requirements for remote connectivity. + To bind all interfaces, use `0.0.0.0` (use caution in untrusted networks). + * `PENPOT_MCP_SERVER_ADDRESS=`: This sets the hostname or IP address + where the MCP server can be reached. The Penpot MCP Plugin uses this to construct + the WebSocket URL as `ws://:` (default port: `4402`). diff --git a/mcp/docs/multi-user-mode.md b/mcp/docs/multi-user-mode.md new file mode 100644 index 0000000000..b4471d18b9 --- /dev/null +++ b/mcp/docs/multi-user-mode.md @@ -0,0 +1,41 @@ +# Multi-User Mode + +> [!WARNING] +> Multi-user mode is under development and not yet fully integrated. +> This information is provided for testing purposes only. + +The Penpot MCP server supports a multi-user mode, allowing multiple Penpot users +to connect to the same MCP server instance simultaneously. +This supports remote deployments of the MCP server, without requiring each user +to run their own server instance. + +## Limitations + +Multi-user mode has the limitation that tools which read from or write to +the local file system are not supported, as the server cannot access +the client's file system. This affects the import and export tools. + +## Running Components in Multi-User Mode + +To run the MCP server and the Penpot MCP plugin in multi-user mode (for testing), +you can use the following command: + +```shell +npm run bootstrap:multi-user +``` + +This will: +* launch the MCP server in multi-user mode (adding the `--multi-user` flag), +* build and launch the Penpot MCP plugin server in multi-user mode. + +See the package.json scripts for both `mcp-server` and `penpot-plugin` for details. + +In multi-user mode, users are required to be authenticated via a token. + +* This token is provided in the URL used to connect to the MCP server, + e.g. `http://localhost:4401/mcp?userToken=USER_TOKEN`. +* The same token must be provided when connecting the Penpot MCP plugin + to the MCP server. + In the future, the token will, most likely be generated by Penpot and + provided to the plugin automatically. + :warning: For now, it is hard-coded in the plugin's source code for testing purposes. diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000000..0cab1824a8 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,26 @@ +{ + "name": "mcp-meta", + "version": "1.0.0", + "description": "", + "scripts": { + "build": "pnpm -r run build", + "build:multi-user": "pnpm -r run build:multi-user", + "build:types": "./scripts/build-types", + "start": "pnpm -r --parallel run start", + "start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user", + "bootstrap": "pnpm -r install && pnpm run build && pnpm run start", + "bootstrap:multi-user": "pnpm -r install && pnpm run build:multi-user && pnpm run start:multi-user", + "fmt": "prettier --write packages/", + "fmt:check": "prettier --check packages/" + }, + "repository": { + "type": "git", + "url": "https://github.com/penpot/penpot.git" + }, + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", + "private": true, + "devDependencies": { + "concurrently": "^9.2.1", + "prettier": "^3.0.0" + } +} diff --git a/mcp/packages/common/package.json b/mcp/packages/common/package.json new file mode 100644 index 0000000000..6c014b34ac --- /dev/null +++ b/mcp/packages/common/package.json @@ -0,0 +1,20 @@ +{ + "name": "mcp-common", + "version": "1.0.0", + "description": "Shared type definitions and interfaces for Penpot MCP", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", + "scripts": { + "build": "tsc --build --clean && tsc --build", + "watch": "tsc --watch", + "types:check": "tsc --noEmit", + "clean": "rm -rf dist/" + }, + "devDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "dist/**/*" + ] +} diff --git a/mcp/packages/common/src/index.ts b/mcp/packages/common/src/index.ts new file mode 100644 index 0000000000..eea524d655 --- /dev/null +++ b/mcp/packages/common/src/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/mcp/packages/common/src/types.ts b/mcp/packages/common/src/types.ts new file mode 100644 index 0000000000..36e52cc531 --- /dev/null +++ b/mcp/packages/common/src/types.ts @@ -0,0 +1,85 @@ +/** + * Result of a plugin task execution. + * + * Contains the outcome status of a task and any additional result data. + */ +export interface PluginTaskResult { + /** + * Optional result data from the task execution. + */ + data?: T; +} + +/** + * Request message sent from server to plugin. + * + * Contains a unique identifier, task name, and parameters for execution. + */ +export interface PluginTaskRequest { + /** + * Unique identifier for request/response correlation. + */ + id: string; + + /** + * The name of the task to execute. + */ + task: string; + + /** + * The parameters for task execution. + */ + params: any; +} + +/** + * Response message sent from plugin back to server. + * + * Contains the original request ID and the execution result. + */ +export interface PluginTaskResponse { + /** + * Unique identifier matching the original request. + */ + id: string; + + /** + * Whether the task completed successfully. + */ + success: boolean; + + /** + * Optional error message if the task failed. + */ + error?: string; + + /** + * The result of the task execution. + */ + data?: T; +} + +/** + * Parameters for the executeCode task. + */ +export interface ExecuteCodeTaskParams { + /** + * The JavaScript code to be executed. + */ + code: string; +} + +/** + * Result data for the executeCode task. + */ +export interface ExecuteCodeTaskResultData { + /** + * The result of the executed code, if any. + */ + result: T; + + /** + * Captured console output during code execution. + */ + log: string; +} diff --git a/mcp/packages/common/tsconfig.json b/mcp/packages/common/tsconfig.json new file mode 100644 index 0000000000..89b057e684 --- /dev/null +++ b/mcp/packages/common/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/mcp/packages/plugin/.gitignore b/mcp/packages/plugin/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/mcp/packages/plugin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/mcp/packages/plugin/README.md b/mcp/packages/plugin/README.md new file mode 100644 index 0000000000..1a194fa19f --- /dev/null +++ b/mcp/packages/plugin/README.md @@ -0,0 +1,21 @@ +# Penpot MCP Plugin + +This project contains a Penpot plugin that accompanies the Penpot MCP server. +It connects to the MCP server via WebSocket, subsequently allowing the MCP +server to execute tasks in Penpot using the Plugin API. + +## Setup + +1. Install Dependencies + + pnpm install + +2. Build the Project + + pnpm run build + +3. Start a Local Development Server + + pnpm run start + + This will start a local development server at `http://localhost:4400`. diff --git a/mcp/packages/plugin/index.html b/mcp/packages/plugin/index.html new file mode 100644 index 0000000000..b2c08b5dae --- /dev/null +++ b/mcp/packages/plugin/index.html @@ -0,0 +1,15 @@ + + + + + + Penpot plugin example + + + + +
Not connected
+ + + + diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json new file mode 100644 index 0000000000..2fca8aeaae --- /dev/null +++ b/mcp/packages/plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "mcp-plugin", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "vite build --watch --config vite.config.ts", + "start:multi-user": "cross-env MULTI_USER_MODE=true vite build --watch --config vite.config.ts", + "build": "tsc && vite build --config vite.release.config.ts", + "build:multi-user": "tsc && cross-env MULTI_USER_MODE=true vite build --config vite.release.config.ts", + "types:check": "tsc --noEmit", + "clean": "rm -rf dist/" + }, + "dependencies": { + "@penpot/plugin-styles": "1.4.1", + "@penpot/plugin-types": "1.4.1" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "typescript": "^5.8.3", + "vite": "^7.0.8", + "vite-live-preview": "^0.3.2" + } +} diff --git a/mcp/packages/plugin/public/manifest.json b/mcp/packages/plugin/public/manifest.json new file mode 100644 index 0000000000..506021c29e --- /dev/null +++ b/mcp/packages/plugin/public/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Penpot MCP Plugin", + "code": "plugin.js", + "description": "This plugin enables interaction with the Penpot MCP server", + "permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"] +} diff --git a/mcp/packages/plugin/src/PenpotUtils.ts b/mcp/packages/plugin/src/PenpotUtils.ts new file mode 100644 index 0000000000..d5044e658a --- /dev/null +++ b/mcp/packages/plugin/src/PenpotUtils.ts @@ -0,0 +1,425 @@ +import { Board, Fill, FlexLayout, GridLayout, Page, Rectangle, Shape } from "@penpot/plugin-types"; + +export class PenpotUtils { + /** + * Generates an overview structure of the given shape, + * providing its id, name and type, and recursively its children's attributes. + * The `type` field indicates the type in the Penpot API. + * If the shape has a layout system (flex or grid), includes layout information. + * + * @param shape - The root shape to generate the structure from + * @param maxDepth - Optional maximum depth to traverse (leave undefined for unlimited) + * @returns An object representing the shape structure + */ + public static shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): object { + let children = undefined; + if (maxDepth === undefined || maxDepth > 0) { + if ("children" in shape && shape.children) { + children = shape.children.map((child) => + this.shapeStructure(child, maxDepth === undefined ? undefined : maxDepth - 1) + ); + } + } + + const result: any = { + id: shape.id, + name: shape.name, + type: shape.type, + children: children, + }; + + // add layout information if present + if ("flex" in shape && shape.flex) { + const flex: FlexLayout = shape.flex; + result.layout = { + type: "flex", + dir: flex.dir, + rowGap: flex.rowGap, + columnGap: flex.columnGap, + }; + } else if ("grid" in shape && shape.grid) { + const grid: GridLayout = shape.grid; + result.layout = { + type: "grid", + rows: grid.rows, + columns: grid.columns, + rowGap: grid.rowGap, + columnGap: grid.columnGap, + }; + } + + return result; + } + + /** + * 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 root - The root shape to start the search from (defaults to penpot.root) + */ + public static findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = penpot.root): Shape[] { + let result = new Array(); + + let find = function (shape: Shape | null) { + if (!shape) { + return; + } + if (predicate(shape)) { + result.push(shape); + } + if ("children" in shape && shape.children) { + for (let child of shape.children) { + find(child); + } + } + }; + + find(root); + return result; + } + + /** + * Finds the first shape 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 root - The root shape to start the search from (if null, searches all pages) + */ + public static findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null { + let find = function (shape: Shape | null): Shape | null { + if (!shape) { + return null; + } + if (predicate(shape)) { + return shape; + } + if ("children" in shape && shape.children) { + for (let child of shape.children) { + let result = find(child); + if (result) { + return result; + } + } + } + return null; + }; + + if (root === null) { + const pages = penpot.currentFile?.pages; + if (pages) { + for (let page of pages) { + let result = find(page.root); + if (result) { + return result; + } + } + } + return null; + } else { + return find(root); + } + } + + /** + * Finds a shape by its unique ID. + * + * @param id - The unique ID of the shape to find + * @returns The shape with the matching ID, or null if not found + */ + public static findShapeById(id: string): Shape | null { + return this.findShape((shape) => shape.id === id); + } + + public static findPage(predicate: (page: Page) => boolean): Page | null { + let page = penpot.currentFile!.pages.find(predicate); + return page || null; + } + + public static getPages(): { id: string; name: string }[] { + return penpot.currentFile!.pages.map((page) => ({ id: page.id, name: page.name })); + } + + public static getPageById(id: string): Page | null { + return this.findPage((page) => page.id === id); + } + + public static getPageByName(name: string): Page | null { + return this.findPage((page) => page.name.toLowerCase() === name.toLowerCase()); + } + + public static getPageForShape(shape: Shape): Page | null { + for (const page of penpot.currentFile!.pages) { + if (page.getShapeById(shape.id)) { + return page; + } + } + return null; + } + + public static generateCss(shape: Shape): string { + const page = this.getPageForShape(shape); + if (!page) { + throw new Error("Shape is not part of any page"); + } + penpot.openPage(page); + return penpot.generateStyle([shape], { type: "css", includeChildren: true }); + } + + /** + * 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. + * + * @param child - The child shape to check + * @param parent - The parent shape to check against + * @returns true if child is fully contained within parent bounds, false otherwise + */ + public static isContainedIn(child: Shape, parent: Shape): boolean { + 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 + ); + } + + /** + * Sets the position of a shape relative to its parent's position. + * This is a convenience method since parentX and parentY are read-only properties. + * + * @param shape - The shape to position + * @param parentX - The desired X position relative to the parent + * @param parentY - The desired Y position relative to the parent + * @throws Error if the shape has no parent + */ + public static setParentXY(shape: Shape, parentX: number, parentY: number): void { + if (!shape.parent) { + throw new Error("Shape has no parent - cannot set parent-relative position"); + } + shape.x = shape.parent.x + parentX; + shape.y = shape.parent.y + parentY; + } + + /** + * Adds a flex layout to a container while preserving the visual order of existing children. + * Without this, adding a flex layout can arbitrarily reorder children. + * + * The method sorts children by their current position (x for "row", y for "column") before + * adding the layout, then reorders them to maintain that visual sequence. + * + * @param container - The container (board) to add the flex layout to + * @param dir - The layout direction: "row" for horizontal, "column" for vertical + * @returns The created FlexLayout instance + */ + public static addFlexLayout(container: Board, dir: "column" | "row"): FlexLayout { + // obtain children sorted by position (ascending) + const children = "children" in container && container.children ? [...container.children] : []; + const sortedChildren = children.sort((a, b) => (dir === "row" ? a.x - b.x : a.y - b.y)); + + // add the flex layout + const flexLayout = container.addFlexLayout(); + flexLayout.dir = dir; + + // reorder children to preserve visual order; since the children array is reversed + // relative to visual order for dir="column" or dir="row", we insert each child at + // index 0 in sorted order, which places the first (smallest position) at the highest + // index, making it appear first visually + for (const child of sortedChildren) { + child.setParentIndex(0); + } + + return flexLayout; + } + + /** + * Analyzes all descendants of a shape by applying an evaluator function to each. + * Only descendants for which the evaluator returns a non-null/non-undefined value are included in the result. + * This is a general-purpose utility for validation, analysis, or collecting corrector functions. + * + * @param root - The root shape whose descendants to analyze + * @param evaluator - Function called for each descendant with (root, descendant); return null/undefined to skip + * @param maxDepth - Optional maximum depth to traverse (undefined for unlimited) + * @returns Array of objects containing the shape and the evaluator's result + */ + public static analyzeDescendants( + root: Shape, + evaluator: (root: Shape, descendant: Shape) => T | null | undefined, + maxDepth: number | undefined = undefined + ): Array<{ shape: Shape; result: NonNullable }> { + const results: Array<{ shape: Shape; result: NonNullable }> = []; + + const traverse = (shape: Shape, currentDepth: number): void => { + const result = evaluator(root, shape); + if (result !== null && result !== undefined) { + results.push({ shape, result: result as NonNullable }); + } + + if (maxDepth === undefined || currentDepth < maxDepth) { + if ("children" in shape && shape.children) { + for (const child of shape.children) { + traverse(child, currentDepth + 1); + } + } + } + }; + + // Start traversal with root's children (not root itself) + if ("children" in root && root.children) { + for (const child of root.children) { + traverse(child, 1); + } + } + + return results; + } + + /** + * 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; + } + + 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; + } + + /** + * Imports an image from base64 data into the Penpot design as a Rectangle shape filled with the image. + * The rectangle has the image's original proportions by default. + * Optionally accepts position (x, y) and dimensions (width, height) parameters. + * If only one dimension is provided, the other is calculated to maintain the image's aspect ratio. + * + * This function is used internally by the ImportImageTool in the MCP server. + * + * @param base64 - The base64-encoded image data + * @param mimeType - The MIME type of the image (e.g., "image/png") + * @param name - The name to assign to the newly created rectangle shape + * @param x - The x-coordinate for positioning the rectangle (optional) + * @param y - The y-coordinate for positioning the rectangle (optional) + * @param width - The desired width of the rectangle (optional) + * @param height - The desired height of the rectangle (optional) + */ + public static async importImage( + base64: string, + mimeType: string, + name: string, + x: number | undefined, + y: number | undefined, + width: number | undefined, + height: number | undefined + ): Promise { + // convert base64 to Uint8Array + const bytes = PenpotUtils.atob(base64); + + // upload the image data to Penpot + const imageData = await penpot.uploadMediaData(name, bytes, mimeType); + + // create a rectangle shape + const rect = penpot.createRectangle(); + rect.name = name; + + // calculate dimensions + let rectWidth, rectHeight; + const hasWidth = width !== undefined; + const hasHeight = height !== undefined; + + if (hasWidth && hasHeight) { + // both width and height provided - use them directly + rectWidth = width; + rectHeight = height; + } else if (hasWidth) { + // only width provided - maintain aspect ratio + rectWidth = width; + rectHeight = rectWidth * (imageData.height / imageData.width); + } else if (hasHeight) { + // only height provided - maintain aspect ratio + rectHeight = height; + rectWidth = rectHeight * (imageData.width / imageData.height); + } else { + // neither provided - use original dimensions + rectWidth = imageData.width; + rectHeight = imageData.height; + } + + // set rectangle dimensions + rect.resize(rectWidth, rectHeight); + + // set position if provided + if (x !== undefined) { + rect.x = x; + } + if (y !== undefined) { + rect.y = y; + } + + // apply the image as a fill + rect.fills = [{ fillOpacity: 1, fillImage: imageData }]; + + return rect; + } + + /** + * Exports the given shape (or its fill) to BASE64 image data. + * + * This function is used internally by the ExportImageTool in the MCP server. + * + * @param shape - The shape whose image data to export + * @param mode - Either "shape" (to export the entire shape, including descendants) or "fill" + * to export the shape's raw fill image data + * @param asSVG - Whether to export as SVG rather than as a pixel image (only supported for mode "shape") + * @returns A byte array containing the exported image data. + * - For mode="shape", it will be PNG or SVG data depending on the value of `asSVG`. + * - For mode="fill", it will be whatever format the fill image is stored in. + */ + public static async exportImage(shape: Shape, mode: "shape" | "fill", asSVG: boolean): Promise { + switch (mode) { + case "shape": + return shape.export({ type: asSVG ? "svg" : "png" }); + case "fill": + if (asSVG) { + throw new Error("Image fills cannot be exported as SVG"); + } + // check whether the shape has the `fills` member + if (!("fills" in shape)) { + throw new Error("Shape with `fills` member is required for fill export mode"); + } + // find first fill that has fillImage + const fills: Fill[] = (shape as any).fills; + for (const fill of fills) { + if (fill.fillImage) { + const imageData = fill.fillImage; + return imageData.data(); + } + } + throw new Error("No fill with image data found in the shape"); + default: + throw new Error(`Unsupported export mode: ${mode}`); + } + } +} diff --git a/mcp/packages/plugin/src/TaskHandler.ts b/mcp/packages/plugin/src/TaskHandler.ts new file mode 100644 index 0000000000..b09a6c0c17 --- /dev/null +++ b/mcp/packages/plugin/src/TaskHandler.ts @@ -0,0 +1,77 @@ +/** + * Represents a task received from the MCP server in the Penpot MCP plugin + */ +export class Task { + public isResponseSent: boolean = false; + + /** + * @param requestId Unique identifier for the task request + * @param taskType The type of the task to execute + * @param params Task parameters/arguments + */ + constructor( + public requestId: string, + public taskType: string, + public params: TParams + ) {} + + /** + * Sends a task response back to the MCP server. + */ + protected sendResponse(success: boolean, data: any = undefined, error: any = undefined): void { + if (this.isResponseSent) { + console.error("Response already sent for task:", this.requestId); + return; + } + + const response = { + type: "task-response", + response: { + id: this.requestId, + success: success, + data: data, + error: error, + }, + }; + + // Send to main.ts which will forward to MCP server via WebSocket + penpot.ui.sendMessage(response); + console.log("Sent task response:", response); + this.isResponseSent = true; + } + + public sendSuccess(data: any = undefined): void { + this.sendResponse(true, data); + } + + public sendError(error: string): void { + this.sendResponse(false, undefined, error); + } +} + +/** + * Abstract base class for task handlers in the Penpot MCP plugin. + * + * @template TParams - The type of parameters this handler expects + */ +export abstract class TaskHandler { + /** The task type this handler is responsible for */ + abstract readonly taskType: string; + + /** + * Checks if this handler can process the given task. + * + * @param task - The task identifier to check + * @returns True if this handler applies to the given task + */ + isApplicableTo(task: Task): boolean { + return this.taskType === task.taskType; + } + + /** + * Handles the task with the provided parameters. + * + * @param task - The task to be handled + */ + abstract handle(task: Task): Promise; +} diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts new file mode 100644 index 0000000000..18877d35dd --- /dev/null +++ b/mcp/packages/plugin/src/main.ts @@ -0,0 +1,110 @@ +import "./style.css"; + +// get the current theme from the URL +const searchParams = new URLSearchParams(window.location.search); +document.body.dataset.theme = searchParams.get("theme") ?? "light"; + +// Determine whether multi-user mode is enabled based on URL parameters +const isMultiUserMode = searchParams.get("multiUser") === "true"; +console.log("Penpot MCP multi-user mode:", isMultiUserMode); + +// WebSocket connection management +let ws: WebSocket | null = null; +const statusElement = document.getElementById("connection-status"); + +/** + * Updates the connection status display element. + * + * @param status - the base status text to display + * @param isConnectedState - whether the connection is in a connected state (affects color) + * @param message - optional additional message to append to the status + */ +function updateConnectionStatus(status: string, isConnectedState: boolean, message?: string): void { + if (statusElement) { + const displayText = message ? `${status}: ${message}` : status; + statusElement.textContent = displayText; + statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)"; + } +} + +/** + * Sends a task response back to the MCP server via WebSocket. + * + * @param response - The response containing task ID and result + */ +function sendTaskResponse(response: any): void { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(response)); + console.log("Sent response to MCP server:", response); + } else { + console.error("WebSocket not connected, cannot send response"); + } +} + +/** + * Establishes a WebSocket connection to the MCP server. + */ +function connectToMcpServer(): void { + if (ws?.readyState === WebSocket.OPEN) { + updateConnectionStatus("Already connected", true); + return; + } + + try { + let wsUrl = PENPOT_MCP_WEBSOCKET_URL; + if (isMultiUserMode) { + // TODO obtain proper userToken from penpot + const userToken = "dummyToken"; + wsUrl += `?userToken=${encodeURIComponent(userToken)}`; + } + ws = new WebSocket(wsUrl); + updateConnectionStatus("Connecting...", false); + + ws.onopen = () => { + console.log("Connected to MCP server"); + updateConnectionStatus("Connected to MCP server", true); + }; + + ws.onmessage = (event) => { + console.log("Received from MCP server:", event.data); + try { + const request = JSON.parse(event.data); + // Forward the task request to the plugin for execution + parent.postMessage(request, "*"); + } catch (error) { + console.error("Failed to parse WebSocket message:", error); + } + }; + + ws.onclose = (event: CloseEvent) => { + console.log("Disconnected from MCP server"); + const message = event.reason || undefined; + updateConnectionStatus("Disconnected", false, message); + ws = null; + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + // note: WebSocket error events typically don't contain detailed error messages + updateConnectionStatus("Connection error", false); + }; + } catch (error) { + console.error("Failed to connect to MCP server:", error); + const message = error instanceof Error ? error.message : undefined; + updateConnectionStatus("Connection failed", false, message); + } +} + +document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => { + connectToMcpServer(); +}); + +// Listen plugin.ts messages +window.addEventListener("message", (event) => { + if (event.data.source === "penpot") { + document.body.dataset.theme = event.data.theme; + } else if (event.data.type === "task-response") { + // Forward task response back to MCP server + sendTaskResponse(event.data.response); + } +}); diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts new file mode 100644 index 0000000000..e6a1fad33e --- /dev/null +++ b/mcp/packages/plugin/src/plugin.ts @@ -0,0 +1,69 @@ +import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler"; +import { Task, TaskHandler } from "./TaskHandler"; + +/** + * Registry of all available task handlers. + */ +const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()]; + +// Determine whether multi-user mode is enabled based on build-time configuration +declare const IS_MULTI_USER_MODE: boolean; +const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false; + +// Open the plugin UI (main.ts) +penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { width: 158, height: 200 }); + +// Handle messages +penpot.ui.onMessage((message) => { + // Handle plugin task requests + if (typeof message === "object" && message.task && message.id) { + handlePluginTaskRequest(message).catch((error) => { + console.error("Error in handlePluginTaskRequest:", error); + }); + } +}); + +/** + * Handles plugin task requests received from the MCP server via WebSocket. + * + * @param request - The task request containing ID, task type and parameters + */ +async function handlePluginTaskRequest(request: { id: string; task: string; params: any }): Promise { + console.log("Executing plugin task:", request.task, request.params); + const task = new Task(request.id, request.task, request.params); + + // Find the appropriate handler + const handler = taskHandlers.find((h) => h.isApplicableTo(task)); + + if (handler) { + try { + // Cast the params to the expected type and handle the task + console.log("Processing task with handler:", handler); + await handler.handle(task); + + // check whether a response was sent and send a generic success if not + if (!task.isResponseSent) { + console.warn("Handler did not send a response, sending generic success."); + task.sendSuccess("Task completed without a specific response."); + } + + console.log("Task handled successfully:", task); + } catch (error) { + console.error("Error handling task:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + task.sendError(`Error handling task: ${errorMessage}`); + } + } else { + console.error("Unknown plugin task:", request.task); + task.sendError(`Unknown task type: ${request.task}`); + } +} + +// Handle theme change in the iframe +penpot.on("themechange", (theme) => { + penpot.ui.sendMessage({ + source: "penpot", + type: "themechange", + theme, + }); +}); diff --git a/mcp/packages/plugin/src/style.css b/mcp/packages/plugin/src/style.css new file mode 100644 index 0000000000..030f2204e9 --- /dev/null +++ b/mcp/packages/plugin/src/style.css @@ -0,0 +1,10 @@ +@import "@penpot/plugin-styles/styles.css"; + +body { + line-height: 1.5; + padding: 10px; +} + +p { + margin-block-end: 0.75rem; +} diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts new file mode 100644 index 0000000000..86fc62710f --- /dev/null +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -0,0 +1,212 @@ +import { Task, TaskHandler } from "../TaskHandler"; +import { ExecuteCodeTaskParams, ExecuteCodeTaskResultData } from "../../../common/src"; +import { PenpotUtils } from "../PenpotUtils.ts"; + +/** + * Console implementation that captures all log output for code execution. + * + * Provides the same interface as the native console object but appends + * all output to an internal log string that can be retrieved. + */ +class ExecuteCodeTaskConsole { + /** + * Accumulated log output from all console method calls. + */ + private logOutput: string = ""; + + /** + * Resets the accumulated log output to empty string. + * Should be called before each code execution to start with clean logs. + */ + resetLog(): void { + this.logOutput = ""; + } + + /** + * Gets the accumulated log output from all console method calls. + * @returns The complete log output as a string + */ + getLog(): string { + return this.logOutput; + } + + /** + * Appends a formatted message to the log output. + * @param level - Log level prefix (e.g., "LOG", "WARN", "ERROR") + * @param args - Arguments to log, will be stringified and joined + */ + private appendToLog(level: string, ...args: any[]): void { + const message = args + .map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg))) + .join(" "); + this.logOutput += `[${level}] ${message}\n`; + } + + /** + * Logs a message to the captured output. + */ + log(...args: any[]): void { + this.appendToLog("LOG", ...args); + } + + /** + * Logs a warning message to the captured output. + */ + warn(...args: any[]): void { + this.appendToLog("WARN", ...args); + } + + /** + * Logs an error message to the captured output. + */ + error(...args: any[]): void { + this.appendToLog("ERROR", ...args); + } + + /** + * Logs an informational message to the captured output. + */ + info(...args: any[]): void { + this.appendToLog("INFO", ...args); + } + + /** + * Logs a debug message to the captured output. + */ + debug(...args: any[]): void { + this.appendToLog("DEBUG", ...args); + } + + /** + * Logs a message with trace information to the captured output. + */ + trace(...args: any[]): void { + this.appendToLog("TRACE", ...args); + } + + /** + * Logs a table to the captured output (simplified as JSON). + */ + table(data: any): void { + this.appendToLog("TABLE", data); + } + + /** + * Starts a timer (simplified implementation that just logs). + */ + time(label?: string): void { + this.appendToLog("TIME", `Timer started: ${label || "default"}`); + } + + /** + * Ends a timer (simplified implementation that just logs). + */ + timeEnd(label?: string): void { + this.appendToLog("TIME_END", `Timer ended: ${label || "default"}`); + } + + /** + * Logs messages in a group (simplified to just log the label). + */ + group(label?: string): void { + this.appendToLog("GROUP", label || ""); + } + + /** + * Logs messages in a collapsed group (simplified to just log the label). + */ + groupCollapsed(label?: string): void { + this.appendToLog("GROUP_COLLAPSED", label || ""); + } + + /** + * Ends the current group (simplified implementation). + */ + groupEnd(): void { + this.appendToLog("GROUP_END", ""); + } + + /** + * Clears the console (no-op in this implementation since we want to capture logs). + */ + clear(): void { + // intentionally empty - we don't want to clear captured logs + } + + /** + * Counts occurrences of calls with the same label (simplified implementation). + */ + count(label?: string): void { + this.appendToLog("COUNT", label || "default"); + } + + /** + * Resets the count for a label (simplified implementation). + */ + countReset(label?: string): void { + this.appendToLog("COUNT_RESET", label || "default"); + } + + /** + * Logs an assertion (simplified to just log if condition is false). + */ + assert(condition: boolean, ...args: any[]): void { + if (!condition) { + this.appendToLog("ASSERT", ...args); + } + } +} + +/** + * Task handler for executing JavaScript code in the plugin context. + * + * Maintains a persistent context object that preserves state between code executions + * and captures all console output during execution. + */ +export class ExecuteCodeTaskHandler extends TaskHandler { + readonly taskType = "executeCode"; + + /** + * Persistent context object that maintains state between code executions. + * Contains the penpot API, storage object, and custom console implementation. + */ + private readonly context: any; + + constructor() { + super(); + + // initialize context, making penpot, penpotUtils, storage and the custom console available + this.context = { + penpot: penpot, + storage: {}, + console: new ExecuteCodeTaskConsole(), + penpotUtils: PenpotUtils, + }; + } + + async handle(task: Task): Promise { + if (!task.params.code) { + task.sendError("executeCode task requires 'code' parameter"); + return; + } + + this.context.console.resetLog(); + + 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); + + console.log("Code execution result:", result); + + // return result and captured log + let resultData: ExecuteCodeTaskResultData = { + result: result, + log: this.context.console.getLog(), + }; + task.sendSuccess(resultData); + } +} diff --git a/mcp/packages/plugin/src/vite-env.d.ts b/mcp/packages/plugin/src/vite-env.d.ts new file mode 100644 index 0000000000..ddbf746e04 --- /dev/null +++ b/mcp/packages/plugin/src/vite-env.d.ts @@ -0,0 +1,4 @@ +/// + +declare const IS_MULTI_USER_MODE: boolean; +declare const PENPOT_MCP_WEBSOCKET_URL: string; diff --git a/mcp/packages/plugin/tsconfig.json b/mcp/packages/plugin/tsconfig.json new file mode 100644 index 0000000000..0db6d8a2c6 --- /dev/null +++ b/mcp/packages/plugin/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "typeRoots": ["./node_modules/@types", "./node_modules/@penpot"], + "types": ["plugin-types"], + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/mcp/packages/plugin/vite.config.ts b/mcp/packages/plugin/vite.config.ts new file mode 100644 index 0000000000..82613250ff --- /dev/null +++ b/mcp/packages/plugin/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from "vite"; +import livePreview from "vite-live-preview"; + +// Debug: Log the environment variables +console.log("MULTI_USER_MODE env:", process.env.MULTI_USER_MODE); +console.log("Will define IS_MULTI_USER_MODE as:", JSON.stringify(process.env.MULTI_USER_MODE === "true")); + +let WS_URI = "http://localhost:4402"; +console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI)); + +export default defineConfig({ + plugins: [ + livePreview({ + reload: true, + config: { + build: { + sourcemap: true, + }, + }, + }), + ], + build: { + rollupOptions: { + input: { + plugin: "src/plugin.ts", + index: "./index.html", + }, + output: { + entryFileNames: "[name].js", + }, + }, + }, + preview: { + host: "0.0.0.0", + port: 4400, + cors: true, + allowedHosts: [], + }, + define: { + IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"), + PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(WS_URI), + }, +}); diff --git a/mcp/packages/plugin/vite.release.config.ts b/mcp/packages/plugin/vite.release.config.ts new file mode 100644 index 0000000000..462fc9f598 --- /dev/null +++ b/mcp/packages/plugin/vite.release.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, mergeConfig } from "vite"; +import baseConfig from "./vite.config"; + +export default mergeConfig( + baseConfig, + defineConfig({ + base: "./", + plugins: [], + }) +); diff --git a/mcp/packages/server/.gitignore b/mcp/packages/server/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mcp/packages/server/README.md b/mcp/packages/server/README.md new file mode 100644 index 0000000000..78be5e1219 --- /dev/null +++ b/mcp/packages/server/README.md @@ -0,0 +1,24 @@ +# Penpot MCP Server + +A Model Context Protocol (MCP) server that provides Penpot integration +capabilities for AI clients supporting the model context protocol (MCP). + +## Setup + +1. Install Dependencies + + pnpm install + +2. Build the Project + + pnpm run build + +3. Run the Server + + pnpm run start + + +## Penpot Plugin API REPL + +The MCP server includes a REPL interface for testing Penpot Plugin API calls. +To use it, connect to the URL reported at startup. diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml new file mode 100644 index 0000000000..d7094c06b0 --- /dev/null +++ b/mcp/packages/server/data/api_types.yml @@ -0,0 +1,18268 @@ +Penpot: + overview: |- + Interface Penpot + ================ + + These are methods and properties available on the `penpot` global object. + + ``` + interface Penpot { + ui: { + open: ((name: string, url: string, options?: { + width: number; + height: number; + }) => void); + size: null | { + width: number; + height: number; + }; + resize: ((width: number, height: number) => void); + sendMessage: ((message: unknown) => void); + onMessage: ((callback: ((message: T) => void)) => void); + }; + utils: ContextUtils; + closePlugin: (() => void); + on(type: T, callback: ((event: EventsMap[T]) => void), props?: { + [key: string]: unknown; + }): symbol; + off(listenerId: symbol): void; + root: null | Shape; + currentFile: null | File; + currentPage: null | Page; + viewport: Viewport; + history: HistoryContext; + library: LibraryContext; + fonts: FontsContext; + currentUser: User; + activeUsers: ActiveUser[]; + theme: Theme; + localStorage: LocalStorage; + selection: Shape[]; + shapesColors(shapes: Shape[]): (Color & ColorShapeInfo)[]; + replaceColor(shapes: Shape[], oldColor: Color, newColor: Color): void; + uploadMediaUrl(name: string, url: string): Promise; + uploadMediaData(name: string, data: Uint8Array, mimeType: string): Promise; + group(shapes: Shape[]): null | Group; + ungroup(group: Group, ...other: Group[]): void; + createRectangle(): Rectangle; + createBoard(): Board; + createEllipse(): Ellipse; + createPath(): Path; + createBoolean(boolType: BooleanType, shapes: Shape[]): null | Boolean; + createShapeFromSvg(svgString: string): null | Group; + createShapeFromSvgWithImages(svgString: string): Promise; + createText(text: string): null | Text; + generateMarkup(shapes: Shape[], options?: { + type?: "html" | "svg"; + }): string; + generateStyle(shapes: Shape[], options?: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + }): string; + generateFontFaces(shapes: Shape[]): Promise; + openViewer(): void; + createPage(): Page; + openPage(page: Page, newWindow?: boolean): void; + alignHorizontal(shapes: Shape[], direction: "center" | "left" | "right"): void; + alignVertical(shapes: Shape[], direction: "center" | "top" | "bottom"): void; + distributeHorizontal(shapes: Shape[]): void; + distributeVertical(shapes: Shape[]): void; + flatten(shapes: Shape[]): Path[]; + } + ``` + + Hierarchy + + * Omit + + Penpot + members: + Properties: + ui: |- + ``` + ui: { + open: ((name: string, url: string, options?: { + width: number; + height: number; + }) => void); + size: null | { + width: number; + height: number; + }; + resize: ((width: number, height: number) => void); + sendMessage: ((message: unknown) => void); + onMessage: ((callback: ((message: T) => void)) => void); + } + ``` + + Type declaration + + * open: ((name: string, url: string, options?: { width: number; height: number; }) => 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. + + Example + ``` + penpot.ui.open('Plugin name', 'url', {width: 150, height: 300}); + ``` + + + ``` + (name, url, options?): void + ``` + + - Parameters + * name: string + + title of the plugin, it'll be displayed on the top of the modal + * url: string + + of the plugin + * options: { + width: number; + height: number; + } + + height and width of the modal. + + + width: number + + height: number + + Returns void + * size: null | { width: number; height: number; } + * resize: ((width: number, height: number) => void) + + Resizes the plugin UI. + + Example + ``` + penpot.ui.resize(300, 400); + ``` + + + ``` + (width, height): void + ``` + + - Parameters + * width: number + + The width of the modal. + * height: number + + The height of the modal. + + Returns void + * sendMessage: ((message: unknown) => void) + + Sends a message to the plugin UI. + + Example + ``` + this.sendMessage({ type: 'example-type', content: 'data we want to share' }); + ``` + + + ``` + (message): void + ``` + + - Parameters + * message: unknown + + content usually is an object + + Returns void + * onMessage: ((callback: ((message: T) => void)) => void) + + This is usually used in the `plugin.ts` file in order to handle the data sent by our plugin + + Example + ``` + penpot.ui.onMessage((message) => {if(message.type === 'example-type' { ...do something })}); + ``` + + + ``` + (callback): void + ``` + + - Type Parameters + * T + + Parameters + * callback: ((message: T) => void) + + A function that will be called whenever a message is received. + The function receives a single argument, `message`, which is of type `T`. + + + ``` + (message): void + ``` + + - Parameters + * message: T + + Returns void + + Returns void + utils: |- + ``` + utils: ContextUtils + ``` + + Provides access to utility functions and context-specific operations. + closePlugin: |- + ``` + closePlugin: (() => void) + ``` + + Closes the plugin. When this method is called the UI will be closed. + + Example + ``` + penpot.closePlugin(); + ``` + root: |- + ``` + readonly root: null | Shape + ``` + + The root shape in the current Penpot context. Requires `content:read` permission. + + Example + ``` + const rootShape = context.root;console.log(rootShape); + ``` + currentFile: |- + ``` + readonly currentFile: null | File + ``` + + Retrieves file data from the current Penpot context. Requires `content:read` permission. + + Returns + + Returns the file data or `null` if no file is available. + + Example + ``` + const fileData = context.currentFile;console.log(fileData); + ``` + currentPage: |- + ``` + readonly currentPage: null | Page + ``` + + The current page in the Penpot context. Requires `content:read` permission. + + Example + ``` + const currentPage = context.currentPage;console.log(currentPage); + ``` + viewport: |- + ``` + readonly viewport: Viewport + ``` + + The viewport settings in the Penpot context. + + Example + ``` + const viewportSettings = context.viewport;console.log(viewportSettings); + ``` + history: |- + ``` + readonly history: HistoryContext + ``` + + Context encapsulating the history operations + + Example + ``` + const historyContext = context.history;console.log(historyContext); + ``` + library: |- + ``` + readonly library: LibraryContext + ``` + + The library context in the Penpot context, including both local and connected libraries. Requires `library:read` permission. + + Example + ``` + const libraryContext = context.library;console.log(libraryContext); + ``` + fonts: |- + ``` + readonly fonts: FontsContext + ``` + + The fonts context in the Penpot context, providing methods to manage fonts. Requires `content:read` permission. + + Example + ``` + const fontsContext = context.fonts;console.log(fontsContext); + ``` + currentUser: |- + ``` + readonly currentUser: User + ``` + + The current user in the Penpot context. Requires `user:read` permission. + + Example + ``` + const currentUser = context.currentUser;console.log(currentUser); + ``` + activeUsers: |- + ``` + readonly activeUsers: ActiveUser[] + ``` + + An array of active users in the Penpot context. Requires `user:read` permission. + + Example + ``` + const activeUsers = context.activeUsers;console.log(activeUsers); + ``` + theme: |- + ``` + readonly theme: Theme + ``` + + The current theme (light or dark) in Penpot. + + Example + ``` + const currentTheme = context.theme;console.log(currentTheme); + ``` + localStorage: |- + ``` + readonly localStorage: LocalStorage + ``` + + Access to the localStorage proxy + selection: |- + ``` + selection: Shape[] + ``` + + The currently selected shapes in Penpot. Requires `content:read` permission. + + Example + ``` + const selectedShapes = context.selection;console.log(selectedShapes); + ``` + Methods: + on: |- + ``` + on(type, callback, props?): symbol + ``` + + * Adds an event listener for the specified event type. + Subscribing to events requires `content:read` permission. + + The following are the possible event types: + + + pagechange: event emitted when the current page changes. The callback will receive the new page. + + shapechange: event emitted when the shape changes. This event requires to send inside the `props` object the shape + that will be observed. For example: + ``` + // Observe the current selected shapepenpot.on('shapechange', (shape) => console.log(shape.name), { shapeId: penpot.selection[0].id }); + ``` + + + selectionchange: event emitted when the current selection changes. The callback will receive the list of ids for the new selection + + themechange: event emitted when the user changes its theme. The callback will receive the new theme (currently: either `dark` or `light`) + + documentsaved: event emitted after the document is saved in the backend. + + Type Parameters + + T extends keyof EventsMap + + Parameters + + type: T + + The event type to listen for. + + callback: ((event: EventsMap[T]) => void) + + The callback function to execute when the event is triggered. + + - ``` + (event): void + ``` + + * Parameters + + event: EventsMap[T] + + Returns void + + props: { + [key: string]: unknown; + } + + The properties for the current event handler. Only makes sense for specific events. + + - [key: string]: unknown + + Returns symbol + + the listener id that can be used to call `off` and cancel the listener + + Example + ``` + penpot.on('pagechange', () => {...do something}). + ``` + off: |- + ``` + off(listenerId): void + ``` + + * Removes an event listener for the specified event type. + + Parameters + + listenerId: symbol + + the id returned by the `on` method when the callback was set + + Returns void + + Example + ``` + const listenerId = penpot.on('contentsave', () => console.log("Changed"));penpot.off(listenerId); + ``` + shapesColors: |- + ``` + shapesColors(shapes): (Color & ColorShapeInfo)[] + ``` + + * Retrieves colors applied to the given shapes in Penpot. Requires `content:read` permission. + + Parameters + + shapes: Shape[] + + Returns (Color & ColorShapeInfo)[] + + Returns an array of colors and their shape information. + + Example + ``` + const colors = context.shapesColors(shapes);console.log(colors); + ``` + replaceColor: |- + ``` + replaceColor(shapes, oldColor, newColor): void + ``` + + * Replaces a specified old color with a new color in the given shapes. Requires `content:write` permission. + + Parameters + + shapes: Shape[] + + oldColor: Color + + newColor: Color + + Returns void + + Example + ``` + context.replaceColor(shapes, oldColor, newColor); + ``` + uploadMediaUrl: |- + ``` + uploadMediaUrl(name, url): Promise + ``` + + * Uploads media to Penpot and retrieves its image data. Requires `content:write` permission. + + Parameters + + name: string + + The name of the media. + + url: string + + The URL of the media to be uploaded. + + Returns Promise + + Returns a promise that resolves to the image data of the uploaded media. + + Example + ``` + const imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');console.log(imageData);// to insert the image in a shape we can doconst board = penpot.createBoard();const shape = penpot.createRectangle();board.appendChild(shape);shape.fills = [{ fillOpacity: 1, fillImage: imageData }]; + ``` + uploadMediaData: |- + ``` + uploadMediaData(name, data, mimeType): Promise + ``` + + * Uploads media to penpot and retrieves the image data. Requires `content:write` permission. + + Parameters + + name: string + + The name of the media. + + data: Uint8Array + + The image content data + + mimeType: string + + Returns Promise + + Returns a promise that resolves to the image data of the uploaded media. + + Example + ``` + const imageData = await context.uploadMediaData('example', imageData, 'image/jpeg');console.log(imageData); + ``` + group: |- + ``` + group(shapes): null | Group + ``` + + * Groups the specified shapes. Requires `content:write` permission. + + Parameters + + shapes: Shape[] + + An array of shapes to group. + + Returns null | Group + + Returns the newly created group or `null` if the group could not be created. + + Example + ``` + const penpotShapesArray = penpot.selection;penpot.group(penpotShapesArray); + ``` + ungroup: |- + ``` + ungroup(group, ...other): void + ``` + + * Ungroups the specified group. Requires `content:write` permission. + + Parameters + + group: Group + + The group to ungroup. + + ...other: Group[] + + Additional groups to ungroup. + + Returns void + + Example + ``` + const penpotShapesArray = penpot.selection;// We need to make sure that something is selected, and if the selected shape is a group,if (selected.length && penpot.utils.types.isGroup(penpotShapesArray[0])) { penpot.group(penpotShapesArray[0]);} + ``` + createRectangle: |- + ``` + createRectangle(): Rectangle + ``` + + * Use this method to create the shape of a rectangle. Requires `content:write` permission. + + Returns Rectangle + + Example + ``` + const shape = penpot.createRectangle();// just change the values like thisshape.name = "Example rectangle";// for solid colorshape.fills = [{ fillColor: "#7EFFF5" }];// for linear gradient colorshape.fills = [{ fillColorGradient: { "type": "linear", "startX": 0.5, "startY": 0, "endX": 0.5, "endY": 1, "width": 1, "stops": [ { "color": "#003ae9", "opacity": 1, "offset": 0 }, { "color": "#003ae9", "opacity": 0, "offset": 1 } ] }}];// for a image fillconst imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');shape.fills = [{ fillOpacity: 1, fillImage: imageData }];shape.borderRadius = 8;shape.strokes = [ { strokeColor: "#2e3434", strokeStyle: "solid", strokeWidth: 2, strokeAlignment: "center", },]; + ``` + createBoard: |- + ``` + createBoard(): Board + ``` + + * Use this method to create a board. This is the first step before anything else, the container. Requires `content:write` permission. + Then you can add a gridlayout, flexlayout or add a shape inside the board. + Just a heads-up: board is a board in Penpot UI. + + Returns Board + + Example + ``` + const board = penpot.createBoard();// to add grid layoutboard.addGridLayout();// to add flex layoutboard.addFlexLayout();// to create a shape inside the boardconst shape = penpot.createRectangle();board.appendChild(shape); + ``` + createEllipse: |- + ``` + createEllipse(): Ellipse + ``` + + * Use this method to create the shape of an ellipse. Requires `content:write` permission. + + Returns Ellipse + + Example + ``` + const shape = penpot.createEllipse();// just change the values like thisshape.name = "Example ellipse";// for solid colorshape.fills = [{ fillColor: "#7EFFF5" }];// for linear gradient colorshape.fills = [{ fillColorGradient: { "type": "linear", "startX": 0.5, "startY": 0, "endX": 0.5, "endY": 1, "width": 1, "stops": [ { "color": "#003ae9", "opacity": 1, "offset": 0 }, { "color": "#003ae9", "opacity": 0, "offset": 1 } ] }}];// for an image fillconst imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');shape.fills = [{ fillOpacity: 1, fillImage: imageData }];shape.strokes = [ { strokeColor: "#2e3434", strokeStyle: "solid", strokeWidth: 2, strokeAlignment: "center", },]; + ``` + createPath: |- + ``` + createPath(): Path + ``` + + * Use this method to create a path. Requires `content:write` permission. + + Returns Path + + Example + ``` + const path = penpot.createPath();path.name = "My path";// for solid colorpath.fills = [{ fillColor: "#7EFFF5" }]; + ``` + createBoolean: |- + ``` + createBoolean(boolType, shapes): null | Boolean + ``` + + * Creates a Boolean shape based on the specified boolean operation and shapes. Requires `content:write` permission. + + Parameters + + boolType: BooleanType + + The type of boolean operation ('union', 'difference', 'exclude', 'intersection'). + + shapes: Shape[] + + An array of shapes to perform the boolean operation on. + + Returns null | Boolean + + Returns the newly created Boolean shape resulting from the boolean operation. + + Example + ``` + const booleanShape = context.createBoolean('union', [shape1, shape2]); + ``` + createShapeFromSvg: |- + ``` + createShapeFromSvg(svgString): null | Group + ``` + + * Creates a Group from an SVG string. Requires `content:write` permission. + + Parameters + + svgString: string + + The SVG string representing the shapes to be converted into a group. + + Returns null | Group + + Returns the newly created Group containing the shapes from the SVG. + + Example + ``` + const svgGroup = context.createShapeFromSvg('...'); + ``` + createShapeFromSvgWithImages: |- + ``` + createShapeFromSvgWithImages(svgString): Promise + ``` + + * Creates a Group from an SVG string. The SVG can have images and the method returns + a Promise because the shape will be available after all images are uploaded. + Requires `content:write` permission. + + Parameters + + svgString: string + + The SVG string representing the shapes to be converted into a group. + + Returns Promise + + Returns a promise with the newly created Group containing the shapes from the SVG. + + Example + ``` + const svgGroup = await context.createShapeFromSvgWithImages('...'); + ``` + createText: |- + ``` + createText(text): null | Text + ``` + + * Creates a Text shape with the specified text content. Requires `content:write` permission. + + Parameters + + text: string + + The text content for the Text shape. + + Returns null | Text + + Returns the new created shape, if the shape wasn't created can return null. + + Example + ``` + const board = penpot.createBoard();let text;text = penpot.createText();// just change the values like thistext.growType = 'auto-height';text.fontFamily = 'Work Sans';text.fontSize = '12';text.fills = [{fillColor: '#9f05ff', fillOpacity: 1}];text.strokes = [{strokeOpacity: 1, strokeStyle: 'solid', strokeWidth: 2, strokeColor: '#deabff', strokeAlignment: 'outer'}];board.appendChild(text); + ``` + generateMarkup: |- + ``` + generateMarkup(shapes, options?): string + ``` + + * Generates markup for the given shapes. Requires `content:read` permission + + Parameters + + shapes: Shape[] + + options: { + type?: "html" | "svg"; + } + - Optionaltype?: "html" | "svg" + + Returns string + + Example + ``` + const markup = context.generateMarkup(shapes, { type: 'html' });console.log(markup); + ``` + generateStyle: |- + ``` + generateStyle(shapes, options?): string + ``` + + * Generates styles for the given shapes. Requires `content:read` permission + + Parameters + + shapes: Shape[] + + options: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + } + - Optionaltype?: "css" + - OptionalwithPrelude?: boolean + - OptionalincludeChildren?: boolean + + Returns string + + Example + ``` + const styles = context.generateStyle(shapes, { type: 'css' });console.log(styles); + ``` + generateFontFaces: |- + ``` + generateFontFaces(shapes): Promise + ``` + + * Generates the fontfaces styles necessaries to render the shapes. + Requires `content:read` permission + + Parameters + + shapes: Shape[] + + Returns Promise + + Example + ``` + const fontfaces = context.generateFontFaces(penpot.selection);console.log(fontfaces); + ``` + openViewer: |- + ``` + openViewer(): void + ``` + + * Opens the viewer section. Requires `content:read` permission. + + Returns void + createPage: |- + ``` + createPage(): Page + ``` + + * Creates a new page. Requires `content:write` permission. + + Returns Page + openPage: |- + ``` + openPage(page, newWindow?): void + ``` + + * Changes the current open page to given page. Requires `content:read` permission. + + Parameters + + page: Page + + the page to open + + newWindow: boolean + + if true opens the page in a new window + + Returns void + + Example + ``` + context.openPage(page); + ``` + alignHorizontal: |- + ``` + alignHorizontal(shapes, direction): void + ``` + + * Aligning will move all the selected layers to a position relative to one + of them in the horizontal direction. + + Parameters + + shapes: Shape[] + + to align + + direction: "center" | "left" | "right" + + where the shapes will be aligned + + Returns void + alignVertical: |- + ``` + alignVertical(shapes, direction): void + ``` + + * Aligning will move all the selected layers to a position relative to one + of them in the vertical direction. + + Parameters + + shapes: Shape[] + + to align + + direction: "center" | "top" | "bottom" + + where the shapes will be aligned + + Returns void + distributeHorizontal: |- + ``` + distributeHorizontal(shapes): void + ``` + + * Distributing objects to position them horizontally with equal distances between them. + + Parameters + + shapes: Shape[] + + to distribute + + Returns void + distributeVertical: |- + ``` + distributeVertical(shapes): void + ``` + + * Distributing objects to position them vertically with equal distances between them. + + Parameters + + shapes: Shape[] + + to distribute + + Returns void + flatten: |- + ``` + flatten(shapes): Path[] + ``` + + * Converts the shapes into Paths. If the shapes are complex will put together + all its paths into one. + + Parameters + + shapes: Shape[] + + to flatten + + Returns Path[] +ActiveUser: + overview: |- + Interface ActiveUser + ==================== + + Represents an active user in Penpot, extending the `User` interface. + This interface includes additional properties specific to active users. + + ``` + interface ActiveUser { + position?: { + x: number; + y: number; + }; + zoom?: number; + id: string; + name?: string; + avatarUrl?: string; + color: string; + sessionId?: string; + } + ``` + + Hierarchy (view full) + + * User + + ActiveUser + + Referenced by: Context, Penpot + members: + Properties: + position: |- + ``` + position?: { + x: number; + y: number; + } + ``` + + The position of the active user. + + Example + ``` + const userPosition = activeUser.position;console.log(userPosition); + ``` + zoom: |- + ``` + readonly zoom?: number + ``` + + The zoom level of the active user. + + Example + ``` + const userZoom = activeUser.zoom;console.log(userZoom); + ``` + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the user. + + Example + ``` + const userId = user.id;console.log(userId); + ``` + name: |- + ``` + readonly name?: string + ``` + + The name of the user. + + Example + ``` + const userName = user.name;console.log(userName); + ``` + avatarUrl: |- + ``` + readonly avatarUrl?: string + ``` + + The URL of the user's avatar image. + + Example + ``` + const avatarUrl = user.avatarUrl;console.log(avatarUrl); + ``` + color: |- + ``` + readonly color: string + ``` + + The color associated with the user. + + Example + ``` + const userColor = user.color;console.log(userColor); + ``` + sessionId: |- + ``` + readonly sessionId?: string + ``` + + The session ID of the user. + + Example + ``` + const sessionId = user.sessionId;console.log(sessionId); + ``` +Blur: + overview: |- + Interface Blur + ============== + + Represents blur properties in Penpot. + This interface includes properties for defining the type and intensity of a blur effect, along with its visibility. + + ``` + interface Blur { + id?: string; + type?: "layer-blur"; + value?: number; + hidden?: boolean; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + id: |- + ``` + id?: string + ``` + + The optional unique identifier for the blur effect. + type: |- + ``` + type + ``` + + The optional type of the blur effect. + Currently, only 'layer-blur' is supported. + value: |- + ``` + value?: number + ``` + + The optional intensity value of the blur effect. + hidden: |- + ``` + hidden?: boolean + ``` + + Specifies whether the blur effect is hidden. + Defaults to false if omitted. +Board: + overview: |- + Interface Board + =============== + + Represents a board in Penpot. + This interface extends `ShapeBase` and includes properties and methods specific to board. + + ``` + interface Board { + type: "board"; + clipContent: boolean; + showInViewMode: boolean; + grid?: GridLayout; + flex?: FlexLayout; + guides: Guide[]; + rulerGuides: RulerGuide[]; + horizontalSizing?: "auto" | "fix"; + verticalSizing?: "auto" | "fix"; + fills: Fill[]; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + addFlexLayout(): FlexLayout; + addGridLayout(): GridLayout; + addRulerGuide(orientation: RulerGuideOrientation, value: number): RulerGuide; + removeRulerGuide(guide: RulerGuide): void; + isVariantContainer(): boolean; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Board + - VariantContainer + + Referenced by: CloseOverlay, CommentThread, Context, ContextTypesUtils, Flow, NavigateTo, OpenOverlay, OverlayAction, Page, Penpot, RulerGuide, Shape, ToggleOverlay + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'board' for boards. + clipContent: |- + ``` + clipContent: boolean + ``` + + When true the board will clip the children inside + showInViewMode: |- + ``` + showInViewMode: boolean + ``` + + WHen true the board will be displayed in the view mode + grid: |- + ``` + readonly grid?: GridLayout + ``` + + The grid layout configuration of the board, if applicable. + flex: |- + ``` + readonly flex?: FlexLayout + ``` + + The flex layout configuration of the board, if applicable. + guides: |- + ``` + guides: Guide[] + ``` + + The guides associated with the board. + rulerGuides: |- + ``` + readonly rulerGuides: RulerGuide[] + ``` + + The ruler guides attached to the board + horizontalSizing: |- + ``` + horizontalSizing?: "auto" | "fix" + ``` + + The horizontal sizing behavior of the board. + verticalSizing: |- + ``` + verticalSizing?: "auto" | "fix" + ``` + + The vertical sizing behavior of the board. + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + children: |- + ``` + children: Shape[] + ``` + + The children shapes contained within the board. + When writing into this property, you can only reorder the shapes, not + changing the structure. If the new shapes don't match the current shapes + it will give a validation error. + + Example + ``` + board.children = board.children.reverse(); + ``` + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + appendChild: |- + ``` + appendChild(child): void + ``` + + * Appends a child shape to the board. + + Parameters + + child: Shape + + The child shape to append. + + Returns void + + Example + ``` + board.appendChild(childShape); + ``` + insertChild: |- + ``` + insertChild(index, child): void + ``` + + * Inserts a child shape at the specified index within the board. + + Parameters + + index: number + + The index at which to insert the child shape. + + child: Shape + + The child shape to insert. + + Returns void + + Example + ``` + board.insertChild(0, childShape); + ``` + addFlexLayout: |- + ``` + addFlexLayout(): FlexLayout + ``` + + * Adds a flex layout configuration to the board (so it's necessary to create a board first of all). + + Returns FlexLayout + + Returns the flex layout configuration added to the board. + + Example + ``` + const board = penpot.createBoard();const flex = board.addFlexLayout();// You can change the flex properties as follows.flex.dir = "column";flex.wrap = "wrap";flex.alignItems = "center";flex.justifyContent = "center";flex.horizontalSizing = "fill";flex.verticalSizing = "fill"; + ``` + addGridLayout: |- + ``` + addGridLayout(): GridLayout + ``` + + * Adds a grid layout configuration to the board (so it's necessary to create a board first of all). You can add rows and columns, check addRow/addColumn. + + Returns GridLayout + + Returns the grid layout configuration added to the board. + + Example + ``` + const board = penpot.createBoard();const grid = board.addGridLayout();// You can change the grid properties as follows.grid.alignItems = "center";grid.justifyItems = "start";grid.rowGap = 10;grid.columnGap = 10;grid.verticalPadding = 5;grid.horizontalPadding = 5 + ``` + addRulerGuide: |- + ``` + addRulerGuide(orientation, value): RulerGuide + ``` + + * Creates a new ruler guide. + + Parameters + + orientation: RulerGuideOrientation + + value: number + + Returns RulerGuide + removeRulerGuide: |- + ``` + removeRulerGuide(guide): void + ``` + + * Removes the `guide` from the current page. + + Parameters + + guide: RulerGuide + + Returns void + isVariantContainer: |- + ``` + isVariantContainer(): boolean + ``` + + * Returns boolean + + Returns true when the current board is a VariantContainer + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +VariantContainer: + overview: |- + Interface VariantContainer + ========================== + + Represents a VariantContainer in Penpot + This interface extends `Board` and includes properties and methods specific to VariantContainer. + + ``` + interface VariantContainer { + type: "board"; + clipContent: boolean; + showInViewMode: boolean; + grid?: GridLayout; + flex?: FlexLayout; + guides: Guide[]; + rulerGuides: RulerGuide[]; + horizontalSizing?: "auto" | "fix"; + verticalSizing?: "auto" | "fix"; + fills: Fill[]; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + addFlexLayout(): FlexLayout; + addGridLayout(): GridLayout; + addRulerGuide(orientation: RulerGuideOrientation, value: number): RulerGuide; + removeRulerGuide(guide: RulerGuide): void; + isVariantContainer(): boolean; + variants: null | Variants; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * Board + + VariantContainer + + Referenced by: ContextTypesUtils + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'board' for boards. + clipContent: |- + ``` + clipContent: boolean + ``` + + When true the board will clip the children inside + showInViewMode: |- + ``` + showInViewMode: boolean + ``` + + WHen true the board will be displayed in the view mode + grid: |- + ``` + readonly grid?: GridLayout + ``` + + The grid layout configuration of the board, if applicable. + flex: |- + ``` + readonly flex?: FlexLayout + ``` + + The flex layout configuration of the board, if applicable. + guides: |- + ``` + guides: Guide[] + ``` + + The guides associated with the board. + rulerGuides: |- + ``` + readonly rulerGuides: RulerGuide[] + ``` + + The ruler guides attached to the board + horizontalSizing: |- + ``` + horizontalSizing?: "auto" | "fix" + ``` + + The horizontal sizing behavior of the board. + verticalSizing: |- + ``` + verticalSizing?: "auto" | "fix" + ``` + + The vertical sizing behavior of the board. + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + children: |- + ``` + children: Shape[] + ``` + + The children shapes contained within the board. + When writing into this property, you can only reorder the shapes, not + changing the structure. If the new shapes don't match the current shapes + it will give a validation error. + + Example + ``` + board.children = board.children.reverse(); + ``` + variants: |- + ``` + readonly variants: null | Variants + ``` + + Access to the Variant interface, for attributes and actions over the full Variant (not only this VariantContainer) + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + appendChild: |- + ``` + appendChild(child): void + ``` + + * Appends a child shape to the board. + + Parameters + + child: Shape + + The child shape to append. + + Returns void + + Example + ``` + board.appendChild(childShape); + ``` + insertChild: |- + ``` + insertChild(index, child): void + ``` + + * Inserts a child shape at the specified index within the board. + + Parameters + + index: number + + The index at which to insert the child shape. + + child: Shape + + The child shape to insert. + + Returns void + + Example + ``` + board.insertChild(0, childShape); + ``` + addFlexLayout: |- + ``` + addFlexLayout(): FlexLayout + ``` + + * Adds a flex layout configuration to the board (so it's necessary to create a board first of all). + + Returns FlexLayout + + Returns the flex layout configuration added to the board. + + Example + ``` + const board = penpot.createBoard();const flex = board.addFlexLayout();// You can change the flex properties as follows.flex.dir = "column";flex.wrap = "wrap";flex.alignItems = "center";flex.justifyContent = "center";flex.horizontalSizing = "fill";flex.verticalSizing = "fill"; + ``` + addGridLayout: |- + ``` + addGridLayout(): GridLayout + ``` + + * Adds a grid layout configuration to the board (so it's necessary to create a board first of all). You can add rows and columns, check addRow/addColumn. + + Returns GridLayout + + Returns the grid layout configuration added to the board. + + Example + ``` + const board = penpot.createBoard();const grid = board.addGridLayout();// You can change the grid properties as follows.grid.alignItems = "center";grid.justifyItems = "start";grid.rowGap = 10;grid.columnGap = 10;grid.verticalPadding = 5;grid.horizontalPadding = 5 + ``` + addRulerGuide: |- + ``` + addRulerGuide(orientation, value): RulerGuide + ``` + + * Creates a new ruler guide. + + Parameters + + orientation: RulerGuideOrientation + + value: number + + Returns RulerGuide + removeRulerGuide: |- + ``` + removeRulerGuide(guide): void + ``` + + * Removes the `guide` from the current page. + + Parameters + + guide: RulerGuide + + Returns void + isVariantContainer: |- + ``` + isVariantContainer(): boolean + ``` + + * Returns boolean + + Returns true when the current board is a VariantContainer + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +Boolean: + overview: |- + Interface Boolean + ================= + + Represents a boolean operation shape in Penpot. + This interface extends `ShapeBase` and includes properties and methods specific to boolean operations. + + ``` + interface Boolean { + type: "boolean"; + toD(): string; + content: string; + d: string; + commands: PathCommand[]; + fills: Fill[]; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Boolean + + Referenced by: Context, ContextTypesUtils, Penpot, Shape + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'bool' for boolean operation shapes. + content: |- + ``` + content: string + ``` + + The content of the boolean shape, defined as the path string. + + Deprecated + + Use either `d` or `commands`. + d: |- + ``` + d: string + ``` + + The content of the boolean shape, defined as the path string. + commands: |- + ``` + commands: PathCommand[] + ``` + + The content of the boolean shape, defined as an array of path commands. + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + children: |- + ``` + readonly children: Shape[] + ``` + + The children shapes contained within the boolean shape. + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + toD: |- + ``` + toD(): string + ``` + + * Converts the boolean shape to its path data representation. + + Returns string + + Returns the path data (d attribute) as a string. + + Deprecated + + Use the `d` attribute + appendChild: |- + ``` + appendChild(child): void + ``` + + * Appends a child shape to the boolean shape. + + Parameters + + child: Shape + + The child shape to append. + + Returns void + + Example + ``` + boolShape.appendChild(childShape); + ``` + insertChild: |- + ``` + insertChild(index, child): void + ``` + + * Inserts a child shape at the specified index within the boolean shape. + + Parameters + + index: number + + The index at which to insert the child shape. + + child: Shape + + The child shape to insert. + + Returns void + + Example + ``` + boolShape.insertChild(0, childShape); + ``` + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +CloseOverlay: + overview: |- + Interface CloseOverlay + ====================== + + This action will close a targeted board that is opened as an overlay. + + ``` + interface CloseOverlay { + type: "close-overlay"; + destination?: Board; + animation: Animation; + } + ``` + + Referenced by: Action + members: + Properties: + type: |- + ``` + readonly type + ``` + + The action type + destination: |- + ``` + readonly destination?: Board + ``` + + The overlay to be closed with this action. + animation: |- + ``` + readonly animation: Animation + ``` + + Animation displayed with this interaction. +Color: + overview: |- + Interface Color + =============== + + Represents color properties in Penpot. + This interface includes properties for defining solid colors, gradients, and image fills, along with metadata. + + ``` + interface Color { + id?: string; + fileId?: string; + name?: string; + path?: string; + color?: string; + opacity?: number; + refId?: string; + refFile?: string; + gradient?: Gradient; + image?: ImageData; + } + ``` + + Referenced by: Context, Penpot, Shadow + members: + Properties: + id: |- + ``` + id?: string + ``` + + The optional reference ID for an external color definition. + fileId: |- + ``` + fileId?: string + ``` + + The optional reference to an external file for the color definition. + name: |- + ``` + name?: string + ``` + + The optional name of the color. + path: |- + ``` + path?: string + ``` + + The optional path or category to which this color belongs. + color: |- + ``` + color?: string + ``` + + The optional solid color, represented as a string (e.g., '#FF5733'). + opacity: |- + ``` + opacity?: number + ``` + + The optional opacity level of the color, ranging from 0 (fully transparent) to 1 (fully opaque). + Defaults to 1 if omitted. + refId: |- + ``` + refId?: string + ``` + + The optional reference ID for an external color definition. + + Deprecated + + Use `id` instead + refFile: |- + ``` + refFile?: string + ``` + + The optional reference to an external file for the color definition. + + Deprecated + + Use `fileId` + gradient: |- + ``` + gradient?: Gradient + ``` + + The optional gradient fill defined by a Gradient object. + image: |- + ``` + image?: ImageData + ``` + + The optional image fill defined by an ImageData object. +ColorShapeInfo: + overview: |- + Interface ColorShapeInfo + ======================== + + Additional color information for the methods to extract colors from a list of shapes. + + ``` + interface ColorShapeInfo { + shapesInfo: ColorShapeInfoEntry[]; + } + ``` + + Referenced by: Context, Penpot + members: + Properties: + shapesInfo: |- + ``` + readonly shapesInfo: ColorShapeInfoEntry[] + ``` + + List of shapes with additional information +ColorShapeInfoEntry: + overview: |- + Interface ColorShapeInfoEntry + ============================= + + Entry for the color shape additional information. + + ``` + interface ColorShapeInfoEntry { + property: string; + index?: number; + shapeId: string; + } + ``` + + Referenced by: ColorShapeInfo + members: + Properties: + property: |- + ``` + readonly property: string + ``` + + Property that has the color (example: fill, stroke...) + index: |- + ``` + readonly index?: number + ``` + + For properties that are indexes (such as fill) represent the index + of the color inside that property. + shapeId: |- + ``` + readonly shapeId: string + ``` + + Identifier of the shape that contains the color +Comment: + overview: |- + Interface Comment + ================= + + Comments allow the team to have one priceless conversation getting and + providing feedback right over the designs and prototypes. + + ``` + interface Comment { + user: User; + date: Date; + content: string; + remove(): void; + } + ``` + + Referenced by: CommentThread + members: + Properties: + user: |- + ``` + readonly user: User + ``` + + The `user` that has created the comment. + date: |- + ``` + readonly date: Date + ``` + + The `date` the comment has been created. + content: |- + ``` + content: string + ``` + + The `content` for the commentary. The owner can modify the comment. + Methods: + remove: |- + ``` + remove(): void + ``` + + * Remove the current comment from its comment thread. Only the owner can remove their comments. + Requires the `comment:write` permission. + + Returns void +CommentThread: + overview: |- + Interface CommentThread + ======================= + + Represents a list of comments one after the other. Usually these threads + are conversations the users have in Penpot. + + ``` + interface CommentThread { + seqNumber: number; + board?: Board; + owner?: User; + position: Point; + resolved: boolean; + findComments(): Promise; + reply(content: string): Promise; + remove(): void; + } + ``` + + Referenced by: Page + members: + Properties: + seqNumber: |- + ``` + readonly seqNumber: number + ``` + + This is the number that is displayed on the workspace. Is an increasing + sequence for each comment. + board: |- + ``` + readonly board?: Board + ``` + + If the thread is attached to a `board` this will have that board + reference. + owner: |- + ``` + readonly owner?: User + ``` + + Owner of the comment thread + position: |- + ``` + position: Point + ``` + + The `position` in absolute coordinates in the canvas. + resolved: |- + ``` + resolved: boolean + ``` + + Whether the thread has been marked as `resolved` or not. + Methods: + findComments: |- + ``` + findComments(): Promise + ``` + + * List of `comments` ordered by creation date. + Requires the `comment:read` or `comment:write` permission. + + Returns Promise + reply: |- + ``` + reply(content): Promise + ``` + + * Creates a new comment after the last one in the thread. The current user will + be used as the creation user. + Requires the `comment:write` permission. + + Parameters + + content: string + + Returns Promise + remove: |- + ``` + remove(): void + ``` + + * Removes the current comment thread. Only the user that created the thread can + remove it. + Requires the `comment:write` permission. + + Returns void +CommonLayout: + overview: |- + Interface CommonLayout + ====================== + + CommonLayout represents a common layout interface in the Penpot application. + It includes various properties for alignment, spacing, padding, and sizing, as well as a method to remove the layout. + + ``` + interface CommonLayout { + alignItems?: + | "center" + | "start" + | "end" + | "stretch"; + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + justifyItems?: + | "center" + | "start" + | "end" + | "stretch"; + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + rowGap: number; + columnGap: number; + verticalPadding: number; + horizontalPadding: number; + topPadding: number; + rightPadding: number; + bottomPadding: number; + leftPadding: number; + horizontalSizing: "fill" | "auto" | "fit-content"; + verticalSizing: "fill" | "auto" | "fit-content"; + remove(): void; + } + ``` + + Hierarchy (view full) + + * CommonLayout + + FlexLayout + + GridLayout + members: + Properties: + alignItems: |- + ``` + alignItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `alignItems` property specifies the default alignment for items inside the container. + It can be one of the following values: + + * 'start': Items are aligned at the start. + * 'end': Items are aligned at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + alignContent: |- + ``` + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `alignContent` property specifies how the content is aligned within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is aligned at the start. + * 'end': Content is aligned at the end. + * 'center': Content is centered. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + justifyItems: |- + ``` + justifyItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `justifyItems` property specifies the default justification for items inside the container. + It can be one of the following values: + + * 'start': Items are justified at the start. + * 'end': Items are justified at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + justifyContent: |- + ``` + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `justifyContent` property specifies how the content is justified within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is justified at the start. + * 'center': Content is centered. + * 'end': Content is justified at the end. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + rowGap: |- + ``` + rowGap: number + ``` + + The `rowGap` property specifies the gap between rows in the layout. + columnGap: |- + ``` + columnGap: number + ``` + + The `columnGap` property specifies the gap between columns in the layout. + verticalPadding: |- + ``` + verticalPadding: number + ``` + + The `verticalPadding` property specifies the vertical padding inside the container. + horizontalPadding: |- + ``` + horizontalPadding: number + ``` + + The `horizontalPadding` property specifies the horizontal padding inside the container. + topPadding: |- + ``` + topPadding: number + ``` + + The `topPadding` property specifies the padding at the top of the container. + rightPadding: |- + ``` + rightPadding: number + ``` + + The `rightPadding` property specifies the padding at the right of the container. + bottomPadding: |- + ``` + bottomPadding: number + ``` + + The `bottomPadding` property specifies the padding at the bottom of the container. + leftPadding: |- + ``` + leftPadding: number + ``` + + The `leftPadding` property specifies the padding at the left of the container. + horizontalSizing: |- + ``` + horizontalSizing: "fill" | "auto" | "fit-content" + ``` + + The `horizontalSizing` property specifies the horizontal sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + verticalSizing: |- + ``` + verticalSizing: "fill" | "auto" | "fit-content" + ``` + + The `verticalSizing` property specifies the vertical sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + Methods: + remove: |- + ``` + remove(): void + ``` + + * The `remove` method removes the layout. + + Returns void +Context: + overview: |- + Interface Context + ================= + + Represents the context of Penpot, providing access to various Penpot functionalities and data. + + ``` + interface Context { + root: null | Shape; + currentFile: null | File; + currentPage: null | Page; + viewport: Viewport; + history: HistoryContext; + library: LibraryContext; + fonts: FontsContext; + currentUser: User; + activeUsers: ActiveUser[]; + theme: Theme; + localStorage: LocalStorage; + selection: Shape[]; + shapesColors(shapes: Shape[]): (Color & ColorShapeInfo)[]; + replaceColor(shapes: Shape[], oldColor: Color, newColor: Color): void; + uploadMediaUrl(name: string, url: string): Promise; + uploadMediaData(name: string, data: Uint8Array, mimeType: string): Promise; + group(shapes: Shape[]): null | Group; + ungroup(group: Group, ...other: Group[]): void; + createRectangle(): Rectangle; + createBoard(): Board; + createEllipse(): Ellipse; + createPath(): Path; + createBoolean(boolType: BooleanType, shapes: Shape[]): null | Boolean; + createShapeFromSvg(svgString: string): null | Group; + createShapeFromSvgWithImages(svgString: string): Promise; + createText(text: string): null | Text; + generateMarkup(shapes: Shape[], options?: { + type?: "html" | "svg"; + }): string; + generateStyle(shapes: Shape[], options?: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + }): string; + generateFontFaces(shapes: Shape[]): Promise; + addListener(type: T, callback: ((event: EventsMap[T]) => void), props?: { + [key: string]: unknown; + }): symbol; + removeListener(listenerId: symbol): void; + openViewer(): void; + createPage(): Page; + openPage(page: Page, newWindow?: boolean): void; + alignHorizontal(shapes: Shape[], direction: "center" | "left" | "right"): void; + alignVertical(shapes: Shape[], direction: "center" | "top" | "bottom"): void; + distributeHorizontal(shapes: Shape[]): void; + distributeVertical(shapes: Shape[]): void; + flatten(shapes: Shape[]): Path[]; + } + ``` + members: + Properties: + root: |- + ``` + readonly root: null | Shape + ``` + + The root shape in the current Penpot context. Requires `content:read` permission. + + Example + ``` + const rootShape = context.root;console.log(rootShape); + ``` + currentFile: |- + ``` + readonly currentFile: null | File + ``` + + Retrieves file data from the current Penpot context. Requires `content:read` permission. + + Returns + + Returns the file data or `null` if no file is available. + + Example + ``` + const fileData = context.currentFile;console.log(fileData); + ``` + currentPage: |- + ``` + readonly currentPage: null | Page + ``` + + The current page in the Penpot context. Requires `content:read` permission. + + Example + ``` + const currentPage = context.currentPage;console.log(currentPage); + ``` + viewport: |- + ``` + readonly viewport: Viewport + ``` + + The viewport settings in the Penpot context. + + Example + ``` + const viewportSettings = context.viewport;console.log(viewportSettings); + ``` + history: |- + ``` + readonly history: HistoryContext + ``` + + Context encapsulating the history operations + + Example + ``` + const historyContext = context.history;console.log(historyContext); + ``` + library: |- + ``` + readonly library: LibraryContext + ``` + + The library context in the Penpot context, including both local and connected libraries. Requires `library:read` permission. + + Example + ``` + const libraryContext = context.library;console.log(libraryContext); + ``` + fonts: |- + ``` + readonly fonts: FontsContext + ``` + + The fonts context in the Penpot context, providing methods to manage fonts. Requires `content:read` permission. + + Example + ``` + const fontsContext = context.fonts;console.log(fontsContext); + ``` + currentUser: |- + ``` + readonly currentUser: User + ``` + + The current user in the Penpot context. Requires `user:read` permission. + + Example + ``` + const currentUser = context.currentUser;console.log(currentUser); + ``` + activeUsers: |- + ``` + readonly activeUsers: ActiveUser[] + ``` + + An array of active users in the Penpot context. Requires `user:read` permission. + + Example + ``` + const activeUsers = context.activeUsers;console.log(activeUsers); + ``` + theme: |- + ``` + readonly theme: Theme + ``` + + The current theme (light or dark) in Penpot. + + Example + ``` + const currentTheme = context.theme;console.log(currentTheme); + ``` + localStorage: |- + ``` + readonly localStorage: LocalStorage + ``` + + Access to the localStorage proxy + selection: |- + ``` + selection: Shape[] + ``` + + The currently selected shapes in Penpot. Requires `content:read` permission. + + Example + ``` + const selectedShapes = context.selection;console.log(selectedShapes); + ``` + Methods: + shapesColors: |- + ``` + shapesColors(shapes): (Color & ColorShapeInfo)[] + ``` + + * Retrieves colors applied to the given shapes in Penpot. Requires `content:read` permission. + + Parameters + + shapes: Shape[] + + Returns (Color & ColorShapeInfo)[] + + Returns an array of colors and their shape information. + + Example + ``` + const colors = context.shapesColors(shapes);console.log(colors); + ``` + replaceColor: |- + ``` + replaceColor(shapes, oldColor, newColor): void + ``` + + * Replaces a specified old color with a new color in the given shapes. Requires `content:write` permission. + + Parameters + + shapes: Shape[] + + oldColor: Color + + newColor: Color + + Returns void + + Example + ``` + context.replaceColor(shapes, oldColor, newColor); + ``` + uploadMediaUrl: |- + ``` + uploadMediaUrl(name, url): Promise + ``` + + * Uploads media to Penpot and retrieves its image data. Requires `content:write` permission. + + Parameters + + name: string + + The name of the media. + + url: string + + The URL of the media to be uploaded. + + Returns Promise + + Returns a promise that resolves to the image data of the uploaded media. + + Example + ``` + const imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');console.log(imageData);// to insert the image in a shape we can doconst board = penpot.createBoard();const shape = penpot.createRectangle();board.appendChild(shape);shape.fills = [{ fillOpacity: 1, fillImage: imageData }]; + ``` + uploadMediaData: |- + ``` + uploadMediaData(name, data, mimeType): Promise + ``` + + * Uploads media to penpot and retrieves the image data. Requires `content:write` permission. + + Parameters + + name: string + + The name of the media. + + data: Uint8Array + + The image content data + + mimeType: string + + Returns Promise + + Returns a promise that resolves to the image data of the uploaded media. + + Example + ``` + const imageData = await context.uploadMediaData('example', imageData, 'image/jpeg');console.log(imageData); + ``` + group: |- + ``` + group(shapes): null | Group + ``` + + * Groups the specified shapes. Requires `content:write` permission. + + Parameters + + shapes: Shape[] + + An array of shapes to group. + + Returns null | Group + + Returns the newly created group or `null` if the group could not be created. + + Example + ``` + const penpotShapesArray = penpot.selection;penpot.group(penpotShapesArray); + ``` + ungroup: |- + ``` + ungroup(group, ...other): void + ``` + + * Ungroups the specified group. Requires `content:write` permission. + + Parameters + + group: Group + + The group to ungroup. + + ...other: Group[] + + Additional groups to ungroup. + + Returns void + + Example + ``` + const penpotShapesArray = penpot.selection;// We need to make sure that something is selected, and if the selected shape is a group,if (selected.length && penpot.utils.types.isGroup(penpotShapesArray[0])) { penpot.group(penpotShapesArray[0]);} + ``` + createRectangle: |- + ``` + createRectangle(): Rectangle + ``` + + * Use this method to create the shape of a rectangle. Requires `content:write` permission. + + Returns Rectangle + + Example + ``` + const shape = penpot.createRectangle();// just change the values like thisshape.name = "Example rectangle";// for solid colorshape.fills = [{ fillColor: "#7EFFF5" }];// for linear gradient colorshape.fills = [{ fillColorGradient: { "type": "linear", "startX": 0.5, "startY": 0, "endX": 0.5, "endY": 1, "width": 1, "stops": [ { "color": "#003ae9", "opacity": 1, "offset": 0 }, { "color": "#003ae9", "opacity": 0, "offset": 1 } ] }}];// for a image fillconst imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');shape.fills = [{ fillOpacity: 1, fillImage: imageData }];shape.borderRadius = 8;shape.strokes = [ { strokeColor: "#2e3434", strokeStyle: "solid", strokeWidth: 2, strokeAlignment: "center", },]; + ``` + createBoard: |- + ``` + createBoard(): Board + ``` + + * Use this method to create a board. This is the first step before anything else, the container. Requires `content:write` permission. + Then you can add a gridlayout, flexlayout or add a shape inside the board. + Just a heads-up: board is a board in Penpot UI. + + Returns Board + + Example + ``` + const board = penpot.createBoard();// to add grid layoutboard.addGridLayout();// to add flex layoutboard.addFlexLayout();// to create a shape inside the boardconst shape = penpot.createRectangle();board.appendChild(shape); + ``` + createEllipse: |- + ``` + createEllipse(): Ellipse + ``` + + * Use this method to create the shape of an ellipse. Requires `content:write` permission. + + Returns Ellipse + + Example + ``` + const shape = penpot.createEllipse();// just change the values like thisshape.name = "Example ellipse";// for solid colorshape.fills = [{ fillColor: "#7EFFF5" }];// for linear gradient colorshape.fills = [{ fillColorGradient: { "type": "linear", "startX": 0.5, "startY": 0, "endX": 0.5, "endY": 1, "width": 1, "stops": [ { "color": "#003ae9", "opacity": 1, "offset": 0 }, { "color": "#003ae9", "opacity": 0, "offset": 1 } ] }}];// for an image fillconst imageData = await context.uploadMediaUrl('example', 'https://example.com/image.jpg');shape.fills = [{ fillOpacity: 1, fillImage: imageData }];shape.strokes = [ { strokeColor: "#2e3434", strokeStyle: "solid", strokeWidth: 2, strokeAlignment: "center", },]; + ``` + createPath: |- + ``` + createPath(): Path + ``` + + * Use this method to create a path. Requires `content:write` permission. + + Returns Path + + Example + ``` + const path = penpot.createPath();path.name = "My path";// for solid colorpath.fills = [{ fillColor: "#7EFFF5" }]; + ``` + createBoolean: |- + ``` + createBoolean(boolType, shapes): null | Boolean + ``` + + * Creates a Boolean shape based on the specified boolean operation and shapes. Requires `content:write` permission. + + Parameters + + boolType: BooleanType + + The type of boolean operation ('union', 'difference', 'exclude', 'intersection'). + + shapes: Shape[] + + An array of shapes to perform the boolean operation on. + + Returns null | Boolean + + Returns the newly created Boolean shape resulting from the boolean operation. + + Example + ``` + const booleanShape = context.createBoolean('union', [shape1, shape2]); + ``` + createShapeFromSvg: |- + ``` + createShapeFromSvg(svgString): null | Group + ``` + + * Creates a Group from an SVG string. Requires `content:write` permission. + + Parameters + + svgString: string + + The SVG string representing the shapes to be converted into a group. + + Returns null | Group + + Returns the newly created Group containing the shapes from the SVG. + + Example + ``` + const svgGroup = context.createShapeFromSvg('...'); + ``` + createShapeFromSvgWithImages: |- + ``` + createShapeFromSvgWithImages(svgString): Promise + ``` + + * Creates a Group from an SVG string. The SVG can have images and the method returns + a Promise because the shape will be available after all images are uploaded. + Requires `content:write` permission. + + Parameters + + svgString: string + + The SVG string representing the shapes to be converted into a group. + + Returns Promise + + Returns a promise with the newly created Group containing the shapes from the SVG. + + Example + ``` + const svgGroup = await context.createShapeFromSvgWithImages('...'); + ``` + createText: |- + ``` + createText(text): null | Text + ``` + + * Creates a Text shape with the specified text content. Requires `content:write` permission. + + Parameters + + text: string + + The text content for the Text shape. + + Returns null | Text + + Returns the new created shape, if the shape wasn't created can return null. + + Example + ``` + const board = penpot.createBoard();let text;text = penpot.createText();// just change the values like thistext.growType = 'auto-height';text.fontFamily = 'Work Sans';text.fontSize = '12';text.fills = [{fillColor: '#9f05ff', fillOpacity: 1}];text.strokes = [{strokeOpacity: 1, strokeStyle: 'solid', strokeWidth: 2, strokeColor: '#deabff', strokeAlignment: 'outer'}];board.appendChild(text); + ``` + generateMarkup: |- + ``` + generateMarkup(shapes, options?): string + ``` + + * Generates markup for the given shapes. Requires `content:read` permission + + Parameters + + shapes: Shape[] + + options: { + type?: "html" | "svg"; + } + - Optionaltype?: "html" | "svg" + + Returns string + + Example + ``` + const markup = context.generateMarkup(shapes, { type: 'html' });console.log(markup); + ``` + generateStyle: |- + ``` + generateStyle(shapes, options?): string + ``` + + * Generates styles for the given shapes. Requires `content:read` permission + + Parameters + + shapes: Shape[] + + options: { + type?: "css"; + withPrelude?: boolean; + includeChildren?: boolean; + } + - Optionaltype?: "css" + - OptionalwithPrelude?: boolean + - OptionalincludeChildren?: boolean + + Returns string + + Example + ``` + const styles = context.generateStyle(shapes, { type: 'css' });console.log(styles); + ``` + generateFontFaces: |- + ``` + generateFontFaces(shapes): Promise + ``` + + * Generates the fontfaces styles necessaries to render the shapes. + Requires `content:read` permission + + Parameters + + shapes: Shape[] + + Returns Promise + + Example + ``` + const fontfaces = context.generateFontFaces(penpot.selection);console.log(fontfaces); + ``` + addListener: |- + ``` + addListener(type, callback, props?): symbol + ``` + + * Adds the current callback as an event listener + + Type Parameters + + T extends keyof EventsMap + + Parameters + + type: T + + callback: ((event: EventsMap[T]) => void) + - ``` + (event): void + ``` + + * Parameters + + event: EventsMap[T] + + Returns void + + props: { + [key: string]: unknown; + } + - [key: string]: unknown + + Returns symbol + + Example + ``` + const listenerId = context.addListener('selectionchange', (event) => { console.log(event);}); + ``` + removeListener: |- + ``` + removeListener(listenerId): void + ``` + + * Removes the listenerId from the list of listeners + + Parameters + + listenerId: symbol + + Returns void + + Example + ``` + context.removeListener(listenerId); + ``` + openViewer: |- + ``` + openViewer(): void + ``` + + * Opens the viewer section. Requires `content:read` permission. + + Returns void + createPage: |- + ``` + createPage(): Page + ``` + + * Creates a new page. Requires `content:write` permission. + + Returns Page + openPage: |- + ``` + openPage(page, newWindow?): void + ``` + + * Changes the current open page to given page. Requires `content:read` permission. + + Parameters + + page: Page + + the page to open + + newWindow: boolean + + if true opens the page in a new window + + Returns void + + Example + ``` + context.openPage(page); + ``` + alignHorizontal: |- + ``` + alignHorizontal(shapes, direction): void + ``` + + * Aligning will move all the selected layers to a position relative to one + of them in the horizontal direction. + + Parameters + + shapes: Shape[] + + to align + + direction: "center" | "left" | "right" + + where the shapes will be aligned + + Returns void + alignVertical: |- + ``` + alignVertical(shapes, direction): void + ``` + + * Aligning will move all the selected layers to a position relative to one + of them in the vertical direction. + + Parameters + + shapes: Shape[] + + to align + + direction: "center" | "top" | "bottom" + + where the shapes will be aligned + + Returns void + distributeHorizontal: |- + ``` + distributeHorizontal(shapes): void + ``` + + * Distributing objects to position them horizontally with equal distances between them. + + Parameters + + shapes: Shape[] + + to distribute + + Returns void + distributeVertical: |- + ``` + distributeVertical(shapes): void + ``` + + * Distributing objects to position them vertically with equal distances between them. + + Parameters + + shapes: Shape[] + + to distribute + + Returns void + flatten: |- + ``` + flatten(shapes): Path[] + ``` + + * Converts the shapes into Paths. If the shapes are complex will put together + all its paths into one. + + Parameters + + shapes: Shape[] + + to flatten + + Returns Path[] +ContextGeometryUtils: + overview: |- + Interface ContextGeometryUtils + ============================== + + Utility methods for geometric calculations in Penpot. + + Example + ``` + const centerPoint = geometryUtils.center(shapes);console.log(centerPoint); + ``` + + ``` + interface ContextGeometryUtils { + center(shapes: Shape[]): null | { + x: number; + y: number; + }; + } + ``` + + Referenced by: ContextUtils + members: + Methods: + center: |- + ``` + center(shapes): null | { + x: number; + y: number; + } + ``` + + * Calculates the center point of a given array of shapes. + This method computes the geometric center (centroid) of the bounding boxes of the provided shapes. + + Parameters + + shapes: Shape[] + + The array of shapes to calculate the center for. + + Returns null | { x: number; y: number; } + + Returns the center point as an object with `x` and `y` coordinates, or null if the array is empty. + + Example + ``` + const centerPoint = geometryUtils.center(shapes);console.log(centerPoint); + ``` +ContextTypesUtils: + overview: |- + Interface ContextTypesUtils + =========================== + + Utility methods for determining the types of Penpot shapes. + + Example + ``` + const isBoard = typesUtils.isBoard(shape);console.log(isBoard); + ``` + + ``` + interface ContextTypesUtils { + isBoard(shape: Shape): shape is Board; + isGroup(shape: Shape): shape is Group; + isMask(shape: Shape): shape is Group; + isBool(shape: Shape): shape is Boolean; + isRectangle(shape: Shape): shape is Rectangle; + isPath(shape: Shape): shape is Path; + isText(shape: Shape): shape is Text; + isEllipse(shape: Shape): shape is Ellipse; + isSVG(shape: Shape): shape is SvgRaw; + isVariantContainer(shape: Shape): shape is VariantContainer; + isVariantComponent(component: LibraryComponent): component is LibraryVariantComponent; + } + ``` + + Referenced by: ContextUtils + members: + Methods: + isBoard: |- + ``` + isBoard(shape): shape is Board + ``` + + * Checks if the given shape is a board. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Board + + Returns true if the shape is a board, otherwise false. + isGroup: |- + ``` + isGroup(shape): shape is Group + ``` + + * Checks if the given shape is a group. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Group + + Returns true if the shape is a Group, otherwise false. + isMask: |- + ``` + isMask(shape): shape is Group + ``` + + * Checks if the given shape is a mask. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Group + + Returns true if the shape is a Group (acting as a mask), otherwise false. + isBool: |- + ``` + isBool(shape): shape is Boolean + ``` + + * Checks if the given shape is a boolean operation. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Boolean + + Returns true if the shape is a Bool, otherwise false. + isRectangle: |- + ``` + isRectangle(shape): shape is Rectangle + ``` + + * Checks if the given shape is a rectangle. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Rectangle + + Returns true if the shape is a Rectangle, otherwise false. + isPath: |- + ``` + isPath(shape): shape is Path + ``` + + * Checks if the given shape is a path. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Path + + Returns true if the shape is a Path, otherwise false. + isText: |- + ``` + isText(shape): shape is Text + ``` + + * Checks if the given shape is a text element. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Text + + Returns true if the shape is a Text, otherwise false. + isEllipse: |- + ``` + isEllipse(shape): shape is Ellipse + ``` + + * Checks if the given shape is an ellipse. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is Ellipse + + Returns true if the shape is an Ellipse, otherwise false. + isSVG: |- + ``` + isSVG(shape): shape is SvgRaw + ``` + + * Checks if the given shape is an SVG. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is SvgRaw + + Returns true if the shape is a SvgRaw, otherwise false. + isVariantContainer: |- + ``` + isVariantContainer(shape): shape is VariantContainer + ``` + + * Checks if the given shape is a variant container. + + Parameters + + shape: Shape + + The shape to check. + + Returns shape is VariantContainer + + Returns true if the shape is a variant container, otherwise false. + isVariantComponent: |- + ``` + isVariantComponent(component): component is LibraryVariantComponent + ``` + + * Checks if the given component is a VariantComponent. + + Parameters + + component: LibraryComponent + + The component to check. + + Returns component is LibraryVariantComponent + + Returns true if the component is a VariantComponent, otherwise false. +ContextUtils: + overview: |- + Interface ContextUtils + ====================== + + Utility methods for various operations in Penpot. + + ``` + interface ContextUtils { + geometry: ContextGeometryUtils; + types: ContextTypesUtils; + } + ``` + + Referenced by: Penpot + members: + Properties: + geometry: |- + ``` + readonly geometry: ContextGeometryUtils + ``` + + Geometry utility methods for Penpot. + Provides methods for geometric calculations, such as finding the center of a group of shapes. + + Example + ``` + const centerPoint = penpot.utils.geometry.center(shapes);console.log(centerPoint); + ``` + types: |- + ``` + readonly types: ContextTypesUtils + ``` + + Type utility methods for Penpot. + Provides methods for determining the types of various shapes in Penpot. + + Example + ``` + const isBoard = utils.types.isBoard(shape);console.log(isBoard); + ``` +Dissolve: + overview: |- + Interface Dissolve + ================== + + Dissolve animation + + ``` + interface Dissolve { + type: "dissolve"; + duration: number; + easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out"; + } + ``` + + Referenced by: Animation + members: + Properties: + type: |- + ``` + readonly type + ``` + + Type of the animation + duration: |- + ``` + readonly duration: number + ``` + + Duration of the animation effect + easing: |- + ``` + readonly easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out" + ``` + + Function that the dissolve effect will follow for the interpolation. + Defaults to `linear`. +Ellipse: + overview: |- + Interface Ellipse + ================= + + Represents an ellipse shape in Penpot. + This interface extends `ShapeBase` and includes properties specific to ellipses. + + ``` + interface Ellipse { + type: "ellipse"; + fills: Fill[]; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Ellipse + + Referenced by: Context, ContextTypesUtils, Penpot, Shape + members: + Properties: + type: |- + ``` + type + ``` + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +EventsMap: + overview: |- + Interface EventsMap + =================== + + Represents a mapping of events to their corresponding types in Penpot. + This interface provides information about various events that can be triggered in the application. + + Example + ``` + penpot.on('pagechange', (event) => { console.log(event);}); + ``` + + ``` + interface EventsMap { + pagechange: Page; + filechange: File; + selectionchange: string[]; + themechange: Theme; + finish: string; + shapechange: Shape; + contentsave: void; + } + ``` + + Referenced by: Context, Penpot + members: + Properties: + pagechange: |- + ``` + pagechange: Page + ``` + + The `pagechange` event is triggered when the active page in the project is changed. + filechange: |- + ``` + filechange: File + ``` + + The `filechange` event is triggered when there are changes in the current file. + selectionchange: |- + ``` + selectionchange: string[] + ``` + + The `selectionchange` event is triggered when the selection of elements changes. + This event passes a list of identifiers of the selected elements. + themechange: |- + ``` + themechange: Theme + ``` + + The `themechange` event is triggered when the application theme is changed. + finish: |- + ``` + finish: string + ``` + + The `finish` event is triggered when some operation is finished. + shapechange: |- + ``` + shapechange: Shape + ``` + + This event will trigger whenever the shape in the props change. It's mandatory to send + with the props an object like `{ shapeId: '' }` + contentsave: |- + ``` + contentsave: void + ``` + + The `contentsave` event will trigger when the content file changes. +Export: + overview: |- + Interface Export + ================ + + Represents export settings in Penpot. + This interface includes properties for defining export configurations. + + ``` + interface Export { + type: + | "svg" + | "png" + | "jpeg" + | "pdf"; + scale?: number; + suffix?: string; + skipChildren?: boolean; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + type: |- + ``` + type: + | "svg" + | "png" + | "jpeg" + | "pdf" + ``` + + Type of the file to export. Can be one of the following values: png, jpeg, svg, pdf + scale: |- + ``` + scale?: number + ``` + + For bitmap formats represent the scale of the original size to resize the export + suffix: |- + ``` + suffix?: string + ``` + + Suffix that will be appended to the resulting exported file + skipChildren: |- + ``` + skipChildren?: boolean + ``` + + If true will ignore the children when exporting the shape +File: + overview: |- + Interface File + ============== + + File represents a file in the Penpot application. + It includes properties for the file's identifier, name, and revision number. + + ``` + interface File { + id: string; + name: string; + revn: number; + pages: Page[]; + export(exportType: "penpot" | "zip", libraryExportType?: "all" | "merge" | "detach"): Promise; + findVersions(criteria?: { + createdBy: User; + }): Promise; + saveVersion(label: string): Promise; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * PluginData + + File + + Referenced by: Context, EventsMap, Penpot + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The `id` property is a unique identifier for the file. + name: |- + ``` + name: string + ``` + + The `name` for the file + revn: |- + ``` + revn: number + ``` + + The `revn` will change for every document update + pages: |- + ``` + pages: Page[] + ``` + + List all the pages for the current file + Methods: + export: |- + ``` + export(exportType, libraryExportType?): Promise + ``` + + * Parameters + + exportType: "penpot" | "zip" + + libraryExportType: "all" | "merge" | "detach" + + Returns Promise + findVersions: |- + ``` + findVersions(criteria?): Promise + ``` + + * Retrieves the versions for the file. + + Parameters + + criteria: { + createdBy: User; + } + - createdBy: User + + Returns Promise + saveVersion: |- + ``` + saveVersion(label): Promise + ``` + + * Saves the current version into the versions history. + Requires the `content:write` permission. + + Parameters + + label: string + + Returns Promise + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +FileVersion: + overview: |- + Interface FileVersion + ===================== + + Type defining the file version properties. + + ``` + interface FileVersion { + label: string; + createdBy?: User; + createdAt: Date; + isAutosave: boolean; + restore(): void; + remove(): Promise; + pin(): Promise; + } + ``` + + Referenced by: File, FileVersion + members: + Properties: + label: |- + ``` + label: string + ``` + + The current label to identify the version. + createdBy: |- + ``` + readonly createdBy?: User + ``` + + The user that created the version. If not present, the + version is an autosave. + createdAt: |- + ``` + readonly createdAt: Date + ``` + + The date when the version was created. + isAutosave: |- + ``` + readonly isAutosave: boolean + ``` + + If the current version has been generated automatically. + Methods: + restore: |- + ``` + restore(): void + ``` + + * Returns void + remove: |- + ``` + remove(): Promise + ``` + + * Remove the current version. + Requires the `content:write` permission. + + Returns Promise + pin: |- + ``` + pin(): Promise + ``` + + * Converts an autosave version into a permanent version. + Requires the `content:write` permission. + + Returns Promise +Fill: + overview: |- + Interface Fill + ============== + + Represents fill properties in Penpot. You can add a fill to any shape except for groups. + This interface includes properties for defining solid color fills, gradient fills, and image fills. + + ``` + interface Fill { + fillColor?: string; + fillOpacity?: number; + fillColorGradient?: Gradient; + fillColorRefFile?: string; + fillColorRefId?: string; + fillImage?: ImageData; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, LibraryColor, Path, Rectangle, ShapeBase, SvgRaw, Text, TextRange, VariantContainer + members: + Properties: + fillColor: |- + ``` + fillColor?: string + ``` + + The optional solid fill color, represented as a string (e.g., '#FF5733'). + fillOpacity: |- + ``` + fillOpacity?: number + ``` + + The optional opacity level of the solid fill color, ranging from 0 (fully transparent) to 1 (fully opaque). + Defaults to 1 if omitted. + fillColorGradient: |- + ``` + fillColorGradient?: Gradient + ``` + + The optional gradient fill defined by a Gradient object. + fillColorRefFile: |- + ``` + fillColorRefFile?: string + ``` + + The optional reference to an external file for the fill color. + fillColorRefId: |- + ``` + fillColorRefId?: string + ``` + + The optional reference ID within the external file for the fill color. + fillImage: |- + ``` + fillImage?: ImageData + ``` + + The optional image fill defined by an ImageData object. +FlexLayout: + overview: |- + Interface FlexLayout + ==================== + + Represents a flexible layout configuration in Penpot. + This interface extends `CommonLayout` and includes properties for defining the direction, + wrapping behavior, and child management of a flex layout. + + ``` + interface FlexLayout { + alignItems?: + | "center" + | "start" + | "end" + | "stretch"; + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + justifyItems?: + | "center" + | "start" + | "end" + | "stretch"; + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + rowGap: number; + columnGap: number; + verticalPadding: number; + horizontalPadding: number; + topPadding: number; + rightPadding: number; + bottomPadding: number; + leftPadding: number; + horizontalSizing: "fill" | "auto" | "fit-content"; + verticalSizing: "fill" | "auto" | "fit-content"; + remove(): void; + dir: + | "row" + | "row-reverse" + | "column" + | "column-reverse"; + wrap?: "wrap" | "nowrap"; + appendChild(child: Shape): void; + } + ``` + + Hierarchy (view full) + + * CommonLayout + + FlexLayout + + Referenced by: Board, VariantContainer + members: + Properties: + alignItems: |- + ``` + alignItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `alignItems` property specifies the default alignment for items inside the container. + It can be one of the following values: + + * 'start': Items are aligned at the start. + * 'end': Items are aligned at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + alignContent: |- + ``` + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `alignContent` property specifies how the content is aligned within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is aligned at the start. + * 'end': Content is aligned at the end. + * 'center': Content is centered. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + justifyItems: |- + ``` + justifyItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `justifyItems` property specifies the default justification for items inside the container. + It can be one of the following values: + + * 'start': Items are justified at the start. + * 'end': Items are justified at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + justifyContent: |- + ``` + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `justifyContent` property specifies how the content is justified within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is justified at the start. + * 'center': Content is centered. + * 'end': Content is justified at the end. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + rowGap: |- + ``` + rowGap: number + ``` + + The `rowGap` property specifies the gap between rows in the layout. + columnGap: |- + ``` + columnGap: number + ``` + + The `columnGap` property specifies the gap between columns in the layout. + verticalPadding: |- + ``` + verticalPadding: number + ``` + + The `verticalPadding` property specifies the vertical padding inside the container. + horizontalPadding: |- + ``` + horizontalPadding: number + ``` + + The `horizontalPadding` property specifies the horizontal padding inside the container. + topPadding: |- + ``` + topPadding: number + ``` + + The `topPadding` property specifies the padding at the top of the container. + rightPadding: |- + ``` + rightPadding: number + ``` + + The `rightPadding` property specifies the padding at the right of the container. + bottomPadding: |- + ``` + bottomPadding: number + ``` + + The `bottomPadding` property specifies the padding at the bottom of the container. + leftPadding: |- + ``` + leftPadding: number + ``` + + The `leftPadding` property specifies the padding at the left of the container. + horizontalSizing: |- + ``` + horizontalSizing: "fill" | "auto" | "fit-content" + ``` + + The `horizontalSizing` property specifies the horizontal sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + verticalSizing: |- + ``` + verticalSizing: "fill" | "auto" | "fit-content" + ``` + + The `verticalSizing` property specifies the vertical sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + dir: |- + ``` + dir: + | "row" + | "row-reverse" + | "column" + | "column-reverse" + ``` + + The direction of the flex layout. + + * 'row': Main axis is horizontal, from left to right. + * 'row-reverse': Main axis is horizontal, from right to left. + * 'column': Main axis is vertical, from top to bottom. + * 'column-reverse': Main axis is vertical, from bottom to top. + wrap: |- + ``` + wrap?: "wrap" | "nowrap" + ``` + + The optional wrapping behavior of the flex layout. + + * 'wrap': Child elements will wrap onto multiple lines. + * 'nowrap': Child elements will not wrap. + Methods: + remove: |- + ``` + remove(): void + ``` + + * The `remove` method removes the layout. + + Returns void + appendChild: |- + ``` + appendChild(child): void + ``` + + * Appends a child element to the flex layout. + + Parameters + + child: Shape + + The child element to be appended, of type `Shape`. + + Returns void + + Example + ``` + flexLayout.appendChild(childShape); + ``` +Flow: + overview: |- + Interface Flow + ============== + + Defines an interaction flow inside penpot. A flow is defined by a starting board for an interaction. + + ``` + interface Flow { + page: Page; + name: string; + startingBoard: Board; + remove(): void; + } + ``` + + Referenced by: Page + members: + Properties: + page: |- + ``` + readonly page: Page + ``` + + The page in which the flow is defined + name: |- + ``` + name: string + ``` + + The name for the current flow + startingBoard: |- + ``` + startingBoard: Board + ``` + + The starting board for this interaction flow + Methods: + remove: |- + ``` + remove(): void + ``` + + * Removes the flow from the page + + Returns void +Font: + overview: |- + Interface Font + ============== + + Represents a font in Penpot, which includes details about the font family, variants, and styling options. + This interface provides properties and methods for describing and applying fonts within Penpot. + + ``` + interface Font { + name: string; + fontId: string; + fontFamily: string; + fontStyle?: null | "normal" | "italic"; + fontVariantId: string; + fontWeight: string; + variants: FontVariant[]; + applyToText(text: Text, variant?: FontVariant): void; + applyToRange(range: TextRange, variant?: FontVariant): void; + } + ``` + + Referenced by: FontsContext, LibraryTypography + members: + Properties: + name: |- + ``` + name: string + ``` + + This property holds the human-readable name of the font. + fontId: |- + ``` + fontId: string + ``` + + The unique identifier of the font. + fontFamily: |- + ``` + fontFamily: string + ``` + + The font family of the font. + fontStyle: |- + ``` + fontStyle?: null | "normal" | "italic" + ``` + + The default font style of the font. + fontVariantId: |- + ``` + fontVariantId: string + ``` + + The default font variant ID of the font. + fontWeight: |- + ``` + fontWeight: string + ``` + + The default font weight of the font. + variants: |- + ``` + variants: FontVariant[] + ``` + + An array of font variants available for the font. + Methods: + applyToText: |- + ``` + applyToText(text, variant?): void + ``` + + * Applies the font styles to a text shape. + + Parameters + + text: Text + + The text shape to apply the font styles to. + + variant: FontVariant + + Optional. The specific font variant to apply. If not provided, applies the default variant. + + Returns void + + Example + ``` + font.applyToText(textShape, fontVariant); + ``` + applyToRange: |- + ``` + applyToRange(range, variant?): void + ``` + + * Applies the font styles to a text range within a text shape. + + Parameters + + range: TextRange + + The text range to apply the font styles to. + + variant: FontVariant + + Optional. The specific font variant to apply. If not provided, applies the default variant. + + Returns void + + Example + ``` + font.applyToRange(textRange, fontVariant); + ``` +FontVariant: + overview: |- + Interface FontVariant + ===================== + + Represents a font variant in Penpot, which defines a specific style variation of a font. + This interface provides properties for describing the characteristics of a font variant. + + ``` + interface FontVariant { + name: string; + fontVariantId: string; + fontWeight: string; + fontStyle: "normal" | "italic"; + } + ``` + + Referenced by: Font, LibraryTypography + members: + Properties: + name: |- + ``` + name: string + ``` + + The name of the font variant. + fontVariantId: |- + ``` + fontVariantId: string + ``` + + The unique identifier of the font variant. + fontWeight: |- + ``` + fontWeight: string + ``` + + The font weight of the font variant. + fontStyle: |- + ``` + fontStyle: "normal" | "italic" + ``` + + The font style of the font variant. +FontsContext: + overview: |- + Interface FontsContext + ====================== + + Represents the context for managing fonts in Penpot. + This interface provides methods to interact with fonts, such as retrieving fonts by ID or name. + + ``` + interface FontsContext { + all: Font[]; + findById(id: string): null | Font; + findByName(name: string): null | Font; + findAllById(id: string): Font[]; + findAllByName(name: string): Font[]; + } + ``` + + Referenced by: Context, Penpot + members: + Properties: + all: |- + ``` + all: Font[] + ``` + + An array containing all available fonts. + Methods: + findById: |- + ``` + findById(id): null | Font + ``` + + * Finds a font by its unique identifier. + + Parameters + + id: string + + The ID of the font to find. + + Returns null | Font + + Returns the `Font` object if found, otherwise `null`. + + Example + ``` + const font = fontsContext.findById('font-id');if (font) { console.log(font.name);} + ``` + findByName: |- + ``` + findByName(name): null | Font + ``` + + * Finds a font by its name. + + Parameters + + name: string + + The name of the font to find. + + Returns null | Font + + Returns the `Font` object if found, otherwise `null`. + + Example + ``` + const font = fontsContext.findByName('font-name');if (font) { console.log(font.name);} + ``` + findAllById: |- + ``` + findAllById(id): Font[] + ``` + + * Finds all fonts matching a specific ID. + + Parameters + + id: string + + The ID to match against. + + Returns Font[] + + Returns an array of `Font` objects matching the provided ID. + + Example + ``` + const fonts = fontsContext.findAllById('font-id');console.log(fonts); + ``` + findAllByName: |- + ``` + findAllByName(name): Font[] + ``` + + * Finds all fonts matching a specific name. + + Parameters + + name: string + + The name to match against. + + Returns Font[] + + Returns an array of `Font` objects matching the provided name. + + Example + ``` + const fonts = fontsContext.findAllByName('font-name');console.log(fonts); + ``` +GridLayout: + overview: |- + Interface GridLayout + ==================== + + GridLayout represents a grid layout in the Penpot application, extending the common layout interface. + It includes properties and methods to manage rows, columns, and child elements within the grid. + + ``` + interface GridLayout { + alignItems?: + | "center" + | "start" + | "end" + | "stretch"; + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + justifyItems?: + | "center" + | "start" + | "end" + | "stretch"; + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly"; + rowGap: number; + columnGap: number; + verticalPadding: number; + horizontalPadding: number; + topPadding: number; + rightPadding: number; + bottomPadding: number; + leftPadding: number; + horizontalSizing: "fill" | "auto" | "fit-content"; + verticalSizing: "fill" | "auto" | "fit-content"; + remove(): void; + dir: "row" | "column"; + rows: Track[]; + columns: Track[]; + addRow(type: TrackType, value?: number): void; + addRowAtIndex(index: number, type: TrackType, value?: number): void; + addColumn(type: TrackType, value?: number): void; + addColumnAtIndex(index: number, type: TrackType, value: number): void; + removeRow(index: number): void; + removeColumn(index: number): void; + setColumn(index: number, type: TrackType, value?: number): void; + setRow(index: number, type: TrackType, value?: number): void; + appendChild(child: Shape, row: number, column: number): void; + } + ``` + + Hierarchy (view full) + + * CommonLayout + + GridLayout + + Referenced by: Board, VariantContainer + members: + Properties: + alignItems: |- + ``` + alignItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `alignItems` property specifies the default alignment for items inside the container. + It can be one of the following values: + + * 'start': Items are aligned at the start. + * 'end': Items are aligned at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + alignContent: |- + ``` + alignContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `alignContent` property specifies how the content is aligned within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is aligned at the start. + * 'end': Content is aligned at the end. + * 'center': Content is centered. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + justifyItems: |- + ``` + justifyItems?: + | "center" + | "start" + | "end" + | "stretch" + ``` + + The `justifyItems` property specifies the default justification for items inside the container. + It can be one of the following values: + + * 'start': Items are justified at the start. + * 'end': Items are justified at the end. + * 'center': Items are centered. + * 'stretch': Items are stretched to fill the container. + justifyContent: |- + ``` + justifyContent?: + | "center" + | "start" + | "end" + | "stretch" + | "space-between" + | "space-around" + | "space-evenly" + ``` + + The `justifyContent` property specifies how the content is justified within the container when there is extra space. + It can be one of the following values: + + * 'start': Content is justified at the start. + * 'center': Content is centered. + * 'end': Content is justified at the end. + * 'space-between': Content is distributed with space between. + * 'space-around': Content is distributed with space around. + * 'space-evenly': Content is distributed with even space around. + * 'stretch': Content is stretched to fill the container. + rowGap: |- + ``` + rowGap: number + ``` + + The `rowGap` property specifies the gap between rows in the layout. + columnGap: |- + ``` + columnGap: number + ``` + + The `columnGap` property specifies the gap between columns in the layout. + verticalPadding: |- + ``` + verticalPadding: number + ``` + + The `verticalPadding` property specifies the vertical padding inside the container. + horizontalPadding: |- + ``` + horizontalPadding: number + ``` + + The `horizontalPadding` property specifies the horizontal padding inside the container. + topPadding: |- + ``` + topPadding: number + ``` + + The `topPadding` property specifies the padding at the top of the container. + rightPadding: |- + ``` + rightPadding: number + ``` + + The `rightPadding` property specifies the padding at the right of the container. + bottomPadding: |- + ``` + bottomPadding: number + ``` + + The `bottomPadding` property specifies the padding at the bottom of the container. + leftPadding: |- + ``` + leftPadding: number + ``` + + The `leftPadding` property specifies the padding at the left of the container. + horizontalSizing: |- + ``` + horizontalSizing: "fill" | "auto" | "fit-content" + ``` + + The `horizontalSizing` property specifies the horizontal sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + verticalSizing: |- + ``` + verticalSizing: "fill" | "auto" | "fit-content" + ``` + + The `verticalSizing` property specifies the vertical sizing behavior of the container. + It can be one of the following values: + + * 'fit-content': The container fits the content. + * 'fill': The container fills the available space. + * 'auto': The container size is determined automatically. + dir: |- + ``` + dir: "row" | "column" + ``` + + The `dir` property specifies the primary direction of the grid layout. + It can be either 'column' or 'row'. + rows: |- + ``` + readonly rows: Track[] + ``` + + The `rows` property represents the collection of rows in the grid. + This property is read-only. + columns: |- + ``` + readonly columns: Track[] + ``` + + The `columns` property represents the collection of columns in the grid. + This property is read-only. + Methods: + remove: |- + ``` + remove(): void + ``` + + * The `remove` method removes the layout. + + Returns void + addRow: |- + ``` + addRow(type, value?): void + ``` + + * Adds a new row to the grid. + + Parameters + + type: TrackType + + The type of the row to add. + + value: number + + The value associated with the row type (optional). + + Returns void + + Example + ``` + const board = penpot.createBoard();const grid = board.addGridLayout();grid.addRow("flex", 1); + ``` + addRowAtIndex: |- + ``` + addRowAtIndex(index, type, value?): void + ``` + + * Adds a new row to the grid at the specified index. + + Parameters + + index: number + + The index at which to add the row. + + type: TrackType + + The type of the row to add. + + value: number + + The value associated with the row type (optional). + + Returns void + + Example + ``` + gridLayout.addRowAtIndex(0, 'fixed', 100); + ``` + addColumn: |- + ``` + addColumn(type, value?): void + ``` + + * Adds a new column to the grid. + + Parameters + + type: TrackType + + The type of the column to add. + + value: number + + The value associated with the column type (optional). + + Returns void + + Example + ``` + const board = penpot.createBoard();const grid = board.addGridLayout();grid.addColumn('percent', 50); + ``` + addColumnAtIndex: |- + ``` + addColumnAtIndex(index, type, value): void + ``` + + * Adds a new column to the grid at the specified index. + + Parameters + + index: number + + The index at which to add the column. + + type: TrackType + + The type of the column to add. + + value: number + + The value associated with the column type. + + Returns void + + Example + ``` + gridLayout.addColumnAtIndex(1, 'auto'); + ``` + removeRow: |- + ``` + removeRow(index): void + ``` + + * Removes a row from the grid at the specified index. + + Parameters + + index: number + + The index of the row to remove. + + Returns void + + Example + ``` + gridLayout.removeRow(2); + ``` + removeColumn: |- + ``` + removeColumn(index): void + ``` + + * Removes a column from the grid at the specified index. + + Parameters + + index: number + + The index of the column to remove. + + Returns void + + Example + ``` + gridLayout.removeColumn(3); + ``` + setColumn: |- + ``` + setColumn(index, type, value?): void + ``` + + * Sets the properties of a column at the specified index. + + Parameters + + index: number + + The index of the column to set. + + type: TrackType + + The type of the column. + + value: number + + The value associated with the column type (optional). + + Returns void + + Example + ``` + gridLayout.setColumn(0, 'fixed', 200); + ``` + setRow: |- + ``` + setRow(index, type, value?): void + ``` + + * Sets the properties of a row at the specified index. + + Parameters + + index: number + + The index of the row to set. + + type: TrackType + + The type of the row. + + value: number + + The value associated with the row type (optional). + + Returns void + + Example + ``` + gridLayout.setRow(1, 'flex'); + ``` + appendChild: |- + ``` + appendChild(child, row, column): void + ``` + + * Appends a child element to the grid at the specified row and column. + + Parameters + + child: Shape + + The child element to append. + + row: number + + The row index where the child will be placed. + + column: number + + The column index where the child will be placed. + + Returns void + + Example + ``` + gridLayout.appendChild(childShape, 0, 1); + ``` +Group: + overview: |- + Interface Group + =============== + + Represents a group of shapes in Penpot. + This interface extends `ShapeBase` and includes properties and methods specific to groups. + + ``` + interface Group { + type: "group"; + children: Shape[]; + appendChild(child: Shape): void; + insertChild(index: number, child: Shape): void; + isMask(): boolean; + makeMask(): void; + removeMask(): void; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Group + + Referenced by: Context, ContextTypesUtils, Penpot, Shape + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'group' for groups. + children: |- + ``` + readonly children: Shape[] + ``` + + The children shapes contained within the group. + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + fills: |- + ``` + fills: Fill[] | "mixed" + ``` + + The fills applied to the shape. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + appendChild: |- + ``` + appendChild(child): void + ``` + + * Appends a child shape to the group. + + Parameters + + child: Shape + + The child shape to append. + + Returns void + + Example + ``` + group.appendChild(childShape); + ``` + insertChild: |- + ``` + insertChild(index, child): void + ``` + + * Inserts a child shape at the specified index within the group. + + Parameters + + index: number + + The index at which to insert the child shape. + + child: Shape + + The child shape to insert. + + Returns void + + Example + ``` + group.insertChild(0, childShape); + ``` + isMask: |- + ``` + isMask(): boolean + ``` + + * Checks if the group is currently a mask. + A mask defines a clipping path for its child shapes. + + Returns boolean + makeMask: |- + ``` + makeMask(): void + ``` + + * Converts the group into a mask. + + Returns void + removeMask: |- + ``` + removeMask(): void + ``` + + * Removes the mask from the group. + + Returns void + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +GuideColumn: + overview: |- + Interface GuideColumn + ===================== + + Represents a goard guide for columns in Penpot. + This interface includes properties for defining the type, visibility, and parameters of column guides within a board. + + ``` + interface GuideColumn { + type: "column"; + display: boolean; + params: GuideColumnParams; + } + ``` + + Referenced by: Guide + members: + Properties: + type: |- + ``` + type + ``` + + The type of the guide, which is always 'column' for column guides. + display: |- + ``` + display: boolean + ``` + + Specifies whether the column guide is displayed. + params: |- + ``` + params: GuideColumnParams + ``` + + The parameters defining the appearance and layout of the column guides. +GuideColumnParams: + overview: |- + Interface GuideColumnParams + =========================== + + Represents parameters for board guide columns in Penpot. + This interface includes properties for defining the appearance and layout of column guides within a board. + + ``` + interface GuideColumnParams { + color: { + color: string; + opacity: number; + }; + type?: + | "center" + | "left" + | "right" + | "stretch"; + size?: number; + margin?: number; + itemLength?: number; + gutter?: number; + } + ``` + + Referenced by: GuideColumn, GuideRow + members: + Properties: + color: |- + ``` + color: { + color: string; + opacity: number; + } + ``` + + The color configuration for the column guides. + type: |- + ``` + type?: + | "center" + | "left" + | "right" + | "stretch" + ``` + + The optional alignment type of the column guides. + + * 'stretch': Columns stretch to fit the available space. + * 'left': Columns align to the left. + * 'center': Columns align to the center. + * 'right': Columns align to the right. + size: |- + ``` + size?: number + ``` + + The optional size of each column. + margin: |- + ``` + margin?: number + ``` + + The optional margin between the columns and the board edges. + itemLength: |- + ``` + itemLength?: number + ``` + + The optional length of each item within the columns. + gutter: |- + ``` + gutter?: number + ``` + + The optional gutter width between columns. +GuideRow: + overview: |- + Interface GuideRow + ================== + + Represents a board guide for rows in Penpot. + This interface includes properties for defining the type, visibility, and parameters of row guides within a board. + + ``` + interface GuideRow { + type: "row"; + display: boolean; + params: GuideColumnParams; + } + ``` + + Referenced by: Guide + members: + Properties: + type: |- + ``` + type + ``` + + The type of the guide, which is always 'row' for row guides. + display: |- + ``` + display: boolean + ``` + + Specifies whether the row guide is displayed. + params: |- + ``` + params: GuideColumnParams + ``` + + The parameters defining the appearance and layout of the row guides. + Note: This reuses the same parameter structure as column guides. +GuideSquare: + overview: |- + Interface GuideSquare + ===================== + + Represents a board guide for squares in Penpot. + This interface includes properties for defining the type, visibility, and parameters of square guides within a board. + + ``` + interface GuideSquare { + type: "square"; + display: boolean; + params: GuideSquareParams; + } + ``` + + Referenced by: Guide + members: + Properties: + type: |- + ``` + type + ``` + + The type of the guide, which is always 'square' for square guides. + display: |- + ``` + display: boolean + ``` + + Specifies whether the square guide is displayed. + params: |- + ``` + params: GuideSquareParams + ``` + + The parameters defining the appearance and layout of the square guides. +GuideSquareParams: + overview: |- + Interface GuideSquareParams + =========================== + + Represents parameters for board guide squares in Penpot. + This interface includes properties for defining the appearance and size of square guides within a board. + + ``` + interface GuideSquareParams { + color: { + color: string; + opacity: number; + }; + size?: number; + } + ``` + + Referenced by: GuideSquare + members: + Properties: + color: |- + ``` + color: { + color: string; + opacity: number; + } + ``` + + The color configuration for the square guides. + size: |- + ``` + size?: number + ``` + + The optional size of each square guide. +HistoryContext: + overview: |- + Interface HistoryContext + ======================== + + This object allows to access to some history functions + + ``` + interface HistoryContext { + undoBlockBegin(): Symbol; + undoBlockFinish(blockId: Symbol): void; + } + ``` + + Referenced by: Context, Penpot + members: + Methods: + undoBlockBegin: |- + ``` + undoBlockBegin(): Symbol + ``` + + * Starts an undo block. All operations done inside this block will be undone together until + a call to `undoBlockFinish` is called. + + Returns Symbol + + the block identifier + undoBlockFinish: |- + ``` + undoBlockFinish(blockId): void + ``` + + * Ends the undo block started with `undoBlockBegin` + + Parameters + + blockId: Symbol + + is the id returned by `undoBlockBegin` + + Returns void + + Example + ``` + historyContext.undoBlockFinish(blockId); + ``` +Image: + overview: |- + Interface Image + =============== + + Represents an image shape in Penpot. + This interface extends `ShapeBase` and includes properties specific to image shapes. + + ``` + interface Image { + type: "image"; + fills: Fill[]; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Image + + Referenced by: Shape + members: + Properties: + type: |- + ``` + type + ``` + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +Interaction: + overview: |- + Interface Interaction + ===================== + + Penpot allows you to prototype interactions by connecting boards, which can act as screens. + + ``` + interface Interaction { + shape?: Shape; + trigger: Trigger; + delay?: null | number; + action: Action; + remove(): void; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + shape: |- + ``` + readonly shape?: Shape + ``` + + The shape that owns the interaction + trigger: |- + ``` + trigger: Trigger + ``` + + The user action that will start the interaction. + delay: |- + ``` + delay?: null | number + ``` + + Time in **milliseconds** after the action will happen. Only applies to `after-delay` triggers. + action: |- + ``` + action: Action + ``` + + The action that will execute after the trigger happens. + Methods: + remove: |- + ``` + remove(): void + ``` + + * Removes the interaction + + Returns void +LayoutCellProperties: + overview: |- + Interface LayoutCellProperties + ============================== + + Properties for defining the layout of a cell in Penpot. + + ``` + interface LayoutCellProperties { + row?: number; + rowSpan?: number; + column?: number; + columnSpan?: number; + areaName?: string; + position?: "area" | "auto" | "manual"; + } + ``` + members: + Properties: + row: |- + ``` + row?: number + ``` + + The row index of the cell. + This value is optional and indicates the starting row of the cell. + rowSpan: |- + ``` + rowSpan?: number + ``` + + The number of rows the cell should span. + This value is optional and determines the vertical span of the cell. + column: |- + ``` + column?: number + ``` + + The column index of the cell. + This value is optional and indicates the starting column of the cell. + columnSpan: |- + ``` + columnSpan?: number + ``` + + The number of columns the cell should span. + This value is optional and determines the horizontal span of the cell. + areaName: |- + ``` + areaName?: string + ``` + + The name of the grid area that this cell belongs to. + This value is optional and can be used to define named grid areas. + position: |- + ``` + position?: "area" | "auto" | "manual" + ``` + + The positioning mode of the cell. + This value can be 'auto', 'manual', or 'area' and determines how the cell is positioned within the layout. +LayoutChildProperties: + overview: |- + Interface LayoutChildProperties + =============================== + + Properties for defining the layout of a child element in Penpot. + + ``` + interface LayoutChildProperties { + absolute: boolean; + zIndex: number; + horizontalSizing: "fill" | "auto" | "fix"; + verticalSizing: "fill" | "auto" | "fix"; + alignSelf: + | "center" + | "auto" + | "start" + | "end" + | "stretch"; + horizontalMargin: number; + verticalMargin: number; + topMargin: number; + rightMargin: number; + bottomMargin: number; + leftMargin: number; + maxWidth: null | number; + maxHeight: null | number; + minWidth: null | number; + minHeight: null | number; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + absolute: |- + ``` + absolute: boolean + ``` + + Specifies whether the child element is positioned absolutely. + When set to true, the element is taken out of the normal document flow and positioned relative to its nearest positioned ancestor. + zIndex: |- + ``` + zIndex: number + ``` + + Defines the stack order of the child element + Elements with a higher zIndex will be displayed in front of those with a lower zIndex. + horizontalSizing: |- + ``` + horizontalSizing: "fill" | "auto" | "fix" + ``` + + Determines the horizontal sizing behavior of the child element + + * 'auto': The width is determined by the content. + * 'fill': The element takes up the available width. + * 'fix': The width is fixed. + verticalSizing: |- + ``` + verticalSizing: "fill" | "auto" | "fix" + ``` + + Determines the vertical sizing behavior of the child element. + + * 'auto': The height is determined by the content. + * 'fill': The element takes up the available height. + * 'fix': The height is fixed. + alignSelf: |- + ``` + alignSelf: + | "center" + | "auto" + | "start" + | "end" + | "stretch" + ``` + + Aligns the child element within its container. + + * 'auto': Default alignment. + * 'start': Aligns the element at the start of the container. + * 'center': Centers the element within the container. + * 'end': Aligns the element at the end of the container. + * 'stretch': Stretches the element to fill the container. + horizontalMargin: |- + ``` + horizontalMargin: number + ``` + + Sets the horizontal margin of the child element. + This is the space on the left and right sides of the element. + verticalMargin: |- + ``` + verticalMargin: number + ``` + + Sets the vertical margin of the child element. + This is the space on the top and bottom sides of the element. + topMargin: |- + ``` + topMargin: number + ``` + + Sets the top margin of the child element. + This is the space above the element. + rightMargin: |- + ``` + rightMargin: number + ``` + + Sets the right margin of the child element. + This is the space to the right of the element. + bottomMargin: |- + ``` + bottomMargin: number + ``` + + Sets the bottom margin of the child element. + This is the space below the element. + leftMargin: |- + ``` + leftMargin: number + ``` + + Sets the left margin of the child element. + This is the space to the left of the element. + maxWidth: |- + ``` + maxWidth: null | number + ``` + + Defines the maximum width of the child element. + If set to null, there is no maximum width constraint. + maxHeight: |- + ``` + maxHeight: null | number + ``` + + Defines the maximum height of the child element. + If set to null, there is no maximum height constraint. + minWidth: |- + ``` + minWidth: null | number + ``` + + Defines the minimum width of the child element. + If set to null, there is no minimum width constraint. + minHeight: |- + ``` + minHeight: null | number + ``` + + Defines the minimum height of the child element. + If set to null, there is no minimum height constraint. +Library: + overview: |- + Interface Library + ================= + + Represents a library in Penpot, containing colors, typographies, and components. + + ``` + interface Library { + id: string; + name: string; + colors: LibraryColor[]; + typographies: LibraryTypography[]; + components: LibraryComponent[]; + createColor(): LibraryColor; + createTypography(): LibraryTypography; + createComponent(shapes: Shape[]): LibraryComponent; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * PluginData + + Library + + Referenced by: LibraryContext + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library. + name: |- + ``` + readonly name: string + ``` + + The name of the library. + colors: |- + ``` + readonly colors: LibraryColor[] + ``` + + An array of color elements in the library. + + Example + ``` + console.log(penpot.library.local.colors); + ``` + typographies: |- + ``` + readonly typographies: LibraryTypography[] + ``` + + An array of typography elements in the library. + components: |- + ``` + readonly components: LibraryComponent[] + ``` + + An array of component elements in the library. + + Example + ``` + console.log(penpot.library.local.components + ``` + Methods: + createColor: |- + ``` + createColor(): LibraryColor + ``` + + * Creates a new color element in the library. + + Returns LibraryColor + + Returns a new `LibraryColor` object representing the created color element. + + Example + ``` + const newColor = penpot.library.local.createColor();console.log(newColor); + ``` + createTypography: |- + ``` + createTypography(): LibraryTypography + ``` + + * Creates a new typography element in the library. + + Returns LibraryTypography + + Returns a new `LibraryTypography` object representing the created typography element. + + Example + ``` + const newTypography = library.createTypography(); + ``` + createComponent: |- + ``` + createComponent(shapes): LibraryComponent + ``` + + * Creates a new component element in the library using the provided shapes. + + Parameters + + shapes: Shape[] + + An array of `Shape` objects representing the shapes to be included in the component. + + Returns LibraryComponent + + Returns a new `LibraryComponent` object representing the created component element. + + Example + ``` + const newComponent = penpot.library.local.createComponent([shape1, shape2]); + ``` + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LibraryColor: + overview: |- + Interface LibraryColor + ====================== + + Represents a color element from a library in Penpot. + This interface extends `LibraryElement` and includes properties specific to color elements. + + ``` + interface LibraryColor { + color?: string; + opacity?: number; + gradient?: Gradient; + image?: ImageData; + asFill(): Fill; + asStroke(): Stroke; + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * LibraryElement + + LibraryColor + + Referenced by: Library + members: + Properties: + color: |- + ``` + color?: string + ``` + + The color value of the library color. + opacity: |- + ``` + opacity?: number + ``` + + The opacity value of the library color. + gradient: |- + ``` + gradient?: Gradient + ``` + + The gradient value of the library color, if it's a gradient. + image: |- + ``` + image?: ImageData + ``` + + The image data of the library color, if it's an image fill. + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library element. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the element belongs. + name: |- + ``` + name: string + ``` + + The name of the library element. + path: |- + ``` + path: string + ``` + + The path of the library element. + Methods: + asFill: |- + ``` + asFill(): Fill + ``` + + * Converts the library color into a fill object. + + Returns Fill + + Returns a `Fill` object representing the color as a fill. + + Example + ``` + const fill = libraryColor.asFill(); + ``` + asStroke: |- + ``` + asStroke(): Stroke + ``` + + * Converts the library color into a stroke object. + + Returns Stroke + + Returns a `Stroke` object representing the color as a stroke. + + Example + ``` + const stroke = libraryColor.asStroke(); + ``` + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LibraryComponent: + overview: |- + Interface LibraryComponent + ========================== + + Represents a component element from a library in Penpot. + This interface extends `LibraryElement` and includes properties specific to component elements. + + ``` + interface LibraryComponent { + instance(): Shape; + mainInstance(): Shape; + isVariant(): boolean; + transformInVariant(): void; + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * LibraryElement + + LibraryComponent + - LibraryVariantComponent + + Referenced by: Board, Boolean, ContextTypesUtils, Ellipse, Group, Image, Library, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer, Variants + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library element. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the element belongs. + name: |- + ``` + name: string + ``` + + The name of the library element. + path: |- + ``` + path: string + ``` + + The path of the library element. + Methods: + instance: |- + ``` + instance(): Shape + ``` + + * Creates an instance of the component. + + Returns Shape + + Returns a `Shape` object representing the instance of the component. + + Example + ``` + const componentInstance = libraryComponent.instance(); + ``` + mainInstance: |- + ``` + mainInstance(): Shape + ``` + + * Returns Shape + + Returns the reference to the main component shape. + isVariant: |- + ``` + isVariant(): boolean + ``` + + * Returns boolean + + true when this component is a VariantComponent + transformInVariant: |- + ``` + transformInVariant(): void + ``` + + * Creates a new Variant from this standard Component. It creates a VariantContainer, transform this Component into a VariantComponent, duplicates it, and creates a + set of properties based on the component name and path. + Similar to doing it with the contextual menu or the shortcut on the Penpot interface + + Returns void + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LibraryVariantComponent: + overview: |- + Interface LibraryVariantComponent + ================================= + + Represents a component element from a library in Penpot. + This interface extends `LibraryElement` and includes properties specific to component elements. + + ``` + interface LibraryVariantComponent { + instance(): Shape; + mainInstance(): Shape; + isVariant(): boolean; + transformInVariant(): void; + variants: null | Variants; + variantProps: { + [property: string]: string; + }; + variantError: string; + addVariant(): void; + setVariantProperty(pos: number, value: string): void; + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * LibraryComponent + + LibraryVariantComponent + + Referenced by: ContextTypesUtils + members: + Properties: + variants: |- + ``` + readonly variants: null | Variants + ``` + + Access to the Variant interface, for attributes and actions over the full Variant (not only this VariantComponent) + variantProps: |- + ``` + readonly variantProps: { + [property: string]: string; + } + ``` + + A list of the variants props of this VariantComponent. Each property have a key and a value + variantError: |- + ``` + variantError: string + ``` + + If this VariantComponent has an invalid name, that does't follow the structure [property]=[value], [property]=[value] + this field stores that invalid name + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library element. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the element belongs. + name: |- + ``` + name: string + ``` + + The name of the library element. + path: |- + ``` + path: string + ``` + + The path of the library element. + Methods: + instance: |- + ``` + instance(): Shape + ``` + + * Creates an instance of the component. + + Returns Shape + + Returns a `Shape` object representing the instance of the component. + + Example + ``` + const componentInstance = libraryComponent.instance(); + ``` + mainInstance: |- + ``` + mainInstance(): Shape + ``` + + * Returns Shape + + Returns the reference to the main component shape. + isVariant: |- + ``` + isVariant(): boolean + ``` + + * Returns boolean + + true when this component is a VariantComponent + transformInVariant: |- + ``` + transformInVariant(): void + ``` + + * Creates a new Variant from this standard Component. It creates a VariantContainer, transform this Component into a VariantComponent, duplicates it, and creates a + set of properties based on the component name and path. + Similar to doing it with the contextual menu or the shortcut on the Penpot interface + + Returns void + addVariant: |- + ``` + addVariant(): void + ``` + + * Creates a duplicate of the current VariantComponent on its Variant + + Returns void + setVariantProperty: |- + ``` + setVariantProperty(pos, value): void + ``` + + * Sets the value of the variant property on the indicated position + + Parameters + + pos: number + + value: string + + Returns void + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LibraryElement: + overview: |- + Interface LibraryElement + ======================== + + Represents an element in a Penpot library. + This interface provides information about a specific element in a library. + + ``` + interface LibraryElement { + id: string; + libraryId: string; + name: string; + path: string; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * PluginData + + LibraryElement + - LibraryColor + - LibraryComponent + - LibraryTypography + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library element. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the element belongs. + name: |- + ``` + name: string + ``` + + The name of the library element. + path: |- + ``` + path: string + ``` + + The path of the library element. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LibrarySummary: + overview: |- + Interface LibrarySummary + ======================== + + Represents a summary of a Penpot library. + This interface provides properties for summarizing various aspects of a Penpot library. + + ``` + interface LibrarySummary { + id: string; + name: string; + numColors: number; + numComponents: number; + numTypographies: number; + } + ``` + + Referenced by: LibraryContext + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library. + name: |- + ``` + readonly name: string + ``` + + The name of the library. + numColors: |- + ``` + readonly numColors: number + ``` + + The number of colors in the library. + numComponents: |- + ``` + readonly numComponents: number + ``` + + The number of components in the library. + numTypographies: |- + ``` + readonly numTypographies: number + ``` + + The number of typographies in the library. +LibraryTypography: + overview: |- + Interface LibraryTypography + =========================== + + Represents a typography element from a library in Penpot. + This interface extends `LibraryElement` and includes properties specific to typography elements. + + ``` + interface LibraryTypography { + id: string; + libraryId: string; + name: string; + path: string; + fontId: string; + fontFamily: string; + fontVariantId: string; + fontSize: string; + fontWeight: string; + fontStyle?: null | "normal" | "italic"; + lineHeight: string; + letterSpacing: string; + textTransform?: + | null + | "uppercase" + | "capitalize" + | "lowercase"; + applyToText(shape: Shape): void; + applyToTextRange(range: TextRange): void; + setFont(font: Font, variant?: FontVariant): void; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * LibraryElement + + LibraryTypography + + Referenced by: Library, Text, TextRange + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the library element. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the element belongs. + name: |- + ``` + name: string + ``` + + The name of the library element. + path: |- + ``` + path: string + ``` + + The path of the library element. + fontId: |- + ``` + fontId: string + ``` + + The unique identifier of the font used in the typography element. + fontFamily: |- + ``` + fontFamily: string + ``` + + The font family of the typography element. + fontVariantId: |- + ``` + fontVariantId: string + ``` + + The unique identifier of the font variant used in the typography element. + fontSize: |- + ``` + fontSize: string + ``` + + The font size of the typography element. + fontWeight: |- + ``` + fontWeight: string + ``` + + The font weight of the typography element. + fontStyle: |- + ``` + fontStyle?: null | "normal" | "italic" + ``` + + The font style of the typography element. + lineHeight: |- + ``` + lineHeight: string + ``` + + The line height of the typography element. + letterSpacing: |- + ``` + letterSpacing: string + ``` + + The letter spacing of the typography element. + textTransform: |- + ``` + textTransform?: + | null + | "uppercase" + | "capitalize" + | "lowercase" + ``` + + The text transform applied to the typography element. + Methods: + applyToText: |- + ``` + applyToText(shape): void + ``` + + * Applies the typography styles to a text shape. + + Parameters + + shape: Shape + + The text shape to apply the typography styles to. + + Returns void + + Example + ``` + typographyElement.applyToText(textShape); + ``` + applyToTextRange: |- + ``` + applyToTextRange(range): void + ``` + + * Applies the typography styles to a range of text within a text shape. + + Parameters + + range: TextRange + + Represents a range of text within a Text shape. This interface provides properties for styling and formatting text ranges. + + Returns void + + Example + ``` + typographyElement.applyToTextRange(textShape); + ``` + setFont: |- + ``` + setFont(font, variant?): void + ``` + + * Sets the font and optionally its variant for the typography element. + + Parameters + + font: Font + + The font to set. + + variant: FontVariant + + The font variant to set (optional). + + Returns void + + Example + ``` + typographyElement.setFont(newFont, newVariant); + ``` + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +LocalStorage: + overview: |- + Interface LocalStorage + ====================== + + Proxy for the local storage. Only elements owned by the plugin + can be stored and accessed. + Warning: other plugins won't be able to access this information but + the user could potentialy access the data through the browser information. + + ``` + interface LocalStorage { + getItem(key: string): string; + setItem(key: string, value: unknown): void; + removeItem(key: string): void; + getKeys(): string[]; + } + ``` + + Referenced by: Context, Penpot + members: + Methods: + getItem: |- + ``` + getItem(key): string + ``` + + * Retrieve the element with the given key + Requires the `allow:localstorage` permission. + + Parameters + + key: string + + Returns string + setItem: |- + ``` + setItem(key, value): void + ``` + + * Set the data given the key. If the value already existed it + will be overriden. The value will be stored in a string representation. + Requires the `allow:localstorage` permission. + + Parameters + + key: string + + value: unknown + + Returns void + removeItem: |- + ``` + removeItem(key): void + ``` + + * Remove the value stored in the key. + Requires the `allow:localstorage` permission. + + Parameters + + key: string + + Returns void + getKeys: |- + ``` + getKeys(): string[] + ``` + + * Return all the keys for the data stored by the plugin. + Requires the `allow:localstorage` permission. + + Returns string[] +NavigateTo: + overview: |- + Interface NavigateTo + ==================== + + It takes the user from one board to the destination set in the interaction. + + ``` + interface NavigateTo { + type: "navigate-to"; + destination: Board; + preserveScrollPosition?: boolean; + animation?: Animation; + } + ``` + + Referenced by: Action + members: + Properties: + type: |- + ``` + readonly type + ``` + + Type of action + destination: |- + ``` + readonly destination: Board + ``` + + Board to which the action targets + preserveScrollPosition: |- + ``` + readonly preserveScrollPosition?: boolean + ``` + + When true the scroll will be preserved. + animation: |- + ``` + readonly animation?: Animation + ``` + + Animation displayed with this interaction. +OpenOverlay: + overview: |- + Interface OpenOverlay + ===================== + + It opens a board right over the current board. + + ``` + interface OpenOverlay { + type: "open-overlay"; + destination: Board; + relativeTo?: Shape; + position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center"; + manualPositionLocation?: Point; + closeWhenClickOutside?: boolean; + addBackgroundOverlay?: boolean; + animation?: Animation; + } + ``` + + Hierarchy (view full) + + * OverlayAction + + OpenOverlay + + Referenced by: Action + members: + Properties: + type: |- + ``` + readonly type + ``` + + The action type + destination: |- + ``` + readonly destination: Board + ``` + + Overlay board that will be opened. + relativeTo: |- + ``` + readonly relativeTo?: Shape + ``` + + Base shape to which the overlay will be positioned taking constraints into account. + position: |- + ``` + readonly position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center" + ``` + + Positioning of the overlay. + manualPositionLocation: |- + ``` + readonly manualPositionLocation?: Point + ``` + + For `position = 'manual'` the location of the overlay. + closeWhenClickOutside: |- + ``` + readonly closeWhenClickOutside?: boolean + ``` + + When true the overlay will be closed when clicking outside + addBackgroundOverlay: |- + ``` + readonly addBackgroundOverlay?: boolean + ``` + + When true a background will be added to the overlay. + animation: |- + ``` + readonly animation?: Animation + ``` + + Animation displayed with this interaction. +OpenUrl: + overview: |- + Interface OpenUrl + ================= + + This action opens an URL in a new tab. + + ``` + interface OpenUrl { + type: "open-url"; + url: string; + } + ``` + + Referenced by: Action + members: + Properties: + type: |- + ``` + readonly type + ``` + + The action type + url: |- + ``` + readonly url: string + ``` + + The URL to open when the action is executed +OverlayAction: + overview: |- + Interface OverlayAction + ======================= + + Base type for the actions "open-overlay" and "toggle-overlay" that share most of their properties + + ``` + interface OverlayAction { + destination: Board; + relativeTo?: Shape; + position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center"; + manualPositionLocation?: Point; + closeWhenClickOutside?: boolean; + addBackgroundOverlay?: boolean; + animation?: Animation; + } + ``` + + Hierarchy (view full) + + * OverlayAction + + OpenOverlay + + ToggleOverlay + members: + Properties: + destination: |- + ``` + readonly destination: Board + ``` + + Overlay board that will be opened. + relativeTo: |- + ``` + readonly relativeTo?: Shape + ``` + + Base shape to which the overlay will be positioned taking constraints into account. + position: |- + ``` + readonly position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center" + ``` + + Positioning of the overlay. + manualPositionLocation: |- + ``` + readonly manualPositionLocation?: Point + ``` + + For `position = 'manual'` the location of the overlay. + closeWhenClickOutside: |- + ``` + readonly closeWhenClickOutside?: boolean + ``` + + When true the overlay will be closed when clicking outside + addBackgroundOverlay: |- + ``` + readonly addBackgroundOverlay?: boolean + ``` + + When true a background will be added to the overlay. + animation: |- + ``` + readonly animation?: Animation + ``` + + Animation displayed with this interaction. +Page: + overview: |- + Interface Page + ============== + + Page represents a page in the Penpot application. + It includes properties for the page's identifier and name, as well as methods for managing shapes on the page. + + ``` + interface Page { + id: string; + name: string; + rulerGuides: RulerGuide[]; + root: Shape; + getShapeById(id: string): null | Shape; + findShapes(criteria?: { + name?: string; + nameLike?: string; + type?: + | "boolean" + | "path" + | "ellipse" + | "image" + | "text" + | "group" + | "board" + | "rectangle" + | "svg-raw"; + }): Shape[]; + flows: Flow[]; + createFlow(name: string, board: Board): Flow; + removeFlow(flow: Flow): void; + addRulerGuide(orientation: RulerGuideOrientation, value: number, board?: Board): RulerGuide; + removeRulerGuide(guide: RulerGuide): void; + addCommentThread(content: string, position: Point): Promise; + removeCommentThread(commentThread: CommentThread): Promise; + findCommentThreads(criteria?: { + onlyYours: boolean; + showResolved: boolean; + }): Promise; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * PluginData + + Page + + Referenced by: Context, EventsMap, File, Flow, Penpot + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The `id` property is a unique identifier for the page. + name: |- + ``` + name: string + ``` + + The `name` property is the name of the page. + rulerGuides: |- + ``` + readonly rulerGuides: RulerGuide[] + ``` + + The ruler guides attached to the board + root: |- + ``` + root: Shape + ``` + + The root shape of the current page. Will be the parent shape of all the shapes inside the document. + Requires `content:read` permission. + flows: |- + ``` + readonly flows: Flow[] + ``` + + The interaction flows defined for the page. + Methods: + getShapeById: |- + ``` + getShapeById(id): null | Shape + ``` + + * Retrieves a shape by its unique identifier. + + Parameters + + id: string + + The unique identifier of the shape. + + Returns null | Shape + + Example + ``` + const shape = penpot.currentPage.getShapeById('shapeId'); + ``` + findShapes: |- + ``` + findShapes(criteria?): Shape[] + ``` + + * Finds all shapes on the page. + Optionaly it gets a criteria object to search for specific criteria + + Parameters + + criteria: { + name?: string; + nameLike?: string; + type?: + | "boolean" + | "path" + | "ellipse" + | "image" + | "text" + | "group" + | "board" + | "rectangle" + | "svg-raw"; + } + - Optionalname?: string + - OptionalnameLike?: string + - Optionaltype?: | "boolean" | "path" | "ellipse" | "image" | "text" | "group" | "board" | "rectangle" | "svg-raw" + + Returns Shape[] + + Example + ``` + const shapes = penpot.currentPage.findShapes({ name: 'exampleName' }); + ``` + createFlow: |- + ``` + createFlow(name, board): Flow + ``` + + * Creates a new flow in the page. + + Parameters + + name: string + + the name identifying the flow + + board: Board + + the starting board for the current flow + + Returns Flow + + Example + ``` + const flow = penpot.currentPage.createFlow('exampleFlow', board); + ``` + removeFlow: |- + ``` + removeFlow(flow): void + ``` + + * Removes the flow from the page + + Parameters + + flow: Flow + + the flow to be removed from the page + + Returns void + addRulerGuide: |- + ``` + addRulerGuide(orientation, value, board?): RulerGuide + ``` + + * Creates a new ruler guide. + + Parameters + + orientation: RulerGuideOrientation + + value: number + + board: Board + + Returns RulerGuide + removeRulerGuide: |- + ``` + removeRulerGuide(guide): void + ``` + + * Removes the `guide` from the current page. + + Parameters + + guide: RulerGuide + + Returns void + addCommentThread: |- + ``` + addCommentThread(content, position): Promise + ``` + + * Creates a new comment thread in the `position`. Optionaly adds + it into the `board`. + Returns the thread created. + Requires the `comment:write` permission. + + Parameters + + content: string + + position: Point + + Returns Promise + removeCommentThread: |- + ``` + removeCommentThread(commentThread): Promise + ``` + + * Removes the comment thread. + Requires the `comment:write` permission. + + Parameters + + commentThread: CommentThread + + Returns Promise + findCommentThreads: |- + ``` + findCommentThreads(criteria?): Promise + ``` + + * Find all the comments that match the criteria. + + + `onlyYours`: if `true` will return the threads where the current + user has engaged. + + `showResolved`: by default resolved comments will be hidden. If `true` + the resolved will be returned. + Requires the `comment:read` or `comment:write` permission. + + Parameters + + criteria: { + onlyYours: boolean; + showResolved: boolean; + } + - onlyYours: boolean + - showResolved: boolean + + Returns Promise + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +Path: + overview: |- + Interface Path + ============== + + Represents a path shape in Penpot. + This interface extends `ShapeBase` and includes properties and methods specific to paths. + + ``` + interface Path { + type: "path"; + toD(): string; + content: string; + d: string; + commands: PathCommand[]; + fills: Fill[]; + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Path + + Referenced by: Context, ContextTypesUtils, Penpot, Shape + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'path' for path shapes. + content: |- + ``` + content: string + ``` + + The content of the boolean shape, defined as the path string. + + Deprecated + + Use either `d` or `commands`. + d: |- + ``` + d: string + ``` + + The content of the boolean shape, defined as the path string. + commands: |- + ``` + commands: PathCommand[] + ``` + + The content of the boolean shape, defined as an array of path commands. + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + toD: |- + ``` + toD(): string + ``` + + * Converts the path shape to its path data representation. + + Returns string + + Returns the path data (d attribute) as a string. + + Deprecated + + Use the `d` attribute + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +PathCommand: + overview: |- + Interface PathCommand + ===================== + + Represents a path command in Penpot. + This interface includes a property for defining the type of command. + + ``` + interface PathCommand { + command: + | "M" + | "move-to" + | "Z" + | "close-path" + | "L" + | "line-to" + | "H" + | "line-to-horizontal" + | "V" + | "line-to-vertical" + | "C" + | "curve-to" + | "S" + | "smooth-curve-to" + | "Q" + | "quadratic-bezier-curve-to" + | "T" + | "smooth-quadratic-bezier-curve-to" + | "A" + | "elliptical-arc"; + params?: { + x?: number; + y?: number; + c1x?: number; + c1y?: number; + c2x?: number; + c2y?: number; + rx?: number; + ry?: number; + xAxisRotation?: number; + largeArcFlag?: boolean; + sweepFlag?: boolean; + }; + } + ``` + + Referenced by: Boolean, Path + members: + Properties: + command: |- + ``` + command: + | "M" + | "move-to" + | "Z" + | "close-path" + | "L" + | "line-to" + | "H" + | "line-to-horizontal" + | "V" + | "line-to-vertical" + | "C" + | "curve-to" + | "S" + | "smooth-curve-to" + | "Q" + | "quadratic-bezier-curve-to" + | "T" + | "smooth-quadratic-bezier-curve-to" + | "A" + | "elliptical-arc" + ``` + + The type of path command. + Possible values include: + + * 'M' or 'move-to': Move to a new point. + * 'Z' or 'close-path': Close the current path. + * 'L' or 'line-to': Draw a straight line to a new point. + * 'H' or 'line-to-horizontal': Draw a horizontal line to a new point. + * 'V' or 'line-to-vertical': Draw a vertical line to a new point. + * 'C' or 'curve-to': Draw a cubic Bezier curve to a new point. + * 'S' or 'smooth-curve-to': Draw a smooth cubic Bezier curve to a new point. + * 'Q' or 'quadratic-bezier-curve-to': Draw a quadratic Bezier curve to a new point. + * 'T' or 'smooth-quadratic-bezier-curve-to': Draw a smooth quadratic Bezier curve to a new point. + * 'A' or 'elliptical-arc': Draw an elliptical arc to a new point. + + Example + ``` + const pathCommand: PathCommand = { command: 'M', params: { x: 0, y: 0 } }; + ``` + params: |- + ``` + params?: { + x?: number; + y?: number; + c1x?: number; + c1y?: number; + c2x?: number; + c2y?: number; + rx?: number; + ry?: number; + xAxisRotation?: number; + largeArcFlag?: boolean; + sweepFlag?: boolean; + } + ``` + + Optional parameters associated with the path command. + + Type declaration + + * Optionalx?: number + + The x-coordinate of the point (or endpoint). + * Optionaly?: number + + The y-coordinate of the point (or endpoint). + * Optionalc1x?: number + + The x-coordinate of the first control point for curves. + * Optionalc1y?: number + + The y-coordinate of the first control point for curves. + * Optionalc2x?: number + + The x-coordinate of the second control point for curves. + * Optionalc2y?: number + + The y-coordinate of the second control point for curves. + * Optionalrx?: number + + The radius of the ellipse's x-axis. + * Optionalry?: number + + The radius of the ellipse's y-axis. + * OptionalxAxisRotation?: number + + The rotation angle of the ellipse's x-axis. + * OptionallargeArcFlag?: boolean + + A flag indicating whether to use the larger arc. + * OptionalsweepFlag?: boolean + + A flag indicating the direction of the arc. +PluginData: + overview: |- + Interface PluginData + ==================== + + Provides methods for managing plugin-specific data associated with a Penpot shape. + + ``` + interface PluginData { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + } + ``` + + Hierarchy (view full) + + * PluginData + + File + + Library + + LibraryElement + + Page + + ShapeBase + members: + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` +PreviousScreen: + overview: |- + Interface PreviousScreen + ======================== + + It takes back to the last board shown. + + ``` + interface PreviousScreen { + type: "previous-screen"; + } + ``` + + Referenced by: Action + members: + Properties: + type: |- + ``` + readonly type + ``` + + The action type +Push: + overview: |- + Interface Push + ============== + + Push animation + + ``` + interface Push { + type: "push"; + direction: + | "left" + | "right" + | "up" + | "down"; + duration: number; + easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out"; + } + ``` + + Referenced by: Animation + members: + Properties: + type: |- + ``` + readonly type + ``` + + Type of the animation + direction: |- + ``` + readonly direction: + | "left" + | "right" + | "up" + | "down" + ``` + + Direction for the push animation + duration: |- + ``` + readonly duration: number + ``` + + Duration of the animation effect + easing: |- + ``` + readonly easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out" + ``` + + Function that the dissolve effect will follow for the interpolation. + Defaults to `linear` +Rectangle: + overview: |- + Interface Rectangle + =================== + + Represents a rectangle shape in Penpot. + This interface extends `ShapeBase` and includes properties specific to rectangles. + + ``` + interface Rectangle { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + type: "rectangle"; + fills: Fill[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Rectangle + + Referenced by: Context, ContextTypesUtils, Penpot, Shape + members: + Properties: + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'rect' for rectangle shapes. + fills: |- + ``` + fills: Fill[] + ``` + + The fills applied to the shape. + + Overrides ShapeBase.fills + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +RulerGuide: + overview: |- + Interface RulerGuide + ==================== + + Represents a ruler guide. These are horizontal or vertical lines that can be + used to position elements in the UI. + + ``` + interface RulerGuide { + orientation: RulerGuideOrientation; + position: number; + board?: Board; + } + ``` + + Referenced by: Board, Page, VariantContainer + members: + Properties: + orientation: |- + ``` + readonly orientation: RulerGuideOrientation + ``` + + `orientation` indicates whether the ruler is either `horizontal` or `vertical` + position: |- + ``` + position: number + ``` + + `position` is the position in the axis in absolute positioning. If this is a board + guide will return the positioning relative to the board. + board: |- + ``` + board?: Board + ``` + + If the guide is attached to a board this will retrieve the board shape +Shadow: + overview: |- + Interface Shadow + ================ + + Represents shadow properties in Penpot. + This interface includes properties for defining drop shadows and inner shadows, along with their visual attributes. + + ``` + interface Shadow { + id?: string; + style?: "drop-shadow" | "inner-shadow"; + offsetX?: number; + offsetY?: number; + blur?: number; + spread?: number; + hidden?: boolean; + color?: Color; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + id: |- + ``` + id?: string + ``` + + The optional unique identifier for the shadow. + style: |- + ``` + style?: "drop-shadow" | "inner-shadow" + ``` + + The optional style of the shadow. + + * 'drop-shadow': A shadow cast outside the element. + * 'inner-shadow': A shadow cast inside the element. + offsetX: |- + ``` + offsetX?: number + ``` + + The optional X-axis offset of the shadow. + offsetY: |- + ``` + offsetY?: number + ``` + + The optional Y-axis offset of the shadow. + blur: |- + ``` + blur?: number + ``` + + The optional blur radius of the shadow. + spread: |- + ``` + spread?: number + ``` + + The optional spread radius of the shadow. + hidden: |- + ``` + hidden?: boolean + ``` + + Specifies whether the shadow is hidden. + Defaults to false if omitted. + color: |- + ``` + color?: Color + ``` + + The optional color of the shadow, defined by a Color object. +ShapeBase: + overview: |- + Interface ShapeBase + =================== + + Represents the base properties and methods of a shape in Penpot. + This interface provides common properties and methods shared by all shapes. + + ``` + interface ShapeBase { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + } + ``` + + Hierarchy (view full) + + * PluginData + + ShapeBase + - Board + - Boolean + - Ellipse + - Group + - Image + - Path + - Rectangle + - SvgRaw + - Text + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + fills: |- + ``` + fills: Fill[] | "mixed" + ``` + + The fills applied to the shape. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +Slide: + overview: |- + Interface Slide + =============== + + Slide animation + + ``` + interface Slide { + type: "slide"; + way: "in" | "out"; + direction: + | "left" + | "right" + | "up" + | "down"; + duration: number; + offsetEffect?: boolean; + easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out"; + } + ``` + + Referenced by: Animation + members: + Properties: + type: |- + ``` + readonly type + ``` + + Type of the animation. + way: |- + ``` + readonly way: "in" | "out" + ``` + + Indicate if the slide will be either in-to-out `in` or out-to-in `out`. + direction: |- + ``` + readonly direction: + | "left" + | "right" + | "up" + | "down" + ``` + + Direction for the slide animation. + duration: |- + ``` + readonly duration: number + ``` + + Duration of the animation effect. + offsetEffect: |- + ``` + readonly offsetEffect?: boolean + ``` + + If `true` the offset effect will be used. + easing: |- + ``` + readonly easing?: + | "linear" + | "ease" + | "ease-in" + | "ease-out" + | "ease-in-out" + ``` + + Function that the dissolve effect will follow for the interpolation. + Defaults to `linear`. +Stroke: + overview: |- + Interface Stroke + ================ + + Represents stroke properties in Penpot. You can add a stroke to any shape except for groups. + This interface includes properties for defining the color, style, width, alignment, and caps of a stroke. + + ``` + interface Stroke { + strokeColor?: string; + strokeColorRefFile?: string; + strokeColorRefId?: string; + strokeOpacity?: number; + strokeStyle?: + | "svg" + | "none" + | "mixed" + | "solid" + | "dotted" + | "dashed"; + strokeWidth?: number; + strokeAlignment?: "center" | "inner" | "outer"; + strokeCapStart?: StrokeCap; + strokeCapEnd?: StrokeCap; + strokeColorGradient?: Gradient; + } + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, LibraryColor, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: + Properties: + strokeColor: |- + ``` + strokeColor?: string + ``` + + The optional color of the stroke, represented as a string (e.g., '#FF5733'). + strokeColorRefFile: |- + ``` + strokeColorRefFile?: string + ``` + + The optional reference to an external file for the stroke color. + strokeColorRefId: |- + ``` + strokeColorRefId?: string + ``` + + The optional reference ID within the external file for the stroke color. + strokeOpacity: |- + ``` + strokeOpacity?: number + ``` + + The optional opacity level of the stroke color, ranging from 0 (fully transparent) to 1 (fully opaque). + Defaults to 1 if omitted. + strokeStyle: |- + ``` + strokeStyle?: + | "svg" + | "none" + | "mixed" + | "solid" + | "dotted" + | "dashed" + ``` + + The optional style of the stroke. + strokeWidth: |- + ``` + strokeWidth?: number + ``` + + The optional width of the stroke. + strokeAlignment: |- + ``` + strokeAlignment?: "center" | "inner" | "outer" + ``` + + The optional alignment of the stroke relative to the shape's boundary. + strokeCapStart: |- + ``` + strokeCapStart?: StrokeCap + ``` + + The optional cap style for the start of the stroke. + strokeCapEnd: |- + ``` + strokeCapEnd?: StrokeCap + ``` + + The optional cap style for the end of the stroke. + strokeColorGradient: |- + ``` + strokeColorGradient?: Gradient + ``` + + The optional gradient stroke defined by a Gradient object. +SvgRaw: + overview: |- + Interface SvgRaw + ================ + + Represents an SVG raw shape in Penpot. + This interface extends `ShapeBase` and includes properties specific to raw SVG shapes. + + ``` + interface SvgRaw { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + type: "svg-raw"; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + SvgRaw + + Referenced by: ContextTypesUtils, Shape + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + fills: |- + ``` + fills: Fill[] | "mixed" + ``` + + The fills applied to the shape. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + type: |- + ``` + type + ``` + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void +Text: + overview: |- + Interface Text + ============== + + Text represents a text element in the Penpot application, extending the base shape interface. + It includes various properties to define the text content and its styling attributes. + + ``` + interface Text { + getPluginData(key: string): string; + setPluginData(key: string, value: string): void; + getPluginDataKeys(): string[]; + getSharedPluginData(namespace: string, key: string): string; + setSharedPluginData(namespace: string, key: string, value: string): void; + getSharedPluginDataKeys(namespace: string): string[]; + id: string; + name: string; + parent: null | Shape; + parentIndex: number; + x: number; + y: number; + width: number; + height: number; + bounds: Bounds; + center: Point; + blocked: boolean; + hidden: boolean; + visible: boolean; + proportionLock: boolean; + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale"; + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom"; + borderRadius: number; + borderRadiusTopLeft: number; + borderRadiusTopRight: number; + borderRadiusBottomRight: number; + borderRadiusBottomLeft: number; + opacity: number; + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity"; + shadows: Shadow[]; + blur?: Blur; + exports: Export[]; + boardX: number; + boardY: number; + parentX: number; + parentY: number; + flipX: boolean; + flipY: boolean; + rotation: number; + fills: Fill[] | "mixed"; + strokes: Stroke[]; + layoutChild?: LayoutChildProperties; + layoutCell?: LayoutChildProperties; + setParentIndex(index: number): void; + isComponentInstance(): boolean; + isComponentMainInstance(): boolean; + isComponentCopyInstance(): boolean; + isComponentRoot(): boolean; + isComponentHead(): boolean; + componentRefShape(): null | Shape; + componentRoot(): null | Shape; + componentHead(): null | Shape; + component(): null | LibraryComponent; + detach(): void; + swapComponent(component: LibraryComponent): void; + switchVariant(pos: number, value: string): void; + combineAsVariants(ids: string[]): void; + isVariantHead(): boolean; + resize(width: number, height: number): void; + rotate(angle: number, center?: null | { + x: number; + y: number; + }): void; + bringToFront(): void; + bringForward(): void; + sendToBack(): void; + sendBackward(): void; + export(config: Export): Promise; + interactions: Interaction[]; + addInteraction(trigger: Trigger, action: Action, delay?: number): Interaction; + removeInteraction(interaction: Interaction): void; + clone(): Shape; + remove(): void; + type: "text"; + characters: string; + growType: "fixed" | "auto-width" | "auto-height"; + fontId: string; + fontFamily: string; + fontVariantId: string; + fontSize: string; + fontWeight: string; + fontStyle: + | null + | "normal" + | "italic" + | "mixed"; + lineHeight: string; + letterSpacing: string; + textTransform: + | null + | "mixed" + | "uppercase" + | "capitalize" + | "lowercase"; + textDecoration: + | null + | "mixed" + | "underline" + | "line-through"; + direction: + | null + | "mixed" + | "ltr" + | "rtl"; + align: + | null + | "center" + | "left" + | "right" + | "mixed" + | "justify"; + verticalAlign: + | null + | "center" + | "top" + | "bottom"; + getRange(start: number, end: number): TextRange; + applyTypography(typography: LibraryTypography): void; + } + ``` + + Hierarchy (view full) + + * ShapeBase + + Text + + Referenced by: Context, ContextTypesUtils, Font, Penpot, Shape, TextRange + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the shape. + name: |- + ``` + name: string + ``` + + The name of the shape. + parent: |- + ``` + readonly parent: null | Shape + ``` + + The parent shape. If the shape is the first level the parent will be the root shape. + For the root shape the parent is null + parentIndex: |- + ``` + readonly parentIndex: number + ``` + + Returns the index of the current shape in the parent + x: |- + ``` + x: number + ``` + + The x-coordinate of the shape's position. + y: |- + ``` + y: number + ``` + + The y-coordinate of the shape's position. + width: |- + ``` + readonly width: number + ``` + + The width of the shape. + height: |- + ``` + readonly height: number + ``` + + The height of the shape. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + Returns + + Returns the bounding box surrounding the current shape + center: |- + ``` + readonly center: Point + ``` + + Returns + + Returns the geometric center of the shape + blocked: |- + ``` + blocked: boolean + ``` + + Indicates whether the shape is blocked. + hidden: |- + ``` + hidden: boolean + ``` + + Indicates whether the shape is hidden. + visible: |- + ``` + visible: boolean + ``` + + Indicates whether the shape is visible. + proportionLock: |- + ``` + proportionLock: boolean + ``` + + Indicates whether the shape has proportion lock enabled. + constraintsHorizontal: |- + ``` + constraintsHorizontal: + | "center" + | "left" + | "right" + | "leftright" + | "scale" + ``` + + The horizontal constraints applied to the shape. + constraintsVertical: |- + ``` + constraintsVertical: + | "center" + | "top" + | "bottom" + | "scale" + | "topbottom" + ``` + + The vertical constraints applied to the shape. + borderRadius: |- + ``` + borderRadius: number + ``` + + The border radius of the shape. + borderRadiusTopLeft: |- + ``` + borderRadiusTopLeft: number + ``` + + The border radius of the top-left corner of the shape. + borderRadiusTopRight: |- + ``` + borderRadiusTopRight: number + ``` + + The border radius of the top-right corner of the shape. + borderRadiusBottomRight: |- + ``` + borderRadiusBottomRight: number + ``` + + The border radius of the bottom-right corner of the shape. + borderRadiusBottomLeft: |- + ``` + borderRadiusBottomLeft: number + ``` + + The border radius of the bottom-left corner of the shape. + opacity: |- + ``` + opacity: number + ``` + + The opacity of the shape. + blendMode: |- + ``` + blendMode: + | "difference" + | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "color-dodge" + | "overlay" + | "soft-light" + | "hard-light" + | "exclusion" + | "hue" + | "saturation" + | "color" + | "luminosity" + ``` + + The blend mode applied to the shape. + shadows: |- + ``` + shadows: Shadow[] + ``` + + The shadows applied to the shape. + blur: |- + ``` + blur?: Blur + ``` + + The blur effect applied to the shape. + exports: |- + ``` + exports: Export[] + ``` + + The export settings of the shape. + boardX: |- + ``` + boardX: number + ``` + + The x-coordinate of the shape relative to its board. + boardY: |- + ``` + boardY: number + ``` + + The y-coordinate of the shape relative to its board. + parentX: |- + ``` + parentX: number + ``` + + The x-coordinate of the shape relative to its parent. + parentY: |- + ``` + parentY: number + ``` + + The y-coordinate of the shape relative to its parent. + flipX: |- + ``` + flipX: boolean + ``` + + Indicates whether the shape is flipped horizontally. + flipY: |- + ``` + flipY: boolean + ``` + + Indicates whether the shape is flipped vertically. + rotation: |- + ``` + rotation: number + ``` + + Returns + + Returns the rotation in degrees of the shape with respect to it's center. + fills: |- + ``` + fills: Fill[] | "mixed" + ``` + + The fills applied to the shape. + strokes: |- + ``` + strokes: Stroke[] + ``` + + The strokes applied to the shape. + layoutChild: |- + ``` + readonly layoutChild?: LayoutChildProperties + ``` + + Layout properties for children of the shape. + layoutCell: |- + ``` + readonly layoutCell?: LayoutChildProperties + ``` + + Layout properties for cells in a grid layout. + interactions: |- + ``` + readonly interactions: Interaction[] + ``` + + The interactions for the current shape. + type: |- + ``` + readonly type + ``` + + The type of the shape, which is always 'text' for text shapes. + characters: |- + ``` + characters: string + ``` + + The characters contained within the text shape. + growType: |- + ``` + growType: "fixed" | "auto-width" | "auto-height" + ``` + + The grow type of the text shape, defining how the text box adjusts its size. + Possible values are: + + * 'fixed': Fixed size. + * 'auto-width': Adjusts width automatically. + * 'auto-height': Adjusts height automatically. + fontId: |- + ``` + fontId: string + ``` + + The font ID used in the text shape, or 'mixed' if multiple fonts are used. + fontFamily: |- + ``` + fontFamily: string + ``` + + The font family used in the text shape, or 'mixed' if multiple font families are used. + fontVariantId: |- + ``` + fontVariantId: string + ``` + + The font variant ID used in the text shape, or 'mixed' if multiple font variants are used. + fontSize: |- + ``` + fontSize: string + ``` + + The font size used in the text shape, or 'mixed' if multiple font sizes are used. + fontWeight: |- + ``` + fontWeight: string + ``` + + The font weight used in the text shape, or 'mixed' if multiple font weights are used. + fontStyle: |- + ``` + fontStyle: + | null + | "normal" + | "italic" + | "mixed" + ``` + + The font style used in the text shape, or 'mixed' if multiple font styles are used. + lineHeight: |- + ``` + lineHeight: string + ``` + + The line height used in the text shape, or 'mixed' if multiple line heights are used. + letterSpacing: |- + ``` + letterSpacing: string + ``` + + The letter spacing used in the text shape, or 'mixed' if multiple letter spacings are used. + textTransform: |- + ``` + textTransform: + | null + | "mixed" + | "uppercase" + | "capitalize" + | "lowercase" + ``` + + The text transform applied to the text shape, or 'mixed' if multiple text transforms are used. + textDecoration: |- + ``` + textDecoration: + | null + | "mixed" + | "underline" + | "line-through" + ``` + + The text decoration applied to the text shape, or 'mixed' if multiple text decorations are used. + direction: |- + ``` + direction: + | null + | "mixed" + | "ltr" + | "rtl" + ``` + + The text direction for the text shape, or 'mixed' if multiple directions are used. + align: |- + ``` + align: + | null + | "center" + | "left" + | "right" + | "mixed" + | "justify" + ``` + + The horizontal alignment of the text shape. It can be a specific alignment or 'mixed' if multiple alignments are used. + verticalAlign: |- + ``` + verticalAlign: + | null + | "center" + | "top" + | "bottom" + ``` + + The vertical alignment of the text shape. It can be a specific alignment or 'mixed' if multiple alignments are used. + Methods: + getPluginData: |- + ``` + getPluginData(key): string + ``` + + * Retrieves the data for our own plugin, given a specific key. + + Parameters + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the data associated with the key as a string. + + Example + ``` + const data = shape.getPluginData('exampleKey');console.log(data); + ``` + setPluginData: |- + ``` + setPluginData(key, value): void + ``` + + * Sets the plugin-specific data for the given key. + + Parameters + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setPluginData('exampleKey', 'exampleValue'); + ``` + getPluginDataKeys: |- + ``` + getPluginDataKeys(): string[] + ``` + + * Retrieves all the keys for the plugin-specific data. + + Returns string[] + + Returns an array of strings representing all the keys. + + Example + ``` + const keys = shape.getPluginDataKeys();console.log(keys); + ``` + getSharedPluginData: |- + ``` + getSharedPluginData(namespace, key): string + ``` + + * If we know the namespace of an external plugin, this is the way to get their data. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to retrieve the data. + + Returns string + + Returns the shared data associated with the key as a string. + + Example + ``` + const sharedData = shape.getSharedPluginData('exampleNamespace', 'exampleKey');console.log(sharedData); + ``` + setSharedPluginData: |- + ``` + setSharedPluginData(namespace, key, value): void + ``` + + * Sets the shared plugin-specific data for the given namespace and key. + + Parameters + + namespace: string + + The namespace for the shared data. + + key: string + + The key for which to set the data. + + value: string + + The data to set for the key. + + Returns void + + Example + ``` + shape.setSharedPluginData('exampleNamespace', 'exampleKey', 'exampleValue'); + ``` + getSharedPluginDataKeys: |- + ``` + getSharedPluginDataKeys(namespace): string[] + ``` + + * Retrieves all the keys for the shared plugin-specific data in the given namespace. + + Parameters + + namespace: string + + The namespace for the shared data. + + Returns string[] + + Returns an array of strings representing all the keys in the namespace. + + Example + ``` + const sharedKeys = shape.getSharedPluginDataKeys('exampleNamespace');console.log(sharedKeys); + ``` + setParentIndex: |- + ``` + setParentIndex(index): void + ``` + + * Changes the index inside the parent of the current shape. + This method will shift the indexes of the shapes around that position to + match the index. + If the index is greater than the number of elements it will positioned last. + + Parameters + + index: number + + the new index for the shape to be in + + Returns void + isComponentInstance: |- + ``` + isComponentInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component instance + isComponentMainInstance: |- + ``` + isComponentMainInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **main** instance + isComponentCopyInstance: |- + ``` + isComponentCopyInstance(): boolean + ``` + + * Returns boolean + + Returns true if the current shape is inside a component **copy** instance + isComponentRoot: |- + ``` + isComponentRoot(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the root of a component tree + isComponentHead: |- + ``` + isComponentHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure + componentRefShape: |- + ``` + componentRefShape(): null | Shape + ``` + + * Returns null | Shape + + Returns the equivalent shape in the component main instance. If the current shape is inside a + main instance will return `null`; + componentRoot: |- + ``` + componentRoot(): null | Shape + ``` + + * Returns null | Shape + + Returns the root of the component tree structure for the current shape. If the current shape + is already a root will return itself. + componentHead: |- + ``` + componentHead(): null | Shape + ``` + + * Returns null | Shape + + Returns the head of the component tree structure for the current shape. If the current shape + is already a head will return itself. + component: |- + ``` + component(): null | LibraryComponent + ``` + + * Returns null | LibraryComponent + + If the shape is a component instance, returns the reference to the component associated + otherwise will return null + detach: |- + ``` + detach(): void + ``` + + * If the current shape is a component it will remove the component information and leave the + shape as a "basic shape" + + Returns void + swapComponent: |- + ``` + swapComponent(component): void + ``` + + * TODO + + Parameters + + component: LibraryComponent + + Returns void + switchVariant: |- + ``` + switchVariant(pos, value): void + ``` + + * Switch a VariantComponent copy to the nearest one that has the specified property value + + Parameters + + pos: number + + The position of the poroperty to update + + value: string + + The new value of the property + + Returns void + combineAsVariants: |- + ``` + combineAsVariants(ids): void + ``` + + * Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu + on the Penpot interface. + The current shape must be a component main instance. + + Parameters + + ids: string[] + + A list of ids of the main instances of the components to combine with this one. + + Returns void + isVariantHead: |- + ``` + isVariantHead(): boolean + ``` + + * Returns boolean + + Returns true when the current shape is the head of a components tree nested structure, + and that component is a VariantComponent + resize: |- + ``` + resize(width, height): void + ``` + + * Resizes the shape to the specified width and height. + + Parameters + + width: number + + The new width of the shape. + + height: number + + The new height of the shape. + + Returns void + + Example + ``` + shape.resize(200, 100); + ``` + rotate: |- + ``` + rotate(angle, center?): void + ``` + + * Rotates the shape in relation with the given center. + + Parameters + + angle: number + + Angle in degrees to rotate. + + center: null | { + x: number; + y: number; + } + + Center of the transform rotation. If not send will use the geometri center of the shapes. + + Returns void + + Example + ``` + shape.rotate(45); + ``` + bringToFront: |- + ``` + bringToFront(): void + ``` + + * Moves the current shape to the front of its siblings + + Returns void + bringForward: |- + ``` + bringForward(): void + ``` + + * Moves the current shape one position forward in its list of siblings + + Returns void + sendToBack: |- + ``` + sendToBack(): void + ``` + + * Moves the current shape to the back of its siblings + + Returns void + sendBackward: |- + ``` + sendBackward(): void + ``` + + * Moves the current shape one position backwards in its list of siblings + + Returns void + export: |- + ``` + export(config): Promise + ``` + + * Generates an export from the current shape. + + Parameters + + config: Export + + Returns Promise + + Example + ``` + shape.export({ type: 'png', scale: 2 }); + ``` + addInteraction: |- + ``` + addInteraction(trigger, action, delay?): Interaction + ``` + + * Adds a new interaction to the shape. + + Parameters + + trigger: Trigger + + defines the conditions under which the action will be triggered + + action: Action + + defines what will be executed when the trigger happens + + delay: number + + for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise. + + Returns Interaction + + Example + ``` + shape.addInteraction('click', { type: 'navigate-to', destination: anotherBoard }); + ``` + removeInteraction: |- + ``` + removeInteraction(interaction): void + ``` + + * Removes the interaction from the shape. + + Parameters + + interaction: Interaction + + is the interaction to remove from the shape + + Returns void + + Example + ``` + shape.removeInteraction(interaction); + ``` + clone: |- + ``` + clone(): Shape + ``` + + * Creates a clone of the shape. + + Returns Shape + + Returns a new instance of the shape with identical properties. + remove: |- + ``` + remove(): void + ``` + + * Removes the shape from its parent. + + Returns void + getRange: |- + ``` + getRange(start, end): TextRange + ``` + + * Gets a text range within the text shape. + + Parameters + + start: number + + The start index of the text range. + + end: number + + The end index of the text range. + + Returns TextRange + + Returns a TextRange object representing the specified text range. + + Example + ``` + const textRange = textShape.getRange(0, 10);console.log(textRange.characters); + ``` + applyTypography: |- + ``` + applyTypography(typography): void + ``` + + * Applies a typography style to the text shape. + + Parameters + + typography: LibraryTypography + + The typography style to apply. + + Returns void + + Remarks + + This method sets various typography properties for the text shape according to the given typography style. + + Example + ``` + textShape.applyTypography(typography); + ``` +TextRange: + overview: |- + Interface TextRange + =================== + + Represents a range of text within a Text shape. + This interface provides properties for styling and formatting text ranges. + + ``` + interface TextRange { + shape: Text; + characters: string; + fontId: string; + fontFamily: string; + fontVariantId: string; + fontSize: string; + fontWeight: string; + fontStyle: + | null + | "normal" + | "italic" + | "mixed"; + lineHeight: string; + letterSpacing: string; + textTransform: + | null + | "none" + | "mixed" + | "uppercase" + | "capitalize" + | "lowercase"; + textDecoration: + | null + | "none" + | "mixed" + | "underline" + | "line-through"; + direction: + | null + | "mixed" + | "ltr" + | "rtl"; + fills: Fill[] | "mixed"; + align: + | null + | "center" + | "left" + | "right" + | "mixed" + | "justify"; + verticalAlign: + | null + | "center" + | "top" + | "bottom" + | "mixed"; + applyTypography(typography: LibraryTypography): void; + } + ``` + + Referenced by: Font, LibraryTypography, Text + members: + Properties: + shape: |- + ``` + readonly shape: Text + ``` + + The Text shape to which this text range belongs. + characters: |- + ``` + readonly characters: string + ``` + + The characters associated with the current text range. + fontId: |- + ``` + fontId: string + ``` + + The font ID of the text range. It can be a specific font ID or 'mixed' if multiple fonts are used. + fontFamily: |- + ``` + fontFamily: string + ``` + + The font family of the text range. It can be a specific font family or 'mixed' if multiple font families are used. + fontVariantId: |- + ``` + fontVariantId: string + ``` + + The font variant ID of the text range. It can be a specific font variant ID or 'mixed' if multiple font variants are used. + fontSize: |- + ``` + fontSize: string + ``` + + The font size of the text range. It can be a specific font size or 'mixed' if multiple font sizes are used. + fontWeight: |- + ``` + fontWeight: string + ``` + + The font weight of the text range. It can be a specific font weight or 'mixed' if multiple font weights are used. + fontStyle: |- + ``` + fontStyle: + | null + | "normal" + | "italic" + | "mixed" + ``` + + The font style of the text range. It can be a specific font style or 'mixed' if multiple font styles are used. + lineHeight: |- + ``` + lineHeight: string + ``` + + The line height of the text range. It can be a specific line height or 'mixed' if multiple line heights are used. + letterSpacing: |- + ``` + letterSpacing: string + ``` + + The letter spacing of the text range. It can be a specific letter spacing or 'mixed' if multiple letter spacings are used. + textTransform: |- + ``` + textTransform: + | null + | "none" + | "mixed" + | "uppercase" + | "capitalize" + | "lowercase" + ``` + + The text transform applied to the text range. It can be a specific text transform or 'mixed' if multiple text transforms are used. + textDecoration: |- + ``` + textDecoration: + | null + | "none" + | "mixed" + | "underline" + | "line-through" + ``` + + The text decoration applied to the text range. It can be a specific text decoration or 'mixed' if multiple text decorations are used. + direction: |- + ``` + direction: + | null + | "mixed" + | "ltr" + | "rtl" + ``` + + The text direction for the text range. It can be a specific direction or 'mixed' if multiple directions are used. + fills: |- + ``` + fills: Fill[] | "mixed" + ``` + + The fill styles applied to the text range. + align: |- + ``` + align: + | null + | "center" + | "left" + | "right" + | "mixed" + | "justify" + ``` + + The horizontal alignment of the text range. It can be a specific alignment or 'mixed' if multiple alignments are used. + verticalAlign: |- + ``` + verticalAlign: + | null + | "center" + | "top" + | "bottom" + | "mixed" + ``` + + The vertical alignment of the text range. It can be a specific alignment or 'mixed' if multiple alignments are used. + Methods: + applyTypography: |- + ``` + applyTypography(typography): void + ``` + + * Applies a typography style to the text range. + This method sets various typography properties for the text range according to the given typography style. + + Parameters + + typography: LibraryTypography + + The typography style to apply. + + Returns void + + Example + ``` + textRange.applyTypography(typography); + ``` +ToggleOverlay: + overview: |- + Interface ToggleOverlay + ======================= + + It opens an overlay if it is not already opened or closes it if it is already opened. + + ``` + interface ToggleOverlay { + destination: Board; + relativeTo?: Shape; + position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center"; + manualPositionLocation?: Point; + closeWhenClickOutside?: boolean; + addBackgroundOverlay?: boolean; + animation?: Animation; + type: "toggle-overlay"; + } + ``` + + Hierarchy (view full) + + * OverlayAction + + ToggleOverlay + + Referenced by: Action + members: + Properties: + destination: |- + ``` + readonly destination: Board + ``` + + Overlay board that will be opened. + relativeTo: |- + ``` + readonly relativeTo?: Shape + ``` + + Base shape to which the overlay will be positioned taking constraints into account. + position: |- + ``` + readonly position?: + | "center" + | "manual" + | "top-left" + | "top-right" + | "top-center" + | "bottom-left" + | "bottom-right" + | "bottom-center" + ``` + + Positioning of the overlay. + manualPositionLocation: |- + ``` + readonly manualPositionLocation?: Point + ``` + + For `position = 'manual'` the location of the overlay. + closeWhenClickOutside: |- + ``` + readonly closeWhenClickOutside?: boolean + ``` + + When true the overlay will be closed when clicking outside + addBackgroundOverlay: |- + ``` + readonly addBackgroundOverlay?: boolean + ``` + + When true a background will be added to the overlay. + animation: |- + ``` + readonly animation?: Animation + ``` + + Animation displayed with this interaction. + type: |- + ``` + readonly type + ``` + + The action type +Track: + overview: |- + Interface Track + =============== + + Represents a track configuration in Penpot. + This interface includes properties for defining the type and value of a track used in layout configurations. + + ``` + interface Track { + type: TrackType; + value: null | number; + } + ``` + + Referenced by: GridLayout + members: + Properties: + type: |- + ``` + type: TrackType + ``` + + The type of the track. + This can be one of the following values: + + * 'flex': A flexible track type. + * 'fixed': A fixed track type. + * 'percent': A track type defined by a percentage. + * 'auto': An automatic track type. + value: |- + ``` + value: null | number + ``` + + The value of the track. + This can be a number representing the size of the track, or null if not applicable. +User: + overview: |- + Interface User + ============== + + Represents a user in Penpot. + + ``` + interface User { + id: string; + name?: string; + avatarUrl?: string; + color: string; + sessionId?: string; + } + ``` + + Hierarchy (view full) + + * User + + ActiveUser + + Referenced by: Comment, CommentThread, Context, File, FileVersion, Penpot + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the user. + + Example + ``` + const userId = user.id;console.log(userId); + ``` + name: |- + ``` + readonly name?: string + ``` + + The name of the user. + + Example + ``` + const userName = user.name;console.log(userName); + ``` + avatarUrl: |- + ``` + readonly avatarUrl?: string + ``` + + The URL of the user's avatar image. + + Example + ``` + const avatarUrl = user.avatarUrl;console.log(avatarUrl); + ``` + color: |- + ``` + readonly color: string + ``` + + The color associated with the user. + + Example + ``` + const userColor = user.color;console.log(userColor); + ``` + sessionId: |- + ``` + readonly sessionId?: string + ``` + + The session ID of the user. + + Example + ``` + const sessionId = user.sessionId;console.log(sessionId); + ``` +Variants: + overview: |- + Interface Variants + ================== + + TODO + + ``` + interface Variants { + id: string; + libraryId: string; + properties: string[]; + currentValues(property: string): string[]; + removeProperty(pos: number): void; + renameProperty(pos: number, name: string): void; + variantComponents(): LibraryComponent[]; + addVariant(): void; + addProperty(): void; + } + ``` + + Referenced by: LibraryVariantComponent, VariantContainer + members: + Properties: + id: |- + ``` + readonly id: string + ``` + + The unique identifier of the variant element. It is the id of the VariantContainer, and all the VariantComponents + that belong to this variant have an attribute variantId which this is as value. + libraryId: |- + ``` + readonly libraryId: string + ``` + + The unique identifier of the library to which the variant belongs. + properties: |- + ``` + properties: string[] + ``` + + A list with the names of the properties of the Variant + Methods: + currentValues: |- + ``` + currentValues(property): string[] + ``` + + * A list of all the values of a property along all the variantComponents of this Variant + + Parameters + + property: string + + The name of the property + + Returns string[] + removeProperty: |- + ``` + removeProperty(pos): void + ``` + + * Remove a property of the Variant + + Parameters + + pos: number + + The position of the property to remove + + Returns void + renameProperty: |- + ``` + renameProperty(pos, name): void + ``` + + * Rename a property of the Variant + + Parameters + + pos: number + + The position of the property to rename + + name: string + + The new name of the property + + Returns void + variantComponents: |- + ``` + variantComponents(): LibraryComponent[] + ``` + + * List all the VariantComponents on this Variant. + + Returns LibraryComponent[] + addVariant: |- + ``` + addVariant(): void + ``` + + * Creates a duplicate of the main VariantComponent of this Variant + + Returns void + addProperty: |- + ``` + addProperty(): void + ``` + + * Adds a new property to this Variant + + Returns void +Viewport: + overview: |- + Interface Viewport + ================== + + Viewport represents the viewport in the Penpot application. + It includes the center point, zoom level, and the bounds of the viewport. + + ``` + interface Viewport { + center: Point; + zoom: number; + bounds: Bounds; + zoomReset(): void; + zoomToFitAll(): void; + zoomIntoView(shapes: Shape[]): void; + } + ``` + + Referenced by: Context, Penpot + members: + Properties: + center: |- + ``` + center: Point + ``` + + the `center` point of the current viewport. If changed will change the + viewport position. + zoom: |- + ``` + zoom: number + ``` + + the `zoom` level as a number where `1` represents 100%. + bounds: |- + ``` + readonly bounds: Bounds + ``` + + the `bounds` are the current coordinates of the viewport. + Methods: + zoomReset: |- + ``` + zoomReset(): void + ``` + + * Resets the zoom level. + + Returns void + zoomToFitAll: |- + ``` + zoomToFitAll(): void + ``` + + * Changes the viewport and zoom so can fit all the current shapes in the page. + + Returns void + zoomIntoView: |- + ``` + zoomIntoView(shapes): void + ``` + + * Changes the viewport and zoom so all the `shapes` in the argument are + visible. + + Parameters + + shapes: Shape[] + + Returns void +Action: + overview: |- + Type Alias Action + ================= + + ``` + Action: + | NavigateTo + | OpenOverlay + | ToggleOverlay + | CloseOverlay + | PreviousScreen + | OpenUrl + ``` + + Type for all the possible types of actions in an interaction. + + Referenced by: Board, Boolean, Ellipse, Group, Image, Interaction, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: {} +Animation: + overview: |- + Type Alias Animation + ==================== + + ``` + Animation: Dissolve | Slide | Push + ``` + + Type of all the animations that can be added to an interaction. + + Referenced by: CloseOverlay, NavigateTo, OpenOverlay, OverlayAction, ToggleOverlay + members: {} +BooleanType: + overview: |- + Type Alias BooleanType + ====================== + + ``` + BooleanType: + | "union" + | "difference" + | "exclude" + | "intersection" + ``` + + Represents the boolean operation types available in Penpot. + These types define how shapes can be combined or modified using boolean operations. + + Referenced by: Context, Penpot + members: {} +Bounds: + overview: |- + Type Alias Bounds + ================= + + ``` + Bounds: { + x: number; + y: number; + width: number; + height: number; + } + ``` + + Bounds represents the boundaries of a rectangular area, + defined by the coordinates of the top-left corner and the dimensions of the rectangle. + + Type declaration + + * x: number + + Top-left x position of the rectangular area defined + * y: number + + Top-left y position of the rectangular area defined + * width: number + + Width of the represented area + * height: number + + Height of the represented area + + Example + ``` + const bounds = { x: 50, y: 50, width: 200, height: 100 };console.log(bounds); + ``` + + Referenced by: Board, Boolean, Ellipse, Group, Image, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer, Viewport + members: {} +Gradient: + overview: |- + Type Alias Gradient + =================== + + ``` + Gradient: { + type: "linear" | "radial"; + startX: number; + startY: number; + endX: number; + endY: number; + width: number; + stops: { + color: string; + opacity?: number; + offset: number; + }[]; + } + ``` + + Represents a gradient configuration in Penpot. + A gradient can be either linear or radial and includes properties to define its shape, position, and color stops. + + Type declaration + + * type: "linear" | "radial" + + Specifies the type of gradient. + + + 'linear': A gradient that transitions colors along a straight line. + + 'radial': A gradient that transitions colors radiating outward from a central point. + + Example + ``` + const gradient: Gradient = { type: 'linear', startX: 0, startY: 0, endX: 100, endY: 100, width: 100, stops: [{ color: '#FF5733', offset: 0 }] }; + ``` + * startX: number + + The X-coordinate of the starting point of the gradient. + * startY: number + + The Y-coordinate of the starting point of the gradient. + * endX: number + + The X-coordinate of the ending point of the gradient. + * endY: number + + The Y-coordinate of the ending point of the gradient. + * width: number + + The width of the gradient. For radial gradients, this could be interpreted as the radius. + * stops: { color: string; opacity?: number; offset: number; }[] + + An array of color stops that define the gradient. + + Referenced by: Color, Fill, LibraryColor, Stroke + members: {} +Guide: + overview: |- + Type Alias Guide + ================ + + ``` + Guide: GuideColumn | GuideRow | GuideSquare + ``` + + Represents a board guide in Penpot. + This type can be one of several specific board guide types: column, row, or square. + + Referenced by: Board, VariantContainer + members: {} +ImageData: + overview: |- + Type Alias ImageData + ==================== + + ``` + ImageData: { + name?: string; + width: number; + height: number; + mtype?: string; + id: string; + keepAspectRatio?: boolean; + data(): Promise; + } + ``` + + Represents image data in Penpot. + This includes properties for defining the image's dimensions, metadata, and aspect ratio handling. + + Type declaration + + * Optionalname?: string + + The optional name of the image. + * width: number + + The width of the image. + * height: number + + The height of the image. + * Optionalmtype?: string + + The optional media type of the image (e.g., 'image/png', 'image/jpeg'). + * id: string + + The unique identifier for the image. + * OptionalkeepAspectRatio?: boolean + + Whether to keep the aspect ratio of the image when resizing. + Defaults to false if omitted. + * data:function + + ``` + data(): Promise + ``` + + + Returns the imaged data as a byte array. + + Returns Promise + + Referenced by: Color, Context, Fill, LibraryColor, Penpot + members: {} +LibraryContext: + overview: |- + Type Alias LibraryContext + ========================= + + ``` + LibraryContext: { + local: Library; + connected: Library[]; + availableLibraries(): Promise; + connectLibrary(libraryId: string): Promise; + } + ``` + + Represents the context of Penpot libraries, including both local and connected libraries. + This type contains references to the local library and an array of connected libraries. + + Type declaration + + * Readonlylocal: Library + + The local library in the Penpot context. + + Example + ``` + const localLibrary = libraryContext.local; + ``` + * Readonlyconnected: Library[] + + An array of connected libraries in the Penpot context. + + Example + ``` + const connectedLibraries = libraryContext.connected; + ``` + * availableLibraries:function + + ``` + availableLibraries(): Promise + ``` + + + Retrieves a summary of available libraries that can be connected to. + + Returns Promise + + Returns a promise that resolves to an array of `LibrarySummary` objects representing available libraries. + + Example + ``` + const availableLibraries = await libraryContext.availableLibraries(); + ``` + * connectLibrary:function + + ``` + connectLibrary(libraryId): Promise + ``` + + + Connects to a specific library identified by its ID. + + Parameters + - libraryId: string + + The ID of the library to connect to. + + Returns Promise + + Returns a promise that resolves to the `Library` object representing the connected library. + + Example + ``` + const connectedLibrary = await libraryContext.connectLibrary('library-id'); + ``` + + Referenced by: Context, Penpot + members: {} +Point: + overview: |- + Type Alias Point + ================ + + ``` + Point: { + x: number; + y: number; + } + ``` + + Point represents a point in 2D space, typically with x and y coordinates. + + Referenced by: Board, Boolean, CommentThread, Ellipse, Group, Image, OpenOverlay, OverlayAction, Page, Path, Rectangle, ShapeBase, SvgRaw, Text, ToggleOverlay, VariantContainer, Viewport + members: {} +RulerGuideOrientation: + overview: |- + Type Alias RulerGuideOrientation + ================================ + + ``` + RulerGuideOrientation: "horizontal" | "vertical" + ``` + + Referenced by: Board, Page, RulerGuide, VariantContainer + members: {} +Shape: + overview: |- + Type Alias Shape + ================ + + ``` + Shape: + | Board + | Group + | Boolean + | Rectangle + | Path + | Text + | Ellipse + | SvgRaw + | Image + ``` + + Shape represents a union of various shape types used in the Penpot project. + This type allows for different shapes to be handled under a single type umbrella. + + Example + ``` + let shape: Shape;if (penpot.utils.types.isRectangle(shape)) { console.log(shape.type);} + ``` + + Referenced by: Board, Boolean, Context, ContextGeometryUtils, ContextTypesUtils, Ellipse, EventsMap, FlexLayout, GridLayout, Group, Image, Interaction, Library, LibraryComponent, LibraryTypography, LibraryVariantComponent, OpenOverlay, OverlayAction, Page, Path, Penpot, Rectangle, ShapeBase, SvgRaw, Text, ToggleOverlay, VariantContainer, Viewport + members: {} +StrokeCap: + overview: |- + Type Alias StrokeCap + ==================== + + ``` + StrokeCap: + | "round" + | "square" + | "line-arrow" + | "triangle-arrow" + | "square-marker" + | "circle-marker" + | "diamond-marker" + ``` + + Represents the cap style of a stroke in Penpot. + This type defines various styles for the ends of a stroke. + + Referenced by: Stroke + members: {} +Theme: + overview: |- + Type Alias Theme + ================ + + ``` + Theme: "light" | "dark" + ``` + + This type specifies the possible themes: 'light' or 'dark'. + + Referenced by: Context, EventsMap, Penpot + members: {} +TrackType: + overview: |- + Type Alias TrackType + ==================== + + ``` + TrackType: + | "flex" + | "fixed" + | "percent" + | "auto" + ``` + + Represents the type of track in Penpot. + This type defines various track types that can be used in layout configurations. + + Referenced by: GridLayout, Track + members: {} +Trigger: + overview: |- + Type Alias Trigger + ================== + + ``` + Trigger: + | "click" + | "mouse-enter" + | "mouse-leave" + | "after-delay" + ``` + + Types of triggers defined: + + * `click` triggers when the user uses the mouse to click on a shape + * `mouse-enter` triggers when the user moves the mouse inside the shape (even if no mouse button is pressed) + * `mouse-leave` triggers when the user moves the mouse outside the shape. + * `after-delay` triggers after the `delay` time has passed even if no interaction from the user happens. + + Referenced by: Board, Boolean, Ellipse, Group, Image, Interaction, Path, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer + members: {} diff --git a/mcp/packages/server/data/prompts.yml b/mcp/packages/server/data/prompts.yml new file mode 100644 index 0000000000..dabed7a590 --- /dev/null +++ b/mcp/packages/server/data/prompts.yml @@ -0,0 +1,267 @@ +# 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(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`) - Libraries available to connect + * `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise`) - 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. diff --git a/mcp/packages/server/package.json b/mcp/packages/server/package.json new file mode 100644 index 0000000000..09e47a4dce --- /dev/null +++ b/mcp/packages/server/package.json @@ -0,0 +1,54 @@ +{ + "name": "mcp-server", + "version": "1.0.0", + "description": "MCP server for Penpot integration", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:js-yaml --external:sharp", + "build": "pnpm run build:server && cp -r src/static dist/static && cp -r data dist/data", + "build:multi-user": "pnpm run build", + "build:types": "tsc --emitDeclarationOnly --outDir dist", + "start": "node dist/index.js", + "start:multi-user": "node dist/index.js --multi-user", + "start:dev": "node --import ts-node/register src/index.ts", + "start:dev:multi-user": "node --loader ts-node/esm src/index.ts --multi-user", + "types:check": "tsc --noEmit", + "clean": "rm -rf dist/" + }, + "keywords": [ + "mcp", + "penpot", + "server" + ], + "author": "", + "license": "MIT", + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "express": "^5.1.0", + "js-yaml": "^4.1.1", + "penpot-mcp": "file:..", + "pino": "^9.10.0", + "pino-pretty": "^13.1.1", + "reflect-metadata": "^0.1.13", + "sharp": "^0.34.5", + "ws": "^8.18.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@penpot/mcp-common": "workspace:../common", + "@types/express": "^4.17.0", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", + "esbuild": "^0.25.0", + "ts-node": "^10.9.2", + "typescript": "^5.0.0" + }, + "ts-node": { + "esm": true + } +} diff --git a/mcp/packages/server/pnpm-lock.yaml b/mcp/packages/server/pnpm-lock.yaml new file mode 100644 index 0000000000..b8229de386 --- /dev/null +++ b/mcp/packages/server/pnpm-lock.yaml @@ -0,0 +1,2840 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + "@modelcontextprotocol/sdk": + specifier: ^1.24.0 + version: 1.25.3(hono@4.11.7)(zod@4.3.6) + "@penpot-mcp/common": + specifier: file:../common + version: file:../common + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.3 + version: 0.14.3 + express: + specifier: ^5.1.0 + version: 5.2.1 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 + penpot-mcp: + specifier: file:.. + version: file:.. + pino: + specifier: ^9.10.0 + version: 9.14.0 + pino-pretty: + specifier: ^13.1.1 + version: 13.1.3 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + "@types/express": + specifier: ^4.17.0 + version: 4.17.25 + "@types/js-yaml": + specifier: ^4.0.9 + version: 4.0.9 + "@types/node": + specifier: ^20.0.0 + version: 20.19.30 + "@types/ws": + specifier: ^8.5.10 + version: 8.18.1 + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + ts-node: + specifier: ^10.9.0 + version: 10.9.2(@types/node@20.19.30)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + "@babel/runtime@7.28.6": + resolution: + { + integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==, + } + engines: { node: ">=6.9.0" } + + "@cspotcode/source-map-support@0.8.1": + resolution: + { + integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, + } + engines: { node: ">=12" } + + "@emnapi/runtime@1.8.1": + resolution: + { + integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==, + } + + "@esbuild/aix-ppc64@0.25.12": + resolution: + { + integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.25.12": + resolution: + { + integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.25.12": + resolution: + { + integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.25.12": + resolution: + { + integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.25.12": + resolution: + { + integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-x64@0.25.12": + resolution: + { + integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] + + "@esbuild/freebsd-arm64@0.25.12": + resolution: + { + integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.25.12": + resolution: + { + integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] + + "@esbuild/linux-arm64@0.25.12": + resolution: + { + integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.25.12": + resolution: + { + integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-ia32@0.25.12": + resolution: + { + integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-loong64@0.25.12": + resolution: + { + integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.25.12": + resolution: + { + integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, + } + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-ppc64@0.25.12": + resolution: + { + integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-riscv64@0.25.12": + resolution: + { + integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.25.12": + resolution: + { + integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.25.12": + resolution: + { + integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.25.12": + resolution: + { + integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.25.12": + resolution: + { + integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.25.12": + resolution: + { + integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.25.12": + resolution: + { + integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.25.12": + resolution: + { + integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.25.12": + resolution: + { + integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.25.12": + resolution: + { + integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.25.12": + resolution: + { + integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.25.12": + resolution: + { + integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@hono/node-server@1.19.9": + resolution: + { + integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==, + } + engines: { node: ">=18.14.1" } + peerDependencies: + hono: ^4 + + "@img/colour@1.0.0": + resolution: + { + integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==, + } + engines: { node: ">=18" } + + "@img/sharp-darwin-arm64@0.34.5": + resolution: + { + integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [darwin] + + "@img/sharp-darwin-x64@0.34.5": + resolution: + { + integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [darwin] + + "@img/sharp-libvips-darwin-arm64@1.2.4": + resolution: + { + integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, + } + cpu: [arm64] + os: [darwin] + + "@img/sharp-libvips-darwin-x64@1.2.4": + resolution: + { + integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, + } + cpu: [x64] + os: [darwin] + + "@img/sharp-libvips-linux-arm64@1.2.4": + resolution: + { + integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, + } + cpu: [arm64] + os: [linux] + + "@img/sharp-libvips-linux-arm@1.2.4": + resolution: + { + integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, + } + cpu: [arm] + os: [linux] + + "@img/sharp-libvips-linux-ppc64@1.2.4": + resolution: + { + integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, + } + cpu: [ppc64] + os: [linux] + + "@img/sharp-libvips-linux-riscv64@1.2.4": + resolution: + { + integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, + } + cpu: [riscv64] + os: [linux] + + "@img/sharp-libvips-linux-s390x@1.2.4": + resolution: + { + integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, + } + cpu: [s390x] + os: [linux] + + "@img/sharp-libvips-linux-x64@1.2.4": + resolution: + { + integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, + } + cpu: [x64] + os: [linux] + + "@img/sharp-libvips-linuxmusl-arm64@1.2.4": + resolution: + { + integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, + } + cpu: [arm64] + os: [linux] + + "@img/sharp-libvips-linuxmusl-x64@1.2.4": + resolution: + { + integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, + } + cpu: [x64] + os: [linux] + + "@img/sharp-linux-arm64@0.34.5": + resolution: + { + integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [linux] + + "@img/sharp-linux-arm@0.34.5": + resolution: + { + integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm] + os: [linux] + + "@img/sharp-linux-ppc64@0.34.5": + resolution: + { + integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [ppc64] + os: [linux] + + "@img/sharp-linux-riscv64@0.34.5": + resolution: + { + integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [riscv64] + os: [linux] + + "@img/sharp-linux-s390x@0.34.5": + resolution: + { + integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [s390x] + os: [linux] + + "@img/sharp-linux-x64@0.34.5": + resolution: + { + integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [linux] + + "@img/sharp-linuxmusl-arm64@0.34.5": + resolution: + { + integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [linux] + + "@img/sharp-linuxmusl-x64@0.34.5": + resolution: + { + integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [linux] + + "@img/sharp-wasm32@0.34.5": + resolution: + { + integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [wasm32] + + "@img/sharp-win32-arm64@0.34.5": + resolution: + { + integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [arm64] + os: [win32] + + "@img/sharp-win32-ia32@0.34.5": + resolution: + { + integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [ia32] + os: [win32] + + "@img/sharp-win32-x64@0.34.5": + resolution: + { + integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + cpu: [x64] + os: [win32] + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } + + "@jridgewell/trace-mapping@0.3.9": + resolution: + { + integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, + } + + "@modelcontextprotocol/sdk@1.25.3": + resolution: + { + integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==, + } + engines: { node: ">=18" } + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + + "@penpot-mcp/common@file:../common": + resolution: { directory: ../common, type: directory } + + "@pinojs/redact@0.4.0": + resolution: + { + integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==, + } + + "@tsconfig/node10@1.0.12": + resolution: + { + integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==, + } + + "@tsconfig/node12@1.0.11": + resolution: + { + integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==, + } + + "@tsconfig/node14@1.0.3": + resolution: + { + integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==, + } + + "@tsconfig/node16@1.0.4": + resolution: + { + integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==, + } + + "@types/body-parser@1.19.6": + resolution: + { + integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==, + } + + "@types/connect@3.4.38": + resolution: + { + integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, + } + + "@types/express-serve-static-core@4.19.8": + resolution: + { + integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==, + } + + "@types/express@4.17.25": + resolution: + { + integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==, + } + + "@types/http-errors@2.0.5": + resolution: + { + integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==, + } + + "@types/js-yaml@4.0.9": + resolution: + { + integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==, + } + + "@types/mime@1.3.5": + resolution: + { + integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==, + } + + "@types/node@20.19.30": + resolution: + { + integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==, + } + + "@types/qs@6.14.0": + resolution: + { + integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==, + } + + "@types/range-parser@1.2.7": + resolution: + { + integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==, + } + + "@types/send@0.17.6": + resolution: + { + integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==, + } + + "@types/send@1.2.1": + resolution: + { + integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==, + } + + "@types/serve-static@1.15.10": + resolution: + { + integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==, + } + + "@types/validator@13.15.10": + resolution: + { + integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==, + } + + "@types/ws@8.18.1": + resolution: + { + integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==, + } + + accepts@2.0.0: + resolution: + { + integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, + } + engines: { node: ">= 0.6" } + + acorn-walk@8.3.4: + resolution: + { + integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==, + } + engines: { node: ">=0.4.0" } + + acorn@8.15.0: + resolution: + { + integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==, + } + engines: { node: ">=0.4.0" } + hasBin: true + + ajv-formats@3.0.1: + resolution: + { + integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==, + } + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: + { + integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==, + } + + ansi-regex@5.0.1: + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } + + arg@4.1.3: + resolution: + { + integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==, + } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } + + atomic-sleep@1.0.0: + resolution: + { + integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==, + } + engines: { node: ">=8.0.0" } + + body-parser@2.2.2: + resolution: + { + integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==, + } + engines: { node: ">=18" } + + bytes@3.1.2: + resolution: + { + integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, + } + engines: { node: ">= 0.8" } + + call-bind-apply-helpers@1.0.2: + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: ">= 0.4" } + + call-bound@1.0.4: + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, + } + engines: { node: ">= 0.4" } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } + + class-transformer@0.5.1: + resolution: + { + integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==, + } + + class-validator@0.14.3: + resolution: + { + integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==, + } + + cliui@8.0.1: + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: ">=12" } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + colorette@2.0.20: + resolution: + { + integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, + } + + concurrently@8.2.2: + resolution: + { + integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==, + } + engines: { node: ^14.13.0 || >=16.0.0 } + hasBin: true + + content-disposition@1.0.1: + resolution: + { + integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==, + } + engines: { node: ">=18" } + + content-type@1.0.5: + resolution: + { + integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, + } + engines: { node: ">= 0.6" } + + cookie-signature@1.2.2: + resolution: + { + integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, + } + engines: { node: ">=6.6.0" } + + cookie@0.7.2: + resolution: + { + integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, + } + engines: { node: ">= 0.6" } + + cors@2.8.6: + resolution: + { + integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, + } + engines: { node: ">= 0.10" } + + create-require@1.1.1: + resolution: + { + integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } + + date-fns@2.30.0: + resolution: + { + integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, + } + engines: { node: ">=0.11" } + + dateformat@4.6.3: + resolution: + { + integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==, + } + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: + { + integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, + } + engines: { node: ">= 0.8" } + + detect-libc@2.1.2: + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } + + diff@4.0.4: + resolution: + { + integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==, + } + engines: { node: ">=0.3.1" } + + dunder-proto@1.0.1: + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: ">= 0.4" } + + ee-first@1.1.1: + resolution: + { + integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, + } + + emoji-regex@8.0.0: + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, + } + + encodeurl@2.0.0: + resolution: + { + integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, + } + engines: { node: ">= 0.8" } + + end-of-stream@1.4.5: + resolution: + { + integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==, + } + + es-define-property@1.0.1: + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: ">= 0.4" } + + es-errors@1.3.0: + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: ">= 0.4" } + + es-object-atoms@1.1.1: + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, + } + engines: { node: ">= 0.4" } + + esbuild@0.25.12: + resolution: + { + integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, + } + engines: { node: ">=18" } + hasBin: true + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + + escape-html@1.0.3: + resolution: + { + integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, + } + + etag@1.8.1: + resolution: + { + integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, + } + engines: { node: ">= 0.6" } + + eventsource-parser@3.0.6: + resolution: + { + integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==, + } + engines: { node: ">=18.0.0" } + + eventsource@3.0.7: + resolution: + { + integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==, + } + engines: { node: ">=18.0.0" } + + express-rate-limit@7.5.1: + resolution: + { + integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==, + } + engines: { node: ">= 16" } + peerDependencies: + express: ">= 4.11" + + express@5.2.1: + resolution: + { + integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==, + } + engines: { node: ">= 18" } + + fast-copy@4.0.2: + resolution: + { + integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==, + } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-safe-stringify@2.1.1: + resolution: + { + integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==, + } + + fast-uri@3.1.0: + resolution: + { + integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==, + } + + finalhandler@2.1.1: + resolution: + { + integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==, + } + engines: { node: ">= 18.0.0" } + + forwarded@0.2.0: + resolution: + { + integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, + } + engines: { node: ">= 0.6" } + + fresh@2.0.0: + resolution: + { + integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, + } + engines: { node: ">= 0.8" } + + function-bind@1.1.2: + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } + + get-caller-file@2.0.5: + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + + get-intrinsic@1.3.0: + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: ">= 0.4" } + + get-proto@1.0.1: + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: ">= 0.4" } + + get-them-args@1.3.2: + resolution: + { + integrity: sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==, + } + + gopd@1.2.0: + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: ">= 0.4" } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } + + has-symbols@1.1.0: + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: ">= 0.4" } + + hasown@2.0.2: + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, + } + engines: { node: ">= 0.4" } + + help-me@5.0.0: + resolution: + { + integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==, + } + + hono@4.11.7: + resolution: + { + integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==, + } + engines: { node: ">=16.9.0" } + + http-errors@2.0.1: + resolution: + { + integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, + } + engines: { node: ">= 0.8" } + + iconv-lite@0.7.2: + resolution: + { + integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==, + } + engines: { node: ">=0.10.0" } + + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + + ipaddr.js@1.9.1: + resolution: + { + integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, + } + engines: { node: ">= 0.10" } + + is-fullwidth-code-point@3.0.0: + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, + } + engines: { node: ">=8" } + + is-promise@4.0.0: + resolution: + { + integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, + } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + jose@6.1.3: + resolution: + { + integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==, + } + + joycon@3.1.1: + resolution: + { + integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==, + } + engines: { node: ">=10" } + + js-yaml@4.1.1: + resolution: + { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, + } + hasBin: true + + json-schema-traverse@1.0.0: + resolution: + { + integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==, + } + + json-schema-typed@8.0.2: + resolution: + { + integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==, + } + + kill-port@2.0.1: + resolution: + { + integrity: sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==, + } + hasBin: true + + libphonenumber-js@1.12.35: + resolution: + { + integrity: sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==, + } + + lodash@4.17.23: + resolution: + { + integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==, + } + + make-error@1.3.6: + resolution: + { + integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, + } + + math-intrinsics@1.1.0: + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: ">= 0.4" } + + media-typer@1.1.0: + resolution: + { + integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, + } + engines: { node: ">= 0.8" } + + merge-descriptors@2.0.0: + resolution: + { + integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, + } + engines: { node: ">=18" } + + mime-db@1.54.0: + resolution: + { + integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, + } + engines: { node: ">= 0.6" } + + mime-types@3.0.2: + resolution: + { + integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, + } + engines: { node: ">=18" } + + minimist@1.2.8: + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + negotiator@1.0.0: + resolution: + { + integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, + } + engines: { node: ">= 0.6" } + + object-assign@4.1.1: + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: ">=0.10.0" } + + object-inspect@1.13.4: + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, + } + engines: { node: ">= 0.4" } + + on-exit-leak-free@2.1.2: + resolution: + { + integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==, + } + engines: { node: ">=14.0.0" } + + on-finished@2.4.1: + resolution: + { + integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, + } + engines: { node: ">= 0.8" } + + once@1.4.0: + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } + + parseurl@1.3.3: + resolution: + { + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, + } + engines: { node: ">= 0.8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } + + path-to-regexp@8.3.0: + resolution: + { + integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==, + } + + penpot-mcp@file:..: + resolution: { directory: .., type: directory } + + pino-abstract-transport@2.0.0: + resolution: + { + integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==, + } + + pino-abstract-transport@3.0.0: + resolution: + { + integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==, + } + + pino-pretty@13.1.3: + resolution: + { + integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==, + } + hasBin: true + + pino-std-serializers@7.1.0: + resolution: + { + integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==, + } + + pino@9.14.0: + resolution: + { + integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==, + } + hasBin: true + + pkce-challenge@5.0.1: + resolution: + { + integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==, + } + engines: { node: ">=16.20.0" } + + process-warning@5.0.0: + resolution: + { + integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==, + } + + proxy-addr@2.0.7: + resolution: + { + integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, + } + engines: { node: ">= 0.10" } + + pump@3.0.3: + resolution: + { + integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==, + } + + qs@6.14.1: + resolution: + { + integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==, + } + engines: { node: ">=0.6" } + + quick-format-unescaped@4.0.4: + resolution: + { + integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==, + } + + range-parser@1.2.1: + resolution: + { + integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, + } + engines: { node: ">= 0.6" } + + raw-body@3.0.2: + resolution: + { + integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==, + } + engines: { node: ">= 0.10" } + + real-require@0.2.0: + resolution: + { + integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==, + } + engines: { node: ">= 12.13.0" } + + reflect-metadata@0.1.14: + resolution: + { + integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==, + } + + require-directory@2.1.1: + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: ">=0.10.0" } + + require-from-string@2.0.2: + resolution: + { + integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, + } + engines: { node: ">=0.10.0" } + + router@2.2.0: + resolution: + { + integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, + } + engines: { node: ">= 18" } + + rxjs@7.8.2: + resolution: + { + integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==, + } + + safe-stable-stringify@2.5.0: + resolution: + { + integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==, + } + engines: { node: ">=10" } + + safer-buffer@2.1.2: + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } + + secure-json-parse@4.1.0: + resolution: + { + integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==, + } + + semver@7.7.3: + resolution: + { + integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, + } + engines: { node: ">=10" } + hasBin: true + + send@1.2.1: + resolution: + { + integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==, + } + engines: { node: ">= 18" } + + serve-static@2.2.1: + resolution: + { + integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, + } + engines: { node: ">= 18" } + + setprototypeof@1.2.0: + resolution: + { + integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, + } + + sharp@0.34.5: + resolution: + { + integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, + } + engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } + + shell-exec@1.0.2: + resolution: + { + integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==, + } + + shell-quote@1.8.3: + resolution: + { + integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==, + } + engines: { node: ">= 0.4" } + + side-channel-list@1.0.0: + resolution: + { + integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==, + } + engines: { node: ">= 0.4" } + + side-channel-map@1.0.1: + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, + } + engines: { node: ">= 0.4" } + + side-channel-weakmap@1.0.2: + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, + } + engines: { node: ">= 0.4" } + + side-channel@1.1.0: + resolution: + { + integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, + } + engines: { node: ">= 0.4" } + + sonic-boom@4.2.0: + resolution: + { + integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==, + } + + spawn-command@0.0.2: + resolution: + { + integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==, + } + + split2@4.2.0: + resolution: + { + integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==, + } + engines: { node: ">= 10.x" } + + statuses@2.0.2: + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, + } + engines: { node: ">= 0.8" } + + string-width@4.2.3: + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, + } + engines: { node: ">=8" } + + strip-ansi@6.0.1: + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: ">=8" } + + strip-json-comments@5.0.3: + resolution: + { + integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==, + } + engines: { node: ">=14.16" } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } + + supports-color@8.1.1: + resolution: + { + integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, + } + engines: { node: ">=10" } + + thread-stream@3.1.0: + resolution: + { + integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==, + } + + toidentifier@1.0.1: + resolution: + { + integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, + } + engines: { node: ">=0.6" } + + tree-kill@1.2.2: + resolution: + { + integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==, + } + hasBin: true + + ts-node@10.9.2: + resolution: + { + integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==, + } + hasBin: true + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + type-is@2.0.1: + resolution: + { + integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==, + } + engines: { node: ">= 0.6" } + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } + hasBin: true + + undici-types@6.21.0: + resolution: + { + integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, + } + + unpipe@1.0.0: + resolution: + { + integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, + } + engines: { node: ">= 0.8" } + + v8-compile-cache-lib@3.0.1: + resolution: + { + integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, + } + + validator@13.15.26: + resolution: + { + integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==, + } + engines: { node: ">= 0.10" } + + vary@1.1.2: + resolution: + { + integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, + } + engines: { node: ">= 0.8" } + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } + hasBin: true + + wrap-ansi@7.0.0: + resolution: + { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, + } + engines: { node: ">=10" } + + wrappy@1.0.2: + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } + + ws@8.19.0: + resolution: + { + integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==, + } + engines: { node: ">=10.0.0" } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: ">=10" } + + yargs-parser@21.1.1: + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: ">=12" } + + yargs@17.7.2: + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: ">=12" } + + yn@3.1.1: + resolution: + { + integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, + } + engines: { node: ">=6" } + + zod-to-json-schema@3.25.1: + resolution: + { + integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==, + } + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: + { + integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==, + } + +snapshots: + "@babel/runtime@7.28.6": {} + + "@cspotcode/source-map-support@0.8.1": + dependencies: + "@jridgewell/trace-mapping": 0.3.9 + + "@emnapi/runtime@1.8.1": + dependencies: + tslib: 2.8.1 + optional: true + + "@esbuild/aix-ppc64@0.25.12": + optional: true + + "@esbuild/android-arm64@0.25.12": + optional: true + + "@esbuild/android-arm@0.25.12": + optional: true + + "@esbuild/android-x64@0.25.12": + optional: true + + "@esbuild/darwin-arm64@0.25.12": + optional: true + + "@esbuild/darwin-x64@0.25.12": + optional: true + + "@esbuild/freebsd-arm64@0.25.12": + optional: true + + "@esbuild/freebsd-x64@0.25.12": + optional: true + + "@esbuild/linux-arm64@0.25.12": + optional: true + + "@esbuild/linux-arm@0.25.12": + optional: true + + "@esbuild/linux-ia32@0.25.12": + optional: true + + "@esbuild/linux-loong64@0.25.12": + optional: true + + "@esbuild/linux-mips64el@0.25.12": + optional: true + + "@esbuild/linux-ppc64@0.25.12": + optional: true + + "@esbuild/linux-riscv64@0.25.12": + optional: true + + "@esbuild/linux-s390x@0.25.12": + optional: true + + "@esbuild/linux-x64@0.25.12": + optional: true + + "@esbuild/netbsd-arm64@0.25.12": + optional: true + + "@esbuild/netbsd-x64@0.25.12": + optional: true + + "@esbuild/openbsd-arm64@0.25.12": + optional: true + + "@esbuild/openbsd-x64@0.25.12": + optional: true + + "@esbuild/openharmony-arm64@0.25.12": + optional: true + + "@esbuild/sunos-x64@0.25.12": + optional: true + + "@esbuild/win32-arm64@0.25.12": + optional: true + + "@esbuild/win32-ia32@0.25.12": + optional: true + + "@esbuild/win32-x64@0.25.12": + optional: true + + "@hono/node-server@1.19.9(hono@4.11.7)": + dependencies: + hono: 4.11.7 + + "@img/colour@1.0.0": {} + + "@img/sharp-darwin-arm64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-darwin-arm64": 1.2.4 + optional: true + + "@img/sharp-darwin-x64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-darwin-x64": 1.2.4 + optional: true + + "@img/sharp-libvips-darwin-arm64@1.2.4": + optional: true + + "@img/sharp-libvips-darwin-x64@1.2.4": + optional: true + + "@img/sharp-libvips-linux-arm64@1.2.4": + optional: true + + "@img/sharp-libvips-linux-arm@1.2.4": + optional: true + + "@img/sharp-libvips-linux-ppc64@1.2.4": + optional: true + + "@img/sharp-libvips-linux-riscv64@1.2.4": + optional: true + + "@img/sharp-libvips-linux-s390x@1.2.4": + optional: true + + "@img/sharp-libvips-linux-x64@1.2.4": + optional: true + + "@img/sharp-libvips-linuxmusl-arm64@1.2.4": + optional: true + + "@img/sharp-libvips-linuxmusl-x64@1.2.4": + optional: true + + "@img/sharp-linux-arm64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-arm64": 1.2.4 + optional: true + + "@img/sharp-linux-arm@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-arm": 1.2.4 + optional: true + + "@img/sharp-linux-ppc64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-ppc64": 1.2.4 + optional: true + + "@img/sharp-linux-riscv64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-riscv64": 1.2.4 + optional: true + + "@img/sharp-linux-s390x@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-s390x": 1.2.4 + optional: true + + "@img/sharp-linux-x64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linux-x64": 1.2.4 + optional: true + + "@img/sharp-linuxmusl-arm64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 + optional: true + + "@img/sharp-linuxmusl-x64@0.34.5": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.2.4 + optional: true + + "@img/sharp-wasm32@0.34.5": + dependencies: + "@emnapi/runtime": 1.8.1 + optional: true + + "@img/sharp-win32-arm64@0.34.5": + optional: true + + "@img/sharp-win32-ia32@0.34.5": + optional: true + + "@img/sharp-win32-x64@0.34.5": + optional: true + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.5.5": {} + + "@jridgewell/trace-mapping@0.3.9": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 + + "@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)": + dependencies: + "@hono/node-server": 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - hono + - supports-color + + "@penpot-mcp/common@file:../common": + dependencies: + penpot-mcp: file:.. + + "@pinojs/redact@0.4.0": {} + + "@tsconfig/node10@1.0.12": {} + + "@tsconfig/node12@1.0.11": {} + + "@tsconfig/node14@1.0.3": {} + + "@tsconfig/node16@1.0.4": {} + + "@types/body-parser@1.19.6": + dependencies: + "@types/connect": 3.4.38 + "@types/node": 20.19.30 + + "@types/connect@3.4.38": + dependencies: + "@types/node": 20.19.30 + + "@types/express-serve-static-core@4.19.8": + dependencies: + "@types/node": 20.19.30 + "@types/qs": 6.14.0 + "@types/range-parser": 1.2.7 + "@types/send": 1.2.1 + + "@types/express@4.17.25": + dependencies: + "@types/body-parser": 1.19.6 + "@types/express-serve-static-core": 4.19.8 + "@types/qs": 6.14.0 + "@types/serve-static": 1.15.10 + + "@types/http-errors@2.0.5": {} + + "@types/js-yaml@4.0.9": {} + + "@types/mime@1.3.5": {} + + "@types/node@20.19.30": + dependencies: + undici-types: 6.21.0 + + "@types/qs@6.14.0": {} + + "@types/range-parser@1.2.7": {} + + "@types/send@0.17.6": + dependencies: + "@types/mime": 1.3.5 + "@types/node": 20.19.30 + + "@types/send@1.2.1": + dependencies: + "@types/node": 20.19.30 + + "@types/serve-static@1.15.10": + dependencies: + "@types/http-errors": 2.0.5 + "@types/node": 20.19.30 + "@types/send": 0.17.6 + + "@types/validator@13.15.10": {} + + "@types/ws@8.18.1": + dependencies: + "@types/node": 20.19.30 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arg@4.1.3: {} + + argparse@2.0.1: {} + + atomic-sleep@1.0.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + class-transformer@0.5.1: {} + + class-validator@0.14.3: + dependencies: + "@types/validator": 13.15.10 + libphonenumber-js: 1.12.35 + validator: 13.15.26 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + concurrently@8.2.2: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.23 + rxjs: 7.8.2 + shell-quote: 1.8.3 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + date-fns@2.30.0: + dependencies: + "@babel/runtime": 7.28.6 + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + diff@4.0.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + "@esbuild/aix-ppc64": 0.25.12 + "@esbuild/android-arm": 0.25.12 + "@esbuild/android-arm64": 0.25.12 + "@esbuild/android-x64": 0.25.12 + "@esbuild/darwin-arm64": 0.25.12 + "@esbuild/darwin-x64": 0.25.12 + "@esbuild/freebsd-arm64": 0.25.12 + "@esbuild/freebsd-x64": 0.25.12 + "@esbuild/linux-arm": 0.25.12 + "@esbuild/linux-arm64": 0.25.12 + "@esbuild/linux-ia32": 0.25.12 + "@esbuild/linux-loong64": 0.25.12 + "@esbuild/linux-mips64el": 0.25.12 + "@esbuild/linux-ppc64": 0.25.12 + "@esbuild/linux-riscv64": 0.25.12 + "@esbuild/linux-s390x": 0.25.12 + "@esbuild/linux-x64": 0.25.12 + "@esbuild/netbsd-arm64": 0.25.12 + "@esbuild/netbsd-x64": 0.25.12 + "@esbuild/openbsd-arm64": 0.25.12 + "@esbuild/openbsd-x64": 0.25.12 + "@esbuild/openharmony-arm64": 0.25.12 + "@esbuild/sunos-x64": 0.25.12 + "@esbuild/win32-arm64": 0.25.12 + "@esbuild/win32-ia32": 0.25.12 + "@esbuild/win32-x64": 0.25.12 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-copy@4.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.0: {} + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-them-args@1.3.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + hono@4.11.7: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.1.3: {} + + joycon@3.1.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + kill-port@2.0.1: + dependencies: + get-them-args: 1.3.2 + shell-exec: 1.0.2 + + libphonenumber-js@1.12.35: {} + + lodash@4.17.23: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.3.0: {} + + penpot-mcp@file:..: + dependencies: + concurrently: 8.2.2 + kill-port: 2.0.1 + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.0 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + "@pinojs/redact": 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + pkce-challenge@5.0.1: {} + + process-warning@5.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + real-require@0.2.0: {} + + reflect-metadata@0.1.14: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + secure-json-parse@4.1.0: {} + + semver@7.7.3: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + sharp@0.34.5: + dependencies: + "@img/colour": 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + "@img/sharp-darwin-arm64": 0.34.5 + "@img/sharp-darwin-x64": 0.34.5 + "@img/sharp-libvips-darwin-arm64": 1.2.4 + "@img/sharp-libvips-darwin-x64": 1.2.4 + "@img/sharp-libvips-linux-arm": 1.2.4 + "@img/sharp-libvips-linux-arm64": 1.2.4 + "@img/sharp-libvips-linux-ppc64": 1.2.4 + "@img/sharp-libvips-linux-riscv64": 1.2.4 + "@img/sharp-libvips-linux-s390x": 1.2.4 + "@img/sharp-libvips-linux-x64": 1.2.4 + "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 + "@img/sharp-libvips-linuxmusl-x64": 1.2.4 + "@img/sharp-linux-arm": 0.34.5 + "@img/sharp-linux-arm64": 0.34.5 + "@img/sharp-linux-ppc64": 0.34.5 + "@img/sharp-linux-riscv64": 0.34.5 + "@img/sharp-linux-s390x": 0.34.5 + "@img/sharp-linux-x64": 0.34.5 + "@img/sharp-linuxmusl-arm64": 0.34.5 + "@img/sharp-linuxmusl-x64": 0.34.5 + "@img/sharp-wasm32": 0.34.5 + "@img/sharp-win32-arm64": 0.34.5 + "@img/sharp-win32-ia32": 0.34.5 + "@img/sharp-win32-x64": 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-exec@1.0.2: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + spawn-command@0.0.2: {} + + split2@4.2.0: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@5.0.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + ts-node@10.9.2(@types/node@20.19.30)(typescript@5.9.3): + dependencies: + "@cspotcode/source-map-support": 0.8.1 + "@tsconfig/node10": 1.0.12 + "@tsconfig/node12": 1.0.11 + "@tsconfig/node14": 1.0.3 + "@tsconfig/node16": 1.0.4 + "@types/node": 20.19.30 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + v8-compile-cache-lib@3.0.1: {} + + validator@13.15.26: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/mcp/packages/server/src/ApiDocs.ts b/mcp/packages/server/src/ApiDocs.ts new file mode 100644 index 0000000000..c6374ea3f3 --- /dev/null +++ b/mcp/packages/server/src/ApiDocs.ts @@ -0,0 +1,128 @@ +import * as yaml from "js-yaml"; +import * as fs from "fs"; +import * as path from "path"; + +/** + * Represents a single type/interface defined in the Penpot API + */ +export class ApiType { + private readonly name: string; + private readonly overview: string; + private readonly members: Record>; + private cachedFullText: string | null = null; + + constructor(name: string, overview: string, members: Record>) { + this.name = name; + this.overview = overview; + this.members = members; + } + + /** + * Returns the original name of this API type. + */ + getName(): string { + return this.name; + } + + /** + * Returns the overview text of this API type (which all signature/type declarations) + */ + getOverviewText() { + return this.overview; + } + + /** + * Creates a single markdown text document from all parts of this API type. + * + * The full text is cached within the object for performance. + */ + getFullText(): string { + if (this.cachedFullText === null) { + let text = this.overview; + + for (const [memberType, memberEntries] of Object.entries(this.members)) { + text += `\n\n## ${memberType}\n`; + + for (const [memberName, memberDescription] of Object.entries(memberEntries)) { + text += `\n### ${memberName}\n\n${memberDescription}`; + } + } + + this.cachedFullText = text; + } + + return this.cachedFullText; + } + + /** + * Returns the description of the member with the given name. + * + * The member type doesn't matter for the search, as member names are unique + * across all member types within a single API type. + */ + getMember(memberName: string): string | null { + for (const memberEntries of Object.values(this.members)) { + if (memberName in memberEntries) { + return memberEntries[memberName]; + } + } + return null; + } +} + +/** + * Loads and manages API documentation from YAML files. + * + * This class provides case-insensitive access to API type documentation + * loaded from the data/api_types.yml file. + */ +export class ApiDocs { + private readonly apiTypes: Map = new Map(); + + /** + * Creates a new ApiDocs instance and loads the API types from the YAML file. + */ + constructor() { + this.loadApiTypes(); + } + + /** + * Loads API types from the data/api_types.yml file. + */ + private loadApiTypes(): void { + const yamlPath = path.join(process.cwd(), "data", "api_types.yml"); + const yamlContent = fs.readFileSync(yamlPath, "utf8"); + const data = yaml.load(yamlContent) as Record; + + for (const [typeName, typeData] of Object.entries(data)) { + const overview = typeData.overview || ""; + const members = typeData.members || {}; + + const apiType = new ApiType(typeName, overview, members); + + // store with lower-case key for case-insensitive retrieval + this.apiTypes.set(typeName.toLowerCase(), apiType); + } + } + + /** + * Retrieves an API type by name (case-insensitive). + */ + getType(typeName: string): ApiType | null { + return this.apiTypes.get(typeName.toLowerCase()) || null; + } + + /** + * Returns all available type names. + */ + getTypeNames(): string[] { + return Array.from(this.apiTypes.values()).map((type) => type.getName()); + } + + /** + * Returns the number of loaded API types. + */ + getTypeCount(): number { + return this.apiTypes.size; + } +} diff --git a/mcp/packages/server/src/ConfigurationLoader.ts b/mcp/packages/server/src/ConfigurationLoader.ts new file mode 100644 index 0000000000..0a24d88210 --- /dev/null +++ b/mcp/packages/server/src/ConfigurationLoader.ts @@ -0,0 +1,85 @@ +import { readFileSync, existsSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import yaml from "js-yaml"; +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. + * + * Handles loading and parsing of YAML configuration files, + * providing type-safe access to configuration values with + * appropriate fallbacks for missing files or values. + */ +export class ConfigurationLoader { + private readonly logger = createLogger("ConfigurationLoader"); + private readonly baseDir: string; + private promptsConfig: PromptsConfig | null = null; + + /** + * Creates a new configuration loader instance. + * + * @param baseDir - Base directory for resolving configuration file paths + */ + constructor(baseDir: string) { + this.baseDir = baseDir; + } + + /** + * Loads the prompts configuration from the YAML file. + * + * 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; + } + + 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; + } + + /** + * Gets the initial instructions for the MCP server. + * + * @returns The initial instructions string, or undefined if not configured + */ + public getInitialInstructions(): string { + const config = this.getPromptsConfig(); + 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"); + } +} diff --git a/mcp/packages/server/src/PenpotMcpServer.ts b/mcp/packages/server/src/PenpotMcpServer.ts new file mode 100644 index 0000000000..1797fdec23 --- /dev/null +++ b/mcp/packages/server/src/PenpotMcpServer.ts @@ -0,0 +1,262 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { AsyncLocalStorage } from "async_hooks"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { ExecuteCodeTool } from "./tools/ExecuteCodeTool"; +import { PluginBridge } from "./PluginBridge"; +import { ConfigurationLoader } from "./ConfigurationLoader"; +import { createLogger } from "./logger"; +import { Tool } from "./Tool"; +import { HighLevelOverviewTool } from "./tools/HighLevelOverviewTool"; +import { PenpotApiInfoTool } from "./tools/PenpotApiInfoTool"; +import { ExportShapeTool } from "./tools/ExportShapeTool"; +import { ImportImageTool } from "./tools/ImportImageTool"; +import { ReplServer } from "./ReplServer"; +import { ApiDocs } from "./ApiDocs"; + +/** + * Session context for request-scoped data. + */ +export interface SessionContext { + userToken?: string; +} + +export class PenpotMcpServer { + private readonly logger = createLogger("PenpotMcpServer"); + private readonly server: McpServer; + private readonly tools: Map>; + public readonly configLoader: ConfigurationLoader; + private app: any; + public readonly pluginBridge: PluginBridge; + private readonly replServer: ReplServer; + private apiDocs: ApiDocs; + + /** + * Manages session-specific context, particularly user tokens for each request. + */ + private readonly sessionContext = new AsyncLocalStorage(); + + private readonly transports = { + streamable: {} as Record, + sse: {} as Record, + }; + + private readonly port: number; + private readonly webSocketPort: number; + private readonly replPort: number; + private readonly listenAddress: string; + /** + * the address (domain name or IP address) via which clients can reach the MCP server + */ + public readonly serverAddress: string; + + constructor(private isMultiUser: boolean = false) { + // read port configuration from environment variables + this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10); + this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10); + this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10); + this.listenAddress = process.env.PENPOT_MCP_SERVER_LISTEN_ADDRESS ?? "localhost"; + this.serverAddress = process.env.PENPOT_MCP_SERVER_ADDRESS ?? "localhost"; + + this.configLoader = new ConfigurationLoader(process.cwd()); + this.apiDocs = new ApiDocs(); + + this.server = new McpServer( + { + name: "penpot-mcp-server", + version: "1.0.0", + }, + { + instructions: this.getInitialInstructions(), + } + ); + + this.tools = new Map>(); + this.pluginBridge = new PluginBridge(this, this.webSocketPort); + this.replServer = new ReplServer(this.pluginBridge, this.replPort); + + this.registerTools(); + } + + /** + * Indicates whether the server is running in multi-user mode, + * where user tokens are required for authentication. + */ + public isMultiUserMode(): boolean { + return this.isMultiUser; + } + + /** + * Indicates whether the server is running in remote mode. + * + * In remote mode, the server is not assumed to be accessed only by a local user on the same machine, + * with corresponding limitations being enforced. + * Remote mode can be explicitly enabled by setting the environment variable PENPOT_MCP_REMOTE_MODE + * to "true". Enabling multi-user mode forces remote mode, regardless of the value of the environment + * variable. + */ + public isRemoteMode(): boolean { + const isRemoteModeRequested: boolean = process.env.PENPOT_MCP_REMOTE_MODE === "true"; + return this.isMultiUserMode() || isRemoteModeRequested; + } + + /** + * Indicates whether file system access is enabled for MCP tools. + * Access is enabled only in local mode, where the file system is assumed + * to belong to the user running the server locally. + */ + public isFileSystemAccessEnabled(): boolean { + return !this.isRemoteMode(); + } + + public getInitialInstructions(): string { + let instructions = this.configLoader.getInitialInstructions(); + instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", ")); + return instructions; + } + + /** + * Retrieves the current session context. + * + * @returns The session context for the current request, or undefined if not in a request context + */ + public getSessionContext(): SessionContext | undefined { + return this.sessionContext.getStore(); + } + + private registerTools(): void { + // Create relevant tool instances (depending on file system access) + const toolInstances: Tool[] = [ + new ExecuteCodeTool(this), + new HighLevelOverviewTool(this), + new PenpotApiInfoTool(this, this.apiDocs), + new ExportShapeTool(this), // tool adapts to file system access internally + ]; + if (this.isFileSystemAccessEnabled()) { + toolInstances.push(new ImportImageTool(this)); + } + + for (const tool of toolInstances) { + const toolName = tool.getToolName(); + this.tools.set(toolName, tool); + + // Register each tool with McpServer + this.logger.info(`Registering tool: ${toolName}`); + this.server.registerTool( + toolName, + { + description: tool.getToolDescription(), + inputSchema: tool.getInputSchema(), + }, + async (args) => { + return tool.execute(args); + } + ); + } + } + + private setupHttpEndpoints(): void { + /** + * Modern Streamable HTTP connection endpoint + */ + this.app.all("/mcp", async (req: any, res: any) => { + const userToken = req.query.userToken as string | undefined; + + await this.sessionContext.run({ userToken }, async () => { + const { randomUUID } = await import("node:crypto"); + + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionId && this.transports.streamable[sessionId]) { + transport = this.transports.streamable[sessionId]; + } else { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (id: string) => { + this.transports.streamable[id] = transport; + }, + }); + + transport.onclose = () => { + if (transport.sessionId) { + delete this.transports.streamable[transport.sessionId]; + } + }; + + await this.server.connect(transport); + } + + await transport.handleRequest(req, res, req.body); + }); + }); + + /** + * Legacy SSE connection endpoint + */ + this.app.get("/sse", async (req: any, res: any) => { + const userToken = req.query.userToken as string | undefined; + + await this.sessionContext.run({ userToken }, async () => { + const transport = new SSEServerTransport("/messages", res); + this.transports.sse[transport.sessionId] = { transport, userToken }; + + res.on("close", () => { + delete this.transports.sse[transport.sessionId]; + }); + + await this.server.connect(transport); + }); + }); + + /** + * SSE message POST endpoint (using previously established session) + */ + this.app.post("/messages", async (req: any, res: any) => { + const sessionId = req.query.sessionId as string; + const session = this.transports.sse[sessionId]; + + if (session) { + await this.sessionContext.run({ userToken: session.userToken }, async () => { + await session.transport.handlePostMessage(req, res, req.body); + }); + } else { + res.status(400).send("No transport found for sessionId"); + } + }); + } + + async start(): Promise { + const { default: express } = await import("express"); + this.app = express(); + this.app.use(express.json()); + + this.setupHttpEndpoints(); + + return new Promise((resolve) => { + this.app.listen(this.port, this.listenAddress, async () => { + this.logger.info(`Multi-user mode: ${this.isMultiUserMode()}`); + this.logger.info(`Remote mode: ${this.isRemoteMode()}`); + this.logger.info(`Modern Streamable HTTP endpoint: http://${this.serverAddress}:${this.port}/mcp`); + this.logger.info(`Legacy SSE endpoint: http://${this.serverAddress}:${this.port}/sse`); + this.logger.info(`WebSocket server URL: ws://${this.serverAddress}:${this.webSocketPort}`); + + // start the REPL server + await this.replServer.start(); + + resolve(); + }); + }); + } + + /** + * Stops the MCP server and associated services. + * + * Gracefully shuts down the REPL server and other components. + */ + public async stop(): Promise { + this.logger.info("Stopping Penpot MCP Server..."); + await this.replServer.stop(); + this.logger.info("Penpot MCP Server stopped"); + } +} diff --git a/mcp/packages/server/src/PluginBridge.ts b/mcp/packages/server/src/PluginBridge.ts new file mode 100644 index 0000000000..413ec2fa70 --- /dev/null +++ b/mcp/packages/server/src/PluginBridge.ts @@ -0,0 +1,227 @@ +import { WebSocket, WebSocketServer } from "ws"; +import * as http from "http"; +import { PluginTask } from "./PluginTask"; +import { PluginTaskResponse, PluginTaskResult } from "@penpot/mcp-common"; +import { createLogger } from "./logger"; +import type { PenpotMcpServer } from "./PenpotMcpServer"; + +interface ClientConnection { + socket: WebSocket; + userToken: string | null; +} + +/** + * Manages WebSocket connections to Penpot plugin instances and handles plugin tasks + * over these connections. + */ +export class PluginBridge { + private readonly logger = createLogger("PluginBridge"); + private readonly wsServer: WebSocketServer; + private readonly connectedClients: Map = new Map(); + private readonly clientsByToken: Map = new Map(); + private readonly pendingTasks: Map> = new Map(); + private readonly taskTimeouts: Map = new Map(); + + constructor( + public readonly mcpServer: PenpotMcpServer, + private port: number, + private taskTimeoutSecs: number = 30 + ) { + this.wsServer = new WebSocketServer({ port: port }); + this.setupWebSocketHandlers(); + } + + /** + * Sets up WebSocket connection handlers for plugin communication. + * + * Manages client connections and provides bidirectional communication + * channel between the MCP mcpServer and Penpot plugin instances. + */ + private setupWebSocketHandlers(): void { + this.wsServer.on("connection", (ws: WebSocket, request: http.IncomingMessage) => { + // extract userToken from query parameters + const url = new URL(request.url!, `ws://${request.headers.host}`); + const userToken = url.searchParams.get("userToken"); + + // require userToken if running in multi-user mode + if (this.mcpServer.isMultiUserMode() && !userToken) { + this.logger.warn("Connection attempt without userToken in multi-user mode - rejecting"); + ws.close(1008, "Missing userToken parameter"); + return; + } + + if (userToken) { + this.logger.info("New WebSocket connection established (token provided)"); + } else { + this.logger.info("New WebSocket connection established"); + } + + // register the client connection with both indexes + const connection: ClientConnection = { socket: ws, userToken }; + this.connectedClients.set(ws, connection); + if (userToken) { + // ensure only one connection per userToken + if (this.clientsByToken.has(userToken)) { + this.logger.warn("Duplicate connection for given user token; rejecting new connection"); + ws.close(1008, "Duplicate connection for given user token; close previous connection first."); + } + + this.clientsByToken.set(userToken, connection); + } + + ws.on("message", (data: Buffer) => { + this.logger.debug("Received WebSocket message: %s", data.toString()); + try { + const response: PluginTaskResponse = JSON.parse(data.toString()); + this.handlePluginTaskResponse(response); + } catch (error) { + this.logger.error(error, "Failure while processing WebSocket message"); + } + }); + + ws.on("close", () => { + this.logger.info("WebSocket connection closed"); + const connection = this.connectedClients.get(ws); + this.connectedClients.delete(ws); + if (connection?.userToken) { + this.clientsByToken.delete(connection.userToken); + } + }); + + ws.on("error", (error) => { + this.logger.error(error, "WebSocket connection error"); + const connection = this.connectedClients.get(ws); + this.connectedClients.delete(ws); + if (connection?.userToken) { + this.clientsByToken.delete(connection.userToken); + } + }); + }); + + this.logger.info("WebSocket mcpServer started on port %d", this.port); + } + + /** + * Handles responses from the plugin for completed tasks. + * + * Finds the pending task by ID and resolves or rejects its promise + * based on the execution result. + * + * @param response - The plugin task response containing ID and result + */ + private handlePluginTaskResponse(response: PluginTaskResponse): void { + const task = this.pendingTasks.get(response.id); + if (!task) { + this.logger.info(`Received response for unknown task ID: ${response.id}`); + return; + } + + // Clear the timeout and remove the task from pending tasks + const timeoutHandle = this.taskTimeouts.get(response.id); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + this.taskTimeouts.delete(response.id); + } + this.pendingTasks.delete(response.id); + + // Resolve or reject the task's promise based on the result + if (response.success) { + task.resolveWithResult({ data: response.data }); + } else { + const error = new Error(response.error || "Task execution failed (details not provided)"); + task.rejectWithError(error); + } + + this.logger.info(`Task ${response.id} completed: success=${response.success}`); + } + + /** + * Determines the client connection to use for executing a task. + * + * In single-user mode, returns the single connected client. + * In multi-user mode, returns the client matching the session's userToken. + * + * @returns The client connection to use + * @throws Error if no suitable connection is found or if configuration is invalid + */ + private getClientConnection(): ClientConnection { + if (this.mcpServer.isMultiUserMode()) { + const sessionContext = this.mcpServer.getSessionContext(); + if (!sessionContext?.userToken) { + throw new Error("No userToken found in session context. Multi-user mode requires authentication."); + } + + const connection = this.clientsByToken.get(sessionContext.userToken); + if (!connection) { + throw new Error( + `No plugin instance connected for user token. Please ensure the plugin is running and connected with the correct token.` + ); + } + + return connection; + } else { + // single-user mode: return the single connected client + if (this.connectedClients.size === 0) { + throw new Error( + `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` + ); + } + if (this.connectedClients.size > 1) { + throw new Error( + `Multiple (${this.connectedClients.size}) Penpot MCP Plugin instances are connected. ` + + `Ask the user to ensure that only one instance is connected at a time.` + ); + } + + // return the first (and only) connection + const connection = this.connectedClients.values().next().value; + return connection; + } + } + + /** + * Executes a plugin task by sending it to connected clients. + * + * Registers the task for result correlation and returns a promise + * that resolves when the plugin responds with the execution result. + * + * @param task - The plugin task to execute + * @throws Error if no plugin instances are connected or available + */ + public async executePluginTask>( + task: PluginTask + ): Promise { + // get the appropriate client connection based on mode + const connection = this.getClientConnection(); + + // register the task for result correlation + this.pendingTasks.set(task.id, task); + + // send task to the selected client + const requestMessage = JSON.stringify(task.toRequest()); + if (connection.socket.readyState !== 1) { + // WebSocket is not open + this.pendingTasks.delete(task.id); + throw new Error(`Plugin instance is disconnected. Task could not be sent.`); + } + + connection.socket.send(requestMessage); + + // Set up a timeout to reject the task if no response is received + const timeoutHandle = setTimeout(() => { + const pendingTask = this.pendingTasks.get(task.id); + if (pendingTask) { + this.pendingTasks.delete(task.id); + this.taskTimeouts.delete(task.id); + pendingTask.rejectWithError( + new Error(`Task ${task.id} timed out after ${this.taskTimeoutSecs} seconds`) + ); + } + }, this.taskTimeoutSecs * 1000); + + this.taskTimeouts.set(task.id, timeoutHandle); + this.logger.info(`Sent task ${task.id} to connected client`); + + return await task.getResultPromise(); + } +} diff --git a/mcp/packages/server/src/PluginTask.ts b/mcp/packages/server/src/PluginTask.ts new file mode 100644 index 0000000000..8600cac22b --- /dev/null +++ b/mcp/packages/server/src/PluginTask.ts @@ -0,0 +1,122 @@ +/** + * Base class for plugin tasks that are sent over WebSocket. + * + * Each task defines a specific operation for the plugin to execute + * along with strongly-typed parameters. + * + * @template TParams - The strongly-typed parameters for this task + */ +import { PluginTaskRequest, PluginTaskResult } from "@penpot/mcp-common"; +import { randomUUID } from "crypto"; + +/** + * Base class for plugin tasks that are sent over WebSocket. + * + * Each task defines a specific operation for the plugin to execute + * along with strongly-typed parameters and request/response correlation. + * + * @template TParams - The strongly-typed parameters for this task + * @template TResult - The expected result type from task execution + */ +export abstract class PluginTask = PluginTaskResult> { + /** + * Unique identifier for request/response correlation. + */ + public readonly id: string; + + /** + * The name of the task to execute on the plugin side. + */ + public readonly task: string; + + /** + * The parameters for this task execution. + */ + public readonly params: TParams; + + /** + * Promise that resolves when the task execution completes. + */ + private readonly result: Promise; + + /** + * Resolver function for the result promise. + */ + private resolveResult?: (result: TResult) => void; + + /** + * Rejector function for the result promise. + */ + private rejectResult?: (error: Error) => void; + + /** + * Creates a new plugin task instance. + * + * @param task - The name of the task to execute + * @param params - The parameters for task execution + */ + constructor(task: string, params: TParams) { + this.id = randomUUID(); + this.task = task; + this.params = params; + this.result = new Promise((resolve, reject) => { + this.resolveResult = resolve; + this.rejectResult = reject; + }); + } + + /** + * Gets the result promise for this task. + * + * @returns Promise that resolves when the task execution completes + */ + getResultPromise(): Promise { + if (!this.result) { + throw new Error("Result promise not initialized"); + } + return this.result; + } + + /** + * Resolves the task with the given result. + * + * This method should be called when a task response is received + * from the plugin with matching ID. + * + * @param result - The task execution result + */ + resolveWithResult(result: TResult): void { + if (!this.resolveResult) { + throw new Error("Result promise not initialized"); + } + this.resolveResult(result); + } + + /** + * Rejects the task with the given error. + * + * This method should be called when task execution fails + * or times out. + * + * @param error - The error that occurred during task execution + */ + rejectWithError(error: Error): void { + if (!this.rejectResult) { + throw new Error("Result promise not initialized"); + } + this.rejectResult(error); + } + + /** + * Serializes the task to a request message for WebSocket transmission. + * + * @returns The request message containing ID, task name, and parameters + */ + toRequest(): PluginTaskRequest { + return { + id: this.id, + task: this.task, + params: this.params, + }; + } +} diff --git a/mcp/packages/server/src/ReplServer.ts b/mcp/packages/server/src/ReplServer.ts new file mode 100644 index 0000000000..978f0e2ea4 --- /dev/null +++ b/mcp/packages/server/src/ReplServer.ts @@ -0,0 +1,112 @@ +import express from "express"; +import path from "path"; +import { fileURLToPath } from "url"; +import { PluginBridge } from "./PluginBridge"; +import { ExecuteCodePluginTask } from "./tasks/ExecuteCodePluginTask"; +import { createLogger } from "./logger"; + +/** + * Web-based REPL server for executing code through the PluginBridge. + * + * Provides a REPL-style HTML interface that allows users to input + * JavaScript code and execute it via ExecuteCodePluginTask instances. + * The interface maintains command history, displays logs in <pre> tags, + * and shows results in visually separated blocks. + */ +export class ReplServer { + private readonly logger = createLogger("ReplServer"); + private readonly app: express.Application; + private readonly port: number; + private server: any; + + constructor( + private readonly pluginBridge: PluginBridge, + port: number = 4403 + ) { + this.port = port; + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + /** + * Sets up Express middleware for request parsing and static content. + */ + private setupMiddleware(): void { + this.app.use(express.json()); + } + + /** + * Sets up HTTP routes for the REPL interface and API endpoints. + */ + private setupRoutes(): void { + // serve the main REPL interface + this.app.get("/", (req, res) => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const htmlPath = path.join(__dirname, "static", "repl.html"); + res.sendFile(htmlPath); + }); + + // API endpoint for executing code + this.app.post("/execute", async (req, res) => { + try { + const { code } = req.body; + + if (!code || typeof code !== "string") { + return res.status(400).json({ + error: "Code parameter is required and must be a string", + }); + } + + const task = new ExecuteCodePluginTask({ code }); + const result = await this.pluginBridge.executePluginTask(task); + + // extract the result member from ExecuteCodeTaskResultData + const executeResult = result.data?.result; + + res.json({ + success: true, + result: executeResult, + log: result.data?.log || "", + }); + } catch (error) { + this.logger.error(error, "Failed to execute code in REPL"); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error occurred", + }); + } + }); + } + + /** + * Starts the REPL web server. + * + * Begins listening on the configured port and logs server startup information. + */ + public async start(): Promise { + return new Promise((resolve) => { + this.server = this.app.listen(this.port, () => { + this.logger.info(`REPL server started on port ${this.port}`); + this.logger.info( + `REPL interface URL: http://${this.pluginBridge.mcpServer.serverAddress}:${this.port}` + ); + resolve(); + }); + }); + } + + /** + * Stops the REPL web server. + */ + public async stop(): Promise { + if (this.server) { + return new Promise((resolve) => { + this.server.close(() => { + this.logger.info("REPL server stopped"); + resolve(); + }); + }); + } + } +} diff --git a/mcp/packages/server/src/Tool.ts b/mcp/packages/server/src/Tool.ts new file mode 100644 index 0000000000..65cfe539bd --- /dev/null +++ b/mcp/packages/server/src/Tool.ts @@ -0,0 +1,121 @@ +import { z } from "zod"; +import "reflect-metadata"; +import { TextResponse, ToolResponse } from "./ToolResponse"; +import type { PenpotMcpServer, SessionContext } from "./PenpotMcpServer"; +import { createLogger } from "./logger"; + +/** + * An empty arguments class for tools that do not require any parameters. + */ +export class EmptyToolArgs { + static schema = {}; +} + +/** + * Base class for type-safe tools with automatic schema generation and validation. + * + * This class provides type safety through automatic validation and strongly-typed + * protected methods. All tools should extend this class. + * + * @template TArgs - The strongly-typed arguments class for this tool + */ +export abstract class Tool { + private readonly logger = createLogger("Tool"); + + protected constructor( + protected mcpServer: PenpotMcpServer, + private inputSchema: z.ZodRawShape + ) {} + + /** + * Executes the tool with automatic validation and type safety. + * + * This method handles the unknown args from the MCP protocol, + * delegating to the type-safe implementation. + */ + async execute(args: unknown): Promise { + try { + let argsInstance: TArgs = args as TArgs; + this.logger.info("Executing tool: %s; arguments: %s", this.getToolName(), this.formatArgs(argsInstance)); + + // execute the actual tool logic + let result = await this.executeCore(argsInstance); + + this.logger.info("Tool execution completed: %s", this.getToolName()); + return result; + } catch (error) { + this.logger.error(error); + return new TextResponse(`Tool execution failed: ${String(error)}`); + } + } + + /** + * Formats tool arguments for readable logging. + * + * Multi-line strings are preserved with proper indentation. + */ + protected formatArgs(args: TArgs): string { + const formatted: string[] = []; + + for (const [key, value] of Object.entries(args)) { + if (typeof value === "string" && value.includes("\n")) { + // multi-line string - preserve formatting with indentation + const indentedValue = value + .split("\n") + .map((line, index) => (index === 0 ? line : " " + line)) + .join("\n"); + formatted.push(` ${key}: ${indentedValue}`); + } else if (typeof value === "string") { + // single-line string + formatted.push(` ${key}: "${value}"`); + } else if (value === null || value === undefined) { + formatted.push(` ${key}: ${value}`); + } else { + // other types (numbers, booleans, objects, arrays) + const stringified = JSON.stringify(value, null, 2); + if (stringified.includes("\n")) { + // multi-line JSON - indent it + const indented = stringified + .split("\n") + .map((line, index) => (index === 0 ? line : " " + line)) + .join("\n"); + formatted.push(` ${key}: ${indented}`); + } else { + formatted.push(` ${key}: ${stringified}`); + } + } + } + + return formatted.length > 0 ? "\n" + formatted.join("\n") : "{}"; + } + + /** + * Retrieves the current session context. + * + * @returns The session context for the current request, or undefined if not in a request context + */ + protected getSessionContext(): SessionContext | undefined { + return this.mcpServer.getSessionContext(); + } + + public getInputSchema() { + return this.inputSchema; + } + + /** + * Returns the tool's unique name. + */ + public abstract getToolName(): string; + + /** + * Returns the tool's description. + */ + public abstract getToolDescription(): string; + + /** + * Executes the tool's core logic. + * + * @param args - The (typed) tool arguments + */ + protected abstract executeCore(args: TArgs): Promise; +} diff --git a/mcp/packages/server/src/ToolResponse.ts b/mcp/packages/server/src/ToolResponse.ts new file mode 100644 index 0000000000..6055f24ccf --- /dev/null +++ b/mcp/packages/server/src/ToolResponse.ts @@ -0,0 +1,97 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +type CallToolContent = CallToolResult["content"][number]; +type TextItem = Extract; +type ImageItem = Extract; + +export class TextContent implements TextItem { + [x: string]: unknown; + readonly type = "text" as const; + constructor(public text: string) {} + + /** + * @param data - Text data as string or as object (from JSON representation where indices are mapped to character codes) + */ + public static textData(data: string | object): string { + if (typeof data === "object") { + // convert object containing character codes (as obtained from JSON conversion of string) back to string + return String.fromCharCode(...(Object.values(data) as number[])); + } else { + return data; + } + } +} + +export class ImageContent implements ImageItem { + [x: string]: unknown; + readonly type = "image" as const; + + /** + * @param data - Base64-encoded image data + * @param mimeType - MIME type of the image (e.g., "image/png") + */ + constructor( + public data: string, + public mimeType: string + ) {} + + /** + * Utility function for ensuring a consistent Uint8Array representation of byte data. + * Input can be either a Uint8Array or an object (as obtained from JSON conversion of Uint8Array + * from the plugin). + * + * @param data - data as Uint8Array or as object (from JSON conversion of Uint8Array) + * @return data as Uint8Array + */ + public static byteData(data: Uint8Array | object): Uint8Array { + if (typeof data === "object") { + // convert object (as obtained from JSON conversion of Uint8Array) back to Uint8Array + return new Uint8Array(Object.values(data) as number[]); + } else { + return data; + } + } +} + +export class PNGImageContent extends ImageContent { + /** + * @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array) + */ + constructor(data: Uint8Array | object) { + let array = ImageContent.byteData(data); + super(Buffer.from(array).toString("base64"), "image/png"); + } +} + +export class ToolResponse implements CallToolResult { + [x: string]: unknown; + content: CallToolContent[]; // <- IMPORTANT: protocol’s union + constructor(content: CallToolContent[]) { + this.content = content; + } +} + +export class TextResponse extends ToolResponse { + constructor(text: string) { + super([new TextContent(text)]); + } + + /** + * Creates a TextResponse from text data given as string or as object (from JSON representation where indices are mapped to + * character codes). + * + * @param data - Text data as string or as object (from JSON representation where indices are mapped to character codes) + */ + public static fromData(data: string | object): TextResponse { + return new TextResponse(TextContent.textData(data)); + } +} + +export class PNGResponse extends ToolResponse { + /** + * @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array) + */ + constructor(data: Uint8Array | object) { + super([new PNGImageContent(data)]); + } +} diff --git a/mcp/packages/server/src/index.ts b/mcp/packages/server/src/index.ts new file mode 100644 index 0000000000..356c2cea81 --- /dev/null +++ b/mcp/packages/server/src/index.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import { PenpotMcpServer } from "./PenpotMcpServer"; +import { createLogger, logFilePath } from "./logger"; + +/** + * Entry point for Penpot MCP Server + * + * Creates and starts the MCP server instance, handling any startup errors + * gracefully and ensuring proper process termination. + * + * Configuration via environment variables (see README). + */ +async function main(): Promise { + const logger = createLogger("main"); + + // log the file path early so it appears before any potential errors + logger.info(`Logging to file: ${logFilePath}`); + + try { + const args = process.argv.slice(2); + let multiUser = false; // default to single-user mode + + // parse command line arguments + for (let i = 0; i < args.length; i++) { + if (args[i] === "--multi-user") { + multiUser = true; + } else if (args[i] === "--help" || args[i] === "-h") { + logger.info("Usage: node dist/index.js [options]"); + logger.info("Options:"); + logger.info(" --multi-user Enable multi-user mode (default: single-user)"); + logger.info(" --help, -h Show this help message"); + logger.info(""); + logger.info("Note that configuration is mostly handled through environment variables."); + logger.info("Refer to the README for more information."); + process.exit(0); + } + } + + const server = new PenpotMcpServer(multiUser); + await server.start(); + + // keep the process alive + process.on("SIGINT", async () => { + logger.info("Received SIGINT, shutting down gracefully..."); + await server.stop(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + logger.info("Received SIGTERM, shutting down gracefully..."); + await server.stop(); + process.exit(0); + }); + } catch (error) { + logger.error(error, "Failed to start MCP server"); + process.exit(1); + } +} + +// Start the server if this file is run directly +if (import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("index.js")) { + main().catch((error) => { + createLogger("main").error(error, "Unhandled error in main"); + process.exit(1); + }); +} diff --git a/mcp/packages/server/src/logger.ts b/mcp/packages/server/src/logger.ts new file mode 100644 index 0000000000..1e8f96e5ec --- /dev/null +++ b/mcp/packages/server/src/logger.ts @@ -0,0 +1,80 @@ +import pino from "pino"; +import { join, resolve } from "path"; + +/** + * Configuration for log file location and level. + */ +const LOG_DIR = process.env.PENPOT_MCP_LOG_DIR || "logs"; +const LOG_LEVEL = process.env.PENPOT_MCP_LOG_LEVEL || "info"; + +/** + * Generates a timestamped log file name. + * + * @returns Log file name + */ +function generateLogFileName(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const seconds = String(now.getSeconds()).padStart(2, "0"); + return `penpot-mcp-${year}${month}${day}-${hours}${minutes}${seconds}.log`; +} + +/** + * Absolute path to the log file being written. + */ +export const logFilePath = resolve(join(LOG_DIR, generateLogFileName())); + +/** + * Logger instance configured for both console and file output with metadata. + * + * Both console and file output use pretty formatting for human readability. + * Console output includes colors, while file output is plain text. + */ +export const logger = pino({ + level: LOG_LEVEL, + timestamp: pino.stdTimeFunctions.isoTime, + transport: { + targets: [ + { + // console transport with pretty formatting + target: "pino-pretty", + level: LOG_LEVEL, + options: { + colorize: true, + translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", + ignore: "pid,hostname", + messageFormat: "{msg}", + levelFirst: true, + }, + }, + { + // file transport with pretty formatting (same as console) + target: "pino-pretty", + level: LOG_LEVEL, + options: { + destination: logFilePath, + colorize: false, + translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l", + ignore: "pid,hostname", + messageFormat: "{msg}", + levelFirst: true, + mkdir: true, + }, + }, + ], + }, +}); + +/** + * Creates a child logger with the specified name/origin. + * + * @param name - The name/origin identifier for the logger + * @returns Child logger instance with the specified name + */ +export function createLogger(name: string) { + return logger.child({ name }); +} diff --git a/mcp/packages/server/src/static/repl.html b/mcp/packages/server/src/static/repl.html new file mode 100644 index 0000000000..9fafa13bc0 --- /dev/null +++ b/mcp/packages/server/src/static/repl.html @@ -0,0 +1,554 @@ + + + + + + Penpot API REPL + + + + +
+ +
+ +
Ctrl+Enter to execute • Arrow up/down for command history
+ + + + diff --git a/mcp/packages/server/src/tasks/ExecuteCodePluginTask.ts b/mcp/packages/server/src/tasks/ExecuteCodePluginTask.ts new file mode 100644 index 0000000000..130e69c2ec --- /dev/null +++ b/mcp/packages/server/src/tasks/ExecuteCodePluginTask.ts @@ -0,0 +1,22 @@ +import { PluginTask } from "../PluginTask"; +import { ExecuteCodeTaskParams, ExecuteCodeTaskResultData, PluginTaskResult } from "@penpot/mcp-common"; + +/** + * Task for executing JavaScript code in the plugin context. + * + * This task instructs the plugin to execute arbitrary JavaScript code + * and return the result of execution. + */ +export class ExecuteCodePluginTask extends PluginTask< + ExecuteCodeTaskParams, + PluginTaskResult> +> { + /** + * Creates a new execute code task. + * + * @param params - The parameters containing the code to execute + */ + constructor(params: ExecuteCodeTaskParams) { + super("executeCode", params); + } +} diff --git a/mcp/packages/server/src/tools/ExecuteCodeTool.ts b/mcp/packages/server/src/tools/ExecuteCodeTool.ts new file mode 100644 index 0000000000..adcb6339fa --- /dev/null +++ b/mcp/packages/server/src/tools/ExecuteCodeTool.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import type { ToolResponse } from "../ToolResponse"; +import { TextResponse } from "../ToolResponse"; +import "reflect-metadata"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask"; +import { ExecuteCodeTaskParams } from "@penpot/mcp-common"; + +/** + * Arguments class for ExecuteCodeTool + */ +export class ExecuteCodeArgs { + static schema = { + code: z + .string() + .min(1, "Code cannot be empty") + .describe("The JavaScript code to execute in the plugin context."), + }; + + /** + * The JavaScript code to execute in the plugin context. + */ + code!: string; +} + +/** + * Tool for executing JavaScript code in the Penpot plugin context + */ +export class ExecuteCodeTool extends Tool { + /** + * Creates a new ExecuteCode tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, ExecuteCodeArgs.schema); + } + + public getToolName(): string { + return "execute_code"; + } + + public getToolDescription(): string { + return ( + "Executes JavaScript code in the Penpot plugin context.\n" + + "IMPORTANT: Before using this tool, make sure you have read the 'Penpot High-Level Overview' and know " + + "which Penpot API functionality is necessary and how to use it.\n" + + "You have access two main objects: `penpot` (the Penpot API, of type `Penpot`), `penpotUtils`, " + + "and `storage`.\n" + + "`storage` is an object in which arbitrary data can be stored, simply by adding a new attribute; " + + "stored attributes can be referenced in future calls to this tool, so any intermediate results that " + + "could come in handy later should be stored in `storage` instead of just a fleeting variable; " + + "you can also store functions and thus build up a library).\n" + + "Think of the code being executed as the body of a function: " + + "The tool call returns whatever you return in the applicable `return` statement, if any.\n" + + "If an exception occurs, the exception's message will be returned to you.\n" + + "Any output that you generate via the `console` object will be returned to you separately; so you may use it" + + "to track what your code is doing, but you should *only* do so only if there is an ACTUAL NEED for this! " + + "VERY IMPORTANT: Don't use logging prematurely! NEVER log the data you are returning, as you will otherwise receive it twice!\n" + + "VERY IMPORTANT: In general, try a simple approach first, and only if it fails, try more complex code that involves " + + "handling different cases (in particular error cases) and that applies logging." + ); + } + + protected async executeCore(args: ExecuteCodeArgs): Promise { + const taskParams: ExecuteCodeTaskParams = { code: args.code }; + const task = new ExecuteCodePluginTask(taskParams); + const result = await this.mcpServer.pluginBridge.executePluginTask(task); + + if (result.data !== undefined) { + return new TextResponse(JSON.stringify(result.data, null, 2)); + } else { + return new TextResponse("Code executed successfully with no return value."); + } + } +} diff --git a/mcp/packages/server/src/tools/ExportShapeTool.ts b/mcp/packages/server/src/tools/ExportShapeTool.ts new file mode 100644 index 0000000000..d032b0b770 --- /dev/null +++ b/mcp/packages/server/src/tools/ExportShapeTool.ts @@ -0,0 +1,147 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import { ImageContent, PNGImageContent, PNGResponse, TextContent, TextResponse, ToolResponse } from "../ToolResponse"; +import "reflect-metadata"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask"; +import { FileUtils } from "../utils/FileUtils"; +import sharp from "sharp"; + +/** + * Arguments class for ExportShapeTool + */ +export class ExportShapeArgs { + static schema = { + shapeId: z + .string() + .min(1, "shapeId cannot be empty") + .describe( + "Identifier of the shape to export. Use the special identifier 'selection' to " + + "export the first shape currently selected by the user." + ), + format: z.enum(["svg", "png"]).default("png").describe("The output format, either 'png' (default) or 'svg'."), + mode: z + .enum(["shape", "fill"]) + .default("shape") + .describe( + "The export mode: either 'shape' (full shape as it appears in the design, including descendants; the default) or " + + "'fill' (export the raw image that is used as a fill for the shape; PNG format only)" + ), + filePath: z + .string() + .optional() + .describe( + "Optional file path to save the exported image to. If not provided, " + + "the image data is returned directly for you to see." + ), + }; + + shapeId!: string; + + format: "svg" | "png" = "png"; + + mode: "shape" | "fill" = "shape"; + + filePath?: string; +} + +/** + * Tool for executing JavaScript code in the Penpot plugin context + */ +export class ExportShapeTool extends Tool { + /** + * Creates a new ExecuteCode tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + let schema: any = ExportShapeArgs.schema; + if (!mcpServer.isFileSystemAccessEnabled()) { + // remove filePath key from schema + schema = { ...schema }; + delete schema.filePath; + } + super(mcpServer, schema); + } + + public getToolName(): string { + return "export_shape"; + } + + public getToolDescription(): string { + let description = + "Exports a shape (or a shape's image fill) from the Penpot design to a PNG or SVG image, " + + "such that you can get an impression of what it looks like. "; + if (this.mcpServer.isFileSystemAccessEnabled()) { + description += "\nAlternatively, you can save it to a file."; + } + return description; + } + + protected async executeCore(args: ExportShapeArgs): Promise { + // check arguments + if (args.filePath) { + FileUtils.checkPathIsAbsolute(args.filePath); + } + + // create code for exporting the shape + let shapeCode: string; + if (args.shapeId === "selection") { + shapeCode = `penpot.selection[0]`; + } else { + shapeCode = `penpotUtils.findShapeById("${args.shapeId}")`; + } + const asSvg = args.format === "svg"; + const code = `return penpotUtils.exportImage(${shapeCode}, "${args.mode}", ${asSvg});`; + + // execute the code and obtain the image data + const task = new ExecuteCodePluginTask({ code: code }); + const result = await this.mcpServer.pluginBridge.executePluginTask(task); + const imageData = result.data!.result; + + // handle output and return response + if (!args.filePath) { + // return image data directly (for the LLM to "see" it) + if (args.format === "png") { + return new PNGResponse(await this.toPngImageBytes(imageData)); + } else { + return TextResponse.fromData(imageData); + } + } else { + // save to file requested: make sure file system access is enabled + if (!this.mcpServer.isFileSystemAccessEnabled()) { + throw new Error("File system access is not enabled on the MCP server!"); + } + // save to file + if (args.format === "png") { + FileUtils.writeBinaryFile(args.filePath, await this.toPngImageBytes(imageData)); + } else { + FileUtils.writeTextFile(args.filePath, TextContent.textData(imageData)); + } + return new TextResponse(`The shape has been exported to ${args.filePath}`); + } + } + + /** + * Converts image data to PNG format if necessary. + * + * @param data - The original image data as Uint8Array or as object (from JSON conversion of Uint8Array) + * @return The image data as PNG bytes + */ + private async toPngImageBytes(data: Uint8Array | object): Promise { + const originalBytes = ImageContent.byteData(data); + + // use sharp to detect format and convert to PNG if necessary + const image = sharp(originalBytes); + const metadata = await image.metadata(); + + // if already PNG, return as-is to avoid unnecessary re-encoding + if (metadata.format === "png") { + return originalBytes; + } + + // convert to PNG + const pngBuffer = await image.png().toBuffer(); + return new Uint8Array(pngBuffer); + } +} diff --git a/mcp/packages/server/src/tools/HighLevelOverviewTool.ts b/mcp/packages/server/src/tools/HighLevelOverviewTool.ts new file mode 100644 index 0000000000..ada8829771 --- /dev/null +++ b/mcp/packages/server/src/tools/HighLevelOverviewTool.ts @@ -0,0 +1,26 @@ +import { EmptyToolArgs, Tool } from "../Tool"; +import "reflect-metadata"; +import type { ToolResponse } from "../ToolResponse"; +import { TextResponse } from "../ToolResponse"; +import { PenpotMcpServer } from "../PenpotMcpServer"; + +export class HighLevelOverviewTool extends Tool { + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, EmptyToolArgs.schema); + } + + public getToolName(): string { + return "high_level_overview"; + } + + public getToolDescription(): string { + return ( + "Returns basic high-level instructions on the usage of Penpot-related tools and the Penpot API. " + + "If you have already read the 'Penpot High-Level Overview', you must not call this tool." + ); + } + + protected async executeCore(args: EmptyToolArgs): Promise { + return new TextResponse(this.mcpServer.getInitialInstructions()); + } +} diff --git a/mcp/packages/server/src/tools/ImportImageTool.ts b/mcp/packages/server/src/tools/ImportImageTool.ts new file mode 100644 index 0000000000..7581aaf525 --- /dev/null +++ b/mcp/packages/server/src/tools/ImportImageTool.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import { TextResponse, ToolResponse } from "../ToolResponse"; +import "reflect-metadata"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ExecuteCodePluginTask } from "../tasks/ExecuteCodePluginTask"; +import { FileUtils } from "../utils/FileUtils"; +import * as fs from "fs"; +import * as path from "path"; + +/** + * Arguments class for ImportImageTool + */ +export class ImportImageArgs { + static schema = { + filePath: z.string().min(1, "filePath cannot be empty").describe("Absolute path to the image file to import."), + x: z.number().optional().describe("Optional X coordinate for the rectangle's position."), + y: z.number().optional().describe("Optional Y coordinate for the rectangle's position."), + width: z + .number() + .positive("width must be positive") + .optional() + .describe( + "Optional width for the rectangle. If only width is provided, height is calculated to maintain aspect ratio." + ), + height: z + .number() + .positive("height must be positive") + .optional() + .describe( + "Optional height for the rectangle. If only height is provided, width is calculated to maintain aspect ratio." + ), + }; + + filePath!: string; + + x?: number; + + y?: number; + + width?: number; + + height?: number; +} + +/** + * Tool for importing a raster image from the local file system into Penpot + */ +export class ImportImageTool extends Tool { + /** + * Maps file extensions to MIME types. + */ + protected static readonly MIME_TYPES: { [key: string]: string } = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", + ".webp": "image/webp", + }; + + /** + * Creates a new ImportImage tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer) { + super(mcpServer, ImportImageArgs.schema); + } + + public getToolName(): string { + return "import_image"; + } + + public getToolDescription(): string { + return ( + "Imports a pixel image from the local file system into Penpot by creating a Rectangle instance " + + "that uses the image as a fill. The rectangle has the image's original proportions by default. " + + "Optionally accepts position (x, y) and dimensions (width, height) parameters. " + + "If only one dimension is provided, the other is calculated to maintain the image's aspect ratio. " + + "Supported formats: JPEG, PNG, GIF, WEBP." + ); + } + + protected async executeCore(args: ImportImageArgs): Promise { + // check that file path is absolute + FileUtils.checkPathIsAbsolute(args.filePath); + + // check that file exists + if (!fs.existsSync(args.filePath)) { + throw new Error(`File not found: ${args.filePath}`); + } + + // read the file as binary data + const fileData = fs.readFileSync(args.filePath); + const base64Data = fileData.toString("base64"); + + // determine mime type from file extension + const ext = path.extname(args.filePath).toLowerCase(); + const mimeType = ImportImageTool.MIME_TYPES[ext]; + if (!mimeType) { + const supportedExtensions = Object.keys(ImportImageTool.MIME_TYPES).join(", "); + throw new Error( + `Unsupported image format: ${ext}. Supported formats (file extensions): ${supportedExtensions}` + ); + } + + // generate and execute JavaScript code to import the image + const fileName = path.basename(args.filePath); + const escapedBase64 = base64Data.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); + const escapedFileName = fileName.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); + const code = ` + const rectangle = await penpotUtils.importImage( + '${escapedBase64}', '${mimeType}', '${escapedFileName}', + ${args.x ?? "undefined"}, ${args.y ?? "undefined"}, + ${args.width ?? "undefined"}, ${args.height ?? "undefined"}); + return { shapeId: rectangle.id }; + `; + const task = new ExecuteCodePluginTask({ code: code }); + const executionResult = await this.mcpServer.pluginBridge.executePluginTask(task); + + return new TextResponse(JSON.stringify(executionResult.data?.result, null, 2)); + } +} diff --git a/mcp/packages/server/src/tools/PenpotApiInfoTool.ts b/mcp/packages/server/src/tools/PenpotApiInfoTool.ts new file mode 100644 index 0000000000..935f4c3702 --- /dev/null +++ b/mcp/packages/server/src/tools/PenpotApiInfoTool.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import { Tool } from "../Tool"; +import "reflect-metadata"; +import type { ToolResponse } from "../ToolResponse"; +import { TextResponse } from "../ToolResponse"; +import { PenpotMcpServer } from "../PenpotMcpServer"; +import { ApiDocs } from "../ApiDocs"; + +/** + * Arguments class for the PenpotApiInfoTool + */ +export class PenpotApiInfoArgs { + static schema = { + type: z.string().min(1, "Type name cannot be empty"), + member: z.string().optional(), + }; + + /** + * The API type name to retrieve information for. + */ + type!: string; + + /** + * The specific member name to retrieve (optional). + */ + member?: string; +} + +/** + * Tool for retrieving Penpot API documentation information. + * + * This tool provides access to API type documentation loaded from YAML files, + * allowing retrieval of either full type documentation or specific member details. + */ +export class PenpotApiInfoTool extends Tool { + private static readonly MAX_FULL_TEXT_CHARS = 2000; + private readonly apiDocs: ApiDocs; + + /** + * Creates a new PenpotApiInfo tool instance. + * + * @param mcpServer - The MCP server instance + */ + constructor(mcpServer: PenpotMcpServer, apiDocs: ApiDocs) { + super(mcpServer, PenpotApiInfoArgs.schema); + this.apiDocs = apiDocs; + } + + public getToolName(): string { + return "penpot_api_info"; + } + + public getToolDescription(): string { + return ( + "Retrieves Penpot API documentation for types and their members." + + "Be sure to read the 'Penpot High-Level Overview' first." + ); + } + + protected async executeCore(args: PenpotApiInfoArgs): Promise { + const apiType = this.apiDocs.getType(args.type); + + if (!apiType) { + throw new Error(`API type "${args.type}" not found`); + } + + if (args.member) { + // return specific member documentation + const memberDoc = apiType.getMember(args.member); + if (!memberDoc) { + throw new Error(`Member "${args.member}" not found in type "${args.type}"`); + } + return new TextResponse(memberDoc); + } else { + // return full text or overview based on length + const fullText = apiType.getFullText(); + if (fullText.length <= PenpotApiInfoTool.MAX_FULL_TEXT_CHARS) { + return new TextResponse(fullText); + } else { + return new TextResponse( + apiType.getOverviewText() + + "\n\nMember details not provided (too long). " + + "Call this tool with a member name for more information." + ); + } + } + } +} diff --git a/mcp/packages/server/src/utils/FileUtils.ts b/mcp/packages/server/src/utils/FileUtils.ts new file mode 100644 index 0000000000..8da7c7d6d6 --- /dev/null +++ b/mcp/packages/server/src/utils/FileUtils.ts @@ -0,0 +1,44 @@ +import * as path from "path"; +import * as fs from "fs"; + +export class FileUtils { + /** + * Checks whether the given file path is absolute and raises an error if not. + * + * @param filePath - The file path to check + */ + public static checkPathIsAbsolute(filePath: string): void { + if (!path.isAbsolute(filePath)) { + throw new Error(`The specified file path must be absolute: ${filePath}`); + } + } + + public static createParentDirectories(filePath: string): void { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + + /** + * Writes binary data to a file at the specified path, creating the parent directories if necessary. + * + * @param filePath - The absolute path to the file where data should be written + * @param bytes - The binary data to write to the file + */ + public static writeBinaryFile(filePath: string, bytes: Uint8Array): void { + this.createParentDirectories(filePath); + fs.writeFileSync(filePath, Buffer.from(bytes)); + } + + /** + * Writes text data to a file at the specified path, creating the parent directories if necessary. + * + * @param filePath - The absolute path to the file where data should be written + * @param text - The text data to write to the file + */ + public static writeTextFile(filePath: string, text: string): void { + this.createParentDirectories(filePath); + fs.writeFileSync(filePath, text, { encoding: "utf-8" }); + } +} diff --git a/mcp/packages/server/tsconfig.json b/mcp/packages/server/tsconfig.json new file mode 100644 index 0000000000..f61bd28a33 --- /dev/null +++ b/mcp/packages/server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/mcp/pnpm-lock.yaml b/mcp/pnpm-lock.yaml new file mode 100644 index 0000000000..15088c9791 --- /dev/null +++ b/mcp/pnpm-lock.yaml @@ -0,0 +1,2851 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + prettier: + specifier: ^3.0.0 + version: 3.8.1 + + packages/common: + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.3 + + packages/plugin: + dependencies: + '@penpot/plugin-styles': + specifier: 1.4.1 + version: 1.4.1 + '@penpot/plugin-types': + specifier: 1.4.1 + version: 1.4.1 + devDependencies: + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^7.0.8 + version: 7.3.1(@types/node@20.19.30) + vite-live-preview: + specifier: ^0.3.2 + version: 0.3.2(vite@7.3.1(@types/node@20.19.30)) + + packages/server: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.24.0 + version: 1.25.3(hono@4.11.7)(zod@4.3.6) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.3 + version: 0.14.3 + express: + specifier: ^5.1.0 + version: 5.2.1 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 + penpot-mcp: + specifier: file:.. + version: packages@file:packages + pino: + specifier: ^9.10.0 + version: 9.14.0 + pino-pretty: + specifier: ^13.1.1 + version: 13.1.3 + reflect-metadata: + specifier: ^0.1.13 + version: 0.1.14 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + ws: + specifier: ^8.18.0 + version: 8.19.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@penpot/mcp-common': + specifier: workspace:../common + version: link:../common + '@types/express': + specifier: ^4.17.0 + version: 4.17.25 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^20.0.0 + version: 20.19.30 + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.30)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@commander-js/extra-typings@12.1.0': + resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} + peerDependencies: + commander: ~12.1.0 + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@penpot/plugin-styles@1.4.1': + resolution: {integrity: sha512-6TuJqKQsq1Xmhn2A02R+kCOzIzIdqgFg5z6ncLH2PlAflKIX6aYsGiOF7yFx4RYgCegRVMFPnVis6/hwO+YGQg==} + + '@penpot/plugin-types@1.4.1': + resolution: {integrity: sha512-pHE2B3GI8M5JR03S/NdBoN+z6e1R1IEh3vpFbLG9LN0EZpQE6nEbmCo5jWAWI73Jqlg6CHG/RWVJNmWECnkDTA==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rollup/rollup-android-arm-eabi@4.57.0': + resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.0': + resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.0': + resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.0': + resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.0': + resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.0': + resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.0': + resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.0': + resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.0': + resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.0': + resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.0': + resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.0': + resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.0': + resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.0': + resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==} + cpu: [x64] + os: [win32] + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/ansi-html@0.0.0': + resolution: {integrity: sha512-PEBpUlteD0VW02udY7UjjgjxHwVXmkdanhmRIMkzatGmORJGjzqKylrXVxz1G5xRTEECMxIkwTHpPmZ9Jb7ANQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-html@0.0.9: + resolution: {integrity: sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.3: + resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-copy@4.0.2: + resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + libphonenumber-js@1.12.35: + resolution: {integrity: sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-defer@4.0.1: + resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} + engines: {node: '>=12'} + + packages@file:packages: + resolution: {directory: packages, type: directory} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rollup@4.57.0: + resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-live-preview@0.3.2: + resolution: {integrity: sha512-NrmGaAc85qvkx/+6FluiTo9rLnoY+/NOYnuUvcW5Yb5tSJzUxuloXYrCSS1dtxQB9YKUbpQ95JCb0GRuF//JEQ==} + hasBin: true + peerDependencies: + vite: '>=5.2.13' + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@commander-js/extra-typings@12.1.0(commander@12.1.0)': + dependencies: + commander: 12.1.0 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@hono/node-server@1.19.9(hono@4.11.7)': + dependencies: + hono: 4.11.7 + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - hono + - supports-color + + '@penpot/plugin-styles@1.4.1': {} + + '@penpot/plugin-types@1.4.1': {} + + '@pinojs/redact@0.4.0': {} + + '@rollup/rollup-android-arm-eabi@4.57.0': + optional: true + + '@rollup/rollup-android-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-x64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.0': + optional: true + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/ansi-html@0.0.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.30 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.30 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 20.19.30 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + + '@types/js-yaml@4.0.9': {} + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.30 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.30 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.30 + '@types/send': 0.17.6 + + '@types/validator@13.15.10': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.30 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-html@0.0.9: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arg@4.1.3: {} + + argparse@2.0.1: {} + + atomic-sleep@1.0.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + class-transformer@0.5.1: {} + + class-validator@0.14.3: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.35 + validator: 13.15.26 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + commander@12.1.0: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + diff@4.0.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-copy@4.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + hono@4.11.7: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.1.3: {} + + joycon@3.1.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + libphonenumber-js@1.12.35: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-defer@4.0.1: {} + + packages@file:packages: {} + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.3.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.0 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + pkce-challenge@5.0.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.8.1: {} + + process-warning@5.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + real-require@0.2.0: {} + + reflect-metadata@0.1.14: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + rollup@4.57.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.0 + '@rollup/rollup-android-arm64': 4.57.0 + '@rollup/rollup-darwin-arm64': 4.57.0 + '@rollup/rollup-darwin-x64': 4.57.0 + '@rollup/rollup-freebsd-arm64': 4.57.0 + '@rollup/rollup-freebsd-x64': 4.57.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.0 + '@rollup/rollup-linux-arm-musleabihf': 4.57.0 + '@rollup/rollup-linux-arm64-gnu': 4.57.0 + '@rollup/rollup-linux-arm64-musl': 4.57.0 + '@rollup/rollup-linux-loong64-gnu': 4.57.0 + '@rollup/rollup-linux-loong64-musl': 4.57.0 + '@rollup/rollup-linux-ppc64-gnu': 4.57.0 + '@rollup/rollup-linux-ppc64-musl': 4.57.0 + '@rollup/rollup-linux-riscv64-gnu': 4.57.0 + '@rollup/rollup-linux-riscv64-musl': 4.57.0 + '@rollup/rollup-linux-s390x-gnu': 4.57.0 + '@rollup/rollup-linux-x64-gnu': 4.57.0 + '@rollup/rollup-linux-x64-musl': 4.57.0 + '@rollup/rollup-openbsd-x64': 4.57.0 + '@rollup/rollup-openharmony-arm64': 4.57.0 + '@rollup/rollup-win32-arm64-msvc': 4.57.0 + '@rollup/rollup-win32-ia32-msvc': 4.57.0 + '@rollup/rollup-win32-x64-gnu': 4.57.0 + '@rollup/rollup-win32-x64-msvc': 4.57.0 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + secure-json-parse@4.1.0: {} + + semver@7.7.3: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@5.0.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + ts-node@10.9.2(@types/node@20.19.30)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.30 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + v8-compile-cache-lib@3.0.1: {} + + validator@13.15.26: {} + + vary@1.1.2: {} + + vite-live-preview@0.3.2(vite@7.3.1(@types/node@20.19.30)): + dependencies: + '@commander-js/extra-typings': 12.1.0(commander@12.1.0) + '@types/ansi-html': 0.0.0 + '@types/debug': 4.1.12 + '@types/ws': 8.18.1 + ansi-html: 0.0.9 + chalk: 5.6.2 + commander: 12.1.0 + debug: 4.4.3 + escape-goat: 4.0.0 + p-defer: 4.0.1 + vite: 7.3.1(@types/node@20.19.30) + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + vite@7.3.1(@types/node@20.19.30): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.30 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/mcp/pnpm-workspace.yaml b/mcp/pnpm-workspace.yaml new file mode 100644 index 0000000000..9bc2f715c9 --- /dev/null +++ b/mcp/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +linkWorkspacePackages: true + +packages: + - "./packages/common" + - "./packages/server" + - "./packages/plugin" diff --git a/mcp/resources/architecture.png b/mcp/resources/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f8305fd993888558d0cadafc8bbdc110611b16f4 GIT binary patch literal 66448 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

uNU##{p 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 + +This will generate `../packages/server/data/api_types.yml`. diff --git a/mcp/types-generator/build b/mcp/types-generator/build new file mode 100755 index 0000000000..9f05568c88 --- /dev/null +++ b/mcp/types-generator/build @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +export URL=${1:-http://localhost:9090} + +SCRIPT_DIR=$(dirname $0); +pushd $SCRIPT_DIR; +echo "Scrapping $URL..." + +set -e +pixi install; +pixi run python prepare_api_docs.py $URL; diff --git a/mcp/types-generator/pixi.lock b/mcp/types-generator/pixi.lock new file mode 100644 index 0000000000..f6ee7d4ec7 --- /dev/null +++ b/mcp/types-generator/pixi.lock @@ -0,0 +1,1090 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h7c6b74e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdownify-1.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-pycharm-0.0.9-unix_hf108a03_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.16-py311h49ec1c0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.14-py311h49ec1c0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/3f/85/5a634f5bae4e73c9d11dd7d613338c0414d07455a1a211175bcc52e66aec/sensai_utils-1.6.0-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.5-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py311h3e6a449_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdownify-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.3-h725018a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-pycharm-0.0.9-win_hba80fca_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.13-h3f84c4b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.15-py311h3485c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.12-py311h3485c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + - pypi: https://files.pythonhosted.org/packages/ad/d5/62a0e693230bace8e9a767d6d187a4d9421a7c6ee4b48551f8ff7bd1629a/sensai_utils-1.5.0-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.5-pyha770c72_0.conda + sha256: d2124c0ea13527c7f54582269b3ae19541141a3740d6d779e7aa95aa82eaf561 + md5: de0fd9702fd4c1186e930b8c35af6b6b + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=compressed-mapping + size: 88278 + timestamp: 1756094375546 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + sha256: b949bd0121bb1eabc282c4de0551cc162b621582ee12b415e6f8297398e3b3b4 + md5: 749ebebabc2cae99b2e5b3edd04c6ca2 + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + size: 89146 + timestamp: 1759146127397 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h7c6b74e_0.conda + sha256: 5e6858dae1935793a7fa7f46d8975b0596b546c28586cb463dd2fdeba3bcc193 + md5: 645bc783bc723d67a294a51bc860762d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 h09219d5_0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 368532 + timestamp: 1761592301216 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py311h3e6a449_4.conda + sha256: d524edc172239fec70ad946e3b2fa1b9d7eea145ad80e9e66da25a4d815770ea + md5: 21d3a7fa95d27938158009cd08e461f2 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.1.0 hfd05255_4 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 323289 + timestamp: 1756600106141 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 + md5: 51a19bba1b8ebfb60df25cde030b7ebc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260341 + timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 + md5: 1077e9333c41ff0be8edd1a5ec0ddace + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 55977 + timestamp: 1757437738856 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda + sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773 + md5: f0991f0f84902f6b6009b4d2350a83aa + depends: + - __unix + license: ISC + purls: [] + size: 152432 + timestamp: 1762967197890 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda + sha256: 3b82f62baad3fd33827b01b0426e8203a2786c8f452f633740868296bcbe8485 + md5: c9e0c0f82f6e63323827db462b40ede8 + depends: + - __win + license: ISC + purls: [] + size: 154489 + timestamp: 1754210967212 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.11.12-pyhd8ed1ab_0.conda + sha256: 083a2bdad892ccf02b352ecab38ee86c3e610ba9a4b11b073ea769d55a115d32 + md5: 96a02a5c1a65470a7e4eedb644c872fd + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 157131 + timestamp: 1762976260320 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.8.3-pyhd8ed1ab_0.conda + sha256: a1ad5b0a2a242f439608f22a538d2175cac4444b7b3f4e2b8c090ac337aaea40 + md5: 11f59985f49df4620890f3e746ed7102 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 158692 + timestamp: 1754231530168 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda + sha256: 3ad13377356c86d3a945ae30e9b8c8734300925ef81a3cb0a9db0d755afbe7bb + md5: 3912e4373de46adafd8f1e97e4bd166b + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 303338 + timestamp: 1761202960110 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_0.conda + sha256: 12f5d72b95dbd417367895a92c35922b24bb016d1497f24f3a243224ec6cb81b + md5: 573fd072e80c1a334e19a1f95024d94d + depends: + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=compressed-mapping + size: 298353 + timestamp: 1758716437777 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + sha256: 838d5a011f0e7422be6427becba3de743c78f3874ad2743c341accbba9bb2624 + md5: 7e7d5ef1b9ed630e4a1c358d6bc62284 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + size: 51033 + timestamp: 1754767444665 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.4-pyhd8ed1ab_0.conda + sha256: b32f8362e885f1b8417bac2b3da4db7323faa12d5db62b7fd6691c02d60d6f59 + md5: a22d1fd9bf98827e280a02875d9a007a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + size: 50965 + timestamp: 1760437331772 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=compressed-mapping + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + md5: 8b189310083baabfb622af68fd9d3ae3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + size: 12129203 + timestamp: 1720853576813 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 + md5: 39a4f67be3286c86d696df570b1201b7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 49765 + timestamp: 1733211921194 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + sha256: ae89d0299ada2a3162c2614a9d26557a92aa6a77120ce142f8e0109bbf0342b0 + md5: 53abe63df7e10a6ba605dc5f9f961d36 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 50721 + timestamp: 1760286526795 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-h1aa0949_0.conda + sha256: 32321d38b8785ef8ddcfef652ee370acee8d944681014d47797a18637ff16854 + md5: 1450224b3e7d17dfeb985364b77a4d47 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 753744 + timestamp: 1763060439129 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 + md5: 4211416ecba1866fab0c6470986c22d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 74811 + timestamp: 1752719572741 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda + sha256: 8432ca842bdf8073ccecf016ccc9140c41c7114dc4ec77ca754551c01f780845 + md5: 3608ffde260281fa641e70d6e34b1b96 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 141322 + timestamp: 1752719767870 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54 + md5: 35f29eec58405aaf55e01cb470d8c26a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 57821 + timestamp: 1760295480630 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + sha256: d3b0b8812eab553d3464bbd68204f007f1ebadf96ce30eb0cbc5159f72e353f5 + md5: 85d8fa5e55ed8f93f874b3b23ed54ec6 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 44978 + timestamp: 1743435053850 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_7.conda + sha256: 08f9b87578ab981c7713e4e6a7d935e40766e10691732bba376d4964562bcb45 + md5: c0374badb3a5d4b1372db28d19462c53 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.2.0 h767d61c_7 + - libgcc-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 822552 + timestamp: 1759968052178 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_7.conda + sha256: 2045066dd8e6e58aaf5ae2b722fb6dfdbb57c862b5f34ac7bfb58c40ef39b6ad + md5: 280ea6eee9e2ddefde25ff799c4f0363 + depends: + - libgcc 15.2.0 h767d61c_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29313 + timestamp: 1759968065504 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_7.conda + sha256: e9fb1c258c8e66ee278397b5822692527c5f5786d372fe7a869b900853f3f5ca + md5: f7b4d76975aac7e5d9e6ad13845f92fe + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 447919 + timestamp: 1759967942498 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc + md5: c15148b2e18da456f5108ccb5e411446 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 104935 + timestamp: 1749230611612 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.0-hee844dc_0.conda + sha256: 4c992dcd0e34b68f843e75406f7f303b1b97c248d18f3c7c330bdc0bc26ae0b3 + md5: 729a572a3ebb8c43933b30edcc628ceb + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 945576 + timestamp: 1762299687230 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + sha256: 5dc4f07b2d6270ac0c874caec53c6984caaaa84bc0d3eb593b0edf3dc8492efa + md5: ccb20d946040f86f0c05b644d5eadeca + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1288499 + timestamp: 1753948889360 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_7.conda + sha256: 1b981647d9775e1cdeb2fab0a4dd9cd75a6b0de2963f6c3953dbd712f78334b3 + md5: 5b767048b1b3ee9a954b06f4084f93dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 h767d61c_7 + constrains: + - libstdcxx-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3898269 + timestamp: 1759968103436 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_7.conda + sha256: 024fd46ac3ea8032a5ec3ea7b91c4c235701a8bf0e6520fe5e6539992a6bd05f + md5: f627678cf829bd70bccf141a19c3ad3e + depends: + - libstdcxx 15.2.0 h8f9b012_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29343 + timestamp: 1759968157195 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda + sha256: e5ec6d2ad7eef538ddcb9ea62ad4346fde70a4736342c4ad87bd713641eb9808 + md5: 80c07c68d2f6870250959dcc95b209d1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 37135 + timestamp: 1758626800002 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 55476 + timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdownify-1.1.0-pyhd8ed1ab_1.conda + sha256: 127f040a4ec203d0e9af708600113fcb7641948f0f0f790c6906738397efa947 + md5: 4792499afa7a0fdfcff1533cab5564b3 + depends: + - beautifulsoup4 >=4.9,<5 + - python >=3.8 + - six >=1.15,<2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/markdownify?source=hash-mapping + size: 19359 + timestamp: 1756304996346 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdownify-1.2.0-pyhcf101f3_0.conda + sha256: cb4991ecdb0e08f88ba9438ccd55c577e1d62e9184b5b552b517038774dccee0 + md5: 2b07b14f9a78dec85e80ae4a2fa3d964 + depends: + - python >=3.10 + - beautifulsoup4 >=4.9,<5 + - six >=1.15,<2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/markdownify?source=hash-mapping + size: 21734 + timestamp: 1760956933495 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d + md5: 9ee58d5c534af06558933af3c845a780 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3165399 + timestamp: 1762839186699 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.3-h725018a_1.conda + sha256: 72dc204b0d59a7262bc77ca0e86cba11cbc6706cb9b4d6656fe7fab9593347c9 + md5: c84884e2c1f899de9a895a1f0b7c9cd8 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9276051 + timestamp: 1758599639304 +- conda: https://conda.anaconda.org/conda-forge/noarch/pixi-pycharm-0.0.9-unix_hf108a03_0.conda + sha256: adb49cb011bc758a18d7729431d393c96b1686e9cb8b2b0a76f158a20a590743 + md5: 178205e98910428bf7411888adf63033 + depends: + - __unix + - python >=3.8 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6681 + timestamp: 1758810290525 +- conda: https://conda.anaconda.org/conda-forge/noarch/pixi-pycharm-0.0.9-win_hba80fca_0.conda + sha256: 22c6fedb771249e8b9723a916a3dc558b74681d3d8c2f118667cba81851f2002 + md5: de239ce0ba6c1b8d4bdd791650d8067c + depends: + - __win + - python >=3.8 + license: BSD-3-Clause + purls: [] + size: 6687 + timestamp: 1758810287418 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda + build_number: 2 + sha256: 5b872f7747891e50e990a96d2b235236a5c66cc9f8c9dcb7149aee674ea8145a + md5: c4202a55b4486314fbb8c11bc43a29a0 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libuuid >=2.41.2,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 30874708 + timestamp: 1761174520369 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.13-h3f84c4b_0_cpython.conda + sha256: 723dbca1384f30bd2070f77dd83eefd0e8d7e4dda96ac3332fbf8fe5573a8abb + md5: bedbb6f7bb654839719cd528f9b298ad + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 18242669 + timestamp: 1749048351218 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + build_number: 8 + sha256: fddf123692aa4b1fc48f0471e346400d9852d96eeed77dbfdd746fa50a8ff894 + md5: 8fcb6b0e2161850556231336dae58358 + constrains: + - python 3.11.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7003 + timestamp: 1752805919375 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + sha256: 8dc54e94721e9ab545d7234aa5192b74102263d3e704e6d0c8aa7008f2da2a7b + md5: db0c6b99149880c8ba515cf4abe93ee4 + depends: + - certifi >=2017.4.17 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - python >=3.9 + - urllib3 >=1.21.1,<3 + constrains: + - chardet >=3.0.2,<6 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=hash-mapping + size: 59263 + timestamp: 1755614348400 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.16-py311h49ec1c0_0.conda + sha256: 3ad29409ab3742a4cab9e42fc936a2678249c97bbe014905d215afab3aaf13bb + md5: 945689ec8e69e9f254eceef82c9f932f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ruamel.yaml.clib >=0.1.2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml?source=hash-mapping + size: 276028 + timestamp: 1761160704044 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.15-py311h3485c13_1.conda + sha256: 4a222db2ec50db5a11ace74090045170a611b24f82d80535e8d98bf478c32cc2 + md5: be5d5993e755c3edc3ef860da01c67b4 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ruamel.yaml.clib >=0.1.2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml?source=hash-mapping + size: 274899 + timestamp: 1756839144620 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.14-py311h49ec1c0_0.conda + sha256: c17b1dc975839a35efc434d3391b8aaa1977a541641d8729c883d5106fb64b97 + md5: 4942e6597e5fc980020b648353c711d4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml-clib?source=hash-mapping + size: 141908 + timestamp: 1760564334661 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.12-py311h3485c13_1.conda + sha256: ad383a91985153438817e6b241c9f151692e01ef257279f83dec55f8d024e213 + md5: 713991ee78f7fbbcecfe03c7226dec24 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml-clib?source=hash-mapping + size: 107842 + timestamp: 1756829092915 +- pypi: https://files.pythonhosted.org/packages/ad/d5/62a0e693230bace8e9a767d6d187a4d9421a7c6ee4b48551f8ff7bd1629a/sensai_utils-1.5.0-py3-none-any.whl + name: sensai-utils + version: 1.5.0 + sha256: c23c51d23d353e7a9b77c70aa8ba7f2d0a8fe4e67ee5bc1ef41c69dba5e4befb + requires_dist: + - typing-extensions>=4.6 +- pypi: https://files.pythonhosted.org/packages/3f/85/5a634f5bae4e73c9d11dd7d613338c0414d07455a1a211175bcc52e66aec/sensai_utils-1.6.0-py3-none-any.whl + name: sensai-utils + version: 1.6.0 + sha256: 3298d4d21bdf7a1b91873213614c748ceb17014c7d72c9492bd5bf82ba893f9d + requires_dist: + - typing-extensions>=4.6 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + sha256: c978576cf9366ba576349b93be1cfd9311c00537622a2f9e14ba2b90c97cae9c + md5: 18c019ccf43769d211f2cf78e9ad46c2 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/soupsieve?source=hash-mapping + size: 37803 + timestamp: 1756330614547 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64 + md5: 86bc20552bf46075e3d92b67f089172d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3284905 + timestamp: 1763054914403 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + sha256: e3614b0eb4abcc70d98eae159db59d9b4059ed743ef402081151a948dce95896 + md5: ebd0e761de9aa879a51d22cc721bd095 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: TCL + license_family: BSD + purls: [] + size: 3466348 + timestamp: 1748388121356 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + purls: [] + size: 122968 + timestamp: 1742727099393 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 + md5: 436c165519e140cb08d246a4472a9d6a + depends: + - brotli-python >=1.0.9 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.9 + - zstandard >=0.18.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 101735 + timestamp: 1750271478254 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda + sha256: cb357591d069a1e6cb74199a8a43a7e3611f72a6caed9faa49dbb3d7a0a98e0b + md5: 28f4ca1e0337d0f27afb8602663c5723 + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18249 + timestamp: 1753739241465 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda + sha256: af4b4b354b87a9a8d05b8064ff1ea0b47083274f7c30b4eb96bc2312c9b5f08f + md5: 603e41da40a765fd47995faa021da946 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_31 + constrains: + - vs2015_runtime 14.44.35208.* *_31 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 682424 + timestamp: 1753739239305 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + sha256: 67b317b64f47635415776718d25170a9a6f9a1218c0f5a6202bfd687e07b6ea4 + md5: a6b1d5c1fc3cb89f88f7179ee6a9afe3 + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_31 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 113963 + timestamp: 1753739198723 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 + depends: + - __win + - python >=3.9 + license: LicenseRef-Public-Domain + purls: + - pkg:pypi/win-inet-pton?source=hash-mapping + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda + sha256: d534a6518c2d8eccfa6579d75f665261484f0f2f7377b50402446a9433d46234 + md5: ca45bfd4871af957aaa5035593d5efd2 + depends: + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 466893 + timestamp: 1762512695614 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_0.conda + sha256: 3b66d3cb738a9993e8432d1a03402d207128166c4ef0c928e712958e51aff325 + md5: d26077d20b4bba54f4c718ed1313805f + depends: + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 375866 + timestamp: 1757930134099 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb + md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 567578 + timestamp: 1742433379869 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + sha256: bc64864377d809b904e877a98d0584f43836c9f2ef27d3d2a1421fa6eae7ca04 + md5: 21f56217d6125fb30c3c3f10c786d751 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 354697 + timestamp: 1742433568506 diff --git a/mcp/types-generator/pixi.toml b/mcp/types-generator/pixi.toml new file mode 100644 index 0000000000..d3fa902d5b --- /dev/null +++ b/mcp/types-generator/pixi.toml @@ -0,0 +1,20 @@ +[workspace] +authors = ["Oraios AI "] +channels = ["conda-forge"] +description = "Scripts supporting the development of the Penpot MCP server" +name = "penpot-mcp-scripts" +platforms = ["win-64","linux-64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = "3.11.*" +pixi-pycharm = ">=0.0.9,<0.0.10" +beautifulsoup4 = ">=4.13.5,<5" +markdownify = ">=1.1.0,<2" +requests = ">=2.32.5,<3" +"ruamel.yaml" = ">=0.18.15,<0.19" + +[pypi-dependencies] +sensai-utils = ">=1.5.0, <2" diff --git a/mcp/types-generator/prepare_api_docs.py b/mcp/types-generator/prepare_api_docs.py new file mode 100644 index 0000000000..f5f4c72212 --- /dev/null +++ b/mcp/types-generator/prepare_api_docs.py @@ -0,0 +1,269 @@ +import collections +import dataclasses +import os +from dataclasses import dataclass +from io import StringIO +from pathlib import Path + +import requests +from bs4 import BeautifulSoup, Tag +from markdownify import MarkdownConverter +from ruamel.yaml import YAML +from ruamel.yaml.scalarstring import LiteralScalarString +from sensai.util import logging +import sys + +log = logging.getLogger(__name__) + +class PenpotAPIContentMarkdownConverter(MarkdownConverter): + """ + Markdown converter for Penpot API docs, specifically for the .col-content element + (and sub-elements thereof) + """ + def process_tag(self, node, parent_tags=None): + soup = BeautifulSoup(str(node), "html.parser") + + # skip breadcrumbs + if "class" in node.attrs and "tsd-breadcrumb" in node.attrs["class"]: + return "" + + # convert h5 and h4 to plain text + if node.name in ["h5", "h4"]: + return soup.get_text() + + text = soup.get_text() + + # convert tsd-tag code elements (containing e.g. "Readonly" and "Optional" designations) + # If we encounter them at this level, we just remove them, as they are redundant. + # The significant such tags are handled in the tsd-signature processing below. + if node.name == "code" and "class" in node.attrs and "tsd-tag" in node.attrs["class"]: + return "" + + # skip buttons (e.g. "Copy") + if node.name == "button": + return "" + + # skip links to definitions in

  • elements + if node.name == "li" and text.startswith("Defined in"): + return "" + + # for links, just return the text + if node.name == "a": + return text + + # skip inheritance information + if node.name == "p" and text.startswith("Inherited from"): + return "" + + # remove index with links + if "class" in node.attrs and "tsd-index-content" in node.attrs["class"]: + return "" + + # convert
     blocks to markdown code blocks
    +        if node.name == "pre":
    +            for button in soup.find_all("button"):
    +                button.decompose()
    +            return f"\n```\n{soup.get_text().strip()}\n```\n\n"
    +
    +        # convert tsd-signature elements to code blocks, converting 
    to newlines + if "class" in node.attrs and "tsd-signature" in node.attrs["class"]: + # convert
    to newlines + for br in soup.find_all("br"): + br.replace_with("\n") + # process tsd-tags (keeping only "readonly"; optional is redundant, as it is indicated via "?") + for tag in soup.find_all(attrs={"class": "tsd-tag"}): + tag_lower = tag.get_text().strip().lower() + if tag_lower in ["readonly"]: + tag.replace_with(f"{tag_lower} ") + else: + tag.decompose() + # return as code block + return f"\n```\n{soup.get_text()}\n```\n\n" + + # other cases: use the default processing + return super().process_tag(node, parent_tags=parent_tags) + + +@dataclass +class TypeInfo: + overview: str + """ + the main type information, which contains all the declarations/signatures but no descriptions + """ + members: dict[str, dict[str, str]] + """ + mapping from member type (e.g. "Properties", "Methods") to a mapping of member name to markdown description + """ + + def add_referencing_types(self, referencing_types: set[str]): + if referencing_types: + self.overview += "\n\nReferenced by: " + ", ".join(sorted(referencing_types)) + + def __repr__(self) -> str: + num_members = {k: len(v) for k, v in self.members.items()} + return f"TypeInfo(overview_length={len(self.overview)}, num_members={num_members})" + + +class YamlConverter: + """Converts dictionaries to YAML with all strings in block literal style""" + + def __init__(self): + self.yaml = YAML() + self.yaml.preserve_quotes = True + self.yaml.width = 4096 # Prevent line wrapping + + def _convert_strings_to_block(self, obj): + if isinstance(obj, dict): + return {k: self._convert_strings_to_block(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [self._convert_strings_to_block(item) for item in obj] + elif isinstance(obj, str): + return LiteralScalarString(obj) + else: + return obj + + def to_yaml(self, data): + processed_data = self._convert_strings_to_block(data) + stream = StringIO() + self.yaml.dump(processed_data, stream) + return stream.getvalue() + + def to_file(self, data, filepath): + processed_data = self._convert_strings_to_block(data) + with open(filepath, 'w', encoding='utf-8') as f: + self.yaml.dump(processed_data, f) + + +class PenpotAPIDocsProcessor: + def __init__(self, url=None): + self.md_converter = PenpotAPIContentMarkdownConverter() + self.base_url = url + self.types: dict[str, TypeInfo] = {} + self.type_referenced_by: dict[str, set[str]] = collections.defaultdict(set) + + def run(self, target_dir: str): + os.makedirs(target_dir, exist_ok=True) + + # find links to all interfaces and types + modules_page = self._fetch("modules.html") + soup = BeautifulSoup(modules_page, "html.parser") + content = soup.find(attrs={"class": "col-content"}) + links = content.find_all("a", href=True) + + # process each link, converting interface and type pages to markdown + for link in links: + href = link['href'] + if href.startswith("interfaces/") or href.startswith("types/"): + type_name = href.split("/")[-1].replace(".html", "") + log.info("Processing page: %s", type_name) + type_info = self.process_page(href, type_name) + print(f"Adding '{type_name}' with {type_info}") + self.types[type_name] = type_info + + # add type reference information + for type_name, type_info in self.types.items(): + referencing_types = self.type_referenced_by.get(type_name, set()) + type_info.add_referencing_types(referencing_types) + + # save to yaml + yaml_path = os.path.join(target_dir, "api_types.yml") + log.info("Writing API type information to %s", yaml_path) + data_dict = {k: dataclasses.asdict(v) for k, v in self.types.items()} + YamlConverter().to_file(data_dict, yaml_path) + + def _fetch(self, rel_url: str) -> bytes: + response = requests.get(f"{self.base_url}/{rel_url}") + if response.status_code != 200: + raise Exception(f"Failed to retrieve page: {response.status_code}") + html_content = response.content + return html_content + + def _html_to_markdown(self, html_content: str) -> str: + md = self.md_converter.convert(html_content) + md = md.replace("\xa0", " ") # replace non-breaking spaces + return md.strip() + + def process_page(self, rel_url: str, type_name: str) -> TypeInfo: + html_content = self._fetch(rel_url) + soup = BeautifulSoup(html_content, "html.parser") + + content = soup.find(attrs={"class": "col-content"}) + # full_text = self._html_to_markdown(str(content)) + + # extract individual members + members = {} + member_group_tags = [] + for el in content.children: + if isinstance(el, Tag): + if "class" in el.attrs and "tsd-member-group" in el.attrs["class"]: + member_group_tags.append(el) + members_type = el.find("h2").get_text().strip() + members_in_group = {} + members[members_type] = members_in_group + for member_tag in el.find_all(attrs={"class": "tsd-member"}): + member_anchor = member_tag.find("a", attrs={"class": "tsd-anchor"}, recursive=False) + member_name = member_anchor.attrs["id"] + member_heading = member_tag.find("h3") + # 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. + if member_heading: + tags_in_heading = member_heading.find_all(attrs={"class": "tsd-tag"}) + if tags_in_heading: + signature_tag = member_tag.find(attrs={"class": "tsd-signature"}) + if signature_tag: + for tag in reversed(tags_in_heading): + signature_tag.insert(0, tag) + member_heading.decompose() + # convert to markdown + tag_text = str(member_tag) + members_in_group[member_name] = self._html_to_markdown(tag_text) + + # record references to other types in signature + signature = content.find("div", attrs={"class": "tsd-signature"}) + for link_to_type in signature.find_all("a", attrs={"class": "tsd-signature-type"}): + referenced_type_name = link_to_type.get_text().strip() + self.type_referenced_by[referenced_type_name].add(type_name) + + # remove the member groups from the soup + for tag in member_group_tags: + tag.decompose() + + # overview is what remains in content after removing member groups + overview = self._html_to_markdown(str(content)) + + return TypeInfo( + overview=overview, + members=members + ) + + +DEFAULT_API_DOCS_URL = "http://localhost:9090" + +def main(): + target_dir = Path(__file__).parent.parent / "packages" / "server" / "data" + url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_API_DOCS_URL + + print("Fetching plugin data from: {}".format(url)) + PenpotAPIDocsProcessor(url).run(target_dir=str(target_dir)) + + +def debug_type_conversion(rel_url: str): + """ + This function is for debugging purposes only. + It processes a single type page and prints the converted markdown to the console. + + :param rel_url: relative URL of the type page (e.g., "interfaces/ShapeBase") + """ + type_name = rel_url.split("/")[-1] + processor = PenpotAPIDocsProcessor() + type_info = processor.process_page(rel_url, type_name) + print(f"--- overview ---\n{type_info.overview}\n") + for member_type, members in type_info.members.items(): + print(f"\n{member_type}:") + for member_name, member_md in members.items(): + print(f"--- {member_name} ---\n{member_md}\n") + + +if __name__ == '__main__': + # debug_type_conversion("interfaces/LayoutChildProperties") + logging.run_main(main) From e7d9dca55e115b61c073168ee33c31ee9954a286 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 4 Feb 2026 13:57:00 +0100 Subject: [PATCH 3/6] :arrow_up: Update jdk and node on devenv and other images --- .nvmrc | 2 +- docker/devenv/Dockerfile | 13 +++++++------ docker/images/Dockerfile.backend | 10 +++++----- docker/images/Dockerfile.exporter | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.nvmrc b/.nvmrc index c6a66a6e6a..aa8f8f13f7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.21.1 +v22.22.0 \ No newline at end of file diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index fa01edf221..7a3aa2d024 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -31,7 +31,7 @@ RUN set -ex; \ FROM base AS setup-node -ENV NODE_VERSION=v22.21.1 \ +ENV NODE_VERSION=v22.22.0 \ PATH=/opt/node/bin:$PATH RUN set -eux; \ @@ -97,18 +97,19 @@ RUN set -eux; \ FROM base AS setup-jvm -ENV CLOJURE_VERSION=1.12.3.1577 +# https://clojure.org/releases/tools +ENV CLOJURE_VERSION=1.12.4.1602 RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \ + ESUM='9903c6b19183a33725ca1dfdae5b72400c9d00995c76fafc4a0d31c5152f33f7'; \ + BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_aarch64.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \ + ESUM='946ad9766d98fc6ab495a1a120072197db54997f6925fb96680f1ecd5591db4e'; \ + BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_x64.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index 19193db906..a651415f4e 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \ LC_ALL='C.UTF-8' \ JAVA_HOME="/opt/jdk" \ DEBIAN_FRONTEND=noninteractive \ - NODE_VERSION=v22.21.1 \ + NODE_VERSION=v22.22.0 \ TZ=Etc/UTC RUN set -ex; \ @@ -46,12 +46,12 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \ + ESUM='9903c6b19183a33725ca1dfdae5b72400c9d00995c76fafc4a0d31c5152f33f7'; \ + BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_aarch64.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \ + ESUM='946ad9766d98fc6ab495a1a120072197db54997f6925fb96680f1ecd5591db4e'; \ + BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-linux_x64.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 98c7b0e5c0..3b7883ae04 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -3,7 +3,7 @@ LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v22.21.1 \ + NODE_VERSION=v22.22.0 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/node/bin:/opt/imagick/bin:$PATH From 06afd94a742f816349bf4701061eda45b6f5a3a2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 4 Feb 2026 16:03:47 +0100 Subject: [PATCH 4/6] :arrow_up: Update backend dependencies (mainly bugfixes) --- backend/deps.edn | 12 ++++++------ common/deps.edn | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index cbf1176953..af73aecbc8 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -3,7 +3,7 @@ :deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.12.2"} + org.clojure/clojure {:mvn/version "1.12.4"} org.clojure/tools.namespace {:mvn/version "1.5.0"} com.github.luben/zstd-jni {:mvn/version "1.5.7-4"} @@ -28,8 +28,8 @@ com.google.guava/guava {:mvn/version "33.4.8-jre"} funcool/yetti - {:git/tag "v11.8" - :git/sha "1d1b33f" + {:git/tag "v11.9" + :git/sha "5fad7a9" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} @@ -39,7 +39,7 @@ metosin/reitit-core {:mvn/version "0.9.1"} nrepl/nrepl {:mvn/version "1.4.0"} - org.postgresql/postgresql {:mvn/version "42.7.7"} + org.postgresql/postgresql {:mvn/version "42.7.9"} org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"} com.zaxxer/HikariCP {:mvn/version "7.0.2"} @@ -49,7 +49,7 @@ buddy/buddy-hashers {:mvn/version "2.0.167"} buddy/buddy-sign {:mvn/version "3.6.1-359"} - com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"} + com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.3"} org.jsoup/jsoup {:mvn/version "1.21.2"} org.im4java/im4java @@ -66,7 +66,7 @@ ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.33.10"}} + software.amazon.awssdk/s3 {:mvn/version "2.41.21"}} :paths ["src" "resources" "target/classes"] :aliases diff --git a/common/deps.edn b/common/deps.edn index 8469642b7e..8e1e3ca5b2 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,5 +1,5 @@ {:deps - {org.clojure/clojure {:mvn/version "1.12.2"} + {org.clojure/clojure {:mvn/version "1.12.4"} org.clojure/data.json {:mvn/version "2.5.1"} org.clojure/tools.cli {:mvn/version "1.1.230"} org.clojure/test.check {:mvn/version "1.1.1"} @@ -9,15 +9,15 @@ org.apache.commons/commons-pool2 {:mvn/version "2.12.1"} ;; Logging - org.apache.logging.log4j/log4j-api {:mvn/version "2.25.1"} - org.apache.logging.log4j/log4j-core {:mvn/version "2.25.1"} - org.apache.logging.log4j/log4j-web {:mvn/version "2.25.1"} - org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.1"} - org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.1"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.25.3"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"} + org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.3"} org.slf4j/slf4j-api {:mvn/version "2.0.17"} - pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"} + pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.41"} - selmer/selmer {:mvn/version "1.12.69"} + selmer/selmer {:mvn/version "1.12.70"} criterium/criterium {:mvn/version "0.4.6"} metosin/jsonista {:mvn/version "0.3.13"} @@ -27,7 +27,7 @@ com.cognitect/transit-clj {:mvn/version "1.0.333"} com.cognitect/transit-cljs {:mvn/version "0.8.280"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} - integrant/integrant {:mvn/version "1.0.0"} + integrant/integrant {:mvn/version "1.0.1"} funcool/cuerdas {:mvn/version "2026.415"} funcool/promesa From 3d50aa6cb2285284a3148e6368f72bd7431c321e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 4 Feb 2026 16:04:09 +0100 Subject: [PATCH 5/6] :arrow_up: Update imagemagick version --- docker/devenv/Dockerfile | 2 +- docker/imagemagick/Dockerfile | 2 +- manage.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 7a3aa2d024..efa134d999 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -397,7 +397,7 @@ ENV LANG='C.UTF-8' \ RUSTUP_HOME="/opt/rustup" \ PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH" -COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-13 /opt/imagick /opt/imagick COPY --from=setup-jvm /opt/jdk /opt/jdk COPY --from=setup-jvm /opt/clojure /opt/clojure COPY --from=setup-node /opt/node /opt/node diff --git a/docker/imagemagick/Dockerfile b/docker/imagemagick/Dockerfile index 5800979878..c13221d244 100644 --- a/docker/imagemagick/Dockerfile +++ b/docker/imagemagick/Dockerfile @@ -6,7 +6,7 @@ ENV LANG='C.UTF-8' \ DEBIAN_FRONTEND=noninteractive \ TZ=Etc/UTC -ARG IMAGEMAGICK_VERSION=7.1.1-47 +ARG IMAGEMAGICK_VERSION=7.1.2-13 RUN set -e; \ apt-get -qq update; \ diff --git a/manage.sh b/manage.sh index cc9d21307c..d16e08b613 100755 --- a/manage.sh +++ b/manage.sh @@ -7,7 +7,7 @@ export DEVENV_PNAME="penpotdev"; export CURRENT_USER_ID=$(id -u); export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD); -export IMAGEMAGICK_VERSION=7.1.2-0 +export IMAGEMAGICK_VERSION=7.1.2-13 # Safe directory to avoid ownership errors with Git git config --global --add safe.directory /home/penpot/penpot || true From 44c7d3fbd6bd963c19ef12469bc7f3117e907da5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 4 Feb 2026 16:08:31 +0100 Subject: [PATCH 6/6] :sparkles: Backport .github workflows from develop --- .../ISSUE_TEMPLATE/new-render-bug-report.md | 38 ++++++++++++++++ .github/workflows/build-bundle.yml | 2 +- .github/workflows/build-docker-devenv.yml | 7 ++- .github/workflows/build-docker.yml | 16 ++++++- .github/workflows/commit-checker.yml | 2 +- .github/workflows/tests-mcp.yml | 45 +++++++++++++++++++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/new-render-bug-report.md create mode 100644 .github/workflows/tests-mcp.yml diff --git a/.github/ISSUE_TEMPLATE/new-render-bug-report.md b/.github/ISSUE_TEMPLATE/new-render-bug-report.md new file mode 100644 index 0000000000..b93b98b444 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-render-bug-report.md @@ -0,0 +1,38 @@ +--- +name: New Render Bug Report +about: Create a report about the bugs you have found in the new render +title: '' +labels: new render +assignees: claragvinola + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Steps to Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots or screen recordings** +If applicable, add screenshots or screen recording to help illustrate your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml index 6365ba93cb..4626c4dc2e 100644 --- a/.github/workflows/build-bundle.yml +++ b/.github/workflows/build-bundle.yml @@ -40,7 +40,7 @@ on: jobs: build-bundle: name: Build and Upload Penpot Bundle - runs-on: ubuntu-24.04 + runs-on: penpot-runner-01 env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index 3d5bae1d12..a2aaee24c0 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -7,9 +7,14 @@ jobs: build-and-push: name: Build and push DevEnv Docker image environment: release-admins - runs-on: ubuntu-24.04 + runs-on: penpot-runner-02 steps: + - name: Set common environment variables + run: | + # Each job execution will use its own docker configuration. + echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV + - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 76d5c72bc1..2d03826b65 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -19,9 +19,14 @@ on: jobs: build-and-push: name: Build and Push Penpot Docker Images - runs-on: ubuntu-24.04-arm + runs-on: penpot-runner-02 steps: + - name: Set common environment variables + run: | + # Each job execution will use its own docker configuration. + echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV + - name: Checkout code uses: actions/checkout@v4 with: @@ -66,6 +71,15 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + # To avoid the “429 Too Many Requests” error when downloading + # images from DockerHub for unregistered users. + # https://docs.docker.com/docker-hub/usage/ + - name: Login to DockerHub Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 diff --git a/.github/workflows/commit-checker.yml b/.github/workflows/commit-checker.yml index c5c0790f60..f7126a40cb 100644 --- a/.github/workflows/commit-checker.yml +++ b/.github/workflows/commit-checker.yml @@ -26,7 +26,7 @@ jobs: - name: Check Commit Type uses: gsactions/commit-message-checker@v2 with: - pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$' + pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$' flags: 'gm' error: 'Commit should match CONTRIBUTING.md guideline' checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml new file mode 100644 index 0000000000..a733a76d0f --- /dev/null +++ b/.github/workflows/tests-mcp.yml @@ -0,0 +1,45 @@ +name: "MCP CI" + +on: + pull_request: + branches: + - develop + - staging + - main + + types: + - opened + - synchronize + + paths: + - 'mcp/**' + + push: + branches: + - develop + - staging + - main + + paths: + - 'mcp/**' + +jobs: + test: + name: "Test" + runs-on: penpot-runner-02 + container: penpotapp/devenv:latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup + working-directory: ./mcp + run: ./scripts/setup + + - name: Check + working-directory: ./mcp + run: | + pnpm run fmt:check; + pnpm -r run build; + pnpm -r run types:check;