mirror of
https://github.com/penpot/penpot.git
synced 2026-02-10 14:43:17 -05:00
Compare commits
78 Commits
2.13.0-RC3
...
2.13.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d979894872 | ||
|
|
3d20fc508d | ||
|
|
ccfee34e76 | ||
|
|
6f3f2f9a71 | ||
|
|
ad5e8ccdb3 | ||
|
|
44c7d3fbd6 | ||
|
|
3d50aa6cb2 | ||
|
|
06afd94a74 | ||
|
|
e7d9dca55e | ||
|
|
c14ccc18b8 | ||
|
|
7d09d930fe | ||
|
|
0d9b7ca696 | ||
|
|
d215a5c402 | ||
|
|
f65292a13c | ||
|
|
94722fdec2 | ||
|
|
28509e0418 | ||
|
|
9569fa2bcb | ||
|
|
852b31c3a0 | ||
|
|
84b3f5d7c6 | ||
|
|
76bd31fe7d | ||
|
|
cc81e56d82 | ||
|
|
a9e2fc8d94 | ||
|
|
77bbf30ae4 | ||
|
|
693b52bf45 | ||
|
|
0f51b23ce7 | ||
|
|
ec61aa6b6d | ||
|
|
18aca16f98 | ||
|
|
c6465e27e3 | ||
|
|
1834a18263 | ||
|
|
d220d07875 | ||
|
|
9ca76c745f | ||
|
|
3112b240a0 | ||
|
|
56fd66b91a | ||
|
|
abc1773f65 | ||
|
|
93f5e74bb0 | ||
|
|
1ce0b60e3d | ||
|
|
38179ba11e | ||
|
|
ef80901400 | ||
|
|
719a95246a | ||
|
|
e590cd852d | ||
|
|
a9741073e5 | ||
|
|
5306bed548 | ||
|
|
92a319ddd1 | ||
|
|
68a6d4c9a8 | ||
|
|
f07495ae95 | ||
|
|
23d5fc7408 | ||
|
|
8632b18eec | ||
|
|
33e650242c | ||
|
|
e03ad25118 | ||
|
|
599656c31e | ||
|
|
5d7e6afd76 | ||
|
|
15d369493b | ||
|
|
9c9b672e3e | ||
|
|
5146221513 | ||
|
|
e53f335204 | ||
|
|
16f22a7b5c | ||
|
|
a1460115e8 | ||
|
|
2574ad3315 | ||
|
|
e6b5364a84 | ||
|
|
656f81f89f | ||
|
|
01a4ffeb8b | ||
|
|
b8c70be9a2 | ||
|
|
525adcfcbe | ||
|
|
7cce4c6532 | ||
|
|
a3fdd8b691 | ||
|
|
b6a9579c98 | ||
|
|
8c7fd0af4b | ||
|
|
cf46051f56 | ||
|
|
6393330ee1 | ||
|
|
8252bc485e | ||
|
|
47775a9e2c | ||
|
|
8191d04114 | ||
|
|
88f0f75174 | ||
|
|
1ffa956251 | ||
|
|
31054099ff | ||
|
|
5c71c57dd9 | ||
|
|
5abc1aafb4 | ||
|
|
935728aa39 |
@@ -45,6 +45,15 @@
|
||||
:potok/reify-type
|
||||
{:level :error}
|
||||
|
||||
:redundant-primitive-coercion
|
||||
{:level :off}
|
||||
|
||||
:unused-excluded-var
|
||||
{:level :off}
|
||||
|
||||
:unresolved-excluded-var
|
||||
{:level :off}
|
||||
|
||||
:missing-protocol-method
|
||||
{:level :off}
|
||||
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
Normal file
@@ -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.
|
||||
2
.github/workflows/build-bundle.yml
vendored
2
.github/workflows/build-bundle.yml
vendored
@@ -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 }}
|
||||
|
||||
7
.github/workflows/build-docker-devenv.yml
vendored
7
.github/workflows/build-docker-devenv.yml
vendored
@@ -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
|
||||
|
||||
|
||||
16
.github/workflows/build-docker.yml
vendored
16
.github/workflows/build-docker.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-tag.yml
vendored
2
.github/workflows/build-tag.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
🐳 *[PENPOT] Docker image available: {{ github.ref_name }}*
|
||||
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}*
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -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
|
||||
|
||||
125
.github/workflows/plugins-deploy-api-doc.yml
vendored
Normal file
125
.github/workflows/plugins-deploy-api-doc.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: Plugins/api-doc deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/libs/plugin-types/index.d.ts'
|
||||
- 'plugins/libs/plugin-types/REAME.md'
|
||||
- 'plugins/tools/typedoc.css'
|
||||
- 'plugins/CHANGELOG.md'
|
||||
- 'plugins/wrangler-penpot-plugins-api-doc.toml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
|
||||
# START: Setup Node and PNPM enabling cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable PNPM
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
# END: Setup Node and PNPM enabling cache
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile;
|
||||
pnpm add -D -w wrangler@latest;
|
||||
|
||||
- name: Build docs
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: pnpm run build:doc
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ steps.vars.outputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main)
|
||||
echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||
staging)
|
||||
echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||
develop)
|
||||
echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Set the custom url
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config wrangler-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📚 *[PENPOT PLUGINS] Error deploying API documentation.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Plugins/package deployer
|
||||
|
||||
on:
|
||||
# Deploy package from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
plugin_name:
|
||||
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
plugin_name:
|
||||
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: penpot-runner-01
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
# START: Setup Node and PNPM enabling cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable PNPM
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
# END: Setup Node and PNPM enabling cache
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile;
|
||||
pnpm add -D -w wrangler@latest;
|
||||
|
||||
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: npx nx build ${{ inputs.plugin_name }}-plugin
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ inputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||
staging)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||
develop)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Set the custom url
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
Plugin name: `${{ inputs.plugin_name }}-plugin`
|
||||
Cloudflare worker name: `${{ env.WORKER_NAME }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Plugins/packages deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/apps/*-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
|
||||
create_palette: ${{ steps.filter.outputs.create_palette }}
|
||||
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
|
||||
rename_layers: ${{ steps.filter.outputs.rename_layers }}
|
||||
contrast: ${{ steps.filter.outputs.contrast }}
|
||||
icons: ${{ steps.filter.outputs.icons }}
|
||||
poc_state: ${{ steps.filter.outputs.poc_state }}
|
||||
table: ${{ steps.filter.outputs.table }}
|
||||
# [For new plugins]
|
||||
# Add more outputs here
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
colors_to_tokens:
|
||||
- 'plugins/apps/colors-to-tokens-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
contrast:
|
||||
- 'plugins/apps/contrast-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
create_palette:
|
||||
- 'plugins/apps/create-palette-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
icons:
|
||||
- 'plugins/apps/icons-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
lorem_ipsum:
|
||||
- 'plugins/apps/lorem-ipsum-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
rename_layers:
|
||||
- 'plugins/apps/rename-layers-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
table:
|
||||
- 'plugins/apps/table-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
# [For new plugins]
|
||||
# Add more plugin filters here
|
||||
# another_plugin:
|
||||
# - 'plugins/apps/another-plugin/**'
|
||||
# - 'libs/plugins-styles/**'
|
||||
|
||||
colors-to-tokens-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: colors-to-tokens
|
||||
|
||||
contrast-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: contrast
|
||||
|
||||
create-palette-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: create-palette
|
||||
|
||||
icons-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: icons
|
||||
|
||||
lorem-ipsum-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: lorem-ipsum
|
||||
|
||||
rename-layers-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: rename-layers
|
||||
|
||||
table-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: table
|
||||
|
||||
# [For new plugins]
|
||||
# Add more jobs for other plugins below, following the same pattern
|
||||
# another-plugin:
|
||||
# needs: detect-changes
|
||||
# if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true'
|
||||
# uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
# secrets: inherit
|
||||
# with:
|
||||
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
# plugin_name: another
|
||||
123
.github/workflows/plugins-deploy-styles-doc.yml
vendored
Normal file
123
.github/workflows/plugins-deploy-styles-doc.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
name: Plugins/styles-doc deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/apps/example-styles/**'
|
||||
- 'plugins/libs/plugins-styles/**'
|
||||
- 'plugins/wrangler-penpot-plugins-styles-doc.toml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
|
||||
# START: Setup Node and PNPM enabling cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable PNPM
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
# END: Setup Node and PNPM enabling cache
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile;
|
||||
pnpm add -D -w wrangler@latest;
|
||||
|
||||
- name: Build styles
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: npx nx run example-styles:build
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ steps.vars.outputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main)
|
||||
echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||
staging)
|
||||
echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||
develop)
|
||||
echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=styles-doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Set the custom url
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config wrangler-penpot-plugins-styles-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩💅 *[PENPOT PLUGINS] Error deploying Styles documentation.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
45
.github/workflows/tests-mcp.yml
vendored
Normal file
45
.github/workflows/tests-mcp.yml
vendored
Normal file
@@ -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;
|
||||
25
.github/workflows/tests.yml
vendored
25
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
lint:
|
||||
name: "Linter"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
test-common:
|
||||
name: "Common Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -53,7 +53,8 @@ jobs:
|
||||
|
||||
test-plugins:
|
||||
name: Plugins Runtime Linter & Tests
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -98,7 +99,7 @@ jobs:
|
||||
|
||||
test-frontend:
|
||||
name: "Frontend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -119,7 +120,7 @@ jobs:
|
||||
|
||||
test-render-wasm:
|
||||
name: "Render WASM Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -143,7 +144,7 @@ jobs:
|
||||
|
||||
test-backend:
|
||||
name: "Backend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
services:
|
||||
@@ -182,7 +183,7 @@ jobs:
|
||||
|
||||
test-library:
|
||||
name: "Library Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -196,7 +197,7 @@ jobs:
|
||||
|
||||
build-integration:
|
||||
name: "Build Integration Bundle"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@@ -217,7 +218,7 @@ jobs:
|
||||
|
||||
test-integration-1:
|
||||
name: "Integration Tests 1/4"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
@@ -247,7 +248,7 @@ jobs:
|
||||
|
||||
test-integration-2:
|
||||
name: "Integration Tests 2/4"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
@@ -277,7 +278,7 @@ jobs:
|
||||
|
||||
test-integration-3:
|
||||
name: "Integration Tests 3/4"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
@@ -307,7 +308,7 @@ jobs:
|
||||
|
||||
test-integration-4:
|
||||
name: "Integration Tests 4/4"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
|
||||
40
.travis.yml
40
.travis.yml
@@ -1,40 +0,0 @@
|
||||
dist: xenial
|
||||
|
||||
language: generic
|
||||
sudo: required
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
|
||||
install:
|
||||
- curl -O https://download.clojure.org/install/linux-install-1.10.1.447.sh
|
||||
- chmod +x linux-install-1.10.1.447.sh
|
||||
- sudo ./linux-install-1.10.1.447.sh
|
||||
|
||||
before_script:
|
||||
- env | sort
|
||||
|
||||
script:
|
||||
- ./manage.sh build-devenv
|
||||
- ./manage.sh run-frontend-tests
|
||||
- ./manage.sh run-backend-tests
|
||||
- ./manage.sh build-images
|
||||
- ./manage.sh run
|
||||
|
||||
after_script:
|
||||
- docker images
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
env:
|
||||
- NODE_VERSION=10.16.0
|
||||
21
CHANGES.md
21
CHANGES.md
@@ -1,10 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.13.0 (Unreleased)
|
||||
## 2.13.1
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
### :bug: Bugs fixed
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
- Fix PDF Exporter outputs empty page when board has A4 format [Taiga #13181](https://tree.taiga.io/project/penpot/issue/13181)
|
||||
|
||||
## 2.13.0
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
@@ -29,7 +31,18 @@
|
||||
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
|
||||
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
|
||||
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||
|
||||
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
|
||||
- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167)
|
||||
- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171)
|
||||
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
|
||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
|
||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
|
||||
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
|
||||
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
|
||||
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
|
||||
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -873,11 +873,8 @@
|
||||
(import-storage-objects cfg)
|
||||
|
||||
(let [files (get manifest :files)
|
||||
result (reduce (fn [result {:keys [id] :as file}]
|
||||
result (reduce (fn [result file]
|
||||
(let [name' (get file :name)
|
||||
name' (if (map? name)
|
||||
(get name id)
|
||||
name')
|
||||
file (assoc file :name name')]
|
||||
(conj result (import-file cfg file))))
|
||||
[]
|
||||
|
||||
@@ -124,8 +124,6 @@
|
||||
(throw (IllegalArgumentException. "invalid email body provided")))
|
||||
|
||||
(doseq [[name content] attachments]
|
||||
|
||||
(prn "attachment" name)
|
||||
(let [attachment-part (MimeBodyPart.)]
|
||||
(.setFileName attachment-part ^String name)
|
||||
(.setContent attachment-part ^String content (str "text/plain; charset=" charset))
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
javax.xml.parsers.SAXParserFactory
|
||||
org.apache.commons.io.IOUtils
|
||||
org.im4java.core.ConvertCmd
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
org.im4java.core.IMOperation))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
@@ -224,17 +223,18 @@
|
||||
;; If we are processing an animated gif we use the first frame with -scene 0
|
||||
(let [dim-result (sh/sh "identify" "-format" "%w %h\n" path)
|
||||
orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)]
|
||||
(if (and (= 0 (:exit dim-result))
|
||||
(= 0 (:exit orient-result)))
|
||||
(when (= 0 (:exit dim-result))
|
||||
(let [[w h] (-> (:out dim-result)
|
||||
str/trim
|
||||
(clojure.string/split #"\s+")
|
||||
(->> (mapv #(Integer/parseInt %))))
|
||||
orientation (-> orient-result :out str/trim)]
|
||||
(case orientation
|
||||
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
|
||||
{:width w :height h})) ; Normal or unknown orientation
|
||||
nil)))
|
||||
orientation-exit (:exit orient-result)
|
||||
orientation (-> orient-result :out str/trim)]
|
||||
(if (= 0 orientation-exit)
|
||||
(case orientation
|
||||
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
|
||||
{:width w :height h}) ; Normal or unknown orientation
|
||||
{:width w :height h}))))) ; If orientation can't be read, use dimensions as-is
|
||||
|
||||
(defmethod process :info
|
||||
[{:keys [input] :as params}]
|
||||
@@ -247,26 +247,37 @@
|
||||
:hint "uploaded svg does not provides dimensions"))
|
||||
(merge input info {:ts (ct/now) :size (fs/size path)}))
|
||||
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
(let [path-str (str path)
|
||||
identify-res (sh/sh "identify" "-format" "image/%[magick]\n" path-str)
|
||||
;; identify prints one line per frame (animated GIFs, etc.); we take the first one
|
||||
mtype' (if (zero? (:exit identify-res))
|
||||
(-> identify-res
|
||||
:out
|
||||
str/trim
|
||||
(str/split #"\s+" 2)
|
||||
first
|
||||
str/lower)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:hint "invalid image"))
|
||||
{:keys [width height]}
|
||||
(or (get-dimensions-with-orientation path-str)
|
||||
(do
|
||||
(l/warn "Failed to read image dimensions with orientation" {:path path})
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:hint "invalid image")))]
|
||||
(when (and (string? mtype)
|
||||
(not= mtype mtype'))
|
||||
(not= (str/lower mtype) mtype'))
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-mismatch
|
||||
:hint (str "Seems like you are uploading a file whose content does not match the extension."
|
||||
"Expected: " mtype ". Got: " mtype')))
|
||||
(let [{:keys [width height]}
|
||||
(or (get-dimensions-with-orientation (str path))
|
||||
(do
|
||||
(l/warn "Failed to read image dimensions with orientation; falling back to im4java"
|
||||
{:path path})
|
||||
{:width (.getPageWidth instance)
|
||||
:height (.getPageHeight instance)}))]
|
||||
(assoc input
|
||||
:width width
|
||||
:height height
|
||||
:size (fs/size path)
|
||||
:ts (ct/now)))))))
|
||||
(assoc input
|
||||
:width width
|
||||
:height height
|
||||
:size (fs/size path)
|
||||
:ts (ct/now))))))
|
||||
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
|
||||
@@ -27,7 +27,17 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]))
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.io.SequenceInputStream
|
||||
java.util.Collections))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
@@ -105,7 +115,7 @@
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(letfn [(generate-missing [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||
(when (and (not (contains? data "font/otf"))
|
||||
(not (contains? data "font/ttf"))
|
||||
@@ -116,8 +126,26 @@
|
||||
:hint "invalid font upload, unable to generate missing font assets"))
|
||||
data))
|
||||
|
||||
(process-chunks [chunks]
|
||||
(let [tmp (tmp/tempfile :prefix "penpot.tempfont." :suffix "")
|
||||
streams (map io/input-stream chunks)
|
||||
streams (Collections/enumeration streams)]
|
||||
(with-open [^OutputStream output (io/output-stream tmp)
|
||||
^InputStream input (SequenceInputStream. streams)]
|
||||
(io/copy input output))
|
||||
tmp))
|
||||
|
||||
(join-chunks [data]
|
||||
(reduce-kv (fn [data mtype content]
|
||||
(if (vector? content)
|
||||
(assoc data mtype (process-chunks content))
|
||||
data))
|
||||
data
|
||||
data))
|
||||
|
||||
(prepare-font [data mtype]
|
||||
(when-let [resource (get data mtype)]
|
||||
|
||||
(let [hash (sto/calculate-hash resource)
|
||||
content (-> (sto/content resource)
|
||||
(sto/wrap-with-hash hash))]
|
||||
@@ -156,7 +184,8 @@
|
||||
:otf-file-id (:id otf)
|
||||
:ttf-file-id (:id ttf)}))]
|
||||
|
||||
(let [data (generate-missing! data)
|
||||
(let [data (join-chunks data)
|
||||
data (generate-missing data)
|
||||
assets (persist-fonts-files! data)
|
||||
result (insert-font-variant! assets)]
|
||||
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -97,9 +97,12 @@
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(def token-name-validation-regex
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
|
||||
token-name-validation-regex])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
|
||||
@@ -1462,11 +1462,12 @@ Will return a value that matches this schema:
|
||||
(def ^:private schema:dtcg-node
|
||||
[:schema {:registry
|
||||
{::simple-value
|
||||
[:or :string :int :double]
|
||||
[:or :string :int :double ::sm/boolean]
|
||||
::value
|
||||
[:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]
|
||||
[:vector [:map-of :string ::simple-value]]
|
||||
[:map-of :string [:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]]]]}}
|
||||
|
||||
@@ -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}"; \
|
||||
@@ -179,9 +180,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 +226,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 \
|
||||
@@ -375,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
|
||||
@@ -398,7 +420,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
|
||||
|
||||
|
||||
@@ -46,6 +46,11 @@ services:
|
||||
- 9090:9090
|
||||
- 9091:9091
|
||||
|
||||
# MCP
|
||||
- 4400:4400
|
||||
- 4401:4401
|
||||
- 4402:4402
|
||||
|
||||
environment:
|
||||
- EXTERNAL_UID=${CURRENT_USER_ID}
|
||||
# SMTP setup
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 *(.)'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
set -g default-command "${SHELL}"
|
||||
set -g mouse off
|
||||
set -g history-limit 50000
|
||||
setw -g mode-keys emacs
|
||||
|
||||
@@ -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; \
|
||||
@@ -24,6 +24,7 @@ RUN set -e; \
|
||||
libltdl-dev \
|
||||
liblzma-dev \
|
||||
libopenexr-dev \
|
||||
libxml2-dev \
|
||||
libpng-dev \
|
||||
librsvg2-dev \
|
||||
libtiff-dev \
|
||||
@@ -52,6 +53,7 @@ RUN set -e; \
|
||||
libfftw3-dev \
|
||||
libheif-dev \
|
||||
libjpeg-dev \
|
||||
libxml2-dev \
|
||||
liblcms2-dev \
|
||||
libltdl-dev \
|
||||
liblzma-dev \
|
||||
@@ -77,6 +79,7 @@ RUN set -e; \
|
||||
libopenjp2-7 \
|
||||
libpng16-16 \
|
||||
librsvg2-2 \
|
||||
libxml2 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
|
||||
@@ -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}"; \
|
||||
|
||||
@@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -152,9 +152,9 @@ services:
|
||||
|
||||
# AWS_ACCESS_KEY_ID: <KEY_ID>
|
||||
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
|
||||
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
|
||||
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME>
|
||||
# PENPOT_OBJECTS_STORAGE_BACKEND: s3
|
||||
# PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>
|
||||
|
||||
## Telemetry. When enabled, a periodical process will send anonymous data about this
|
||||
## instance. Telemetry data will enable us to learn how the application is used,
|
||||
|
||||
@@ -6,4 +6,4 @@ desc: Create, deploy, and use the Penpot plugin API with our comprehensive docum
|
||||
|
||||
# Penpot plugins API
|
||||
|
||||
We've got all the documentation you need for the API right <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/">here</a>.
|
||||
We've got all the documentation you need for the API right <a target="_blank" href="https://doc.plugins.penpot.app/">here</a>.
|
||||
|
||||
@@ -9,13 +9,13 @@ desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes
|
||||
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"></g-emoji> Epics and highlights</code>
|
||||
- This marks the release of version 1.0, and from this point forward, we’ll do our best to avoid making any more breaking changes (or make deprecations backward compatible).
|
||||
- We’ve redone the documentation. You can check the API here:
|
||||
[https://penpot-plugins-api-doc.pages.dev/](https://penpot-plugins-api-doc.pages.dev/)
|
||||
[https://doc.plugins.penpot.app/](https://doc.plugins.penpot.app/)
|
||||
- New samples repository with lots of samples to use the API:
|
||||
[https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples)
|
||||
|
||||
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> Breaking changes & Deprecations
|
||||
|
||||
- Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details.
|
||||
- Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://doc.plugins.penpot.app/) for more details.
|
||||
- Changes on the <code class="language-js">penpot.on</code> and <code class="language-js">penpot.off</code> methods.
|
||||
Previously you had to send the original callback to the off method in order to remove an event listener. Now, <code class="language-js">penpot.on</code> will return an *id* that you can pass to the <code class="language-js">penpot.off</code> method in order to remove the listener.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ There are two libraries that can help you with your plugin's development. They a
|
||||
|
||||
### Plugin styles
|
||||
|
||||
<code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">Plugin styles</a>.
|
||||
<code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://styles-doc.plugins.penpot.app/">Plugin styles</a>.
|
||||
|
||||
```bash
|
||||
npm install @penpot/plugin-styles
|
||||
@@ -139,7 +139,7 @@ parent.postMessage(responseMessage, targetOrigin);
|
||||
|
||||
By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly.
|
||||
|
||||
For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/).
|
||||
For more detailed information, refer to the [Penpot Plugins API Documentation](https://doc.plugins.penpot.app/).
|
||||
|
||||
## 2.5. Step 5. Build the plugin file
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ penpot.library.local.createTypography();
|
||||
|
||||
Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design.
|
||||
|
||||
Just a heads-up: if you use the <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example.
|
||||
Just a heads-up: if you use the <a target="_blank" href="https://styles-doc.plugins.penpot.app/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example.
|
||||
|
||||
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/theme">Theme example</a>
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ The plugin <a target="_blank" href="https://www.npmjs.com/package/@penpot/plugin
|
||||
|
||||
### Is the API ready to use the prototyping features?
|
||||
|
||||
Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotFlow">PenpotFlow</a> or <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotInteraction">PenpotInteraction</a> interfaces.
|
||||
Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Flow">Flow</a> or <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Interaction">Interaction</a> interfaces.
|
||||
|
||||
### Are there any security or quality criteria I should be aware of?
|
||||
|
||||
@@ -48,7 +48,8 @@ There are no set requirements. However, we can recommend the use of <a target="_
|
||||
|
||||
### Is it necessary to create plugins with a UI?
|
||||
|
||||
No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot-plugins/tree/main/apps/create-palette-plugin">here</a>
|
||||
No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette.plugins.penpot.app/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot/tree/main/plugins/apps/create-palette-plugin">here</a>
|
||||
|
||||
|
||||
### Can I create components?
|
||||
|
||||
@@ -58,7 +59,7 @@ Yes, it is possible to create components using:
|
||||
createComponent(shapes: Shape[]): LibraryComponent;
|
||||
```
|
||||
|
||||
Take a look at the Penpot Library methods in the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>.
|
||||
Take a look at the Penpot Library methods in the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>.
|
||||
|
||||
### Is there a place where I can share my plugin?
|
||||
|
||||
|
||||
@@ -69,12 +69,13 @@ You need to provide the plugin's manifest URL for the installation. If there are
|
||||
|
||||
| Name | URL |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json |
|
||||
| Color palette | https://create-palette.plugins.penpot.app/assets/manifest.json |
|
||||
| Contrast | https://contrast.plugins.penpot.app/assets/manifest.json |
|
||||
| Feather icons | https://icons.plugins.penpot.app/assets/manifest.json |
|
||||
| Lorem ipsum | https://lorem-ipsum.plugins.penpot.app/assets/manifest.json |
|
||||
| Rename layers | https://rename-layers.plugins.penpot.app/assets/manifest.json |
|
||||
| Tables | https://table.plugins.penpot.app/assets/manifest.json |
|
||||
|
||||
|
||||
## 1.4. Plugin's basics
|
||||
|
||||
|
||||
@@ -114,14 +114,7 @@ configuration.
|
||||
The callback has the following format:
|
||||
|
||||
```html
|
||||
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback
|
||||
```
|
||||
|
||||
You will need to change <your_domain> and <oauth_provider> according to your setup.
|
||||
This is how it looks with Gitlab provider:
|
||||
|
||||
```html
|
||||
https://<your_domain>/api/auth/oauth/gitlab/callback
|
||||
https://<your_domain>/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
#### Google
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{:path path
|
||||
:mtype (mime/get type)
|
||||
:name name
|
||||
:filename (str/concat name (mime/get-extension type))
|
||||
:filename (str/concat (str/slug name) (mime/get-extension type))
|
||||
:id task-id}))
|
||||
|
||||
(defn create-zip
|
||||
|
||||
@@ -38,6 +38,24 @@
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))))
|
||||
|
||||
(sync-page-size! [dom]
|
||||
(bw/eval! dom
|
||||
(fn [elem]
|
||||
;; IMPORTANT: No CLJS runtime allowed. Use only JS
|
||||
;; primitives. This runs in a context without access to
|
||||
;; cljs.core. Avoid any functions that transpile to
|
||||
;; cljs.core/* calls, as they will break in the browser
|
||||
;; runtime.
|
||||
|
||||
(let [width (.getAttribute ^js elem "width")
|
||||
height (.getAttribute ^js elem "height")
|
||||
style-node (let [node (.createElement js/document "style")]
|
||||
(.appendChild (.-head js/document) node)
|
||||
node)]
|
||||
(set! (.-textContent style-node)
|
||||
(dm/str "@page { size: " width "px " height "px; margin: 0; }\n"
|
||||
"html, body, #app { margin: 0; padding: 0; width: " width "px; height: " height "px; overflow: visible; }"))))))
|
||||
|
||||
(render-object [page base-uri {:keys [id] :as object}]
|
||||
(p/let [uri (prepare-uri base-uri id)
|
||||
path (sh/tempfile :prefix "penpot.tmp.pdf." :suffix (mime/get-extension type))]
|
||||
@@ -45,6 +63,7 @@
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
|
||||
(bw/wait-for dom)
|
||||
(sync-page-size! dom)
|
||||
(bw/screenshot dom {:full-page? true})
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(bw/pdf page {:path path})
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"watch": "exit 0",
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:app": "yarn run clear:shadow-cache && yarn run build:wasm && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:storybook": "yarn run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@penpot/draft-js": "portal:./packages/draft-js",
|
||||
"@penpot/mousetrap": "portal:./packages/mousetrap",
|
||||
"@penpot/plugins-runtime": "1.3.2",
|
||||
"@penpot/plugins-runtime": "1.4.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@playwright/test": "1.57.0",
|
||||
|
||||
@@ -110,7 +110,7 @@ test("Update an already created text shape by prepending text", async ({
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by inserting text in between", async ({
|
||||
test.skip("Update an already created text shape by inserting text in between", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
@@ -151,7 +151,7 @@ test("Update a new text shape appending text by pasting text", async ({
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
test.skip("Update a new text shape prepending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
|
||||
@@ -1256,6 +1256,192 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test("User creates shadow token with negative spread", async ({ page }) => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]});
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
||||
name: `Add Token: Shadow`,
|
||||
});
|
||||
|
||||
await addTokenButton.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByPlaceholder(
|
||||
"Enter a value or alias with {alias}",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
const offsetXField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "X",
|
||||
});
|
||||
const offsetYField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Y",
|
||||
});
|
||||
const blurField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
const spreadField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Spread",
|
||||
});
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
|
||||
// 1. Check default values
|
||||
await expect(offsetXField).toHaveValue("4");
|
||||
await expect(offsetYField).toHaveValue("4");
|
||||
await expect(blurField).toHaveValue("4");
|
||||
await expect(spreadField).toHaveValue("0");
|
||||
|
||||
// 2. Name filled + empty value → disabled
|
||||
await nameField.fill("my-token");
|
||||
await expect(submitButton).toBeDisabled();
|
||||
|
||||
// 3. Invalid color → disabled + error message
|
||||
await colorField.fill("1");
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Invalid color value: 1"),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(submitButton).toBeDisabled();
|
||||
|
||||
await colorField.fill("{missing-reference}");
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText(
|
||||
"Missing token references: missing-reference",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// 4. Empty name → disabled + error message
|
||||
await nameField.fill("");
|
||||
|
||||
const emptyNameErrorNode =
|
||||
tokensUpdateCreateModal.getByText(emptyNameError);
|
||||
|
||||
await expect(emptyNameErrorNode).toBeVisible();
|
||||
await expect(submitButton).toBeDisabled();
|
||||
|
||||
//
|
||||
// ------- SUCCESSFUL FIELDS -------
|
||||
//
|
||||
|
||||
// 5. Valid color → resolved
|
||||
|
||||
await colorField.fill("red");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: #ff0000"),
|
||||
).toBeVisible();
|
||||
const colorSwatch = tokensUpdateCreateModal.getByTestId(
|
||||
"token-form-color-bullet",
|
||||
);
|
||||
await colorSwatch.click();
|
||||
const rampSelector = tokensUpdateCreateModal.getByTestId(
|
||||
"value-saturation-selector",
|
||||
);
|
||||
await expect(rampSelector).toBeVisible();
|
||||
await rampSelector.click({ position: { x: 50, y: 50 } });
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value:"),
|
||||
).toBeVisible();
|
||||
|
||||
const sliderOpacity = tokensUpdateCreateModal.getByTestId("slider-opacity");
|
||||
await sliderOpacity.click({ position: { x: 50, y: 0 } });
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByRole("textbox", { name: "Color" }),
|
||||
).toHaveValue(/rgba\s*\([^)]*\)/);
|
||||
|
||||
// 6. Valid offset → resolved
|
||||
await offsetXField.fill("3 + 3");
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 6"),
|
||||
).toBeVisible();
|
||||
|
||||
await offsetYField.fill("3 + 7");
|
||||
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 10"),
|
||||
).toBeVisible();
|
||||
|
||||
// 7. Valid blur → resolved
|
||||
|
||||
await blurField.fill("3 + 1");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 4"),
|
||||
).toBeVisible();
|
||||
|
||||
// 8. Valid spread → resolved
|
||||
|
||||
await spreadField.fill("3 - 3");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 0"),
|
||||
).toBeVisible();
|
||||
|
||||
await spreadField.fill("1 - 3");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: -2"),
|
||||
).toBeVisible();
|
||||
|
||||
await nameField.fill("my-token");
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "my-token" }),
|
||||
).toBeEnabled();
|
||||
|
||||
//
|
||||
// ------- SECOND TOKEN WITH VALID REFERENCE -------
|
||||
//
|
||||
await addTokenButton.click();
|
||||
|
||||
await nameField.fill("my-token-2");
|
||||
const referenceToggle =
|
||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
const compositeToggle =
|
||||
tokensUpdateCreateModal.getByTestId("composite-opt");
|
||||
await referenceToggle.click();
|
||||
|
||||
const referenceInput = tokensUpdateCreateModal.getByPlaceholder(
|
||||
"Enter a token shadow alias",
|
||||
);
|
||||
await expect(referenceInput).toBeVisible();
|
||||
|
||||
await compositeToggle.click();
|
||||
await expect(colorField).toBeVisible();
|
||||
|
||||
await referenceToggle.click();
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Reference",
|
||||
});
|
||||
await referenceField.fill("{my-token}");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText(
|
||||
"Resolved value: - X: 6 - Y: 10 - Blur: 4 - Spread: -2",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(submitButton).toBeEnabled();
|
||||
await submitButton.click();
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test("User creates typography token", async ({ page }) => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
|
||||
@@ -17,17 +17,18 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script type="importmap">{{& manifest.importmap }}</script>
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Penpot - Rasterizer</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Penpot - Render</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,9 +27,11 @@ export function startWorker() {
|
||||
});
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
export const IS_DEBUG = process.env.NODE_ENV !== "production";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || (new Date().toString()) ;
|
||||
export const BUILD_TS = process.env.BUILD_TS || Date.now();
|
||||
export const VERSION = process.env.VERSION || "develop";
|
||||
export const VERSION_TAG = process.env.VERSION_TAG || VERSION;
|
||||
|
||||
async function findFiles(basePath, predicate, options = {}) {
|
||||
predicate =
|
||||
@@ -172,6 +174,7 @@ export async function watch(baseDir, predicate, callback) {
|
||||
const watcher = new Watcher(baseDir, {
|
||||
persistent: true,
|
||||
recursive: true,
|
||||
debounce: 500
|
||||
});
|
||||
|
||||
watcher.on("change", (path) => {
|
||||
@@ -179,8 +182,19 @@ export async function watch(baseDir, predicate, callback) {
|
||||
callback(path);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
watcher.on("error", (cause) => {
|
||||
console.log("WATCHER ERROR", cause);
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureDirectories() {
|
||||
await fs.mkdir("./resources/public/js/worker/", { recursive: true });
|
||||
await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
async function readManifestFile(resource) {
|
||||
const manifestPath = "resources/public/" + resource;
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
@@ -193,25 +207,25 @@ async function generateManifest() {
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
|
||||
config: "./js/config.js?version=" + VERSION_TAG,
|
||||
polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
|
||||
libs: "./js/libs.js?version=" + VERSION_TAG,
|
||||
worker_main: "./js/worker/main.js?version=" + VERSION_TAG,
|
||||
default_translations: "./js/translation.en.js?version=" + VERSION_TAG,
|
||||
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
"./js/shared.js": "./js/shared.js?version=" + VERSION_TAG,
|
||||
"./js/main.js": "./js/main.js?version=" + VERSION_TAG,
|
||||
"./js/render.js": "./js/render.js?version=" + VERSION_TAG,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + VERSION_TAG,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + VERSION_TAG,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + VERSION_TAG,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + VERSION_TAG,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + VERSION_TAG,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + VERSION_TAG,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + VERSION_TAG,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + VERSION_TAG
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -222,11 +236,12 @@ async function generateManifest() {
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
const content = await fs.readFile(path, { encoding: "utf-8" });
|
||||
|
||||
const ts = Math.floor(new Date());
|
||||
|
||||
context = Object.assign({}, context, {
|
||||
ts: ts,
|
||||
isDebug,
|
||||
isDebug: IS_DEBUG,
|
||||
version: VERSION,
|
||||
version_tag: VERSION_TAG,
|
||||
build_date: BUILD_DATE,
|
||||
build_ts: BUILD_TS,
|
||||
});
|
||||
|
||||
return mustache.render(content, context, partials);
|
||||
@@ -257,6 +272,9 @@ const markedOptions = {
|
||||
marked.use(markedOptions);
|
||||
|
||||
export async function compileTranslations() {
|
||||
const outputDir = "resources/public/js/";
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -338,7 +356,6 @@ export async function compileTranslations() {
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
@@ -390,7 +407,6 @@ async function generateSvgSprites() {
|
||||
}
|
||||
|
||||
async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
const manifest = await generateManifest();
|
||||
@@ -415,10 +431,7 @@ async function generateTemplates() {
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
manifest: manifest
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
@@ -487,7 +500,7 @@ export async function compileStyles() {
|
||||
await fs.mkdir("./resources/public/css", { recursive: true });
|
||||
await fs.writeFile("./resources/public/css/main.css", result);
|
||||
|
||||
if (isDebug) {
|
||||
if (IS_DEBUG) {
|
||||
let debugCSS = await compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
}
|
||||
@@ -500,17 +513,43 @@ export async function compileStyles() {
|
||||
export async function compileSvgSprites() {
|
||||
const start = process.hrtime();
|
||||
log.info("init: compile svgsprite");
|
||||
await generateSvgSprites();
|
||||
let error = false;
|
||||
|
||||
try {
|
||||
await generateSvgSprites();
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile svgsprite", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function compileTemplates() {
|
||||
const start = process.hrtime();
|
||||
let error = false;
|
||||
log.info("init: compile templates");
|
||||
await generateTemplates();
|
||||
|
||||
try {
|
||||
await generateTemplates();
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile templates", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile templates", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile templates", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function compilePolyfills() {
|
||||
|
||||
@@ -28,14 +28,12 @@ async function compileFile(path) {
|
||||
],
|
||||
sourceMap: false,
|
||||
});
|
||||
// console.dir(result);
|
||||
resolve({
|
||||
inputPath: path,
|
||||
outputPath: dest,
|
||||
css: result.css,
|
||||
});
|
||||
} catch (cause) {
|
||||
console.error(cause);
|
||||
reject(cause);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
set -ex
|
||||
|
||||
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
|
||||
export INCLUDE_WASM=${BUILD_WASM:-yes};
|
||||
export CURRENT_VERSION=$1;
|
||||
export BUILD_DATE=$(date -R);
|
||||
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
|
||||
export TS=$(date +%s);
|
||||
|
||||
export BUILD_DATE=$(date -R);
|
||||
export BUILD_TS=$(date +%s);
|
||||
|
||||
export VERSION=${1:-develop};
|
||||
export VERSION_TAG="${VERSION}-${BUILD_TS}";
|
||||
|
||||
# Some cljs reacts on this environment variable for define more
|
||||
# performant code on macros (example: rumext)
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install || exit 1;
|
||||
yarn install;
|
||||
|
||||
rm -rf target/dist;
|
||||
rm -rf resources/public;
|
||||
@@ -37,7 +37,7 @@ yarn run build:app:main $EXTRA_PARAMS;
|
||||
yarn run build:app:libs;
|
||||
yarn run build:app:assets;
|
||||
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$VERSION_TAG/g" resources/public/js/worker/main*.js
|
||||
|
||||
rsync -avr resources/public/ target/dist/
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await h.ensureDirectories();
|
||||
await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
export CURRENT_VERSION=$1;
|
||||
set -ex
|
||||
|
||||
export BUILD_TS=$(date +%s);
|
||||
export BUILD_DATE=$(date -R);
|
||||
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
export TS=$(date +%s);
|
||||
|
||||
export VERSION=${1:-develop};
|
||||
export VERSION_TAG="${VERSION}-${BUILD_TS}";
|
||||
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
@@ -12,19 +12,31 @@ let sass = null;
|
||||
|
||||
async function compileSassAll() {
|
||||
const start = process.hrtime();
|
||||
let error = false;
|
||||
|
||||
log.info("init: compile styles");
|
||||
|
||||
sass = await h.compileSassAll(worker);
|
||||
let output = await h.concatSass(sass);
|
||||
await fs.writeFile("./resources/public/css/main.css", output);
|
||||
try {
|
||||
sass = await h.compileSassAll(worker);
|
||||
let output = await h.concatSass(sass);
|
||||
await fs.writeFile("./resources/public/css/main.css", output);
|
||||
|
||||
if (isDebug) {
|
||||
let debugCSS = await h.compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
if (isDebug) {
|
||||
let debugCSS = await h.compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
}
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile styles", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile styles", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile styles", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function compileSass(path) {
|
||||
@@ -48,7 +60,7 @@ async function compileSass(path) {
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await h.ensureDirectories();
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
{:fn-invoke-direct true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
||||
:source-map true
|
||||
:pseudo-names true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off
|
||||
:cross-chunk-method-motion false
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
|
||||
(def version-tag (obj/get global "penpotVersionTag"))
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
@@ -190,9 +191,8 @@
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
(let [href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version-tag)))
|
||||
|
||||
@@ -24,6 +24,20 @@
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:const default-chunk-size
|
||||
(* 1024 1024 4)) ;; 4MiB
|
||||
|
||||
(defn- chunk-array
|
||||
[data chunk-size]
|
||||
(let [total-size (alength data)]
|
||||
(loop [offset 0
|
||||
chunks []]
|
||||
(if (< offset total-size)
|
||||
(let [end (min (+ offset chunk-size) total-size)
|
||||
chunk (.subarray ^js data offset end)]
|
||||
(recur end (conj chunks chunk)))
|
||||
chunks))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; General purpose events & IMPL
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -116,9 +130,9 @@
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))]
|
||||
|
||||
{:content {:data (js/Uint8Array. data)
|
||||
(not= hhea-descender os2-descent))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data (chunk-array data default-chunk-size)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
@@ -152,15 +152,14 @@
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (< (:value parsed-value) 0)
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
out-of-scope (< (:value parsed-value) 0)]
|
||||
(cond
|
||||
(and parsed-value (not out-of-scope))
|
||||
parsed-value
|
||||
|
||||
references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
||||
:references references}
|
||||
missing-references?
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references?)]
|
||||
:references missing-references?}
|
||||
|
||||
(and (not missing-references?) out-of-scope)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)]}
|
||||
@@ -365,7 +364,7 @@
|
||||
"Parses shadow spread value (non-negative number)."
|
||||
[value]
|
||||
(let [parsed (parse-sd-token-general-value value)
|
||||
valid? (and (:value parsed) (>= (:value parsed) 0))]
|
||||
valid? (:value parsed)]
|
||||
(cond
|
||||
valid?
|
||||
parsed
|
||||
|
||||
@@ -70,20 +70,22 @@
|
||||
(= (-> content last :command) :move-to))
|
||||
(into [] (take (dec (count content)) content))
|
||||
content)]
|
||||
(-> state
|
||||
(st/set-content content))))
|
||||
(st/set-content state content)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
id (dm/get-in state [:workspace-local :edition])
|
||||
old-content (dm/get-in state [:workspace-local :edit-path id :old-content])
|
||||
shape (st/get-path state)]
|
||||
local (get state :workspace-local)
|
||||
id (get local :edition)
|
||||
objects (dsh/lookup-page-objects state page-id)]
|
||||
|
||||
(if (and (some? old-content) (some? (:id shape)))
|
||||
(let [changes (generate-path-changes it objects page-id shape old-content (:content shape))]
|
||||
(rx/of (dch/commit-changes changes)))
|
||||
(rx/empty)))))))
|
||||
;; NOTE: we proceed only if the shape is present on the
|
||||
;; objects, if shape is a ephimeral drawing shape, we should
|
||||
;; do nothing
|
||||
(when-let [shape (get objects id)]
|
||||
(when-let [old-content (dm/get-in local [:edit-path id :old-content])]
|
||||
(let [new-content (get shape :content)
|
||||
changes (generate-path-changes it objects page-id shape old-content new-content)]
|
||||
(rx/of (dch/commit-changes changes))))))))))
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
@@ -289,34 +288,34 @@
|
||||
|
||||
(declare stop-path-edit)
|
||||
|
||||
|
||||
(defn start-path-edit
|
||||
[id]
|
||||
(ptk/reify ::start-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
edit-path (dm/get-in state [:workspace-local :edit-path id])
|
||||
content (st/get-path state :content)
|
||||
state (cond-> state
|
||||
(cfh/path-shape? objects id)
|
||||
(st/set-content (path/close-subpaths content)))]
|
||||
shape (get objects id)]
|
||||
|
||||
(cond-> state
|
||||
(or (not edit-path)
|
||||
(= :draw (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id] {:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})
|
||||
(and (some? edit-path)
|
||||
(= :move (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
|
||||
(-> state
|
||||
(st/set-content (path/close-subpaths (:content shape)))
|
||||
(update-in [:workspace-local :edit-path id]
|
||||
(fn [state]
|
||||
(let [state (if state
|
||||
(if (= :move (:edit-mode state))
|
||||
(assoc state :edit-mode :draw)
|
||||
state)
|
||||
{:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})]
|
||||
(assoc state :old-content (:content shape))))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))))]
|
||||
(let [stopper (rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))
|
||||
stream)]
|
||||
(rx/concat
|
||||
(rx/of (undo/start-path-undo))
|
||||
(->> stream
|
||||
@@ -325,7 +324,8 @@
|
||||
(rx/map #(stop-path-edit id))
|
||||
(rx/take-until stopper)))))))
|
||||
|
||||
(defn stop-path-edit [id]
|
||||
(defn stop-path-edit
|
||||
[id]
|
||||
(ptk/reify ::stop-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -335,13 +335,12 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (ptk/data-event :layout/update {:ids [id]})))))
|
||||
|
||||
(defn split-segments
|
||||
[{:keys [from-p to-p t]}]
|
||||
(defn- split-segments
|
||||
[id {:keys [from-p to-p t]}]
|
||||
(ptk/reify ::split-segments
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (st/get-path-id state)
|
||||
content (st/get-path state :content)]
|
||||
(let [content (st/get-path state :content)]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :edit-path id :old-content] content)
|
||||
(st/set-content (-> content
|
||||
@@ -353,10 +352,10 @@
|
||||
(rx/of (changes/save-path-content {:preserve-move-to true})))))
|
||||
|
||||
(defn create-node-at-position
|
||||
[event]
|
||||
[params]
|
||||
(ptk/reify ::create-node-at-position
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [id (st/get-path-id state)]
|
||||
(rx/of (dwsh/update-shapes [id] path/convert-to-path)
|
||||
(split-segments event))))))
|
||||
(split-segments id params))))))
|
||||
|
||||
@@ -480,6 +480,9 @@
|
||||
(def workspace-token-sets-tree
|
||||
(l/derived (d/nilf ctob/get-set-tree) tokens-lib))
|
||||
|
||||
(def workspace-all-tokens-map
|
||||
(l/derived (d/nilf ctob/get-all-tokens) tokens-lib))
|
||||
|
||||
(def workspace-active-theme-paths
|
||||
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
|
||||
|
||||
|
||||
@@ -193,16 +193,15 @@
|
||||
restore-fn
|
||||
(fn [_]
|
||||
(st/emit! (dd/restore-files-immediately
|
||||
(with-meta {:team-id (:id current-team)
|
||||
(with-meta {:team-id current-team-id
|
||||
:ids (into #{} d/xf:map-id files)}
|
||||
{:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
|
||||
(dd/fetch-projects (:id current-team))
|
||||
(dd/fetch-deleted-files (:id current-team)))
|
||||
(dd/fetch-projects current-team-id)
|
||||
(dd/fetch-deleted-files current-team-id))
|
||||
:on-error #(st/emit! (ntf/error (tr "dashboard.errors.error-on-restore-file" (:name file))))}))))
|
||||
|
||||
on-restore-immediately
|
||||
(fn []
|
||||
(prn files)
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "dashboard-restore-file-confirmation.title")
|
||||
@@ -214,7 +213,7 @@
|
||||
on-delete-immediately
|
||||
(fn []
|
||||
(let [accept-fn #(st/emit! (dd/delete-files-immediately
|
||||
{:team-id (:id current-team)
|
||||
{:team-id current-team-id
|
||||
:ids (into #{} d/xf:map-id files)}))]
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
@@ -244,8 +243,7 @@
|
||||
(for [project current-projects]
|
||||
{:name (get-project-name project)
|
||||
:id (get-project-id project)
|
||||
:handler (on-move (:id current-team)
|
||||
(:id project))})
|
||||
:handler (on-move current-team-id (:id project))})
|
||||
(when (seq other-teams)
|
||||
[{:name (tr "dashboard.move-to-other-team")
|
||||
:id "move-to-other-team"
|
||||
|
||||
@@ -628,6 +628,7 @@
|
||||
width: $sz-400;
|
||||
padding: var(--sp-xxxl);
|
||||
background-color: var(--color-background-primary);
|
||||
z-index: var(--z-index-set);
|
||||
|
||||
&.hero {
|
||||
top: px2rem(216);
|
||||
|
||||
@@ -36,10 +36,12 @@
|
||||
|
||||
(defn- hide-popover
|
||||
[node]
|
||||
(dom/unset-css-property! node "block-size")
|
||||
(dom/unset-css-property! node "inset-block-start")
|
||||
(dom/unset-css-property! node "inset-inline-start")
|
||||
(.hidePopover ^js node))
|
||||
(when (and (some? node)
|
||||
(fn? (.-hidePopover node)))
|
||||
(dom/unset-css-property! node "block-size")
|
||||
(dom/unset-css-property! node "inset-block-start")
|
||||
(dom/unset-css-property! node "inset-inline-start")
|
||||
(.hidePopover ^js node)))
|
||||
|
||||
(defn- calculate-placement-bounding-rect
|
||||
"Given a placement, calcultates the bounding rect for it taking in
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
(def context (mf/create-context nil))
|
||||
|
||||
(mf/defc form-input*
|
||||
[{:keys [name] :rest props}]
|
||||
[{:keys [name trim] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx context)
|
||||
input-name name
|
||||
|
||||
touched? (and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
|
||||
error (get-in @form [:errors input-name])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
@@ -32,7 +33,7 @@
|
||||
(mf/deps input-name)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(fm/on-input-change form input-name value true))))
|
||||
(fm/on-input-change form input-name value trim))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr event]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :simple attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))
|
||||
@@ -489,7 +489,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr event]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :multiple attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
@@ -724,7 +724,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change wrap-type ids)
|
||||
(fn [value event attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change (= "nowrap" wrap-type) attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as cl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
@@ -51,12 +52,17 @@
|
||||
;; Both variants provide identical color-picker and text-input behavior, but
|
||||
;; differ in how they persist the value within the form’s nested structure.
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
tokens
|
||||
(-> tokens
|
||||
@@ -131,6 +137,7 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
|
||||
touched?
|
||||
@@ -140,6 +147,9 @@
|
||||
error
|
||||
(get-in @form [:errors input-name])
|
||||
|
||||
extra-error
|
||||
(get-in @form [:extra-errors input-name])
|
||||
|
||||
value
|
||||
(get-in @form [:data input-name] "")
|
||||
|
||||
@@ -247,15 +257,20 @@
|
||||
:hint-type (:type hint)})
|
||||
|
||||
props
|
||||
(if (and error touched?)
|
||||
(cond
|
||||
(and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
(and extra-error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message extra-error)})
|
||||
:else
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name]
|
||||
(mf/with-effect [resolve-stream tokens token input-name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
@@ -301,7 +316,7 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
error
|
||||
(get-in @form [:errors :value value-subfield index input-name])
|
||||
|
||||
@@ -414,10 +429,10 @@
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield]
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
|
||||
@@ -49,10 +49,16 @@
|
||||
;; validate data within the form state.
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value (cto/split-font-family value)
|
||||
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
tokens
|
||||
(-> tokens
|
||||
@@ -73,6 +79,7 @@
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
touched?
|
||||
(and (contains? (:data @form) input-name)
|
||||
@@ -152,10 +159,10 @@
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name touched?]
|
||||
(mf/with-effect [resolve-stream tokens token input-name touched? token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
@@ -200,7 +207,7 @@
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
error
|
||||
(get-in @form [:errors :value input-name])
|
||||
|
||||
@@ -276,10 +283,10 @@
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name]
|
||||
(mf/with-effect [resolve-stream tokens token input-name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
@@ -139,10 +140,16 @@
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
tokens
|
||||
(-> tokens
|
||||
;; Remove previous token when renaming a token
|
||||
@@ -163,6 +170,7 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
touched?
|
||||
(and (contains? (:data @form) input-name)
|
||||
@@ -206,11 +214,11 @@
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name]
|
||||
(mf/with-effect [resolve-stream tokens token input-name token-name]
|
||||
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
@@ -236,12 +244,14 @@
|
||||
(on-composite-input-change form field value false))
|
||||
([form field value trim?]
|
||||
(letfn [(clean-errors [errors]
|
||||
(-> errors
|
||||
(dissoc field)
|
||||
(not-empty)))]
|
||||
(some-> errors
|
||||
(update :value #(when (map? %) (dissoc % field)))
|
||||
(update :value #(when (seq %) %))
|
||||
(not-empty)))]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data :value field] (if trim? (str/trim value) value))
|
||||
(assoc-in [:touched :value field] true)
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
@@ -250,6 +260,7 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
error
|
||||
(get-in @form [:errors :value input-name])
|
||||
@@ -257,6 +268,9 @@
|
||||
value
|
||||
(get-in @form [:data :value input-name] "")
|
||||
|
||||
touched?
|
||||
(get-in @form [:touched :value input-name])
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if-let [value (get-in token [:value input-name])]
|
||||
@@ -284,7 +298,7 @@
|
||||
:hint-message (:message hint)
|
||||
:hint-type (:type hint)})
|
||||
props
|
||||
(if error
|
||||
(if (and touched? error)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)
|
||||
@@ -293,10 +307,10 @@
|
||||
(mf/spread-props props {:hint-formated true})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name name]
|
||||
(mf/with-effect [resolve-stream tokens token input-name name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
@@ -360,7 +374,7 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
error
|
||||
(get-in @form [:errors :value value-subfield index input-name])
|
||||
@@ -405,10 +419,10 @@
|
||||
(mf/spread-props props {:hint-formated true})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield]
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token))
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.refs :as refs]
|
||||
@@ -110,8 +111,7 @@
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
tokens (mf/deref refs/workspace-all-tokens-map)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
@@ -207,7 +207,11 @@
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
(modal/hide)))
|
||||
(fn [{:keys [errors]}]
|
||||
(let [error-messages (wte/humanize-errors errors)
|
||||
error-message (first error-messages)]
|
||||
(swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
@@ -226,6 +230,7 @@
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:trim true
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
|
||||
@@ -282,15 +282,11 @@
|
||||
(let [n (d/parse-double blur)]
|
||||
(or (nil? n) (not (< n 0)))))]]]
|
||||
[:spread {:optional true}
|
||||
[:and
|
||||
[:maybe :string]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")}
|
||||
(fn [spread]
|
||||
(let [n (d/parse-double spread)]
|
||||
(or (nil? n) (not (< n 0)))))]]]
|
||||
[:maybe :string]]
|
||||
[:color {:optional true} [:maybe :string]]
|
||||
[:color-result {:optional true} ::sm/any]
|
||||
[:inset {:optional true} [:maybe :boolean]]]]]
|
||||
|
||||
(if (= active-tab :reference)
|
||||
[:reference {:optional false} ::sm/text]
|
||||
[:reference {:optional true} [:maybe :string]])]]
|
||||
|
||||
@@ -1185,7 +1185,6 @@
|
||||
{:cmd :export-shapes
|
||||
:profile-id (:profile-id @st/state)
|
||||
:wait true
|
||||
:skip-children (:skip-children value false)
|
||||
:exports [{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id id
|
||||
|
||||
@@ -106,17 +106,20 @@
|
||||
|
||||
(defn stop-propagation
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-stopPropagation event)))
|
||||
(.stopPropagation event)))
|
||||
|
||||
(defn stop-immediate-propagation
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-stopImmediatePropagation event)))
|
||||
(.stopImmediatePropagation event)))
|
||||
|
||||
(defn prevent-default
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-preventDefault event)))
|
||||
(.preventDefault event)))
|
||||
|
||||
(defn get-target
|
||||
@@ -802,9 +805,10 @@
|
||||
([uri name]
|
||||
(open-new-window uri name "noopener,noreferrer"))
|
||||
([uri name features]
|
||||
(let [new-window (.open js/window (str uri) name features)]
|
||||
(when-let [new-window (.open js/window (str uri) name features)]
|
||||
(when (not= name "_blank")
|
||||
(.reload (.-location new-window))))))
|
||||
(when-let [location (.-location new-window)]
|
||||
(.reload location))))))
|
||||
|
||||
(defn browser-back
|
||||
[]
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
(defn- load
|
||||
[locale]
|
||||
(let [path (str "./translation." locale ".js?version=" (:full cf/version))]
|
||||
(let [path (str "./translation." locale ".js?version=" cf/version-tag)]
|
||||
(->> (mod/import path)
|
||||
(p/fmap (fn [result] (unchecked-get result "default")))
|
||||
(p/fnly (fn [data cause]
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
(ns app.util.keyboard
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
|
||||
Object
|
||||
(preventDefault [_]
|
||||
(.preventDefault native-event))
|
||||
(dom/prevent-default native-event))
|
||||
|
||||
(stopPropagation [_]
|
||||
(.stopPropagation native-event)))
|
||||
(dom/stop-propagation native-event)))
|
||||
|
||||
(defn keyboard-event?
|
||||
[o]
|
||||
|
||||
@@ -7594,6 +7594,10 @@ msgstr "Error al importar: No se pudo procesar el JSON."
|
||||
msgid "workspace.tokens.export"
|
||||
msgstr "Exportar"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/export/modal.cljs:125
|
||||
msgid "workspace.tokens.export-tokens"
|
||||
msgstr "Exportar tokens"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/export/modal.cljs:118
|
||||
msgid "workspace.tokens.export.multiple-files"
|
||||
msgstr "Múltiples ficheros"
|
||||
@@ -7638,10 +7642,26 @@ msgstr "Nombre del grupo"
|
||||
msgid "workspace.tokens.grouping-set-alert"
|
||||
msgstr "La agrupación de sets aun no está soportada."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:233
|
||||
msgid "workspace.tokens.import-button-prefix"
|
||||
msgstr "Importar %s"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37
|
||||
msgid "workspace.tokens.import-error"
|
||||
msgstr "Error al importar:"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:273
|
||||
msgid "workspace.tokens.import-menu-folder-option"
|
||||
msgstr "Carpeta"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:271
|
||||
msgid "workspace.tokens.import-menu-json-option"
|
||||
msgstr "Archivo JSON único"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:272
|
||||
msgid "workspace.tokens.import-menu-zip-option"
|
||||
msgstr "Archivo ZIP"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:241
|
||||
msgid "workspace.tokens.import-multiple-files"
|
||||
msgstr ""
|
||||
@@ -7656,7 +7676,7 @@ msgstr ""
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:237
|
||||
msgid "workspace.tokens.import-tokens"
|
||||
msgstr "Import tokens"
|
||||
msgstr "Importar tokens"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415
|
||||
#, unused
|
||||
@@ -7757,7 +7777,7 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:57
|
||||
msgid "workspace.tokens.missing-references"
|
||||
msgstr "Referéncias de tokens no encontradas:"
|
||||
msgstr "Referencias de tokens no encontradas: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||
msgid "workspace.tokens.more-options"
|
||||
|
||||
@@ -1191,21 +1191,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: soft
|
||||
|
||||
"@penpot/plugin-types@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "@penpot/plugin-types@npm:1.3.2"
|
||||
checksum: 10c0/3f624472c260721ad89bf8d944e75acf6a9c9577271a757acb77574102213914051d1a32d5ab16e6ba16ae077fff78cf7a0f6d11d18351dfc214426677a67468
|
||||
"@penpot/plugin-types@npm:^1.4.2":
|
||||
version: 1.4.2
|
||||
resolution: "@penpot/plugin-types@npm:1.4.2"
|
||||
checksum: 10c0/b0972fe75c97e697eb1044c89db660393886b3c30676f8436ff4ab56c5bf0397b2c675697ae1b9c5fe40bc95a803aecf6d7ac356dbf6d3535bf8baec5d05eab1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@penpot/plugins-runtime@npm:1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "@penpot/plugins-runtime@npm:1.3.2"
|
||||
"@penpot/plugins-runtime@npm:1.4.2":
|
||||
version: 1.4.2
|
||||
resolution: "@penpot/plugins-runtime@npm:1.4.2"
|
||||
dependencies:
|
||||
"@penpot/plugin-types": "npm:^1.3.2"
|
||||
"@penpot/plugin-types": "npm:^1.4.2"
|
||||
ses: "npm:^1.1.0"
|
||||
zod: "npm:^3.22.4"
|
||||
checksum: 10c0/b6d2cb3a57bcbe58232db52b8224d1817495e96b34997bfa72421629b5f34a8c9cc71357c315dcab9d52ea036ed632a5efe0ac50f52e730901c02d498dfa1313
|
||||
checksum: 10c0/af084d906cce9a6dea956fe5420111d7ea37c7620737a1e3d4f12958cb302a8f697c1229c237107c28fbb3b9f37eee308e6d16262b04ad56ae6f76c7a12f12e5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4176,7 +4176,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@penpot/draft-js": "portal:./packages/draft-js"
|
||||
"@penpot/mousetrap": "portal:./packages/mousetrap"
|
||||
"@penpot/plugins-runtime": "npm:1.3.2"
|
||||
"@penpot/plugins-runtime": "npm:1.4.2"
|
||||
"@penpot/svgo": "penpot/svgo#v3.2"
|
||||
"@penpot/text-editor": "portal:./text-editor"
|
||||
"@playwright/test": "npm:1.57.0"
|
||||
|
||||
@@ -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
|
||||
|
||||
11
mcp/.gitignore
vendored
Normal file
11
mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.idea
|
||||
node_modules
|
||||
dist
|
||||
*.bak
|
||||
*.orig
|
||||
temp
|
||||
*.tsbuildinfo
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
7
mcp/.prettierignore
Normal file
7
mcp/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
*.md
|
||||
*.json
|
||||
python-scripts/
|
||||
.serena/
|
||||
|
||||
# auto-generated files
|
||||
mcp-server/data/api_types.yml
|
||||
20
mcp/.prettierrc
Normal file
20
mcp/.prettierrc
Normal file
@@ -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"
|
||||
}
|
||||
1
mcp/.serena/.gitignore
vendored
Normal file
1
mcp/.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
25
mcp/.serena/memories/code_style_conventions.md
Normal file
25
mcp/.serena/memories/code_style_conventions.md
Normal file
@@ -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)
|
||||
|
||||
91
mcp/.serena/memories/project_overview.md
Normal file
91
mcp/.serena/memories/project_overview.md
Normal file
@@ -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
|
||||
```
|
||||
|
||||
70
mcp/.serena/memories/suggested_commands.md
Normal file
70
mcp/.serena/memories/suggested_commands.md
Normal file
@@ -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
|
||||
```
|
||||
56
mcp/.serena/memories/task_completion_guidelines.md
Normal file
56
mcp/.serena/memories/task_completion_guidelines.md
Normal file
@@ -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
|
||||
130
mcp/.serena/project.yml
Normal file
130
mcp/.serena/project.yml
Normal file
@@ -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
|
||||
291
mcp/README.md
Normal file
291
mcp/README.md
Normal file
@@ -0,0 +1,291 @@
|
||||

|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
[](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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
[](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=<your-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://<your-address>:<port>` (default port: `4402`).
|
||||
41
mcp/docs/multi-user-mode.md
Normal file
41
mcp/docs/multi-user-mode.md
Normal file
@@ -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.
|
||||
26
mcp/package.json
Normal file
26
mcp/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
20
mcp/packages/common/package.json
Normal file
20
mcp/packages/common/package.json
Normal file
@@ -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/**/*"
|
||||
]
|
||||
}
|
||||
1
mcp/packages/common/src/index.ts
Normal file
1
mcp/packages/common/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./types";
|
||||
85
mcp/packages/common/src/types.ts
Normal file
85
mcp/packages/common/src/types.ts
Normal file
@@ -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<T> {
|
||||
/**
|
||||
* 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<T> {
|
||||
/**
|
||||
* 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<T> {
|
||||
/**
|
||||
* The result of the executed code, if any.
|
||||
*/
|
||||
result: T;
|
||||
|
||||
/**
|
||||
* Captured console output during code execution.
|
||||
*/
|
||||
log: string;
|
||||
}
|
||||
19
mcp/packages/common/tsconfig.json
Normal file
19
mcp/packages/common/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
24
mcp/packages/plugin/.gitignore
vendored
Normal file
24
mcp/packages/plugin/.gitignore
vendored
Normal file
@@ -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?
|
||||
21
mcp/packages/plugin/README.md
Normal file
21
mcp/packages/plugin/README.md
Normal file
@@ -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`.
|
||||
15
mcp/packages/plugin/index.html
Normal file
15
mcp/packages/plugin/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Penpot plugin example</title>
|
||||
</head>
|
||||
<body>
|
||||
<button type="button" data-appearance="secondary" data-handler="connect-mcp">Connect to MCP server</button>
|
||||
|
||||
<div id="connection-status" style="margin-top: 10px; font-size: 12px; color: #666">Not connected</div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
mcp/packages/plugin/package.json
Normal file
24
mcp/packages/plugin/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
6
mcp/packages/plugin/public/manifest.json
Normal file
6
mcp/packages/plugin/public/manifest.json
Normal file
@@ -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"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user