From a1460115e843325673e60b593430485af800cc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 21 Jan 2026 18:31:54 +0100 Subject: [PATCH 01/23] :wrench: Deploy penpot api documentation --- .github/workflows/plugins-deploy-api-doc.yml | 73 ++++++++++++++++++++ plugins/wrangle-penpot-plugins-api-doc.toml | 4 ++ 2 files changed, 77 insertions(+) create mode 100644 .github/workflows/plugins-deploy-api-doc.yml create mode 100644 plugins/wrangle-penpot-plugins-api-doc.toml diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml new file mode 100644 index 0000000000..1c67d4f47b --- /dev/null +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -0,0 +1,73 @@ +name: Plugins/api-doc deployer + +on: + push: + branches: + - develop + - staging + - main + paths: + - "plugins/**" + - ".github/workflows/deploy-plugin-docs.yml" + - "wrangle-penpot-plugins-api-doc.toml" + workflow_dispatch: + inputs: + gh_ref: + description: 'Name of the branch or ref' + type: string + required: true + default: 'develop' + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: plugins + 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 }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + cache: "pnpm" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs + 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 ;; + staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;; + develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;; + *) echo "Unsupported branch ${REF}" && exit 1 ;; + esac + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --config wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }} diff --git a/plugins/wrangle-penpot-plugins-api-doc.toml b/plugins/wrangle-penpot-plugins-api-doc.toml new file mode 100644 index 0000000000..e9535be2d8 --- /dev/null +++ b/plugins/wrangle-penpot-plugins-api-doc.toml @@ -0,0 +1,4 @@ +name = "penpot-plugins-api-doc" +compatibility_date = "2025-01-01" + +assets = { directory = "dist/doc" } From 16f22a7b5c5fbd817db40fd67368cb629a3da56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 22 Jan 2026 11:38:22 +0100 Subject: [PATCH 02/23] :wrench: Fixes to the API documentation deployer --- .github/workflows/plugins-deploy-api-doc.yml | 58 +++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index 1c67d4f47b..62a87745bb 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -7,16 +7,22 @@ on: - staging - main paths: - - "plugins/**" - - ".github/workflows/deploy-plugin-docs.yml" - - "wrangle-penpot-plugins-api-doc.toml" + - "plugins/libs/plugin-types/index.d.ts" + - "plugins/libs/plugin-types/REAME.md" + - "plugins/tools/typedoc.css" + - "plugins/CHANGELOG.md" + - "plugins/wrangle-penpot-plugins-api-doc.toml" workflow_dispatch: inputs: gh_ref: - description: 'Name of the branch or ref' - type: string + description: 'Name of the branch' + type: choice required: true default: 'develop' + options: + - develop + - staging + - main permissions: contents: read @@ -24,9 +30,6 @@ permissions: jobs: deploy: runs-on: ubuntu-latest - defaults: - run: - working-directory: plugins steps: - name: Extract some useful variables id: vars @@ -39,20 +42,44 @@ jobs: fetch-depth: 0 ref: ${{ steps.vars.outputs.gh_ref }} + # START: Setup Node and PNPM enabling cache - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - cache: "pnpm" + node-version-file: .nvmrc - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - 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: - run_install: false + 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 dependencies - run: pnpm install --frozen-lockfile + - 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 @@ -68,6 +95,7 @@ jobs: - 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 wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }} From 599656c31e7f6e1d5049c8597f8f7442525258de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 5 Jan 2026 09:13:14 +0100 Subject: [PATCH 03/23] :wrench: Fix a typo in an interpolation --- .github/workflows/build-tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-tag.yml b/.github/workflows/build-tag.yml index 80ef7bcaeb..c32e363888 100644 --- a/.github/workflows/build-tag.yml +++ b/.github/workflows/build-tag.yml @@ -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 From a9741073e59efa1f6969047b569dce8c5ca9312e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 23 Jan 2026 20:13:04 +0100 Subject: [PATCH 04/23] :wrench: Add deploy plugin packages workflow placeholder and wrangle config files --- .github/workflows/plugins-deploy-package.yml | 11 +++++++++++ .github/workflows/plugins-deploy-packages.yml | 11 +++++++++++ plugins/apps/colors-to-tokens-plugin/wrangle.toml | 4 ++++ plugins/apps/contrast-plugin/wrangle.toml | 4 ++++ plugins/apps/create-palette-plugin/wrangle.toml | 4 ++++ plugins/apps/icons-plugin/wrangle.toml | 4 ++++ plugins/apps/lorem-ipsum-plugin/wrangle.toml | 4 ++++ plugins/apps/rename-layers-plugin/wrangle.toml | 5 +++++ plugins/apps/table-plugin/wrangle.toml | 5 +++++ 9 files changed, 52 insertions(+) create mode 100644 .github/workflows/plugins-deploy-package.yml create mode 100644 .github/workflows/plugins-deploy-packages.yml create mode 100644 plugins/apps/colors-to-tokens-plugin/wrangle.toml create mode 100644 plugins/apps/contrast-plugin/wrangle.toml create mode 100644 plugins/apps/create-palette-plugin/wrangle.toml create mode 100644 plugins/apps/icons-plugin/wrangle.toml create mode 100644 plugins/apps/lorem-ipsum-plugin/wrangle.toml create mode 100644 plugins/apps/rename-layers-plugin/wrangle.toml create mode 100644 plugins/apps/table-plugin/wrangle.toml diff --git a/.github/workflows/plugins-deploy-package.yml b/.github/workflows/plugins-deploy-package.yml new file mode 100644 index 0000000000..ca5fe817de --- /dev/null +++ b/.github/workflows/plugins-deploy-package.yml @@ -0,0 +1,11 @@ +name: Plugins/package deployer + +on: + workflow_dispatch: + +jobs: + print_text_job: + runs-on: ubuntu-latest + steps: + - name: Print Hello World + run: echo "Hello, World!" diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml new file mode 100644 index 0000000000..cabc045f00 --- /dev/null +++ b/.github/workflows/plugins-deploy-packages.yml @@ -0,0 +1,11 @@ +name: Plugins/packages deployer + +on: + workflow_dispatch: + +jobs: + print_text_job: + runs-on: ubuntu-latest + steps: + - name: Print Hello World + run: echo "Hello, World!" diff --git a/plugins/apps/colors-to-tokens-plugin/wrangle.toml b/plugins/apps/colors-to-tokens-plugin/wrangle.toml new file mode 100644 index 0000000000..7722755890 --- /dev/null +++ b/plugins/apps/colors-to-tokens-plugin/wrangle.toml @@ -0,0 +1,4 @@ +name = "color-to-tokens-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/color-to-tokens-plugin/browser" } diff --git a/plugins/apps/contrast-plugin/wrangle.toml b/plugins/apps/contrast-plugin/wrangle.toml new file mode 100644 index 0000000000..726ae60d6e --- /dev/null +++ b/plugins/apps/contrast-plugin/wrangle.toml @@ -0,0 +1,4 @@ +name = "contrast-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/contrast-plugin/browser" } diff --git a/plugins/apps/create-palette-plugin/wrangle.toml b/plugins/apps/create-palette-plugin/wrangle.toml new file mode 100644 index 0000000000..74c4b73cb6 --- /dev/null +++ b/plugins/apps/create-palette-plugin/wrangle.toml @@ -0,0 +1,4 @@ +name = "create-palette-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/create-palette-plugin" } diff --git a/plugins/apps/icons-plugin/wrangle.toml b/plugins/apps/icons-plugin/wrangle.toml new file mode 100644 index 0000000000..26e7514ec8 --- /dev/null +++ b/plugins/apps/icons-plugin/wrangle.toml @@ -0,0 +1,4 @@ +name = "icons-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/icons-plugin/browser" } diff --git a/plugins/apps/lorem-ipsum-plugin/wrangle.toml b/plugins/apps/lorem-ipsum-plugin/wrangle.toml new file mode 100644 index 0000000000..9e4d9366f0 --- /dev/null +++ b/plugins/apps/lorem-ipsum-plugin/wrangle.toml @@ -0,0 +1,4 @@ +name = "lorem-ipsum-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" } diff --git a/plugins/apps/rename-layers-plugin/wrangle.toml b/plugins/apps/rename-layers-plugin/wrangle.toml new file mode 100644 index 0000000000..1dc6c2fe0a --- /dev/null +++ b/plugins/apps/rename-layers-plugin/wrangle.toml @@ -0,0 +1,5 @@ +name = "rename-layers-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/rename-layers-plugin/browser" } + diff --git a/plugins/apps/table-plugin/wrangle.toml b/plugins/apps/table-plugin/wrangle.toml new file mode 100644 index 0000000000..516dd54b1e --- /dev/null +++ b/plugins/apps/table-plugin/wrangle.toml @@ -0,0 +1,5 @@ +name = "table-plugin" +compatibility_date = "2025-01-01" + +assets = { directory = "../../dist/apps/table-plugin/browser" } + From e590cd852d27ef01fea3ac3d952dc0cb9b5b81cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Jan 2026 09:33:37 +0100 Subject: [PATCH 05/23] :wrench: Rename wrangle to wrangler --- .github/workflows/plugins-deploy-api-doc.yml | 16 ++++++++++++++-- .../{wrangle.toml => wrangler.toml} | 4 ++++ .../{wrangle.toml => wrangler.toml} | 4 ++++ .../{wrangle.toml => wrangler.toml} | 4 ++++ .../icons-plugin/{wrangle.toml => wrangler.toml} | 4 ++++ .../{wrangle.toml => wrangler.toml} | 4 ++++ .../{wrangle.toml => wrangler.toml} | 3 +++ .../table-plugin/{wrangle.toml => wrangler.toml} | 3 +++ ...toml => wrangler-penpot-plugins-api-doc.toml} | 0 9 files changed, 40 insertions(+), 2 deletions(-) rename plugins/apps/colors-to-tokens-plugin/{wrangle.toml => wrangler.toml} (71%) rename plugins/apps/contrast-plugin/{wrangle.toml => wrangler.toml} (69%) rename plugins/apps/create-palette-plugin/{wrangle.toml => wrangler.toml} (70%) rename plugins/apps/icons-plugin/{wrangle.toml => wrangler.toml} (68%) rename plugins/apps/lorem-ipsum-plugin/{wrangle.toml => wrangler.toml} (70%) rename plugins/apps/rename-layers-plugin/{wrangle.toml => wrangler.toml} (71%) rename plugins/apps/table-plugin/{wrangle.toml => wrangler.toml} (68%) rename plugins/{wrangle-penpot-plugins-api-doc.toml => wrangler-penpot-plugins-api-doc.toml} (100%) diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index 62a87745bb..f8451e9816 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -11,7 +11,7 @@ on: - "plugins/libs/plugin-types/REAME.md" - "plugins/tools/typedoc.css" - "plugins/CHANGELOG.md" - - "plugins/wrangle-penpot-plugins-api-doc.toml" + - "plugins/wrangler-penpot-plugins-api-doc.toml" workflow_dispatch: inputs: gh_ref: @@ -98,4 +98,16 @@ jobs: workingDirectory: plugins apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy --config wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }} + 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 diff --git a/plugins/apps/colors-to-tokens-plugin/wrangle.toml b/plugins/apps/colors-to-tokens-plugin/wrangler.toml similarity index 71% rename from plugins/apps/colors-to-tokens-plugin/wrangle.toml rename to plugins/apps/colors-to-tokens-plugin/wrangler.toml index 7722755890..c1d45f3e87 100644 --- a/plugins/apps/colors-to-tokens-plugin/wrangle.toml +++ b/plugins/apps/colors-to-tokens-plugin/wrangler.toml @@ -2,3 +2,7 @@ name = "color-to-tokens-plugin" compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/color-to-tokens-plugin/browser" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/contrast-plugin/wrangle.toml b/plugins/apps/contrast-plugin/wrangler.toml similarity index 69% rename from plugins/apps/contrast-plugin/wrangle.toml rename to plugins/apps/contrast-plugin/wrangler.toml index 726ae60d6e..86f456ec95 100644 --- a/plugins/apps/contrast-plugin/wrangle.toml +++ b/plugins/apps/contrast-plugin/wrangler.toml @@ -2,3 +2,7 @@ name = "contrast-plugin" compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/contrast-plugin/browser" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/create-palette-plugin/wrangle.toml b/plugins/apps/create-palette-plugin/wrangler.toml similarity index 70% rename from plugins/apps/create-palette-plugin/wrangle.toml rename to plugins/apps/create-palette-plugin/wrangler.toml index 74c4b73cb6..40f4f67a38 100644 --- a/plugins/apps/create-palette-plugin/wrangle.toml +++ b/plugins/apps/create-palette-plugin/wrangler.toml @@ -2,3 +2,7 @@ name = "create-palette-plugin" compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/create-palette-plugin" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/icons-plugin/wrangle.toml b/plugins/apps/icons-plugin/wrangler.toml similarity index 68% rename from plugins/apps/icons-plugin/wrangle.toml rename to plugins/apps/icons-plugin/wrangler.toml index 26e7514ec8..0a690dac57 100644 --- a/plugins/apps/icons-plugin/wrangle.toml +++ b/plugins/apps/icons-plugin/wrangler.toml @@ -2,3 +2,7 @@ name = "icons-plugin" compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/icons-plugin/browser" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/lorem-ipsum-plugin/wrangle.toml b/plugins/apps/lorem-ipsum-plugin/wrangler.toml similarity index 70% rename from plugins/apps/lorem-ipsum-plugin/wrangle.toml rename to plugins/apps/lorem-ipsum-plugin/wrangler.toml index 9e4d9366f0..398691c3ba 100644 --- a/plugins/apps/lorem-ipsum-plugin/wrangle.toml +++ b/plugins/apps/lorem-ipsum-plugin/wrangler.toml @@ -2,3 +2,7 @@ name = "lorem-ipsum-plugin" compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/rename-layers-plugin/wrangle.toml b/plugins/apps/rename-layers-plugin/wrangler.toml similarity index 71% rename from plugins/apps/rename-layers-plugin/wrangle.toml rename to plugins/apps/rename-layers-plugin/wrangler.toml index 1dc6c2fe0a..4fdc18597d 100644 --- a/plugins/apps/rename-layers-plugin/wrangle.toml +++ b/plugins/apps/rename-layers-plugin/wrangler.toml @@ -3,3 +3,6 @@ compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/rename-layers-plugin/browser" } +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/apps/table-plugin/wrangle.toml b/plugins/apps/table-plugin/wrangler.toml similarity index 68% rename from plugins/apps/table-plugin/wrangle.toml rename to plugins/apps/table-plugin/wrangler.toml index 516dd54b1e..9c95160a01 100644 --- a/plugins/apps/table-plugin/wrangle.toml +++ b/plugins/apps/table-plugin/wrangler.toml @@ -3,3 +3,6 @@ compatibility_date = "2025-01-01" assets = { directory = "../../dist/apps/table-plugin/browser" } +[[routes]] +pattern = "WORKER_URI" +custom_domain = true diff --git a/plugins/wrangle-penpot-plugins-api-doc.toml b/plugins/wrangler-penpot-plugins-api-doc.toml similarity index 100% rename from plugins/wrangle-penpot-plugins-api-doc.toml rename to plugins/wrangler-penpot-plugins-api-doc.toml From 719a95246ac9c3753f2a53fc56936050ce68c89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Jan 2026 09:34:01 +0100 Subject: [PATCH 06/23] :wrench: Define deploy plugin packages workflows --- .github/workflows/plugins-deploy-package.yml | 124 ++++++++++++++++- .github/workflows/plugins-deploy-packages.yml | 130 +++++++++++++++++- 2 files changed, 247 insertions(+), 7 deletions(-) diff --git a/.github/workflows/plugins-deploy-package.yml b/.github/workflows/plugins-deploy-package.yml index ca5fe817de..cad4b1524f 100644 --- a/.github/workflows/plugins-deploy-package.yml +++ b/.github/workflows/plugins-deploy-package.yml @@ -1,11 +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)' + 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)' + type: string + required: true + +permissions: + contents: read jobs: - print_text_job: - runs-on: ubuntu-latest + deploy: + runs-on: penpot-runner-01 steps: - - name: Print Hello World - run: echo "Hello, World!" + - 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 diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml index cabc045f00..d1060389d5 100644 --- a/.github/workflows/plugins-deploy-packages.yml +++ b/.github/workflows/plugins-deploy-packages.yml @@ -1,11 +1,135 @@ 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: - print_text_job: + 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: - - name: Print Hello World - run: echo "Hello, World!" + - 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: needs.detect-changes.outputs.colors_to_tokens == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: colors-to-tokens + + contrast-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.contrast == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: contrast + + create-palette-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.create_palette == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: create-palette + + icons-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.icons == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: icons + + lorem-ipsum-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.lorem_ipsum == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: lorem-ipsum + + rename-layers-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.rename_layers == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + with: + gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + plugin_name: rename-layers + + table-plugin: + needs: detect-changes + if: needs.detect-changes.outputs.table == 'true' + uses: ./.github/workflows/plugins-deploy-package.yml + 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: needs.detect-changes.outputs.another_plugin == 'true' + # uses: ./.github/workflows/plugins-deploy-package.yml + # with: + # gh_ref: "${{ inputs.gh_ref || github.ref_name }}" + # plugin_name: another From 38179ba11e5ee4ab3546d7c6efe82cbc253c9b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Jan 2026 14:00:09 +0100 Subject: [PATCH 07/23] :wrench: Enable secret inheritance --- .github/workflows/plugins-deploy-packages.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml index d1060389d5..b7c9c3af47 100644 --- a/.github/workflows/plugins-deploy-packages.yml +++ b/.github/workflows/plugins-deploy-packages.yml @@ -72,6 +72,7 @@ jobs: needs: detect-changes if: 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 @@ -80,6 +81,7 @@ jobs: needs: detect-changes if: 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 @@ -88,6 +90,7 @@ jobs: needs: detect-changes if: 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 @@ -96,6 +99,7 @@ jobs: needs: detect-changes if: 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 @@ -104,6 +108,7 @@ jobs: needs: detect-changes if: 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 @@ -112,6 +117,7 @@ jobs: needs: detect-changes if: 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 @@ -120,6 +126,7 @@ jobs: needs: detect-changes if: 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 @@ -130,6 +137,7 @@ jobs: # needs: detect-changes # if: 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 From 93f5e74bb0cff9dd9b049b5ea1395fe4ecc6b20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Jan 2026 16:46:03 +0100 Subject: [PATCH 08/23] :wrench: Run all the jobs if the workflow is launched manually --- .github/workflows/plugins-deploy-packages.yml | 16 ++++++++-------- .../apps/colors-to-tokens-plugin/wrangler.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml index b7c9c3af47..3223bc52a6 100644 --- a/.github/workflows/plugins-deploy-packages.yml +++ b/.github/workflows/plugins-deploy-packages.yml @@ -70,7 +70,7 @@ jobs: colors-to-tokens-plugin: needs: detect-changes - if: needs.detect-changes.outputs.colors_to_tokens == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -79,7 +79,7 @@ jobs: contrast-plugin: needs: detect-changes - if: needs.detect-changes.outputs.contrast == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -88,7 +88,7 @@ jobs: create-palette-plugin: needs: detect-changes - if: needs.detect-changes.outputs.create_palette == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -97,7 +97,7 @@ jobs: icons-plugin: needs: detect-changes - if: needs.detect-changes.outputs.icons == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -106,7 +106,7 @@ jobs: lorem-ipsum-plugin: needs: detect-changes - if: needs.detect-changes.outputs.lorem_ipsum == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -115,7 +115,7 @@ jobs: rename-layers-plugin: needs: detect-changes - if: needs.detect-changes.outputs.rename_layers == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -124,7 +124,7 @@ jobs: table-plugin: needs: detect-changes - if: needs.detect-changes.outputs.table == 'true' + if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true' uses: ./.github/workflows/plugins-deploy-package.yml secrets: inherit with: @@ -135,7 +135,7 @@ jobs: # Add more jobs for other plugins below, following the same pattern # another-plugin: # needs: detect-changes - # if: needs.detect-changes.outputs.another_plugin == 'true' + # if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true' # uses: ./.github/workflows/plugins-deploy-package.yml # secrets: inherit # with: diff --git a/plugins/apps/colors-to-tokens-plugin/wrangler.toml b/plugins/apps/colors-to-tokens-plugin/wrangler.toml index c1d45f3e87..7f48730a36 100644 --- a/plugins/apps/colors-to-tokens-plugin/wrangler.toml +++ b/plugins/apps/colors-to-tokens-plugin/wrangler.toml @@ -1,7 +1,7 @@ name = "color-to-tokens-plugin" compatibility_date = "2025-01-01" -assets = { directory = "../../dist/apps/color-to-tokens-plugin/browser" } +assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" } [[routes]] pattern = "WORKER_URI" From ec61aa6b6d1d3560694753b43d9efc36a3f2ad90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 20:27:30 +0100 Subject: [PATCH 09/23] :wrench: Add custom domain --- .github/workflows/plugins-deploy-api-doc.yml | 28 ++++++++++++++------ plugins/wrangler-penpot-plugins-api-doc.toml | 4 +++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index 62a87745bb..67f0542b48 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -7,11 +7,11 @@ on: - staging - main paths: - - "plugins/libs/plugin-types/index.d.ts" - - "plugins/libs/plugin-types/REAME.md" - - "plugins/tools/typedoc.css" - - "plugins/CHANGELOG.md" - - "plugins/wrangle-penpot-plugins-api-doc.toml" + - '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: @@ -86,12 +86,24 @@ jobs: run: | REF="${{ steps.vars.outputs.gh_ref }}" case "$REF" in - main) echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV ;; - staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;; - develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;; + 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: diff --git a/plugins/wrangler-penpot-plugins-api-doc.toml b/plugins/wrangler-penpot-plugins-api-doc.toml index e9535be2d8..b09adf8b1a 100644 --- a/plugins/wrangler-penpot-plugins-api-doc.toml +++ b/plugins/wrangler-penpot-plugins-api-doc.toml @@ -2,3 +2,7 @@ name = "penpot-plugins-api-doc" compatibility_date = "2025-01-01" assets = { directory = "dist/doc" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true From 0f51b23ce73299d43cca3a9d94d8939a56e0060a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 20:28:30 +0100 Subject: [PATCH 10/23] :wrench: Deploy plugin styles documentation --- .../workflows/plugins-deploy-styles-doc.yml | 123 ++++++++++++++++++ .../wrangler-penpot-plugins-styles-doc.toml | 8 ++ 2 files changed, 131 insertions(+) create mode 100644 .github/workflows/plugins-deploy-styles-doc.yml create mode 100644 plugins/wrangler-penpot-plugins-styles-doc.toml diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml new file mode 100644 index 0000000000..7c759a62e6 --- /dev/null +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -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-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-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 diff --git a/plugins/wrangler-penpot-plugins-styles-doc.toml b/plugins/wrangler-penpot-plugins-styles-doc.toml new file mode 100644 index 0000000000..7aa86eb469 --- /dev/null +++ b/plugins/wrangler-penpot-plugins-styles-doc.toml @@ -0,0 +1,8 @@ +name = "penpot-plugins-style-doc" +compatibility_date = "2025-01-01" + +assets = { directory = "dist/apps/example-styles" } + +[[routes]] +pattern = "WORKER_URI" +custom_domain = true From 693b52bf45b7000a1bb711d7620d8e034c5ffc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 14:13:42 +0100 Subject: [PATCH 11/23] :books: Fix links related to penpot plugins --- docs/plugins/api.md | 2 +- docs/plugins/beta-changelog.md | 4 ++-- docs/plugins/create-a-plugin.md | 4 ++-- docs/plugins/examples-templates.md | 2 +- docs/plugins/faq.md | 7 ++++--- docs/plugins/getting-started.md | 13 +++++++------ plugins/CONTRIBUTING.md | 2 +- plugins/docs/api-docs.md | 2 +- plugins/libs/plugins-styles/README.md | 2 +- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/plugins/api.md b/docs/plugins/api.md index 9abf6a9fa7..d1cd0bf4c3 100644 --- a/docs/plugins/api.md +++ b/docs/plugins/api.md @@ -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 here. +We've got all the documentation you need for the API right here. diff --git a/docs/plugins/beta-changelog.md b/docs/plugins/beta-changelog.md index 1f58235752..fb8a656f97 100644 --- a/docs/plugins/beta-changelog.md +++ b/docs/plugins/beta-changelog.md @@ -9,13 +9,13 @@ desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes ### boom Epics and highlights - 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) ### boom Breaking changes & Deprecations -- Changed types names to remove the Penpot prefix. So for example: PenpotShape becomes Shape; PenpotFile becomes File, 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: PenpotShape becomes Shape; PenpotFile becomes File, and so on. Check the [API documentation](https://doc.plugins.penpot.app/) for more details. - Changes on the penpot.on and penpot.off methods. Previously you had to send the original callback to the off method in order to remove an event listener. Now, penpot.on will return an *id* that you can pass to the penpot.off method in order to remove the listener. diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 80c4a08df6..42fc096dbe 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -49,7 +49,7 @@ There are two libraries that can help you with your plugin's development. They a ### Plugin styles -@penpot/plugin-styles contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles. +@penpot/plugin-styles contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles. ```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 diff --git a/docs/plugins/examples-templates.md b/docs/plugins/examples-templates.md index 1836b132c2..d50e49e460 100644 --- a/docs/plugins/examples-templates.md +++ b/docs/plugins/examples-templates.md @@ -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 plugin-styles library, 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 styles.css of the example. +Just a heads-up: if you use the plugin-styles library, 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 styles.css of the example. Theme example diff --git a/docs/plugins/faq.md b/docs/plugins/faq.md index 4050358597..0d761bde10 100644 --- a/docs/plugins/faq.md +++ b/docs/plugins/faq.md @@ -40,7 +40,7 @@ The plugin PenpotFlow or PenpotInteraction 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 Flow or Interaction 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 https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json or check the code here +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: https:\/\/create-palette.plugins.penpot.app/assets/manifest.json or check the code here + ### 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 API documentation or this simple example. +Take a look at the Penpot Library methods in the API documentation or this simple example. ### Is there a place where I can share my plugin? diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md index 9765e737b0..abfbc508b1 100644 --- a/docs/plugins/getting-started.md +++ b/docs/plugins/getting-started.md @@ -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 diff --git a/plugins/CONTRIBUTING.md b/plugins/CONTRIBUTING.md index e4440cf083..10b243cd19 100644 --- a/plugins/CONTRIBUTING.md +++ b/plugins/CONTRIBUTING.md @@ -7,7 +7,7 @@ different parts of the platform, please refer to `docs/` directory. ## Reporting Bugs -We are using [GitHub Issues](https://github.com/penpot/penpot-plugins/issues) +We are using [GitHub Issues](https://github.com/penpot/penpot/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. diff --git a/plugins/docs/api-docs.md b/plugins/docs/api-docs.md index a8cf00b157..dd38659fc9 100644 --- a/plugins/docs/api-docs.md +++ b/plugins/docs/api-docs.md @@ -19,7 +19,7 @@ the latest changes from the `main` branch. This will trigger the deployment at Cloudfare if the `libs/plugin-types/index.d.ts` or the `tools/typedoc.css` files have been updated. -Take a look at the [Penpot plugins API](https://penpot-plugins-api-doc.pages.dev/) to see what's new. +Take a look at the [Penpot plugins API](https://doc.plugins.penpot.app/) to see what's new. #### Styles diff --git a/plugins/libs/plugins-styles/README.md b/plugins/libs/plugins-styles/README.md index 6739a9c393..9291f21cfe 100644 --- a/plugins/libs/plugins-styles/README.md +++ b/plugins/libs/plugins-styles/README.md @@ -20,7 +20,7 @@ Import the CSS file into your project: For detailed examples and to see how to use the styles and components, visit the documentation at: -[Penpot Plugin Styles Documentation](https://penpot-plugins-styles.pages.dev) +[Penpot Plugin Styles Documentation](https://styles-doc.plugins.penpot.app) #### Icons From 77bbf30ae45a2edc7a931a1c437757d006b9d3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Jan 2026 21:04:25 +0100 Subject: [PATCH 12/23] :wrench: Fix file name --- .github/workflows/plugins-deploy-styles-doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml index 7c759a62e6..0ab376cb6e 100644 --- a/.github/workflows/plugins-deploy-styles-doc.yml +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -100,7 +100,7 @@ jobs: working-directory: plugins shell: bash run: | - sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml + 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 From 76bd31fe7d25a378ac3981ed6ff3c914b5791d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Jan 2026 11:58:19 +0100 Subject: [PATCH 13/23] :wrench: Fix CORS error --- plugins/apps/colors-to-tokens-plugin/project.json | 1 + plugins/apps/colors-to-tokens-plugin/src/_headers | 4 ++++ plugins/apps/contrast-plugin/project.json | 1 + plugins/apps/contrast-plugin/src/_headers | 4 ++++ plugins/apps/create-palette-plugin/public/_headers | 4 ++++ plugins/apps/icons-plugin/project.json | 1 + plugins/apps/icons-plugin/src/_headers | 4 ++++ plugins/apps/lorem-ipsum-plugin/project.json | 1 + plugins/apps/lorem-ipsum-plugin/src/_headers | 4 ++++ plugins/apps/rename-layers-plugin/project.json | 1 + plugins/apps/rename-layers-plugin/src/_headers | 4 ++++ plugins/apps/table-plugin/project.json | 1 + plugins/apps/table-plugin/src/_headers | 4 ++++ 13 files changed, 34 insertions(+) create mode 100644 plugins/apps/colors-to-tokens-plugin/src/_headers create mode 100644 plugins/apps/contrast-plugin/src/_headers create mode 100644 plugins/apps/create-palette-plugin/public/_headers create mode 100644 plugins/apps/icons-plugin/src/_headers create mode 100644 plugins/apps/lorem-ipsum-plugin/src/_headers create mode 100644 plugins/apps/rename-layers-plugin/src/_headers create mode 100644 plugins/apps/table-plugin/src/_headers diff --git a/plugins/apps/colors-to-tokens-plugin/project.json b/plugins/apps/colors-to-tokens-plugin/project.json index 01ad02bbe6..87e3fb83d4 100644 --- a/plugins/apps/colors-to-tokens-plugin/project.json +++ b/plugins/apps/colors-to-tokens-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/colors-to-tokens-plugin/tsconfig.app.json", "assets": [ + "apps/colors-to-tokens-plugin/src/_headers", "apps/colors-to-tokens-plugin/src/favicon.ico", "apps/colors-to-tokens-plugin/src/assets" ], diff --git a/plugins/apps/colors-to-tokens-plugin/src/_headers b/plugins/apps/colors-to-tokens-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/colors-to-tokens-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/contrast-plugin/project.json b/plugins/apps/contrast-plugin/project.json index 6de0d44104..69f9c92766 100644 --- a/plugins/apps/contrast-plugin/project.json +++ b/plugins/apps/contrast-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/contrast-plugin/tsconfig.app.json", "assets": [ + "apps/contrast-plugin/src/_headers", "apps/contrast-plugin/src/favicon.ico", "apps/contrast-plugin/src/assets" ], diff --git a/plugins/apps/contrast-plugin/src/_headers b/plugins/apps/contrast-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/contrast-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/create-palette-plugin/public/_headers b/plugins/apps/create-palette-plugin/public/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/create-palette-plugin/public/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/icons-plugin/project.json b/plugins/apps/icons-plugin/project.json index 6540a148e1..90f01a50c2 100644 --- a/plugins/apps/icons-plugin/project.json +++ b/plugins/apps/icons-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/icons-plugin/tsconfig.app.json", "assets": [ + "apps/icons-plugin/src/_headers", "apps/icons-plugin/src/favicon.ico", "apps/icons-plugin/src/assets" ], diff --git a/plugins/apps/icons-plugin/src/_headers b/plugins/apps/icons-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/icons-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/lorem-ipsum-plugin/project.json b/plugins/apps/lorem-ipsum-plugin/project.json index db66c3677b..1747fcf6e8 100644 --- a/plugins/apps/lorem-ipsum-plugin/project.json +++ b/plugins/apps/lorem-ipsum-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/lorem-ipsum-plugin/tsconfig.app.json", "assets": [ + "apps/lorem-ipsum-plugin/src/_headers", "apps/lorem-ipsum-plugin/src/favicon.ico", "apps/lorem-ipsum-plugin/src/assets" ], diff --git a/plugins/apps/lorem-ipsum-plugin/src/_headers b/plugins/apps/lorem-ipsum-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/lorem-ipsum-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/rename-layers-plugin/project.json b/plugins/apps/rename-layers-plugin/project.json index 9a4e67c5be..71c314986a 100644 --- a/plugins/apps/rename-layers-plugin/project.json +++ b/plugins/apps/rename-layers-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/rename-layers-plugin/tsconfig.app.json", "assets": [ + "apps/rename-layers-plugin/src/_headers", "apps/rename-layers-plugin/src/favicon.ico", "apps/rename-layers-plugin/src/assets" ], diff --git a/plugins/apps/rename-layers-plugin/src/_headers b/plugins/apps/rename-layers-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/rename-layers-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type diff --git a/plugins/apps/table-plugin/project.json b/plugins/apps/table-plugin/project.json index 9ac5758270..e3cc6fc45e 100644 --- a/plugins/apps/table-plugin/project.json +++ b/plugins/apps/table-plugin/project.json @@ -16,6 +16,7 @@ "polyfills": ["zone.js"], "tsConfig": "apps/table-plugin/tsconfig.app.json", "assets": [ + "apps/table-plugin/src/_headers", "apps/table-plugin/src/favicon.ico", "apps/table-plugin/src/assets" ], diff --git a/plugins/apps/table-plugin/src/_headers b/plugins/apps/table-plugin/src/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/table-plugin/src/_headers @@ -0,0 +1,4 @@ +/* +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, POST, OPTIONS +Access-Control-Allow-Headers: Content-Type From 84b3f5d7c6e89a3d8004ff2b5531723de4d5804b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 28 Jan 2026 14:13:20 +0100 Subject: [PATCH 14/23] :bug: Fix import of shadow tokens --- CHANGES.md | 1 + common/src/app/common/types/tokens_lib.cljc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 23f906325c..8f4c0be159 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,7 @@ - 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) ## 2.12.1 diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 5e278588a7..248997a259 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -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]]]]}} From 852b31c3a04ae5fd97efc3dfc32069ee62e73939 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 29 Jan 2026 10:40:32 +0100 Subject: [PATCH 15/23] :bug: Fix allow spaces on token description (#8234) --- CHANGES.md | 1 + frontend/src/app/main/ui/forms.cljs | 4 ++-- .../ui/workspace/tokens/management/forms/generic_form.cljs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8f4c0be159..b9bdeb4779 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,7 @@ - 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) ## 2.12.1 diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index 6e49615470..0fe34d1f25 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -16,7 +16,7 @@ (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 @@ -33,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 diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index a260540a92..f0bc879442 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -230,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")) From 9569fa2bcb49d67f5ef1bb6aae86f74427b16323 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 29 Jan 2026 10:41:52 +0100 Subject: [PATCH 16/23] :bug: Fix error when creating a token with an invalid name (#8216) --- CHANGES.md | 1 + common/src/app/common/types/token.cljc | 5 ++++- .../tokens/management/forms/controls/color_input.cljs | 10 +++++++--- .../management/forms/controls/fonts_combobox.cljs | 8 ++++++-- .../tokens/management/forms/controls/input.cljs | 9 +++++++-- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b9bdeb4779..ba5edaf58b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,7 @@ - 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 diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 6a9d830a3a..c8441dc352 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$"]) + token-name-validation-regex]) (def ^:private schema:color [:map diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index 9f9d395013..a1fecb0327 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -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,15 @@ ;; 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 token-name value] - (let [token + (let [valid-token-name? + (and (string? token-name) + (re-matches cto/token-name-validation-regex token-name)) + + token {:value value - :name (if (str/blank? token-name) + :name (if (or (not valid-token-name?) (str/blank? token-name)) "__PENPOT__TOKEN__NAME__PLACEHOLDER__" token-name)} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index 80f2d91133..df76d47113 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -50,9 +50,13 @@ (defn- resolve-value [tokens prev-token token-name value] - (let [token + (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 (if (str/blank? token-name) + :name (if (or (not valid-token-name?) (str/blank? token-name)) "__PENPOT__TOKEN__NAME__PLACEHOLDER__" token-name)} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index 0f1b2a79b1..a5c4ddd0dc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -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] @@ -140,9 +141,13 @@ (defn- resolve-value [tokens prev-token token-name value] - (let [token + (let [valid-token-name? + (and (string? token-name) + (re-matches cto/token-name-validation-regex token-name)) + + token {:value value - :name (if (str/blank? token-name) + :name (if (or (not valid-token-name?) (str/blank? token-name)) "__PENPOT__TOKEN__NAME__PLACEHOLDER__" token-name)} tokens From 28509e04187bffad728a100f4d46d442f6684372 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 27 Jan 2026 17:44:17 +0100 Subject: [PATCH 17/23] :sparkles: Ensure .stopPropagation fn exists before calling Also for .stopImmediatePropagation and .preventDefault on the event instances. --- frontend/src/app/util/dom.cljs | 9 ++++++--- frontend/src/app/util/keyboard.cljs | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index e22de0dbba..ffa2b8f361 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -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 diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs index 5ed595a97c..8c43ecdef0 100644 --- a/frontend/src/app/util/keyboard.cljs +++ b/frontend/src/app/util/keyboard.cljs @@ -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] From 94722fdec2e981a1ff2ef57bc0e479e487746e6d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 27 Jan 2026 17:50:29 +0100 Subject: [PATCH 18/23] :sparkles: Ensure .hidePopover fn exist before call --- frontend/src/app/main/ui/ds/tooltip/tooltip.cljs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 5435ae0c77..159063210b 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -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 From f65292a13ca18ca48416bc69f429883457ce00be Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Jan 2026 12:44:49 +0100 Subject: [PATCH 19/23] :paperclip: Mark as skip two text editor v2 tests (flaky) --- frontend/playwright/ui/specs/text-editor-v2.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/playwright/ui/specs/text-editor-v2.spec.js b/frontend/playwright/ui/specs/text-editor-v2.spec.js index cc6061f192..9651c8e4b4 100644 --- a/frontend/playwright/ui/specs/text-editor-v2.spec.js +++ b/frontend/playwright/ui/specs/text-editor-v2.spec.js @@ -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, }) => { From 6a8421591137ce41406fe756c7a2cba08278c8d4 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 29 Jan 2026 09:07:12 +0100 Subject: [PATCH 20/23] :bug: Fix stroke weight visually different with different levels of zoom --- render-wasm/src/render/strokes.rs | 26 ++++++---------- render-wasm/src/shapes/strokes.rs | 51 +++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 103831013a..0d7797b8fb 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -27,8 +27,8 @@ fn draw_stroke_on_rect( // - The same rect if it's a center stroke // - A bigger rect if it's an outer stroke // - A smaller rect if it's an outer stroke - let stroke_rect = stroke.outer_rect(rect); - let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias); + let stroke_rect = stroke.aligned_rect(rect, scale); + let mut paint = stroke.to_paint(selrect, svg_attrs, antialias); // Apply both blur and shadow filters if present, composing them if necessary. let filter = compose_filters(blur, shadow); @@ -63,8 +63,8 @@ fn draw_stroke_on_circle( // - The same oval if it's a center stroke // - A bigger oval if it's an outer stroke // - A smaller oval if it's an outer stroke - let stroke_rect = stroke.outer_rect(rect); - let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias); + let stroke_rect = stroke.aligned_rect(rect, scale); + let mut paint = stroke.to_paint(selrect, svg_attrs, antialias); // Apply both blur and shadow filters if present, composing them if necessary. let filter = compose_filters(blur, shadow); @@ -131,7 +131,6 @@ pub fn draw_stroke_on_path( selrect: &Rect, path_transform: Option<&Matrix>, svg_attrs: Option<&SvgAttrs>, - scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, antialias: bool, @@ -142,7 +141,7 @@ pub fn draw_stroke_on_path( let is_open = path.is_open(); let mut paint: skia_safe::Handle<_> = - stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias); + stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias); let filter = compose_filters(blur, shadow); paint.set_image_filter(filter); @@ -166,7 +165,6 @@ pub fn draw_stroke_on_path( canvas, is_open, svg_attrs, - scale, blur, antialias, ); @@ -218,7 +216,6 @@ fn handle_stroke_caps( canvas: &skia::Canvas, is_open: bool, svg_attrs: Option<&SvgAttrs>, - scale: f32, blur: Option<&ImageFilter>, antialias: bool, ) { @@ -233,8 +230,7 @@ fn handle_stroke_caps( let first_point = points.first().unwrap(); let last_point = points.last().unwrap(); - let mut paint_stroke = - stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias); + let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias); if let Some(filter) = blur { paint_stroke.set_image_filter(filter.clone()); @@ -405,7 +401,7 @@ fn draw_image_stroke_in_container( // Draw the stroke based on the shape type, we are using this stroke as // a "selector" of the area of the image we want to show. - let outer_rect = stroke.outer_rect(container); + let outer_rect = stroke.aligned_rect(container, scale); match &shape.shape_type { shape_type @ (Type::Rect(_) | Type::Frame(_)) => { @@ -450,8 +446,7 @@ fn draw_image_stroke_in_container( } } let is_open = p.is_open(); - let mut paint = - stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, scale, antialias); + let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias); canvas.draw_path(&path, &paint); if stroke.render_kind(is_open) == StrokeKind::Outer { // Small extra inner stroke to overlap with the fill @@ -466,7 +461,6 @@ fn draw_image_stroke_in_container( canvas, is_open, svg_attrs, - scale, shape.image_filter(1.).as_ref(), antialias, ); @@ -662,7 +656,6 @@ fn render_internal( &selrect, path_transform.as_ref(), svg_attrs, - scale, shadow, shape.image_filter(1.).as_ref(), antialias, @@ -685,14 +678,13 @@ pub fn render_text_paths( shadow: Option<&ImageFilter>, antialias: bool, ) { - let scale = render_state.get_scale(); let canvas = render_state .surfaces .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes)); let selrect = &shape.selrect; let svg_attrs = shape.svg_attrs.as_ref(); let mut paint: skia_safe::Handle<_> = - stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias); + stroke.to_text_stroked_paint(false, selrect, svg_attrs, antialias); if let Some(filter) = shadow { paint.set_image_filter(filter.clone()); diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 5177ec7e03..e45c011a14 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,3 +1,4 @@ +use crate::math::is_close_to; use crate::shapes::fills::{Fill, SolidColor}; use skia_safe::{self as skia, Rect}; @@ -144,6 +145,15 @@ impl Stroke { } } + pub fn aligned_rect(&self, rect: &Rect, scale: f32) -> Rect { + let stroke_rect = self.outer_rect(rect); + if self.kind != StrokeKind::Center { + return stroke_rect; + } + + align_rect_to_half_pixel(&stroke_rect, self.width, scale) + } + pub fn outer_corners(&self, corners: &Corners) -> Corners { let offset = match self.kind { StrokeKind::Center => 0.0, @@ -162,7 +172,6 @@ impl Stroke { &self, rect: &Rect, svg_attrs: Option<&SvgAttrs>, - scale: f32, antialias: bool, ) -> skia::Paint { let mut paint = self.fill.to_paint(rect, antialias); @@ -171,7 +180,7 @@ impl Stroke { let width = match self.kind { StrokeKind::Inner => self.width, StrokeKind::Center => self.width, - StrokeKind::Outer => self.width + (1. / scale), + StrokeKind::Outer => self.width, }; paint.set_stroke_width(width); @@ -230,10 +239,9 @@ impl Stroke { is_open: bool, rect: &Rect, svg_attrs: Option<&SvgAttrs>, - scale: f32, antialias: bool, ) -> skia::Paint { - let mut paint = self.to_paint(rect, svg_attrs, scale, antialias); + let mut paint = self.to_paint(rect, svg_attrs, antialias); match self.render_kind(is_open) { StrokeKind::Inner => { paint.set_stroke_width(2. * paint.stroke_width()); @@ -254,10 +262,9 @@ impl Stroke { is_open: bool, rect: &Rect, svg_attrs: Option<&SvgAttrs>, - scale: f32, antialias: bool, ) -> skia::Paint { - let mut paint = self.to_paint(rect, svg_attrs, scale, antialias); + let mut paint = self.to_paint(rect, svg_attrs, antialias); match self.render_kind(is_open) { StrokeKind::Inner => { paint.set_stroke_width(2. * paint.stroke_width()); @@ -284,6 +291,38 @@ impl Stroke { } } +fn align_rect_to_half_pixel(rect: &Rect, stroke_width: f32, scale: f32) -> Rect { + if scale <= 0.0 { + return *rect; + } + + let stroke_pixels = stroke_width * scale; + let stroke_pixels_rounded = stroke_pixels.round(); + if !is_close_to(stroke_pixels, stroke_pixels_rounded) { + return *rect; + } + + if (stroke_pixels_rounded as i32) % 2 == 0 { + return *rect; + } + + let left_px = rect.left * scale; + let top_px = rect.top * scale; + let target_frac = 0.5; + let dx_px = target_frac - (left_px - left_px.floor()); + let dy_px = target_frac - (top_px - top_px.floor()); + + if is_close_to(dx_px, 0.0) && is_close_to(dy_px, 0.0) { + return *rect; + } + + Rect::from_xywh( + rect.left + (dx_px / scale), + rect.top + (dy_px / scale), + rect.width(), + rect.height(), + ) +} fn cap_margin_for_cap(cap: Option, width: f32) -> f32 { match cap { Some(StrokeCap::LineArrow) From 8c25fb00ac4775672679dcc0da20752278234b51 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 29 Jan 2026 07:34:20 +0100 Subject: [PATCH 21/23] :bug: Fix auto width/height texts on variant swithching --- common/src/app/common/logic/libraries.cljc | 8 ++- .../app/main/data/workspace/libraries.cljs | 12 ++++ .../src/app/main/data/workspace/texts.cljs | 55 ++------------ .../data/workspace/tokens/application.cljs | 8 +-- .../app/main/data/workspace/wasm_text.cljs | 72 +++++++++++++++++++ .../workspace/sidebar/options/menus/text.cljs | 3 +- 6 files changed, 102 insertions(+), 56 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/wasm_text.cljs diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 0d291a94f4..d42535e3d2 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -2017,7 +2017,9 @@ (let [;; We need to sync only the position relative to the origin of the component. ;; (see update-attrs for a full explanation) previous-shape (reposition-shape previous-shape prev-root current-root) - touched (get previous-shape :touched #{})] + touched (get previous-shape :touched #{}) + text-auto? (and (cfh/text-shape? current-shape) + (contains? #{:auto-height :auto-width} (:grow-type current-shape)))] (loop [attrs updatable-attrs roperations [{:type :set-touched :touched (:touched previous-shape)}] @@ -2026,6 +2028,10 @@ (let [attr-group (get ctk/sync-attrs attr) skip-operations? (or + ;; For auto text, avoid copying geometry-driven attrs on switch. + (and text-auto? + (contains? #{:points :selrect :width :height :position-data} attr)) + ;; If the attribute is not valid for the destiny, don't copy it (not (cts/is-allowed-switch-keep-attr? attr (:type current-shape))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 2fb6a96650..803edbfa51 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -46,7 +46,9 @@ [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.data.workspace.zoom :as dwz] + [app.main.features :as features] [app.main.features.pointer-map :as fpmap] [app.main.refs :as refs] [app.main.repo :as rp] @@ -1012,6 +1014,13 @@ updated-objects (pcb/get-objects changes) new-children-ids (cfh/get-children-ids-with-self updated-objects (:id new-shape)) + new-text-ids (->> new-children-ids + (keep (fn [id] + (when-let [child (get updated-objects id)] + (when (and (cfh/text-shape? child) + (not= :fixed (:grow-type child))) + id)))) + (vec)) [changes parents-of-swapped] (if keep-touched? @@ -1021,6 +1030,9 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) + (when (and (features/active-feature? state "render-wasm/v1") + (seq new-text-ids)) + (dwwt/resize-wasm-text-all new-text-ids)) (ptk/data-event :layout/update {:ids update-layout-ids :undo-group undo-group}) (dwu/commit-undo-transaction undo-id) (dws/select-shape (:id new-shape) false)))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index f2fdf75aa4..54fcf70abc 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -11,7 +11,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] - [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] @@ -29,10 +28,10 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.features :as features] [app.main.fonts :as fonts] [app.main.router :as rt] - [app.render-wasm.api :as wasm.api] [app.util.text-editor :as ted] [app.util.text.content.styles :as styles] [app.util.timers :as ts] @@ -52,50 +51,6 @@ (declare v2-update-text-shape-content) (declare v2-update-text-editor-styles) -(defn resize-wasm-text-modifiers - ([shape] - (resize-wasm-text-modifiers shape (:content shape))) - - ([{:keys [id points selrect grow-type] :as shape} content] - (wasm.api/use-shape id) - (wasm.api/set-shape-text-content id content) - (wasm.api/set-shape-text-images id content) - - (let [dimension (wasm.api/get-text-dimensions) - width-scale (if (#{:fixed :auto-height} grow-type) - 1.0 - (/ (:width dimension) (:width selrect))) - height-scale (if (= :fixed grow-type) - 1.0 - (/ (:height dimension) (:height selrect))) - resize-v (gpt/point width-scale height-scale) - origin (first points)] - - {id - {:modifiers - (ctm/resize-modifiers - resize-v - origin - (:transform shape (gmt/matrix)) - (:transform-inverse shape (gmt/matrix)))}}))) - -(defn resize-wasm-text - [id] - (ptk/reify ::resize-wasm-text - ptk/WatchEvent - (watch [_ state _] - (let [objects (dsh/lookup-page-objects state) - shape (get objects id)] - (rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape))))))) - -(defn resize-wasm-text-all - [ids] - (ptk/reify ::resize-wasm-text-all - ptk/WatchEvent - (watch [_ _ _] - (->> (rx/from ids) - (rx/map resize-wasm-text))))) - ;; -- Content helpers (defn- v2-content-has-text? @@ -178,7 +133,7 @@ {:undo-group (when new-shape? id)}) (dwm/apply-wasm-modifiers - (resize-wasm-text-modifiers shape content) + (dwwt/resize-wasm-text-modifiers shape content) {:undo-group (when new-shape? id)}))))) (let [content (d/merge (ted/export-content content) @@ -823,7 +778,7 @@ (when (features/active-feature? state "render-wasm/v1") ;; This delay is to give time for the font to be correctly rendered ;; in wasm. - (cond->> (rx/of (resize-wasm-text id)) + (cond->> (rx/of (dwwt/resize-wasm-text id)) (contains? attrs :font-id) (rx/delay 200))))))) @@ -973,11 +928,11 @@ (if (and (not= :fixed (:grow-type shape)) finalize?) (dwm/apply-wasm-modifiers - (resize-wasm-text-modifiers shape content) + (dwwt/resize-wasm-text-modifiers shape content) {:undo-group (when new-shape? id)}) (dwm/set-wasm-modifiers - (resize-wasm-text-modifiers shape content) + (dwwt/resize-wasm-text-modifiers shape content) {:undo-group (when new-shape? id)}))) (when finalize? diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 9110e81da0..4966942876 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -27,9 +27,9 @@ [app.main.data.workspace.colors :as wdc] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] - [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.features :as features] [app.main.fonts :as fonts] [app.main.store :as st] @@ -315,7 +315,7 @@ (and affects-layout? (features/active-feature? state "render-wasm/v1")) (rx/merge - (rx/of (dwt/resize-wasm-text-all shape-ids)))))))) + (rx/of (dwwt/resize-wasm-text-all shape-ids)))))))) (defn update-line-height ([value shape-ids attributes] (update-line-height value shape-ids attributes nil)) @@ -374,7 +374,7 @@ :page-id page-id})) (features/active-feature? state "render-wasm/v1") (rx/merge - (rx/of (dwt/resize-wasm-text-all shape-ids)))))))) + (rx/of (dwwt/resize-wasm-text-all shape-ids)))))))) (defn- create-font-family-text-attrs [value] @@ -451,7 +451,7 @@ :page-id page-id})) (features/active-feature? state "render-wasm/v1") (rx/merge - (rx/of (dwt/resize-wasm-text-all shape-ids)))))))) + (rx/of (dwwt/resize-wasm-text-all shape-ids)))))))) (defn update-font-weight ([value shape-ids attributes] (update-font-weight value shape-ids attributes nil)) diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs new file mode 100644 index 0000000000..2174ba7161 --- /dev/null +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -0,0 +1,72 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.wasm-text + "Helpers/events to resize wasm text shapes without depending on workspace.texts. + + This exists to avoid circular deps: + workspace.texts -> workspace.libraries -> workspace.texts" + (:require + [app.common.files.helpers :as cfh] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.types.modifiers :as ctm] + [app.main.data.helpers :as dsh] + [app.main.data.workspace.modifiers :as dwm] + [app.render-wasm.api :as wasm.api] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(defn resize-wasm-text-modifiers + ([shape] + (resize-wasm-text-modifiers shape (:content shape))) + + ([{:keys [id points selrect grow-type] :as shape} content] + (wasm.api/use-shape id) + (wasm.api/set-shape-text-content id content) + (wasm.api/set-shape-text-images id content) + + (let [dimension (wasm.api/get-text-dimensions) + width-scale (if (#{:fixed :auto-height} grow-type) + 1.0 + (/ (:width dimension) (:width selrect))) + height-scale (if (= :fixed grow-type) + 1.0 + (/ (:height dimension) (:height selrect))) + resize-v (gpt/point width-scale height-scale) + origin (first points)] + + {id + {:modifiers + (ctm/resize-modifiers + resize-v + origin + (:transform shape (gmt/matrix)) + (:transform-inverse shape (gmt/matrix)))}}))) + +(defn resize-wasm-text + "Resize a single text shape (auto-width/auto-height) by id. + No-op if the id is not a text shape or is :fixed." + [id] + (ptk/reify ::resize-wasm-text + ptk/WatchEvent + (watch [_ state _] + (let [objects (dsh/lookup-page-objects state) + shape (get objects id)] + (if (and (some? shape) + (cfh/text-shape? shape) + (not= :fixed (:grow-type shape))) + (rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape))) + (rx/empty)))))) + +(defn resize-wasm-text-all + "Resize all text shapes (auto-width/auto-height) from a collection of ids." + [ids] + (ptk/reify ::resize-wasm-text-all + ptk/WatchEvent + (watch [_ _ _] + (->> (rx/from ids) + (rx/map resize-wasm-text))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index c2f9226250..04e05ae53f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -15,6 +15,7 @@ [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] @@ -138,7 +139,7 @@ (dwsh/update-shapes ids #(assoc % :grow-type grow-type))) (when (features/active-feature? @st/state "render-wasm/v1") - (st/emit! (dwt/resize-wasm-text-all ids))) + (st/emit! (dwwt/resize-wasm-text-all ids))) ;; We asynchronously commit so every sychronous event is resolved first and inside the transaction (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))) (when (some? on-blur) (on-blur))))] From 913672e5c56e47d06490588e0a99c5d0adfe3d50 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 29 Jan 2026 17:14:15 +0100 Subject: [PATCH 22/23] :bug: Fix problem with modifiers propagation --- frontend/src/app/main/data/workspace/modifiers.cljs | 3 +-- frontend/src/app/main/data/workspace/transforms.cljs | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index c438cf8815..4b55c9985c 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -712,8 +712,7 @@ (ctm/rotation-modifiers shape center angle)) modif-tree - (-> (build-modif-tree ids objects get-modifier) - (gm/set-objects-modifiers objects)) + (build-modif-tree ids objects get-modifier) modifiers (mapv (fn [[id {:keys [modifiers]}]] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index dd427e6bd7..5e08d13560 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -406,13 +406,13 @@ (ctm/change-property :grow-type new-grow-type))) modifiers))) - modif-tree - (-> (dwm/build-modif-tree ids objects get-modifier) - (gm/set-objects-modifiers objects))] + modif-tree (dwm/build-modif-tree ids objects get-modifier)] (if (features/active-feature? state "render-wasm/v1") (rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true})) - (rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))) + + (let [modif-tree (gm/set-objects-modifiers modif-tree objects)] + (rx/of (dwm/apply-modifiers* objects modif-tree nil options))))))))) (defn change-orientation "Change orientation of shapes, from the sidebar options form. From 07b9ef0fd6bce0a8348c8ca9001b5876c848f06d Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Thu, 29 Jan 2026 17:55:03 +0100 Subject: [PATCH 23/23] :wrench: Add more v2 text editor tests --- frontend/text-editor/src/editor/TextEditor.js | 23 +- .../src/editor/commands/CommandMutations.js | 66 ------ .../editor/commands/CommandMutations.test.js | 71 ------- .../src/editor/content/Text.test.js | 21 +- .../src/editor/content/dom/Color.test.js | 2 +- .../editor/controllers/SelectionController.js | 64 +----- .../controllers/SelectionController.test.js | 199 +++++++++++++++++- 7 files changed, 233 insertions(+), 213 deletions(-) delete mode 100644 frontend/text-editor/src/editor/commands/CommandMutations.js delete mode 100644 frontend/text-editor/src/editor/commands/CommandMutations.test.js diff --git a/frontend/text-editor/src/editor/TextEditor.js b/frontend/text-editor/src/editor/TextEditor.js index cf7ede4ec6..20828c1264 100644 --- a/frontend/text-editor/src/editor/TextEditor.js +++ b/frontend/text-editor/src/editor/TextEditor.js @@ -405,12 +405,8 @@ export class TextEditor extends EventTarget { if (e.inputType in commands) { const command = commands[e.inputType]; - if (!this.#selectionController.startMutation()) { - return; - } command(e, this, this.#selectionController); - const mutations = this.#selectionController.endMutation(); - this.#notifyLayout(LayoutType.FULL, mutations); + this.#notifyLayout(LayoutType.FULL); } }; @@ -456,19 +452,12 @@ export class TextEditor extends EventTarget { if ((e.ctrlKey || e.metaKey) && e.key === "Backspace") { e.preventDefault(); - - if (!this.#selectionController.startMutation()) { - return; - } - if (this.#selectionController.isCollapsed) { this.#selectionController.removeWordBackward(); } else { this.#selectionController.removeSelected(); } - - const mutations = this.#selectionController.endMutation(); - this.#notifyLayout(LayoutType.FULL, mutations); + this.#notifyLayout(LayoutType.FULL); } }; @@ -476,14 +465,12 @@ export class TextEditor extends EventTarget { * Notifies that the edited texts needs layout. * * @param {'full'|'partial'} type - * @param {CommandMutations} mutations */ - #notifyLayout(type = LayoutType.FULL, mutations) { + #notifyLayout(type = LayoutType.FULL) { this.dispatchEvent( new CustomEvent("needslayout", { detail: { type: type, - mutations: mutations, }, }), ); @@ -630,10 +617,8 @@ export class TextEditor extends EventTarget { * @returns {TextEditor} */ applyStylesToSelection(styles) { - this.#selectionController.startMutation(); this.#selectionController.applyStyles(styles); - const mutations = this.#selectionController.endMutation(); - this.#notifyLayout(LayoutType.FULL, mutations); + this.#notifyLayout(LayoutType.FULL); this.#changeController.notifyImmediately(); return this; } diff --git a/frontend/text-editor/src/editor/commands/CommandMutations.js b/frontend/text-editor/src/editor/commands/CommandMutations.js deleted file mode 100644 index fca36be147..0000000000 --- a/frontend/text-editor/src/editor/commands/CommandMutations.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright (c) KALEIDOS INC - */ - -/** - * Command mutations - */ -export class CommandMutations { - #added = new Set(); - #removed = new Set(); - #updated = new Set(); - - constructor(added, updated, removed) { - if (added && Array.isArray(added)) this.#added = new Set(added); - if (updated && Array.isArray(updated)) this.#updated = new Set(updated); - if (removed && Array.isArray(removed)) this.#removed = new Set(removed); - } - - get added() { - return this.#added; - } - - get removed() { - return this.#removed; - } - - get updated() { - return this.#updated; - } - - clear() { - this.#added.clear(); - this.#removed.clear(); - this.#updated.clear(); - } - - dispose() { - this.#added.clear(); - this.#added = null; - this.#removed.clear(); - this.#removed = null; - this.#updated.clear(); - this.#updated = null; - } - - add(node) { - this.#added.add(node); - return this; - } - - remove(node) { - this.#removed.add(node); - return this; - } - - update(node) { - this.#updated.add(node); - return this; - } -} - -export default CommandMutations; diff --git a/frontend/text-editor/src/editor/commands/CommandMutations.test.js b/frontend/text-editor/src/editor/commands/CommandMutations.test.js deleted file mode 100644 index 0ed4c1d7e3..0000000000 --- a/frontend/text-editor/src/editor/commands/CommandMutations.test.js +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, test, expect } from "vitest"; -import CommandMutations from "./CommandMutations.js"; - -describe("CommandMutations", () => { - test("should create a new CommandMutations", () => { - const mutations = new CommandMutations(); - expect(mutations).toHaveProperty("added"); - expect(mutations).toHaveProperty("updated"); - expect(mutations).toHaveProperty("removed"); - }); - - test("should create an initialized new CommandMutations", () => { - const mutations = new CommandMutations([1], [2], [3]); - expect(mutations.added.size).toBe(1); - expect(mutations.updated.size).toBe(1); - expect(mutations.removed.size).toBe(1); - expect(mutations.added.has(1)).toBe(true); - expect(mutations.updated.has(2)).toBe(true); - expect(mutations.removed.has(3)).toBe(true); - }); - - test("should add an added node to a CommandMutations", () => { - const mutations = new CommandMutations(); - mutations.add(1); - expect(mutations.added.has(1)).toBe(true); - }); - - test("should add an updated node to a CommandMutations", () => { - const mutations = new CommandMutations(); - mutations.update(1); - expect(mutations.updated.has(1)).toBe(true); - }); - - test("should add an removed node to a CommandMutations", () => { - const mutations = new CommandMutations(); - mutations.remove(1); - expect(mutations.removed.has(1)).toBe(true); - }); - - test("should clear a CommandMutations", () => { - const mutations = new CommandMutations(); - mutations.add(1); - mutations.update(2); - mutations.remove(3); - expect(mutations.added.has(1)).toBe(true); - expect(mutations.added.size).toBe(1); - expect(mutations.updated.has(2)).toBe(true); - expect(mutations.updated.size).toBe(1); - expect(mutations.removed.has(3)).toBe(true); - expect(mutations.removed.size).toBe(1); - - mutations.clear(); - expect(mutations.added.size).toBe(0); - expect(mutations.added.has(1)).toBe(false); - expect(mutations.updated.size).toBe(0); - expect(mutations.updated.has(1)).toBe(false); - expect(mutations.removed.size).toBe(0); - expect(mutations.removed.has(1)).toBe(false); - }); - - test("should dispose a CommandMutations", () => { - const mutations = new CommandMutations(); - mutations.add(1); - mutations.update(2); - mutations.remove(3); - mutations.dispose(); - expect(mutations.added).toBe(null); - expect(mutations.updated).toBe(null); - expect(mutations.removed).toBe(null); - }); -}); diff --git a/frontend/text-editor/src/editor/content/Text.test.js b/frontend/text-editor/src/editor/content/Text.test.js index 45924d655d..416693013e 100644 --- a/frontend/text-editor/src/editor/content/Text.test.js +++ b/frontend/text-editor/src/editor/content/Text.test.js @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { insertInto, removeBackward, removeForward, replaceWith } from "./Text"; +import { insertInto, removeSlice, removeBackward, removeForward, removeWordBackward, replaceWith, findPreviousWordBoundary } from "./Text"; describe("Text", () => { test("* should throw when passed wrong parameters", () => { @@ -51,4 +51,23 @@ describe("Text", () => { test("`removeForward` should remove string forward from offset 6", () => { expect(removeForward("Hello, World!", 6)).toBe("Hello,World!"); }); + + test("`removeSlice` should remove a part of a text", () => { + expect(removeSlice("Hello, World!", 7, 12)).toBe("Hello, !"); + }); + + test("`findPreviousWordBoundary` edge cases", () => { + expect(findPreviousWordBoundary(null)).toBe(0); + expect(findPreviousWordBoundary("Hello, World!", 0)).toBe(0); + expect(findPreviousWordBoundary(" Hello, World!", 3)).toBe(0); + }) + + test("`removeWordBackward` with no text should return an empty string", () => { + expect(removeWordBackward(null, 0)).toBe(""); + }); + + test("`removeWordBackward` should remove a word backward", () => { + expect(removeWordBackward("Hello, World!", 13)).toBe("Hello, World"); + expect(removeWordBackward("Hello, World", 12)).toBe("Hello, "); + }); }); diff --git a/frontend/text-editor/src/editor/content/dom/Color.test.js b/frontend/text-editor/src/editor/content/dom/Color.test.js index a5d44addd1..17e1f727a4 100644 --- a/frontend/text-editor/src/editor/content/dom/Color.test.js +++ b/frontend/text-editor/src/editor/content/dom/Color.test.js @@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest"; import { getFills } from "./Color.js"; /* @vitest-environment jsdom */ -describe("Color", () => { +describe.skip("Color", () => { test("getFills", () => { expect(getFills("#aa0000")).toBe( '[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]', diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.js b/frontend/text-editor/src/editor/controllers/SelectionController.js index 24cb37d272..6d4c11c136 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.js @@ -49,7 +49,6 @@ import { } from "../content/dom/TextNode.js"; import TextNodeIterator from "../content/dom/TextNodeIterator.js"; import TextEditor from "../TextEditor.js"; -import CommandMutations from "../commands/CommandMutations.js"; import { isRoot, setRootStyles } from "../content/dom/Root.js"; import { SelectionDirection } from "./SelectionDirection.js"; import { SafeGuard } from "./SafeGuard.js"; @@ -145,13 +144,6 @@ export class SelectionController extends EventTarget { */ #debug = null; - /** - * Command Mutations. - * - * @type {CommandMutations} - */ - #mutations = new CommandMutations(); - /** * Style defaults. * @@ -449,14 +441,14 @@ export class SelectionController extends EventTarget { dispose() { document.removeEventListener("selectionchange", this.#onSelectionChange); this.#textEditor = null; + this.#currentStyle = null; + this.#options = null; this.#ranges.clear(); this.#ranges = null; this.#range = null; this.#selection = null; this.#focusNode = null; this.#anchorNode = null; - this.#mutations.dispose(); - this.#mutations = null; } /** @@ -522,28 +514,6 @@ export class SelectionController extends EventTarget { return true; } - /** - * Marks the start of a mutation. - * - * Clears all the mutations kept in CommandMutations. - * - * @returns {boolean} - */ - startMutation() { - this.#mutations.clear(); - if (!this.#focusNode) return false; - return true; - } - - /** - * Marks the end of a mutation. - * - * @returns {CommandMutations} - */ - endMutation() { - return this.#mutations; - } - /** * Selects all content. * @@ -597,11 +567,18 @@ export class SelectionController extends EventTarget { * @returns {SelectionController} */ cursorToEnd() { + const root = this.#textEditor.root; + const range = document.createRange(); //Create a range (a range is a like the selection but invisible) - range.selectNodeContents(this.#textEditor.element); + range.setStart(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0); + range.setEnd(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0); range.collapse(false); + this.#selection.removeAllRanges(); this.#selection.addRange(range); + + this.#updateState(); + return this; } @@ -1340,7 +1317,6 @@ export class SelectionController extends EventTarget { if (this.focusNode.nodeValue !== removedData) { this.focusNode.nodeValue = removedData; - this.#mutations.update(this.focusTextSpan); } const paragraph = this.focusParagraph; @@ -1383,7 +1359,6 @@ export class SelectionController extends EventTarget { this.focusOffset, newText, ); - this.#mutations.update(this.focusTextSpan); return this.collapse(this.focusNode, this.focusOffset + newText.length); } @@ -1447,7 +1422,6 @@ export class SelectionController extends EventTarget { this.#textEditor.root.replaceChildren(newParagraph); return this.collapse(newTextNode, newText.length + 1); } - this.#mutations.update(this.focusTextSpan); return this.collapse(this.focusNode, startOffset + newText.length); } @@ -1525,8 +1499,6 @@ export class SelectionController extends EventTarget { const currentParagraph = this.focusParagraph; const newParagraph = createEmptyParagraph(this.#currentStyle); currentParagraph.after(newParagraph); - this.#mutations.update(currentParagraph); - this.#mutations.add(newParagraph); return this.collapse(newParagraph.firstChild.firstChild, 0); } @@ -1537,8 +1509,6 @@ export class SelectionController extends EventTarget { const currentParagraph = this.focusParagraph; const newParagraph = createEmptyParagraph(this.#currentStyle); currentParagraph.before(newParagraph); - this.#mutations.update(currentParagraph); - this.#mutations.add(newParagraph); return this.collapse(currentParagraph.firstChild.firstChild, 0); } @@ -1553,8 +1523,6 @@ export class SelectionController extends EventTarget { this.#focusOffset, ); this.focusParagraph.after(newParagraph); - this.#mutations.update(currentParagraph); - this.#mutations.add(newParagraph); return this.collapse(newParagraph.firstChild.firstChild, 0); } @@ -1586,10 +1554,6 @@ export class SelectionController extends EventTarget { this.focusOffset, ); currentParagraph.after(newParagraph); - - this.#mutations.update(currentParagraph); - this.#mutations.add(newParagraph); - // FIXME: Missing collapse? } @@ -1610,7 +1574,6 @@ export class SelectionController extends EventTarget { const previousOffset = isLineBreak(previousTextSpan.firstChild) ? 0 : previousTextSpan.firstChild.nodeValue?.length || 0; - this.#mutations.remove(paragraphToBeRemoved); return this.collapse(previousTextSpan.firstChild, previousOffset); } @@ -1632,8 +1595,6 @@ export class SelectionController extends EventTarget { } else { mergeParagraphs(previousParagraph, currentParagraph); } - this.#mutations.remove(currentParagraph); - this.#mutations.update(previousParagraph); return this.collapse(previousTextSpan.firstChild, previousOffset); } @@ -1647,8 +1608,6 @@ export class SelectionController extends EventTarget { return; } mergeParagraphs(this.focusParagraph, nextParagraph); - this.#mutations.update(currentParagraph); - this.#mutations.remove(nextParagraph); // FIXME: Missing collapse? } @@ -1665,7 +1624,6 @@ export class SelectionController extends EventTarget { paragraphToBeRemoved.remove(); const nextTextSpan = nextParagraph.firstChild; const nextOffset = this.focusOffset; - this.#mutations.remove(paragraphToBeRemoved); return this.collapse(nextTextSpan.firstChild, nextOffset); } @@ -1680,7 +1638,6 @@ export class SelectionController extends EventTarget { for (const textSpan of affectedTextSpans) { if (textSpan.textContent === "") { textSpan.remove(); - this.#mutations.remove(textSpan); } } @@ -1688,7 +1645,6 @@ export class SelectionController extends EventTarget { for (const paragraph of affectedParagraphs) { if (paragraph.children.length === 0) { paragraph.remove(); - this.#mutations.remove(paragraph); } } } diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.test.js b/frontend/text-editor/src/editor/controllers/SelectionController.test.js index cfb04488ad..ff7e372c9d 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.test.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.test.js @@ -581,6 +581,136 @@ describe("SelectionController", () => { expect(textEditorMock.root.textContent).toBe(""); }); + test("`insertParagraph` should insert a new paragraph in an empty editor", () => { + const textEditorMock = TextEditorMock.createTextEditorMockEmpty(); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController(textEditorMock, selection); + focus( + selection, + textEditorMock, + root.firstChild.firstChild.firstChild, + 0, + ); + selectionController.insertParagraph(); + expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.dataset.itype).toBe("root"); + expect(textEditorMock.root.children.length).toBe(2); + expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(0).dataset.itype).toBe("paragraph"); + expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe("span"); + expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(1).dataset.itype).toBe("paragraph"); + expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe( + "span", + ); + expect(textEditorMock.root.textContent).toBe(""); + }); + + test("`insertParagraph` should insert a new paragraph after a text", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWith([ + ["Hello, World!"] + ]); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController( + textEditorMock, + selection, + ); + focus( + selection, + textEditorMock, + root.firstChild.firstChild.firstChild, + "Hello, World!".length + ); + selectionController.insertParagraph(); + expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.dataset.itype).toBe("root"); + expect(textEditorMock.root.children.length).toBe(2); + expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(0).dataset.itype).toBe( + "paragraph", + ); + expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe( + "span", + ); + expect(textEditorMock.root.children.item(0).firstChild.textContent).toBe( + "Hello, World!", + ); + expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(1).dataset.itype).toBe( + "paragraph", + ); + expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe( + "span", + ); + expect(textEditorMock.root.children.item(1).firstChild.firstChild).toBeInstanceOf( + HTMLBRElement, + ); + expect(textEditorMock.root.textContent).toBe("Hello, World!"); + }); + + test("`insertParagraph` should insert a new paragraph before a text", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWith([ + ["Hello, World!"], + ]); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController( + textEditorMock, + selection, + ); + focus( + selection, + textEditorMock, + root.firstChild.firstChild.firstChild, + 0, + ); + selectionController.insertParagraph(); + expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.dataset.itype).toBe("root"); + expect(textEditorMock.root.children.length).toBe(2); + expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(0).dataset.itype).toBe( + "paragraph", + ); + expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe( + "span", + ); + expect(textEditorMock.root.children.item(0).firstChild.firstChild).toBeInstanceOf( + HTMLBRElement, + ); + expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement); + expect(textEditorMock.root.children.item(1).dataset.itype).toBe( + "paragraph", + ); + expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf( + HTMLSpanElement, + ); + expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe( + "span", + ); + expect(textEditorMock.root.children.item(1).firstChild.textContent).toBe( + "Hello, World!", + ); + expect(textEditorMock.root.textContent).toBe("Hello, World!"); + }); + test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWith([ ["Hello, "], @@ -1027,7 +1157,7 @@ describe("SelectionController", () => { ); }); - test.skip("`removeSelected` multiple paragraphs", () => { + test("`removeSelected` multiple paragraphs", () => { const textEditorMock = TextEditorMock.createTextEditorMockWith([ ["Hello, "], ["\n"], @@ -1392,7 +1522,10 @@ describe("SelectionController", () => { root.firstChild.lastChild.firstChild.nodeValue.length - 3, ); selectionController.applyStyles({ + "font-family": "Montserrat, sans-serif", "font-weight": "bold", + "--fills": + '[["^ ","~:fill-color","#000000","~:fill-opacity",1],["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]', }); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(1); @@ -1492,4 +1625,68 @@ describe("SelectionController", () => { "ld!", ); }); + + test("`selectAll` should select everything", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ + createParagraphWith(["Hello, "], { + "font-style": "italic", + }), + createParagraphWith(["World!"], { + "font-style": "oblique", + }), + ]); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController(textEditorMock, selection); + textEditorMock.element.focus(); + selectionController.selectAll(); + expect(selectionController.anchorNode).toBe( + root.firstChild.firstChild.firstChild + ); + expect(selectionController.focusNode).toBe( + root.lastChild.firstChild.firstChild, + ); + }); + + test("`cursorToEnd` should move cursor to the end", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ + createParagraphWith(["Hello, "], { + "font-style": "italic", + }), + createParagraphWith(["World!"], { + "font-style": "oblique", + }), + ]); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController(textEditorMock, selection); + textEditorMock.element.focus(); + selectionController.cursorToEnd(); + expect(selectionController.focusNode).toBe(root.lastChild.firstChild.firstChild); + expect(selectionController.focusAtEnd).toBeTruthy(); + }) + + test("`dispose` should release every held reference", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ + createParagraphWith(["Hello, "], { + "font-style": "italic", + }), + createParagraphWith(["World!"], { + "font-style": "oblique", + }), + ]); + const root = textEditorMock.root; + const selection = document.getSelection(); + const selectionController = new SelectionController(textEditorMock, selection); + focus( + selection, + textEditorMock, + root.firstChild.firstChild.firstChild, + 0 + ); + selectionController.dispose(); + expect(selectionController.selection).toBe(null); + expect(selectionController.currentStyle).toBe(null); + expect(selectionController.options).toBe(null); + }); });