diff --git a/.github/workflows/tests-plugin-api-suite.yml b/.github/workflows/tests-plugin-api-suite.yml new file mode 100644 index 0000000000..e589c5414c --- /dev/null +++ b/.github/workflows/tests-plugin-api-suite.yml @@ -0,0 +1,133 @@ +name: "CI: Plugin API Test Suite" + +# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it +# needs a running frontend + the plugin runtime). Two jobs: +# +# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the +# prebuilt frontend bundle and intercepts every backend RPC with Playwright +# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API +# binding + in-memory store; backend-result-dependent tests are skipped via the +# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md. +# +# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE +# instance. Point PENPOT_BASE_URL at a reachable instance and provide login +# credentials via repo secrets. Manual because the CI runner has no Docker to +# stand up a full stack. + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + base_url: + description: "Penpot base URL (e.g. https://localhost:3449)" + required: false + default: "https://localhost:3449" + + pull_request: + paths: + - 'plugins/**' + - 'frontend/**' + - 'common/**' + types: + - opened + - synchronize + - ready_for_review + + push: + branches: + - develop + - staging + paths: + - 'plugins/**' + - 'frontend/src/app/plugins/**' + - 'common/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + api-test-suite-mocked: + if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }} + name: "Run Plugin API Test Suite (mocked)" + runs-on: penpot-runner-02 + container: + image: penpotapp/devenv:latest + volumes: + - /var/cache/github-runner/m2:/root/.m2 + - /var/cache/github-runner/gitlib:/root/.gitlibs + + steps: + - uses: actions/checkout@v6 + + # Mocked mode serves the prebuilt bundle from frontend/resources/public. + - name: Build frontend bundle + working-directory: ./frontend + run: ./scripts/build + + - name: Install deps + working-directory: ./plugins + run: | + corepack enable; + corepack install; + pnpm install; + + - name: Install Playwright Chromium + working-directory: ./plugins + run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium + + - name: Generate API surface + working-directory: ./plugins + run: pnpm --filter plugin-api-test-suite run gen:api + + - name: Run API test suite (mocked) + working-directory: ./plugins + env: + MOCK_BACKEND: "1" + run: pnpm --filter plugin-api-test-suite run test:ci + + ## The following job will launch the whole suite of tests but we need + ## to have a full environment in the CI for this to work. + + # api-test-suite-live: + # if: ${{ github.event_name == 'workflow_dispatch' }} + # name: Run Plugin API Test Suite (live) + # runs-on: penpot-runner-02 + # container: + # image: penpotapp/devenv:latest + # + # env: + # PENPOT_BASE_URL: ${{ github.event.inputs.base_url }} + # E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }} + # E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }} + # + # steps: + # - uses: actions/checkout@v6 + # + # - name: Setup Node + # uses: actions/setup-node@v6 + # with: + # node-version-file: .nvmrc + # + # - name: Install deps + # working-directory: ./plugins + # run: | + # corepack enable; + # corepack install; + # pnpm install; + # + # - name: Install Playwright Chromium + # working-directory: ./plugins + # run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium + # + # - name: Generate API surface + # working-directory: ./plugins + # run: pnpm --filter plugin-api-test-suite run gen:api + # + # # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL. + # - name: Run API test suite + # working-directory: ./plugins + # run: pnpm --filter plugin-api-test-suite run test:ci diff --git a/.github/workflows/tests-plugins.yml b/.github/workflows/tests-plugins.yml index 5cc161461c..1415c73db7 100644 --- a/.github/workflows/tests-plugins.yml +++ b/.github/workflows/tests-plugins.yml @@ -40,19 +40,13 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Setup Node - id: setup-node - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - - name: Install deps working-directory: ./plugins shell: bash run: | corepack enable; corepack install; - pnpm install; + pnpm install -r; - name: Run Lint working-directory: ./plugins diff --git a/backend/deps.edn b/backend/deps.edn index 87aac35b6f..c60adf10ca 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -6,7 +6,7 @@ org.clojure/clojure {:mvn/version "1.12.5"} org.clojure/tools.namespace {:mvn/version "1.5.1"} - com.github.luben/zstd-jni {:mvn/version "1.5.7-10"} + com.github.luben/zstd-jni {:mvn/version "1.5.7-11"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} @@ -39,7 +39,7 @@ metosin/reitit-core {:mvn/version "0.10.1"} nrepl/nrepl {:mvn/version "1.7.0"} - org.postgresql/postgresql {:mvn/version "42.7.11"} + org.postgresql/postgresql {:mvn/version "42.7.12"} org.xerial/sqlite-jdbc {:mvn/version "3.53.2.0"} com.zaxxer/HikariCP {:mvn/version "7.0.2"} @@ -63,14 +63,16 @@ ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.46.7"} - software.amazon.awssdk/sts {:mvn/version "2.46.7"}} + software.amazon.awssdk/s3 {:mvn/version "2.46.18"} + software.amazon.awssdk/sts {:mvn/version "2.46.18"}} :paths ["src" "resources" "target/classes"] :aliases {:dev - {:extra-deps - {com.bhauman/rebel-readline {:mvn/version "0.1.7"} + {:jvm-opts ["--sun-misc-unsafe-memory-access=allow" + "--enable-native-access=ALL-UNNAMED"] + :extra-deps + {com.bhauman/rebel-readline {:mvn/version "0.1.11"} clojure-humanize/clojure-humanize {:mvn/version "0.2.2"} org.clojure/data.csv {:mvn/version "1.1.1"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"} @@ -84,9 +86,7 @@ :test {:main-opts ["-m" "kaocha.runner"] - :jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml" - "--sun-misc-unsafe-memory-access=allow" - "--enable-native-access=ALL-UNNAMED"] + :jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"] :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :outdated diff --git a/backend/package.json b/backend/package.json index e6eb030f17..8c99fa923c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,19 +4,19 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" }, "dependencies": { "luxon": "^3.4.4", - "sax": "^1.4.1" + "sax": "^1.6.0" }, "devDependencies": { - "nodemon": "^3.1.2", + "nodemon": "^3.1.14", "source-map-support": "^0.5.21", - "ws": "^8.17.0" + "ws": "^8.21.0" }, "scripts": { "lint": "clj-kondo --parallel --lint ../common/src src/", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 8901ade20f..d789e2c2f8 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -12,18 +12,18 @@ importers: specifier: ^3.4.4 version: 3.7.2 sax: - specifier: ^1.4.1 - version: 1.4.3 + specifier: ^1.6.0 + version: 1.6.0 devDependencies: nodemon: - specifier: ^3.1.2 - version: 3.1.11 + specifier: ^3.1.14 + version: 3.1.14 source-map-support: specifier: ^0.5.21 version: 0.5.21 ws: - specifier: ^8.17.0 - version: 8.18.3 + specifier: ^8.21.0 + version: 8.21.0 packages: @@ -31,15 +31,17 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -52,9 +54,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -104,14 +103,15 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nodemon@3.1.11: - resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} engines: {node: '>=10'} hasBin: true @@ -119,8 +119,8 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} pstree.remy@1.1.8: @@ -130,11 +130,12 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - sax@1.4.3: - resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -164,8 +165,8 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -181,16 +182,15 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 - balanced-match@1.0.2: {} + balanced-match@4.0.4: {} binary-extensions@2.3.0: {} - brace-expansion@1.1.12: + brace-expansion@5.0.7: dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -210,8 +210,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - concat-map@0.0.1: {} - debug@4.4.3(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -247,20 +245,20 @@ snapshots: luxon@3.7.2: {} - minimatch@3.1.2: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 5.0.7 ms@2.1.3: {} - nodemon@3.1.11: + nodemon@3.1.14: dependencies: chokidar: 3.6.0 debug: 4.4.3(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 3.1.2 + minimatch: 10.2.5 pstree.remy: 1.1.8 - semver: 7.7.3 + semver: 7.8.5 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.1 @@ -268,21 +266,21 @@ snapshots: normalize-path@3.0.0: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} pstree.remy@1.1.8: {} readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 - sax@1.4.3: {} + sax@1.6.0: {} - semver@7.7.3: {} + semver@7.8.5: {} simple-update-notifier@2.0.0: dependencies: - semver: 7.7.3 + semver: 7.8.5 source-map-support@0.5.21: dependencies: @@ -303,4 +301,4 @@ snapshots: undefsafe@2.0.5: {} - ws@8.18.3: {} + ws@8.21.0: {} diff --git a/common/deps.edn b/common/deps.edn index 2f7b2a1b76..56714185b7 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -20,7 +20,13 @@ selmer/selmer {:mvn/version "1.13.4"} criterium/criterium {:mvn/version "0.4.6"} - metosin/jsonista {:mvn/version "1.0.0"} + metosin/jsonista {:mvn/version "1.0.0" + :exclusions [com.fasterxml.jackson.core/jackson-core + com.fasterxml.jackson.core/jackson-databind]} + com.fasterxml.jackson.core/jackson-core {:mvn/version "2.22.0"} + com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.0"} + + metosin/malli {:mvn/version "0.19.1"} expound/expound {:mvn/version "0.9.0"} @@ -58,7 +64,7 @@ {org.clojure/tools.namespace {:mvn/version "1.5.1"} thheller/shadow-cljs {:mvn/version "3.2.0"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"} - com.bhauman/rebel-readline {:mvn/version "0.1.5"} + com.bhauman/rebel-readline {:mvn/version "0.1.11"} criterium/criterium {:mvn/version "0.4.6"} mockery/mockery {:mvn/version "0.1.4"}} :extra-paths ["test" "dev"]} diff --git a/common/package.json b/common/package.json index a90c163aaf..7813de8e87 100644 --- a/common/package.json +++ b/common/package.json @@ -13,7 +13,7 @@ "devDependencies": { "concurrently": "^10.0.3", "nodemon": "^3.1.14", - "prettier": "3.8.4", + "prettier": "3.9.4", "source-map-support": "^0.5.21", "ws": "^8.21.0" }, diff --git a/common/pnpm-lock.yaml b/common/pnpm-lock.yaml index 10887c155b..6f6b1d494c 100644 --- a/common/pnpm-lock.yaml +++ b/common/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: ^3.1.14 version: 3.1.14 prettier: - specifier: 3.8.4 - version: 3.8.4 + specifier: 3.9.4 + version: 3.9.4 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -161,8 +161,8 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.9.4: + resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} engines: {node: '>=14'} hasBin: true @@ -378,7 +378,7 @@ snapshots: picomatch@2.3.2: {} - prettier@3.8.4: {} + prettier@3.9.4: {} pstree.remy@1.1.8: {} diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index d111f6d0da..9338a0ad56 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -355,11 +355,15 @@ (defn has-point? [shape point] - (if (or ^boolean (cfh/path-shape? shape) - ^boolean (cfh/bool-shape? shape) - ^boolean (cfh/circle-shape? shape)) - (slow-has-point? shape point) - (fast-has-point? shape point))) + (let [rotation (dm/get-prop shape :rotation)] + ;; Rotated shapes don't match their axis-aligned box, so use the polygon test. + (if (or ^boolean (cfh/path-shape? shape) + ^boolean (cfh/bool-shape? shape) + ^boolean (cfh/circle-shape? shape) + (and (some? rotation) + (not ^boolean (mth/almost-zero? rotation)))) + (slow-has-point? shape point) + (fast-has-point? shape point)))) (defn rect-contains-shape? [rect shape] diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 46337ca813..37a090217b 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -249,7 +249,7 @@ (defn is-variant-container? "Check if this shape is a variant container" [shape] - (:is-variant-container shape)) + (boolean (:is-variant-container shape))) (defn set-touched-group [touched group] diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc index 1be3578cc1..7fe8a4c7d4 100644 --- a/common/src/app/common/types/plugins.cljc +++ b/common/src/app/common/types/plugins.cljc @@ -30,6 +30,7 @@ (def schema:registry-entry [:map [:plugin-id :string] + [:version {:optional true} :int] [:name :string] [:description {:optional true} :string] [:host :string] diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc index 12d2cb4844..3cbc475000 100644 --- a/common/test/common_tests/files_changes_test.cljc +++ b/common/test/common_tests/files_changes_test.cljc @@ -8,6 +8,8 @@ (:require [app.common.features :as ffeat] [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.schema.test :as smt] @@ -736,6 +738,55 @@ {:num 1000}))) +(t/deftest set-comment-thread-position + (let [file-id (uuid/custom 2 2) + page-id (uuid/custom 1 1) + thread-id (uuid/custom 3 1) + frame-id (uuid/custom 4 1) + data (make-file-data file-id page-id)] + + (t/testing "stores position and frame-id" + (let [change {:type :set-comment-thread-position + :page-id page-id + :comment-thread-id thread-id + :frame-id frame-id + :position (gpt/point 10 20)} + res (ch/process-changes data [change])] + (t/is (= {:frame-id frame-id :position (gpt/point 10 20)} + (get-in res [:pages-index page-id :comment-thread-positions thread-id]))))) + + (t/testing "removes the position when frame-id and position are nil" + (let [data (ch/process-changes data [{:type :set-comment-thread-position + :page-id page-id + :comment-thread-id thread-id + :frame-id frame-id + :position (gpt/point 10 20)}]) + res (ch/process-changes data [{:type :set-comment-thread-position + :page-id page-id + :comment-thread-id thread-id + :frame-id nil + :position nil}])] + (t/is (nil? (get-in res [:pages-index page-id :comment-thread-positions thread-id]))))) + + (t/testing "builder round-trips the position through undo and redo" + (let [data (ch/process-changes data [{:type :set-comment-thread-position + :page-id page-id + :comment-thread-id thread-id + :frame-id frame-id + :position (gpt/point 10 20)}]) + page (get-in data [:pages-index page-id]) + changes (-> (pcb/empty-changes) + (pcb/with-page page) + (pcb/set-comment-thread-position {:id thread-id + :frame-id frame-id + :position (gpt/point 100 200)})) + redone (ch/process-changes data (:redo-changes changes)) + undone (ch/process-changes redone (:undo-changes changes))] + (t/is (= (gpt/point 100 200) + (get-in redone [:pages-index page-id :comment-thread-positions thread-id :position]))) + (t/is (= (gpt/point 10 20) + (get-in undone [:pages-index page-id :comment-thread-positions thread-id :position]))))))) + (t/deftest set-plugin-data-json-encode-decode (let [schema ch/schema:set-plugin-data-change encode (sm/encoder schema (sm/json-transformer)) diff --git a/common/test/common_tests/geom_shapes_intersect_test.cljc b/common/test/common_tests/geom_shapes_intersect_test.cljc index 676290de89..a670d938c4 100644 --- a/common/test/common_tests/geom_shapes_intersect_test.cljc +++ b/common/test/common_tests/geom_shapes_intersect_test.cljc @@ -254,3 +254,19 @@ shape {:points points}] (t/is (true? (gint/slow-has-point? shape (pt 50 25)))) (t/is (false? (gint/slow-has-point? shape (pt 150 25))))))) + +(t/deftest has-point-rotated-test + ;; Diamond (a square rotated 45º); its axis-aligned x/y/width/height box does + ;; not match the rotated polygon. + (let [points [(pt 50 0) (pt 100 50) (pt 50 100) (pt 0 50)] + shape {:x 20 :y 20 :width 60 :height 60 :rotation 45 :points points}] + (t/testing "point inside the polygon but outside the box is contained" + (t/is (true? (gint/has-point? shape (pt 50 5))))) + (t/testing "point inside the box but outside the polygon is not contained" + (t/is (false? (gint/has-point? shape (pt 22 22))))))) + +(t/deftest has-point-axis-aligned-test + (let [shape {:x 10 :y 20 :width 100 :height 50 :rotation 0}] + (t/testing "unrotated shape uses the axis-aligned box" + (t/is (true? (gint/has-point? shape (pt 50 40)))) + (t/is (false? (gint/has-point? shape (pt 200 40))))))) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 351499df2c..a996e4d2f1 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -32,7 +32,7 @@ RUN set -ex; \ FROM base AS setup-node -ENV NODE_VERSION=v24.16.0 \ +ENV NODE_VERSION=v24.18.0 \ PATH=/opt/node/bin:$PATH RUN set -eux; \ diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index a92b87d184..260e5fda8d 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -3,7 +3,7 @@ LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v24.16.0 \ + NODE_VERSION=v24.18.0 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/node/bin:/opt/imagick/bin:$PATH \ PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index 96cf10f824..ee1c9f4399 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -3,7 +3,7 @@ LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v22.21.1 \ + NODE_VERSION=v24.18.0 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/node/bin:$PATH \ PENPOT_MCP_SERVER_HOST=0.0.0.0 diff --git a/docs/package.json b/docs/package.json index c880fe9f4a..cf16fae313 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,5 +39,5 @@ "markdown-it-anchor": "^9.0.1", "markdown-it-plantuml": "^1.4.1" }, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620" + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b" } diff --git a/exporter/package.json b/exporter/package.json index d97a978652..fce5a1fad9 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@11.5.3+sha512.7ac1c919341c213a34dc0d02afb7143c5c26ac26ee8c4782deea821b8ac64d2134a081fd8941dae6e29bbb48f58dfc2b7fbceeccc07cb2f09d219d342a4969ed", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" @@ -18,7 +18,7 @@ "generic-pool": "^3.9.0", "inflation": "^2.1.0", "ioredis": "^5.11.1", - "playwright": "^1.61.0", + "playwright": "^1.61.1", "raw-body": "^3.0.2", "source-map-support": "^0.5.21", "undici": "^8.5.0", diff --git a/exporter/pnpm-lock.yaml b/exporter/pnpm-lock.yaml index e72f27ef67..6c1947288c 100644 --- a/exporter/pnpm-lock.yaml +++ b/exporter/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: ^5.11.1 version: 5.11.1 playwright: - specifier: ^1.61.0 - version: 1.61.0 + specifier: ^1.61.1 + version: 1.61.1 raw-body: specifier: ^3.0.2 version: 3.0.2 @@ -330,13 +330,13 @@ packages: resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} engines: {node: '>=20.19.0'} - playwright-core@1.61.0: - resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.61.0: - resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true @@ -714,11 +714,11 @@ snapshots: dependencies: boolbase: 2.0.0 - playwright-core@1.61.0: {} + playwright-core@1.61.1: {} - playwright@1.61.0: + playwright@1.61.1: dependencies: - playwright-core: 1.61.0 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 diff --git a/frontend/deps.edn b/frontend/deps.edn index 563656c803..801a990923 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -46,6 +46,9 @@ :dev {:extra-paths ["dev"] + :jvm-opts ["--sun-misc-unsafe-memory-access=allow" + "--enable-native-access=ALL-UNNAMED"] + :extra-deps {thheller/shadow-cljs {:mvn/version "3.2.2"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} diff --git a/frontend/package.json b/frontend/package.json index 74f85808cf..2a47553243 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "browserslist": [ "defaults" ], @@ -58,18 +58,18 @@ "@penpot/tokenscript": "link:packages/tokenscript", "@penpot/ua-parser": "penpot/ua-parser#1.0.0", "@penpot/ui": "link:packages/ui", - "@playwright/test": "1.61.0", + "@playwright/test": "1.61.1", "@storybook/addon-docs": "10.4.6", "@storybook/addon-themes": "10.4.6", "@storybook/addon-vitest": "10.4.6", "@storybook/react-vite": "10.4.6", "@tokens-studio/sd-transforms": "2.0.3", - "@types/node": "^25.9.3", + "@types/node": "^26.0.1", "@vitest/browser": "4.1.9", "@vitest/browser-playwright": "^4.1.9", "@vitest/coverage-v8": "4.1.9", "@zip.js/zip.js": "2.8.26", - "autoprefixer": "^10.4.27", + "autoprefixer": "^10.5.2", "compression": "^1.8.1", "concurrently": "^10.0.3", "date-fns": "^4.4.0", @@ -92,12 +92,12 @@ "npm-run-all": "^4.1.5", "opentype.js": "^2.0.0", "p-limit": "^7.3.0", - "playwright": "1.61.0", - "postcss": "^8.5.15", + "playwright": "1.61.1", + "postcss": "^8.5.16", "postcss-clean": "^1.2.2", "postcss-modules": "^6.0.1", "postcss-scss": "^4.0.9", - "prettier": "3.8.4", + "prettier": "3.9.4", "pretty-time": "^1.1.0", "prop-types": "^15.8.1", "randomcolor": "^0.6.2", @@ -114,15 +114,15 @@ "source-map-support": "^0.5.21", "storybook": "10.4.6", "style-dictionary": "5.4.4", - "stylelint": "^17.13.0", + "stylelint": "^17.14.0", "stylelint-config-standard-scss": "^17.0.0", - "stylelint-scss": "^7.2.0", "stylelint-plugin-logical-css": "^2.1.0", + "stylelint-scss": "^7.2.0", "svg-sprite": "^2.0.4", "tdigest": "^0.1.2", "tinycolor2": "^1.6.0", "typescript": "^6.0.2", - "vite": "^8.0.16", + "vite": "^8.1.0", "vitest": "^4.1.9", "wait-on": "^9.0.4", "watcher": "^2.3.1", diff --git a/frontend/packages/draft-js/package.json b/frontend/packages/draft-js/package.json index e89303a986..8ad22449d7 100644 --- a/frontend/packages/draft-js/package.json +++ b/frontend/packages/draft-js/package.json @@ -4,12 +4,12 @@ "description": "Penpot Draft-JS Wrapper", "main": "index.js", "type": "module", - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "author": "Andrey Antukh", "license": "MPL-2.0", "dependencies": { "draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0", - "immutable": "^5.1.6" + "immutable": "^5.1.9" }, "peerDependencies": { "react": ">=0.17.0", diff --git a/frontend/packages/mousetrap/package.json b/frontend/packages/mousetrap/package.json index 88c84ba0d2..4b20059b0f 100644 --- a/frontend/packages/mousetrap/package.json +++ b/frontend/packages/mousetrap/package.json @@ -4,7 +4,7 @@ "description": "Simple library for handling keyboard shortcuts", "main": "index.js", "type": "module", - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "author": "Craig Campbell", "license": "Apache-2.0 WITH LLVM-exception" } diff --git a/frontend/packages/tokenscript/package.json b/frontend/packages/tokenscript/package.json index ca1155e7e6..9c4ed11113 100644 --- a/frontend/packages/tokenscript/package.json +++ b/frontend/packages/tokenscript/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "type": "module", - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "author": "Andrey Antukh", "license": "MPL-2.0", "dependencies": { diff --git a/frontend/packages/ui/package.json b/frontend/packages/ui/package.json index 2acac3a1b8..31e2597bd9 100644 --- a/frontend/packages/ui/package.json +++ b/frontend/packages/ui/package.json @@ -14,15 +14,15 @@ "build": "vite build" }, "devDependencies": { - "@babel/core": "^8.0.0", - "@babel/preset-react": "^8.0.0", + "@babel/core": "^8.0.1", + "@babel/preset-react": "^8.0.1", "@storybook/react": "10.4.6", "@storybook/react-vite": "10.4.6", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^6.0.2", + "@vitejs/plugin-react": "^6.0.3", "babel-plugin-react-compiler": "^1.0.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", @@ -30,7 +30,7 @@ "eslint-plugin-react-hooks": "7.1.1", "react-compiler-runtime": "^1.0.0", "storybook": "10.4.6", - "vite-plugin-dts": "^5.0.2" + "vite-plugin-dts": "^5.0.3" }, "peerDependencies": { "react": ">=19.2", diff --git a/frontend/playwright/data/dashboard/create-team-invitations.json b/frontend/playwright/data/dashboard/create-team-invitations.json new file mode 100644 index 0000000000..75601e351f --- /dev/null +++ b/frontend/playwright/data/dashboard/create-team-invitations.json @@ -0,0 +1 @@ +{"~:total": 2} diff --git a/frontend/playwright/data/render-wasm/get-file-blurs.json b/frontend/playwright/data/render-wasm/get-file-blurs.json index 1b7e094606..4861c63b29 100644 --- a/frontend/playwright/data/render-wasm/get-file-blurs.json +++ b/frontend/playwright/data/render-wasm/get-file-blurs.json @@ -389,7 +389,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 281.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "line-through", + "~:letter-spacing": "0px", + "~:x": 2111, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae134c87eab3": { @@ -758,7 +788,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 281.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2719, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae1409412914": { @@ -1143,7 +1203,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 488.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2707, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae1488974651": { @@ -1293,7 +1383,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 111.419998168945, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2313, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.2399978637695, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae149cc8c2f1": { @@ -1432,7 +1552,32 @@ }, "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 201.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2714, + "~:fills": [], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae149cc8c2f0": { @@ -1571,7 +1716,32 @@ }, "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 201.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2514, + "~:fills": [], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae136831d5b7": { @@ -2116,7 +2286,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 281.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2519, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae1409412913": { @@ -2368,7 +2568,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 201.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "underline", + "~:letter-spacing": "0px", + "~:x": 2106, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae149fbf3112": { @@ -2525,7 +2755,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 281.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2318, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae1409412912": { @@ -4487,7 +4747,44 @@ }, "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 111.419998168945, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2714, + "~:fills": [ + { + "~:fill-opacity": 1, + "~:fill-image": { + "~:id": "~uaa0a383a-7553-808a-8006-ae13a3c575eb", + "~:width": 100, + "~:height": 100, + "~:mtype": "image/jpeg", + "~:keep-aspect-ratio": true, + "~:name": "sample" + } + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.2399978637695, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae136eba4581": { @@ -5550,7 +5847,32 @@ }, "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 201.419982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2313, + "~:fills": [], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.239990234375, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae148b39a12f": { @@ -5700,7 +6022,37 @@ "~:fills": [], "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 111.419998168945, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2514, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.2399978637695, + "~:text": "HOLA" + } + ] } }, "~u13fc1849-119a-8028-8006-ae145f7fe46e": { @@ -6244,7 +6596,37 @@ }, "~:flip-x": null, "~:height": 63.999981880188, - "~:flip-y": null + "~:flip-y": null, + "~:position-data": [ + { + "~:y": 111.419998168945, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "72px", + "~:font-weight": "900", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 169.070068359375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 2106, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 93.2399978637695, + "~:text": "HOLA" + } + ] } } }, @@ -6258,4 +6640,4 @@ "~:base-font-size": "16px" } } -} \ No newline at end of file +} diff --git a/frontend/playwright/data/render-wasm/get-file-text-images.json b/frontend/playwright/data/render-wasm/get-file-text-images.json index e04435395a..6df32616a6 100644 --- a/frontend/playwright/data/render-wasm/get-file-text-images.json +++ b/frontend/playwright/data/render-wasm/get-file-text-images.json @@ -637,7 +637,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🔥" }, @@ -654,7 +659,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👩🏿\u200d🚀" }, @@ -671,7 +681,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👺" }, @@ -688,7 +703,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🚀" } @@ -706,7 +726,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" }, { @@ -726,7 +751,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "" } @@ -744,7 +774,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" } ] @@ -2395,7 +2430,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🔥" }, @@ -2412,7 +2452,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👩🏿\u200d🚀" }, @@ -2429,7 +2474,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👺" }, @@ -2446,7 +2496,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🚀" } @@ -2464,7 +2519,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" }, { @@ -2484,7 +2544,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "" } @@ -2502,7 +2567,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" } ] @@ -3433,7 +3503,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🔥" }, @@ -3450,7 +3525,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👩🏿\u200d🚀" }, @@ -3467,7 +3547,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "👺" }, @@ -3484,7 +3569,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "🚀" } @@ -3502,7 +3592,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" }, { @@ -3522,7 +3617,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro", "~:text": "" } @@ -3540,7 +3640,12 @@ "~:font-variant-id": "regular", "~:text-decoration": "none", "~:letter-spacing": "0", - "~:fills": [], + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], "~:font-family": "sourcesanspro" } ] diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 55158b9564..33304fe5f0 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -162,7 +162,7 @@ test("Updates canvas background", async ({ page }) => { const canvasBackgroundInput = workspace.page.getByRole("textbox", { name: "Color", - }); + }).first(); await canvasBackgroundInput.fill("FABADA"); await workspace.page.keyboard.press("Enter"); await workspace.waitForFirstRenderWithoutUI(); diff --git a/frontend/playwright/ui/specs/dashboard-invite-members.spec.js b/frontend/playwright/ui/specs/dashboard-invite-members.spec.js new file mode 100644 index 0000000000..578d2eaa59 --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard-invite-members.spec.js @@ -0,0 +1,86 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); +}); + +test("Open invite members modal from invitations section", async ({ + page, +}) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + await expect(page.getByRole("button", { name: "Invite people" })).toBeVisible(); + await page.getByRole("button", { name: "Invite people" }).click(); + await expect(page.getByText("Invite members to the team")).toBeVisible(); +}); + +test("Invite a new member by email", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await DashboardPage.mockRPC( + page, + "create-team-invitations", + "dashboard/create-team-invitations.json", + { method: "POST" }, + ); + + await dashboardPage.goToSecondTeamInvitationsSection(); + await page.getByRole("button", { name: "Invite people" }).click(); + await expect(page.getByText("Invite members to the team")).toBeVisible(); + + const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" }); + await emailInput.fill("newmember@example.com"); + await emailInput.press("Enter"); + + await page.getByRole("button", { name: "Send invitation" }).click(); + + await expect(page.getByText("Invitation sent successfully")).toBeVisible(); + await expect( + page.getByText("Invite members to the team"), + ).not.toBeVisible(); +}); + +test("Show warning when inviting an existing member", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + await page.getByRole("button", { name: "Invite people" }).click(); + await expect(page.getByText("Invite members to the team")).toBeVisible(); + + const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" }); + await emailInput.fill("foo@example.com"); + await emailInput.press("Enter"); + + await expect( + page.getByText( + "Some members are already on the team. We'll invite the rest.", + ), + ).toBeVisible(); +}); + +test("Disable send button when all entered emails are existing members", async ({ + page, +}) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + await page.getByRole("button", { name: "Invite people" }).click(); + await expect(page.getByText("Invite members to the team")).toBeVisible(); + + const emailInput = page.getByRole("textbox", { name: "Emails, comma separated" }); + await emailInput.fill("foo@example.com"); + await emailInput.press("Enter"); + + const sendButton = page.getByRole("button", { name: "Send invitation" }); + await expect(sendButton).toBeDisabled(); +}); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 03e2aeb73f..edd27334f3 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -50,32 +50,32 @@ importers: specifier: link:packages/ui version: link:packages/ui '@playwright/test': - specifier: 1.61.0 - version: 1.61.0 + specifier: 1.61.1 + version: 1.61.1 '@storybook/addon-docs': specifier: 10.4.6 - version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@storybook/addon-themes': specifier: 10.4.6 - version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) '@storybook/addon-vitest': specifier: 10.4.6 - version: 10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.9) + version: 10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.9) '@storybook/react-vite': specifier: 10.4.6 - version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@tokens-studio/sd-transforms': specifier: 2.0.3 version: 2.0.3(style-dictionary@5.4.4(tslib@2.8.1)) '@types/node': - specifier: ^25.9.3 - version: 25.9.3 + specifier: ^26.0.1 + version: 26.0.1 '@vitest/browser': specifier: 4.1.9 - version: 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + version: 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/browser-playwright': specifier: ^4.1.9 - version: 4.1.9(playwright@1.61.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + version: 4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) @@ -83,8 +83,8 @@ importers: specifier: 2.8.26 version: 2.8.26(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) autoprefixer: - specifier: ^10.4.27 - version: 10.5.0(postcss@8.5.15) + specifier: ^10.5.2 + version: 10.5.2(postcss@8.5.16) compression: specifier: ^1.8.1 version: 1.8.1 @@ -102,7 +102,7 @@ importers: version: 3.1.0 express: specifier: ^5.1.0 - version: 5.2.1 + version: 5.2.1(supports-color@5.5.0) fancy-log: specifier: ^2.0.0 version: 2.0.0 @@ -152,23 +152,23 @@ importers: specifier: ^7.3.0 version: 7.3.0 playwright: - specifier: 1.61.0 - version: 1.61.0 + specifier: 1.61.1 + version: 1.61.1 postcss: - specifier: ^8.5.15 - version: 8.5.15 + specifier: ^8.5.16 + version: 8.5.16 postcss-clean: specifier: ^1.2.2 version: 1.2.2 postcss-modules: specifier: ^6.0.1 - version: 6.0.1(postcss@8.5.15) + version: 6.0.1(postcss@8.5.16) postcss-scss: specifier: ^4.0.9 - version: 4.0.9(postcss@8.5.15) + version: 4.0.9(postcss@8.5.16) prettier: - specifier: 3.8.4 - version: 3.8.4 + specifier: 3.9.4 + version: 3.9.4 pretty-time: specifier: ^1.1.0 version: 1.1.0 @@ -213,22 +213,22 @@ importers: version: 0.5.21 storybook: specifier: 10.4.6 - version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) style-dictionary: specifier: 5.4.4 version: 5.4.4(tslib@2.8.1) stylelint: - specifier: ^17.13.0 - version: 17.13.0(typescript@6.0.3) + specifier: ^17.14.0 + version: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.15)(stylelint@17.13.0(typescript@6.0.3)) + version: 17.0.0(postcss@8.5.16)(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) stylelint-plugin-logical-css: specifier: ^2.1.0 - version: 2.1.0(stylelint@17.13.0(typescript@6.0.3)) + version: 2.1.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) stylelint-scss: specifier: ^7.2.0 - version: 7.2.0(stylelint@17.13.0(typescript@6.0.3)) + version: 7.2.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) svg-sprite: specifier: ^2.0.4 version: 2.0.4 @@ -242,14 +242,14 @@ importers: specifier: ^6.0.2 version: 6.0.3 vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + version: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) wait-on: specifier: ^9.0.4 - version: 9.0.10 + version: 9.0.10(supports-color@5.5.0) watcher: specifier: ^2.3.1 version: 2.3.1 @@ -266,8 +266,8 @@ importers: specifier: penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0 version: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(encoding@0.1.13)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) immutable: - specifier: ^5.1.6 - version: 5.1.6 + specifier: ^5.1.9 + version: 5.1.9 react: specifier: '>=0.17.0' version: 19.2.7 @@ -297,17 +297,17 @@ importers: version: 19.2.7(react@19.2.7) devDependencies: '@babel/core': - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^8.0.1 + version: 8.0.1 '@babel/preset-react': - specifier: ^8.0.0 - version: 8.0.0(@babel/core@8.0.0) + specifier: ^8.0.1 + version: 8.0.1(@babel/core@8.0.1) '@storybook/react': specifier: 10.4.6 - version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) '@storybook/react-vite': specifier: 10.4.6 - version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -321,8 +321,8 @@ importers: specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + specifier: ^6.0.3 + version: 6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -343,22 +343,22 @@ importers: version: 1.0.0(react@19.2.7) storybook: specifier: 10.4.6 - version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) vite-plugin-dts: - specifier: ^5.0.2 - version: 5.0.2(@microsoft/api-extractor@7.56.2(@types/node@25.9.3))(esbuild@0.28.1)(rolldown@1.0.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + specifier: ^5.0.3 + version: 5.0.3(@microsoft/api-extractor@7.56.2(@types/node@26.0.1))(esbuild@0.28.1)(rolldown@1.1.3)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) text-editor: devDependencies: '@playwright/test': - specifier: 1.61.0 - version: 1.61.0 + specifier: 1.61.1 + version: 1.61.1 '@types/node': - specifier: ^25.9.2 - version: 25.9.3 + specifier: ^26.0.1 + version: 26.0.1 '@vitest/browser': specifier: ^4.1.9 - version: 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + version: 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/coverage-v8': specifier: ^4.1.9 version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) @@ -375,17 +375,17 @@ importers: specifier: ^29.1.1 version: 29.1.1(canvas@3.2.3) playwright: - specifier: 1.61.0 - version: 1.61.0 + specifier: 1.61.1 + version: 1.61.1 prettier: - specifier: ^3.8.4 - version: 3.8.4 + specifier: ^3.9.4 + version: 3.9.4 vite: - specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + specifier: ^8.1.1 + version: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + version: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) packages: @@ -433,8 +433,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/core@8.0.0': - resolution: {integrity: sha512-i4r3sKX4MAmOnHMCCNjN0PQ8ZA6V4GobBcMBZrJyKW48S7eV0SlKUlZxw2FXkBbXzWQ8JoQQnpbD9tt1Hc+9Mw==} + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} engines: {node: ^22.18.0 || >=24.11.0} '@babel/generator@7.29.7': @@ -479,8 +479,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@8.0.0': - resolution: {integrity: sha512-s6IyoAZ0BtRfYgw9SpSXG1SNmkVbFrPQIt0Q5vAmBSyEkX3SwHqvstPY+GZ4xuxYf8V+9+dbRCUEmtDcjRnWlw==} + '@babel/helper-plugin-utils@8.0.1': + resolution: {integrity: sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 @@ -497,8 +497,8 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.0': - resolution: {integrity: sha512-kXxQVZHNOctSJJsqzmcbPSCEkM6oHNnDIkua7g9RCO9xRHj2eCiKvRx2KPdfWR9QxcGWnK/oArrtunmie3rL9g==} + '@babel/helper-validator-identifier@8.0.2': + resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} engines: {node: ^22.18.0 || >=24.11.0} '@babel/helper-validator-option@7.29.7': @@ -527,38 +527,38 @@ packages: engines: {node: ^22.18.0 || >=24.11.0} hasBin: true - '@babel/plugin-syntax-jsx@8.0.0': - resolution: {integrity: sha512-feeoayPfDYe7B2otR6JJYNu+a2nDR3gGK4BLeSN8WPofW5MGIVtppOc1Y+Nr37jJtupZSS2c5H/d78A8kbjnhA==} + '@babel/plugin-syntax-jsx@8.0.1': + resolution: {integrity: sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 - '@babel/plugin-transform-react-display-name@8.0.0': - resolution: {integrity: sha512-ENX0gq1S+dDP6Nz4IX5eEdBP0wbI9AsgVcNZl/8qSnS5/oGDeK2LnloSWZdMtvLWQRL7bplRfcJiq98ejJgLZA==} + '@babel/plugin-transform-react-display-name@8.0.1': + resolution: {integrity: sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 - '@babel/plugin-transform-react-jsx-development@8.0.0': - resolution: {integrity: sha512-yVklum9Nxro+bii16JbY+hLcrEEfAuhGXuF96eCaHuDI2bm3vYYv5GIxCc6OG05onPYGd8gzYVGbY/9pRJrX7A==} + '@babel/plugin-transform-react-jsx-development@8.0.1': + resolution: {integrity: sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 - '@babel/plugin-transform-react-jsx@8.0.0': - resolution: {integrity: sha512-SXWmkotVC8p7KROraEfl+LIg3ahtRBDamulTNXt9gmNKOs8DpKWxF/puQ9BAfRbg33mV5Ch5Wys104JhpT5Xtg==} + '@babel/plugin-transform-react-jsx@8.0.1': + resolution: {integrity: sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 - '@babel/plugin-transform-react-pure-annotations@8.0.0': - resolution: {integrity: sha512-haLQLz73xQhFdzh+n4mm6cYO7cZAX0FcSuinfgCl08h7/29g0u0pK65x5eGCi4iJNvmsKOVQHllcezooKqx7ZQ==} + '@babel/plugin-transform-react-pure-annotations@8.0.1': + resolution: {integrity: sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 - '@babel/preset-react@8.0.0': - resolution: {integrity: sha512-sFiob/54pgTyj5G3oQ45RIorZaGQShH7gch5OsSdQMwryOP/rdS+R/sX/CKlHMh0NYHgiBvvA/cP86dQeOm7jg==} + '@babel/preset-react@8.0.1': + resolution: {integrity: sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==} engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: '@babel/core': ^8.0.0 @@ -606,8 +606,8 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - '@bufbuild/protobuf@2.11.0': - resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} + '@bufbuild/protobuf@2.12.1': + resolution: {integrity: sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==} '@bundled-es-modules/deepmerge@4.3.2': resolution: {integrity: sha512-q8doe7ndrY2IolUOFIn0A0++JBX38pMhN7kFhTF4cnjIcILf6X6H2yWczInyv8ZFdR0lrE8088X8XS5efxXz8A==} @@ -663,6 +663,14 @@ packages: css-tree: optional: true + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} @@ -692,18 +700,27 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -1130,6 +1147,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1275,8 +1298,8 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} '@oxc-resolver/binding-android-arm-eabi@11.20.0': resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} @@ -1481,8 +1504,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.61.0': - resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} hasBin: true @@ -1569,97 +1592,97 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2007,6 +2030,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -2052,8 +2078,8 @@ packages: '@types/mdx@2.0.14': resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} - '@types/node@25.9.3': - resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/node@26.0.1': + resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -2069,8 +2095,8 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@vitejs/plugin-react@6.0.2': - resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -2328,8 +2354,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + autoprefixer@10.5.2: + resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -2363,8 +2389,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.37: - resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==} + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2398,16 +2424,16 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2450,6 +2476,9 @@ packages: caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + caniuse-lite@1.0.30001800: + resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + canvas@3.2.3: resolution: {integrity: sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==} engines: {node: ^18.12.0 || >= 20.9.0} @@ -2607,8 +2636,8 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -2638,8 +2667,8 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2894,8 +2923,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.375: - resolution: {integrity: sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==} + electron-to-chromium@1.5.382: + resolution: {integrity: sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3125,8 +3154,8 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} fancy-log@2.0.0: resolution: {integrity: sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==} @@ -3145,8 +3174,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -3243,8 +3272,8 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + fs-extra@11.3.6: + resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} fs.realpath@1.0.0: @@ -3481,8 +3510,8 @@ packages: resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} engines: {node: '>=0.10.0'} - immutable@5.1.6: - resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -3735,6 +3764,10 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -3898,8 +3931,8 @@ packages: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} locate-path@6.0.0: @@ -4066,8 +4099,8 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} @@ -4121,8 +4154,8 @@ packages: encoding: optional: true - node-releases@2.0.47: - resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} nodemon@3.1.14: @@ -4341,16 +4374,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - playwright-core@1.61.0: - resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.61.0: - resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true @@ -4421,14 +4454,18 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + postcss-value-parser@3.3.1: resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -4441,8 +4478,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.9.4: + resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} engines: {node: '>=14'} hasBin: true @@ -4648,8 +4685,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4856,6 +4893,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -5147,8 +5189,8 @@ packages: peerDependencies: stylelint: ^16.8.2 || ^17.0.0 - stylelint@17.13.0: - resolution: {integrity: sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==} + stylelint@17.14.0: + resolution: {integrity: sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==} engines: {node: '>=20.19.0'} hasBin: true @@ -5204,8 +5246,8 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -5358,8 +5400,8 @@ packages: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} @@ -5368,8 +5410,8 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} @@ -5387,12 +5429,12 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-dts@1.0.2: - resolution: {integrity: sha512-VbNiMD0LMl/t6nJueGtrCp79N7ZO1nquxj/FUybJDnKwZGsnW2wjdwBSzA3QEHujoxmxZIptsG43hL7LzXE96w==} + unplugin-dts@1.0.3: + resolution: {integrity: sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==} peerDependencies: '@microsoft/api-extractor': '>=7' '@rspack/core': ^1 - '@vue/language-core': ~3.1.5 + '@vue/language-core': ^3.1.5 esbuild: '*' rolldown: '*' rollup: '>=3' @@ -5462,8 +5504,8 @@ packages: resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} engines: {node: '>= 0.10'} - vite-plugin-dts@5.0.2: - resolution: {integrity: sha512-lNeHS+dwGju6eRmNvZQt8Shwv9j3m98hbHse/lIbLq9q3yE2DcIOBBYQEVUF6tS0kOmv+VA9Z5FqmzFnGe4U8g==} + vite-plugin-dts@5.0.3: + resolution: {integrity: sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==} peerDependencies: '@microsoft/api-extractor': '>=7' rollup: '>=3' @@ -5476,13 +5518,56 @@ packages: vite: optional: true - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.8.3 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@8.1.1: + resolution: {integrity: sha512-X/05/cT+VITy2AeDc1der6smvGWWREtL4hPbPTaVbjSBuuWkmNOjR6HP3NzqcQA2nF6VHGUPaFRJyft/2AE9Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -5775,7 +5860,7 @@ snapshots: '@babel/code-frame@8.0.0': dependencies: - '@babel/helper-validator-identifier': 8.0.0 + '@babel/helper-validator-identifier': 8.0.2 js-tokens: 10.0.0 '@babel/compat-data@7.29.7': {} @@ -5802,7 +5887,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@8.0.0': + '@babel/core@8.0.1': dependencies: '@babel/code-frame': 8.0.0 '@babel/generator': 8.0.0 @@ -5819,7 +5904,7 @@ snapshots: import-meta-resolve: 4.2.0 json5: 2.2.3 obug: 2.1.3 - semver: 7.8.4 + semver: 7.8.5 '@babel/generator@7.29.7': dependencies: @@ -5846,7 +5931,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.4 lru-cache: 5.1.1 semver: 6.3.1 @@ -5854,9 +5939,9 @@ snapshots: dependencies: '@babel/compat-data': 8.0.0 '@babel/helper-validator-option': 8.0.0 - browserslist: 4.28.2 + browserslist: 4.28.4 lru-cache: 11.5.1 - semver: 7.8.4 + semver: 7.8.5 '@babel/helper-globals@7.29.7': {} @@ -5883,9 +5968,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@8.0.0(@babel/core@8.0.0)': + '@babel/helper-plugin-utils@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 + '@babel/core': 8.0.1 '@babel/helper-string-parser@7.29.7': {} @@ -5893,7 +5978,7 @@ snapshots: '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.0': {} + '@babel/helper-validator-identifier@8.0.2': {} '@babel/helper-validator-option@7.29.7': {} @@ -5917,45 +6002,45 @@ snapshots: dependencies: '@babel/types': 8.0.0 - '@babel/plugin-syntax-jsx@8.0.0(@babel/core@8.0.0)': + '@babel/plugin-syntax-jsx@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 - '@babel/helper-plugin-utils': 8.0.0(@babel/core@8.0.0) + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) - '@babel/plugin-transform-react-display-name@8.0.0(@babel/core@8.0.0)': + '@babel/plugin-transform-react-display-name@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 - '@babel/helper-plugin-utils': 8.0.0(@babel/core@8.0.0) + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) - '@babel/plugin-transform-react-jsx-development@8.0.0(@babel/core@8.0.0)': + '@babel/plugin-transform-react-jsx-development@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 - '@babel/plugin-transform-react-jsx': 8.0.0(@babel/core@8.0.0) + '@babel/core': 8.0.1 + '@babel/plugin-transform-react-jsx': 8.0.1(@babel/core@8.0.1) - '@babel/plugin-transform-react-jsx@8.0.0(@babel/core@8.0.0)': + '@babel/plugin-transform-react-jsx@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 + '@babel/core': 8.0.1 '@babel/helper-annotate-as-pure': 8.0.0 '@babel/helper-module-imports': 8.0.0 - '@babel/helper-plugin-utils': 8.0.0(@babel/core@8.0.0) - '@babel/plugin-syntax-jsx': 8.0.0(@babel/core@8.0.0) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-syntax-jsx': 8.0.1(@babel/core@8.0.1) '@babel/types': 8.0.0 - '@babel/plugin-transform-react-pure-annotations@8.0.0(@babel/core@8.0.0)': + '@babel/plugin-transform-react-pure-annotations@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 + '@babel/core': 8.0.1 '@babel/helper-annotate-as-pure': 8.0.0 - '@babel/helper-plugin-utils': 8.0.0(@babel/core@8.0.0) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) - '@babel/preset-react@8.0.0(@babel/core@8.0.0)': + '@babel/preset-react@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 8.0.0 - '@babel/helper-plugin-utils': 8.0.0(@babel/core@8.0.0) + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) '@babel/helper-validator-option': 8.0.0 - '@babel/plugin-transform-react-display-name': 8.0.0(@babel/core@8.0.0) - '@babel/plugin-transform-react-jsx': 8.0.0(@babel/core@8.0.0) - '@babel/plugin-transform-react-jsx-development': 8.0.0(@babel/core@8.0.0) - '@babel/plugin-transform-react-pure-annotations': 8.0.0(@babel/core@8.0.0) + '@babel/plugin-transform-react-display-name': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-jsx': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-jsx-development': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-react-pure-annotations': 8.0.1(@babel/core@8.0.1) '@babel/runtime-corejs3@7.29.0': dependencies: @@ -6005,7 +6090,7 @@ snapshots: '@babel/types@8.0.0': dependencies: '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.0 + '@babel/helper-validator-identifier': 8.0.2 '@bcoe/v8-coverage@1.0.2': {} @@ -6015,7 +6100,7 @@ snapshots: dependencies: css-tree: 3.2.1 - '@bufbuild/protobuf@2.11.0': {} + '@bufbuild/protobuf@2.12.1': {} '@bundled-es-modules/deepmerge@4.3.2': dependencies: @@ -6083,6 +6168,10 @@ snapshots: optionalDependencies: css-tree: 3.2.1 + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@4.0.0': {} '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -6090,13 +6179,13 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.4)': dependencies: - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 - '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.4)': dependencies: - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 '@dabh/diagnostics@2.0.8': dependencies: @@ -6110,6 +6199,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6121,6 +6216,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -6131,6 +6231,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -6240,7 +6345,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -6298,11 +6403,19 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + optionalDependencies: + typescript: 6.0.3 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) optionalDependencies: typescript: 6.0.3 @@ -6466,24 +6579,24 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 - '@microsoft/api-extractor-model@7.32.2(@types/node@25.9.3)': + '@microsoft/api-extractor-model@7.32.2(@types/node@26.0.1)': dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@25.9.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.0.1) transitivePeerDependencies: - '@types/node' optional: true - '@microsoft/api-extractor@7.56.2(@types/node@25.9.3)': + '@microsoft/api-extractor@7.56.2(@types/node@26.0.1)': dependencies: - '@microsoft/api-extractor-model': 7.32.2(@types/node@25.9.3) + '@microsoft/api-extractor-model': 7.32.2(@types/node@26.0.1) '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@25.9.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.0.1) '@rushstack/rig-package': 0.6.0 - '@rushstack/terminal': 0.21.0(@types/node@25.9.3) - '@rushstack/ts-command-line': 5.2.0(@types/node@25.9.3) + '@rushstack/terminal': 0.21.0(@types/node@26.0.1) + '@rushstack/ts-command-line': 5.2.0(@types/node@26.0.1) diff: 8.0.4 lodash: 4.18.1 minimatch: 10.2.5 @@ -6520,6 +6633,13 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6600,7 +6720,7 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.137.0': {} '@oxc-resolver/binding-android-arm-eabi@11.20.0': optional: true @@ -6737,9 +6857,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.61.0': + '@playwright/test@1.61.1': dependencies: - playwright: 1.61.0 + playwright: 1.61.1 '@polka/url@1.0.0-next.29': {} @@ -6794,53 +6914,53 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.3': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -6930,23 +7050,23 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/node-core-library@5.19.1(@types/node@25.9.3)': + '@rushstack/node-core-library@5.19.1(@types/node@26.0.1)': dependencies: ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) ajv-formats: 3.0.1(ajv@8.20.0) - fs-extra: 11.3.5 + fs-extra: 11.3.6 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.12 semver: 7.5.4 optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 26.0.1 optional: true - '@rushstack/problem-matcher@0.1.1(@types/node@25.9.3)': + '@rushstack/problem-matcher@0.1.1(@types/node@26.0.1)': optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 26.0.1 optional: true '@rushstack/rig-package@0.6.0': @@ -6955,18 +7075,18 @@ snapshots: strip-json-comments: 3.1.1 optional: true - '@rushstack/terminal@0.21.0(@types/node@25.9.3)': + '@rushstack/terminal@0.21.0(@types/node@26.0.1)': dependencies: - '@rushstack/node-core-library': 5.19.1(@types/node@25.9.3) - '@rushstack/problem-matcher': 0.1.1(@types/node@25.9.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.0.1) + '@rushstack/problem-matcher': 0.1.1(@types/node@26.0.1) supports-color: 8.1.1 optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 26.0.1 optional: true - '@rushstack/ts-command-line@5.2.0(@types/node@25.9.3)': + '@rushstack/ts-command-line@5.2.0(@types/node@26.0.1)': dependencies: - '@rushstack/terminal': 0.21.0(@types/node@25.9.3) + '@rushstack/terminal': 0.21.0(@types/node@26.0.1) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -6983,15 +7103,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@storybook/addon-docs@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) - '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-dedent: 2.3.0 optionalDependencies: '@types/react': 19.2.17 @@ -7002,44 +7122,64 @@ snapshots: - vite - webpack - '@storybook/addon-themes@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + '@storybook/addon-themes@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-dedent: 2.3.0 - '@storybook/addon-vitest@10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.9)': + '@storybook/addon-vitest@10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.9)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: - '@vitest/browser': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) - '@vitest/browser-playwright': 4.1.9(playwright@1.61.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/browser': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/browser-playwright': 4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/runner': 4.1.9 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@storybook/builder-vite@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: - '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-dedent: 2.3.0 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@storybook/builder-vite@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ts-dedent: 2.3.0 + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + dependencies: + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.1 rollup: 4.61.1 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + + '@storybook/csf-plugin@10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + dependencies: + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + rollup: 4.61.1 + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) '@storybook/global@5.0.0': {} @@ -7048,30 +7188,30 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@storybook/react-dom-shim@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + '@storybook/react-dom-shim@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@rollup/pluginutils': 5.4.0(rollup@4.61.1) - '@storybook/builder-vite': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) - '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + '@storybook/builder-vite': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) empathic: 2.0.1 magic-string: 0.30.21 react: 19.2.7 react-docgen: 8.0.3 react-dom: 19.2.7(react@19.2.7) resolve: 1.22.12 - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tsconfig-paths: 4.2.0 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -7081,15 +7221,39 @@ snapshots: - typescript - webpack - '@storybook/react@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)': + '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@rollup/pluginutils': 5.4.0(rollup@4.61.1) + '@storybook/builder-vite': 10.4.6(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 19.2.7 + react-docgen: 8.0.3 + react-dom: 19.2.7(react@19.2.7) + resolve: 1.22.12 + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tsconfig-paths: 4.2.0 + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - typescript + - webpack + + '@storybook/react@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) react: 19.2.7 react-docgen: 8.0.3 react-docgen-typescript: 2.4.0(typescript@6.0.3) react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) @@ -7155,6 +7319,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/argparse@1.0.38': optional: true @@ -7202,9 +7371,9 @@ snapshots: '@types/mdx@2.0.14': {} - '@types/node@25.9.3': + '@types/node@26.0.1': dependencies: - undici-types: 7.24.6 + undici-types: 8.3.0 '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: @@ -7218,36 +7387,67 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@vitejs/plugin-react@6.0.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@vitejs/plugin-react@6.0.3(babel-plugin-react-compiler@1.0.0)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) optionalDependencies: babel-plugin-react-compiler: 1.0.0 - '@vitest/browser-playwright@4.1.9(playwright@1.61.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': + '@vitest/browser-playwright@4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': dependencies: - '@vitest/browser': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) - '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) - playwright: 1.61.0 + '@vitest/browser': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + playwright: 1.61.1 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': + '@vitest/browser-playwright@4.1.9(playwright@1.61.1)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': + dependencies: + '@vitest/browser': 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/mocker': 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + playwright: 1.61.1 + tinyrainbow: 3.1.0 + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@vitest/utils': 4.1.9 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -7267,9 +7467,9 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) optionalDependencies: - '@vitest/browser': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/browser': 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/expect@3.2.4': dependencies: @@ -7288,13 +7488,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + + '@vitest/mocker@4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -7331,7 +7539,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@vitest/utils@3.2.4': dependencies: @@ -7376,7 +7584,7 @@ snapshots: acorn@8.17.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: @@ -7402,7 +7610,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 optional: true @@ -7410,7 +7618,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7557,13 +7765,13 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.5.0(postcss@8.5.15): + autoprefixer@10.5.2(postcss@8.5.16): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.4 caniuse-lite: 1.0.30001799 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -7572,11 +7780,11 @@ snapshots: axe-core@4.11.1: {} - axios@1.16.1: + axios@1.16.1(supports-color@5.5.0): dependencies: follow-redirects: 1.16.0 form-data: 4.0.5 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@5.5.0) proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -7594,7 +7802,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.37: {} + baseline-browser-mapping@2.10.40: {} bidi-js@1.0.3: dependencies: @@ -7610,7 +7818,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.2: + body-parser@2.2.2(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -7637,7 +7845,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -7645,13 +7853,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.4: dependencies: - baseline-browser-mapping: 2.10.37 - caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.375 - node-releases: 2.0.47 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001800 + electron-to-chromium: 1.5.382 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) buffer-from@1.1.2: {} @@ -7700,6 +7908,8 @@ snapshots: caniuse-lite@1.0.30001799: {} + caniuse-lite@1.0.30001800: {} + canvas@3.2.3: dependencies: node-addon-api: 7.1.1 @@ -7858,7 +8068,7 @@ snapshots: confbox@0.1.8: {} - confbox@0.2.2: {} + confbox@0.2.4: {} config-chain@1.1.13: dependencies: @@ -7879,11 +8089,11 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig@9.0.1(typescript@6.0.3): + cosmiconfig@9.0.2(typescript@6.0.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -8134,7 +8344,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.375: {} + electron-to-chromium@1.5.382: {} emoji-regex@10.6.0: {} @@ -8488,10 +8698,10 @@ snapshots: expr-eval-fork@3.0.3: {} - express@5.2.1: + express@5.2.1(supports-color@5.5.0): dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.2.2(supports-color@5.5.0) content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 @@ -8501,7 +8711,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@5.5.0) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -8512,8 +8722,8 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.2 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 + router: 2.2.0(supports-color@5.5.0) + send: 1.2.1(supports-color@5.5.0) serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.0.1 @@ -8521,7 +8731,7 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.8: {} + exsolve@1.1.0: {} fancy-log@2.0.0: dependencies: @@ -8541,7 +8751,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fastest-levenshtein@1.0.16: {} @@ -8583,7 +8793,7 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -8641,7 +8851,7 @@ snapshots: fs-constants@1.0.0: {} - fs-extra@11.3.5: + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 @@ -8844,9 +9054,9 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@5.5.0): dependencies: - agent-base: 6.0.2 + agent-base: 6.0.2(supports-color@5.5.0) debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -8861,9 +9071,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.15): + icss-utils@5.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 ieee754@1.2.1: {} @@ -8875,7 +9085,7 @@ snapshots: immutable@3.8.3: {} - immutable@5.1.6: {} + immutable@5.1.9: {} import-fresh@3.3.1: dependencies: @@ -9125,6 +9335,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + jsdom@29.1.1(canvas@3.2.3): dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -9274,10 +9488,10 @@ snapshots: loader-utils@3.3.1: {} - local-pkg@1.1.2: + local-pkg@1.2.1: dependencies: - mlly: 1.8.0 - pkg-types: 2.3.0 + mlly: 1.8.2 + pkg-types: 2.3.1 quansync: 0.2.11 locate-path@6.0.0: @@ -9338,7 +9552,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.4 + semver: 7.8.5 map-stream@0.0.7: {} @@ -9404,7 +9618,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: @@ -9422,12 +9636,12 @@ snapshots: mkdirp@3.0.1: {} - mlly@1.8.0: + mlly@1.8.2: dependencies: acorn: 8.17.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.3 + ufo: 1.6.4 mrmime@2.0.1: {} @@ -9451,7 +9665,7 @@ snapshots: node-abi@3.92.0: dependencies: - semver: 7.8.4 + semver: 7.8.5 node-addon-api@7.1.1: {} @@ -9461,7 +9675,7 @@ snapshots: optionalDependencies: encoding: 0.1.13 - node-releases@2.0.47: {} + node-releases@2.0.50: {} nodemon@3.1.14: dependencies: @@ -9733,20 +9947,20 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.0 + mlly: 1.8.2 pathe: 2.0.3 - pkg-types@2.3.0: + pkg-types@2.3.1: dependencies: - confbox: 0.2.2 - exsolve: 1.0.8 + confbox: 0.2.4 + exsolve: 1.1.0 pathe: 2.0.3 - playwright-core@1.61.0: {} + playwright-core@1.61.1: {} - playwright@1.61.0: + playwright@1.61.1: dependencies: - playwright-core: 1.61.0 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 @@ -9761,63 +9975,68 @@ snapshots: postcss-clean@1.2.2: dependencies: clean-css: 4.2.4 - postcss: 8.5.15 + postcss: 8.5.16 postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.15): + postcss-modules-extract-imports@3.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-modules-local-by-default@4.2.0(postcss@8.5.15): + postcss-modules-local-by-default@4.2.0(postcss@8.5.16): dependencies: - icss-utils: 5.1.0(postcss@8.5.15) - postcss: 8.5.15 + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.15): + postcss-modules-scope@3.2.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.5.15): + postcss-modules-values@4.0.0(postcss@8.5.16): dependencies: - icss-utils: 5.1.0(postcss@8.5.15) - postcss: 8.5.15 + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 - postcss-modules@6.0.1(postcss@8.5.15): + postcss-modules@6.0.1(postcss@8.5.16): dependencies: generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.5.15) + icss-utils: 5.1.0(postcss@8.5.16) lodash.camelcase: 4.3.0 - postcss: 8.5.15 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.15) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.15) - postcss-modules-scope: 3.2.1(postcss@8.5.15) - postcss-modules-values: 4.0.0(postcss@8.5.15) + postcss: 8.5.16 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.16) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.16) + postcss-modules-scope: 3.2.1(postcss@8.5.16) + postcss-modules-values: 4.0.0(postcss@8.5.16) string-hash: 1.1.3 postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.15): + postcss-safe-parser@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-scss@4.0.9(postcss@8.5.15): + postcss-scss@4.0.9(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-value-parser@3.3.1: {} postcss-value-parser@4.2.0: {} - postcss@8.5.15: + postcss@8.5.16: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -9835,12 +10054,12 @@ snapshots: pump: 3.0.4 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 2.1.4 + tar-fs: 2.1.5 tunnel-agent: 0.6.0 prelude-ls@1.2.1: {} - prettier@3.8.4: {} + prettier@3.9.4: {} pretty-format@27.5.1: dependencies: @@ -10066,26 +10285,26 @@ snapshots: glob: 13.0.6 package-json-from-dist: 1.0.1 - rolldown@1.0.3: + rolldown@1.1.3: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.137.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 rollup@4.61.1: dependencies: @@ -10119,7 +10338,7 @@ snapshots: fsevents: 2.3.3 optional: true - router@2.2.0: + router@2.2.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 @@ -10228,9 +10447,9 @@ snapshots: sass-embedded@1.100.0: dependencies: - '@bufbuild/protobuf': 2.11.0 + '@bufbuild/protobuf': 2.12.1 colorjs.io: 0.5.2 - immutable: 5.1.6 + immutable: 5.1.9 rxjs: 7.8.2 supports-color: 8.1.1 sync-child-process: 1.0.2 @@ -10258,7 +10477,7 @@ snapshots: sass@1.100.0: dependencies: chokidar: 5.0.0 - immutable: 5.1.6 + immutable: 5.1.9 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 @@ -10267,7 +10486,7 @@ snapshots: sass@1.101.0: dependencies: chokidar: 5.0.0 - immutable: 5.1.6 + immutable: 5.1.9 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 @@ -10291,7 +10510,9 @@ snapshots: semver@7.8.4: {} - send@1.2.1: + semver@7.8.5: {} + + send@1.2.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -10312,7 +10533,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -10455,7 +10676,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -10474,7 +10695,7 @@ snapshots: ws: 8.21.0 optionalDependencies: '@types/react': 19.2.17 - prettier: 3.8.4 + prettier: 3.9.4 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -10614,42 +10835,42 @@ snapshots: is-plain-obj: 4.1.0 json5: 2.2.3 path-unified: 0.2.0 - prettier: 3.8.4 + prettier: 3.9.4 tinycolor2: 1.6.0 transitivePeerDependencies: - tslib - stylelint-config-recommended-scss@17.0.1(postcss@8.5.15)(stylelint@17.13.0(typescript@6.0.3)): + stylelint-config-recommended-scss@17.0.1(postcss@8.5.16)(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.15) - stylelint: 17.13.0(typescript@6.0.3) - stylelint-config-recommended: 18.0.0(stylelint@17.13.0(typescript@6.0.3)) - stylelint-scss: 7.2.0(stylelint@17.13.0(typescript@6.0.3)) + postcss-scss: 4.0.9(postcss@8.5.16) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint-scss: 7.2.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) optionalDependencies: - postcss: 8.5.15 + postcss: 8.5.16 - stylelint-config-recommended@18.0.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-config-recommended@18.0.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.15)(stylelint@17.13.0(typescript@6.0.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.16)(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) - stylelint-config-recommended-scss: 17.0.1(postcss@8.5.15)(stylelint@17.13.0(typescript@6.0.3)) - stylelint-config-standard: 40.0.0(stylelint@17.13.0(typescript@6.0.3)) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended-scss: 17.0.1(postcss@8.5.16)(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint-config-standard: 40.0.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) optionalDependencies: - postcss: 8.5.15 + postcss: 8.5.16 - stylelint-config-standard@40.0.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-config-standard@40.0.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) - stylelint-config-recommended: 18.0.0(stylelint@17.13.0(typescript@6.0.3)) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)) - stylelint-plugin-logical-css@2.1.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-plugin-logical-css@2.1.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-scss@7.2.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-scss@7.2.0(stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -10662,19 +10883,19 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint@17.13.0(typescript@6.0.3): + stylelint@17.14.0(supports-color@5.5.0)(typescript@6.0.3): dependencies: '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) - '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.4) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4) colord: 2.9.3 - cosmiconfig: 9.0.1(typescript@6.0.3) + cosmiconfig: 9.0.2(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 debug: 4.4.3(supports-color@5.5.0) @@ -10692,9 +10913,9 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.15 - postcss-safe-parser: 7.0.1(postcss@8.5.15) - postcss-selector-parser: 7.1.1 + postcss: 8.5.16 + postcss-safe-parser: 7.0.1(postcss@8.5.16) + postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 string-width: 8.2.1 supports-hyperlinks: 4.4.0 @@ -10774,7 +10995,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tar-fs@2.1.4: + tar-fs@2.1.5: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 @@ -10927,7 +11148,7 @@ snapshots: ua-parser-js@1.0.41: {} - ufo@1.6.3: {} + ufo@1.6.4: {} unbox-primitive@1.1.0: dependencies: @@ -10938,7 +11159,7 @@ snapshots: undefsafe@2.0.5: {} - undici-types@7.24.6: {} + undici-types@8.3.0: {} undici@7.25.0: {} @@ -10949,23 +11170,23 @@ snapshots: unpipe@1.0.0: {} - unplugin-dts@1.0.2(@microsoft/api-extractor@7.56.2(@types/node@25.9.3))(esbuild@0.28.1)(rolldown@1.0.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): + unplugin-dts@1.0.3(@microsoft/api-extractor@7.56.2(@types/node@26.0.1))(esbuild@0.28.1)(rolldown@1.1.3)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.61.1) '@volar/typescript': 2.4.28 compare-versions: 6.1.1 debug: 4.4.3(supports-color@5.5.0) kolorist: 1.8.0 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 typescript: 6.0.3 unplugin: 2.3.11 optionalDependencies: - '@microsoft/api-extractor': 7.56.2(@types/node@25.9.3) + '@microsoft/api-extractor': 7.56.2(@types/node@26.0.1) esbuild: 0.28.1 - rolldown: 1.0.3 + rolldown: 1.1.3 rollup: 4.61.1 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) transitivePeerDependencies: - supports-color @@ -10976,9 +11197,9 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 @@ -11027,13 +11248,13 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite-plugin-dts@5.0.2(@microsoft/api-extractor@7.56.2(@types/node@25.9.3))(esbuild@0.28.1)(rolldown@1.0.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): + vite-plugin-dts@5.0.3(@microsoft/api-extractor@7.56.2(@types/node@26.0.1))(esbuild@0.28.1)(rolldown@1.1.3)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): dependencies: - unplugin-dts: 1.0.2(@microsoft/api-extractor@7.56.2(@types/node@25.9.3))(esbuild@0.28.1)(rolldown@1.0.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + unplugin-dts: 1.0.3(@microsoft/api-extractor@7.56.2(@types/node@26.0.1))(esbuild@0.28.1)(rolldown@1.1.3)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) optionalDependencies: - '@microsoft/api-extractor': 7.56.2(@types/node@25.9.3) + '@microsoft/api-extractor': 7.56.2(@types/node@26.0.1) rollup: 4.61.1 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) transitivePeerDependencies: - '@rspack/core' - '@vue/language-core' @@ -11043,24 +11264,38 @@ snapshots: - typescript - webpack - vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0): + vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + postcss: 8.5.16 + rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 26.0.1 esbuild: 0.28.1 fsevents: 2.3.3 sass: 1.101.0 sass-embedded: 1.100.0 - vitest@4.1.9(@types/node@25.9.3)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): + vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.0.1 + esbuild: 0.28.1 + fsevents: 2.3.3 + sass: 1.101.0 + sass-embedded: 1.100.0 + + vitest@4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -11077,11 +11312,42 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.3 - '@vitest/browser-playwright': 4.1.9(playwright@1.61.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@types/node': 26.0.1 + '@vitest/browser-playwright': 4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) + '@vitest/coverage-v8': 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) + '@vitest/ui': 4.1.9(vitest@4.1.9) + jsdom: 29.1.1(canvas@3.2.3) + transitivePeerDependencies: + - msw + + vitest@4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(canvas@3.2.3))(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.0.1 + '@vitest/browser-playwright': 4.1.9(playwright@1.61.1)(vite@8.1.1(@types/node@26.0.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))(vitest@4.1.9) '@vitest/coverage-v8': 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) '@vitest/ui': 4.1.9(vitest@4.1.9) jsdom: 29.1.1(canvas@3.2.3) @@ -11094,9 +11360,9 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wait-on@9.0.10: + wait-on@9.0.10(supports-color@5.5.0): dependencies: - axios: 1.16.1 + axios: 1.16.1(supports-color@5.5.0) joi: 18.2.1 lodash: 4.18.1 minimist: 1.2.8 diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs index 77f5d55013..6635fc070b 100644 --- a/frontend/src/app/main/data/plugins.cljs +++ b/frontend/src/app/main/data/plugins.cljs @@ -82,7 +82,7 @@ (defn- load-plugin! [{:keys [plugin-id name version description host code icon permissions]}] - (st/emit! (pflag/clear plugin-id) + (st/emit! (pflag/initialize plugin-id version) (save-current-plugin plugin-id)) (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5a2d4446c6..ae6458efa8 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -72,7 +72,6 @@ [app.main.refs :as refs] [app.main.repo :as rp] [app.main.router :as rt] - [app.main.store :as st] [app.plugins.register :as preg] [app.render-wasm :as wasm] [app.render-wasm.api :as wasm.api] @@ -352,10 +351,8 @@ (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) rparams (rt/get-params state) features (features/get-enabled-features state team-id) - ;; since render-wasm/v1 can be hot-toggled by the user, we need to query it - ;; from the state with active-feature? - render-wasm-enabled? #(features/active-feature? @st/state "render-wasm/v1") - render-wasm-ready? #(and (render-wasm-enabled?) + render-wasm-enabled? (features/active-feature? state "render-wasm/v1") + render-wasm-ready? #(and render-wasm-enabled? wasm-state/context-initialized? (not @wasm-state/context-lost?))] @@ -368,7 +365,7 @@ (rx/concat ;; Fetch all essential data that should be loaded before the file (rx/merge - (if ^boolean (render-wasm-enabled?) + (if ^boolean render-wasm-enabled? (->> (rx/from @wasm/module) (rx/filter true?) (rx/tap (fn [_] @@ -441,6 +438,9 @@ (rx/take 1) (rx/map #(dwcm/navigate-to-comment-id comment-id)))) + ;; Keep comment thread positions in sync on undo/redo + (rx/of (dwcm/watch-comment-thread-position-changes stoper-s)) + (let [local-commits-s (->> stream (rx/filter dch/commit?) @@ -775,44 +775,46 @@ #{:up :down :bottom :top}) (defn vertical-order-selected - [loc] - (dm/assert! - "expected valid location" - (contains? valid-vertical-locations loc)) - (ptk/reify ::vertical-order-selected - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - selected-ids (dsh/lookup-selected state) - selected-shapes (map (d/getf objects) selected-ids) - undo-id (js/Symbol) + ([loc] + (vertical-order-selected loc nil)) + ([loc ids] + (dm/assert! + "expected valid location" + (contains? valid-vertical-locations loc)) + (ptk/reify ::vertical-order-selected + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + selected-ids (or ids (dsh/lookup-selected state)) + selected-shapes (map (d/getf objects) selected-ids) + undo-id (js/Symbol) - move-shape - (fn [changes shape] - (let [parent (get objects (:parent-id shape)) - sibling-ids (:shapes parent) - current-index (d/index-of sibling-ids (:id shape)) - index-in-selection (d/index-of selected-ids (:id shape)) - new-index (case loc - :top (count sibling-ids) - :down (max 0 (- current-index 1)) - :up (min (count sibling-ids) (+ (inc current-index) 1)) - :bottom index-in-selection)] - (pcb/change-parent changes - (:id parent) - [shape] - new-index))) + move-shape + (fn [changes shape] + (let [parent (get objects (:parent-id shape)) + sibling-ids (:shapes parent) + current-index (d/index-of sibling-ids (:id shape)) + index-in-selection (d/index-of selected-ids (:id shape)) + new-index (case loc + :top (count sibling-ids) + :down (max 0 (- current-index 1)) + :up (min (count sibling-ids) (+ (inc current-index) 1)) + :bottom index-in-selection)] + (pcb/change-parent changes + (:id parent) + [shape] + new-index))) - changes (reduce move-shape - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) - selected-shapes)] + changes (reduce move-shape + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) + selected-shapes)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (ptk/data-event :layout/update {:ids selected-ids}) - (dwu/commit-undo-transaction undo-id)))))) + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids selected-ids}) + (dwu/commit-undo-transaction undo-id))))))) (defn set-shape-index [file-id page-id id new-index] diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index 450268cbd1..968cc8194e 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -8,10 +8,13 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] + [app.main.data.changes :as dwc] [app.main.data.comments :as dcmt] [app.main.data.common :as dcm] [app.main.data.event :as ev] @@ -126,6 +129,14 @@ ny (- (:y position) nh)] (update local :vbox assoc :x nx :y ny))))))) +(defn- set-comment-thread + "Stores the comment thread in the workspace state so its bubble re-renders." + [thread] + (ptk/reify ::set-comment-thread + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:comment-threads (:id thread)] thread)))) + (defn update-comment-thread-position ([thread [new-x new-y]] (update-comment-thread-position thread [new-x new-y] nil)) @@ -136,35 +147,106 @@ (dcmt/check-comment-thread! thread)) (ptk/reify ::update-comment-thread-position ptk/WatchEvent - (watch [_ state _] + (watch [it state _] (let [page (dsh/lookup-page state) page-id (:id page) objects (dsh/lookup-page-objects state page-id) frame-id (if (nil? frame-id) (ctst/get-frame-id-by-position objects (gpt/point new-x new-y)) (:frame-id thread)) - thread (-> thread - (assoc :position (gpt/point new-x new-y)) - (assoc :frame-id frame-id)) - thread-id (:id thread)] + position (gpt/point new-x new-y) + thread (-> thread + (assoc :position position) + (assoc :frame-id frame-id)) + thread-id (:id thread) + + ;; Record the position as a change so it joins the undo entry + set-position-changes + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/set-comment-thread-position thread))] (rx/concat - (rx/of (fn [state] - (-> state - (update :comment-threads assoc thread-id thread) - ;; Keep the page positions map in sync so subsequent - ;; frame moves compute the relative offset from the - ;; latest position instead of a stale one. - (dsh/update-page page-id - #(update-in % [:comment-thread-positions thread-id] - (fn [pos] - (-> pos - (assoc :position (:position thread)) - (assoc :frame-id (:frame-id thread))))))))) - (->> (rp/cmd! :update-comment-thread-position thread) + ;; Update the new position in the rendered thread, and commit the + ;; change so the move is part of the undo entry + (rx/of (set-comment-thread thread) + (dwc/commit-changes set-position-changes)) + (->> (rp/cmd! :update-comment-thread-position {:id thread-id + :position position + :frame-id frame-id}) (rx/catch #(rx/throw {:type :update-comment-thread-position})) (rx/ignore)))))))) +(def ^:private undo-origins + #{:app.main.data.workspace.undo/undo + :app.main.data.workspace.undo/redo + :app.main.data.workspace.undo/undo-to-index}) + +(defn- sync-comment-thread-position + "Syncs the rendered thread and the backend for a comment position change." + [{:keys [comment-thread-id position frame-id]}] + (ptk/reify ::sync-comment-thread-position + ptk/UpdateEvent + (update [_ state] + (cond-> state + (and position frame-id) + (update-in [:comment-threads comment-thread-id] + (fn [thread] + (some-> thread (assoc :position position :frame-id frame-id)))))) + + ptk/WatchEvent + (watch [_ _ _] + (if (and position frame-id) + (->> (rp/cmd! :update-comment-thread-position {:id comment-thread-id + :position position + :frame-id frame-id}) + (rx/catch #(rx/throw {:type :update-comment-thread-position})) + (rx/ignore)) + (rx/empty))))) + +(defn watch-comment-thread-position-changes + "Syncs rendered threads and the backend when an undo/redo changes a comment position." + [stopper] + (ptk/reify ::watch-comment-thread-position-changes + ptk/WatchEvent + (watch [_ _ stream] + (->> stream + (rx/filter dwc/commit?) + (rx/map deref) + (rx/filter #(contains? undo-origins (:origin %))) + (rx/mapcat (fn [commit] + (->> (:redo-changes commit) + (filter #(= :set-comment-thread-position (:type %))) + (rx/from)))) + (rx/map sync-comment-thread-position) + (rx/take-until stopper))))) + +(defn frame-pin-transform + "Matrix that moves a comment pinned inside `frame` as the frame is transformed, + following its translation and rotation but not its resize scale." + [frame modifiers transform] + (when (and (some? frame) (or (some? modifiers) (some? transform))) + (let [frame' (cond-> frame + (some? modifiers) (gsh/transform-shape modifiers) + (some? transform) (gsh/apply-transform transform)) + + c (gsh/shape->center frame) + c' (gsh/shape->center frame') + tfi (or (:transform-inverse frame) (gmt/matrix)) + tf' (or (:transform frame') (gmt/matrix)) + sr (:selrect frame) + sr' (:selrect frame') + d (gpt/point (- (:x sr') (:x sr)) + (- (:y sr') (:y sr)))] + (-> (gmt/matrix) + (gmt/translate! c') + (gmt/multiply! tf') + (gmt/translate! (gpt/negate c')) + (gmt/translate! d) + (gmt/translate! c) + (gmt/multiply! tfi) + (gmt/translate! (gpt/negate c)))))) + ;; Move comment threads that are inside a frame when that frame is moved" (defn- move-frame-comment-threads @@ -187,25 +269,18 @@ build-move-event (fn [comment-thread] - (let [frame-id (:frame-id comment-thread) + (let [frame-id (:frame-id comment-thread) frame (get objects frame-id) modifiers (get-in object-modifiers [frame-id :modifiers]) transform (get transforms frame-id) - frame' - (cond-> frame - (some? modifiers) - (gsh/transform-shape modifiers) + matrix (frame-pin-transform frame modifiers transform) - (some? transform) - (gsh/apply-transform transform)) - - moved (gpt/to-vec (gpt/point (:x frame) (:y frame)) - (gpt/point (:x frame') (:y frame'))) position (get-in threads-position-map [(:id comment-thread) :position]) - new-x (+ (:x position) (:x moved)) - new-y (+ (:y position) (:y moved))] - (update-comment-thread-position comment-thread [new-x new-y] (:id frame))))] + position' (cond-> position + (some? matrix) + (gpt/transform matrix))] + (update-comment-thread-position comment-thread [(:x position') (:y position')] frame-id)))] (->> (:comment-threads state) (vals) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 5225f5cecf..08f812e45e 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1380,7 +1380,7 @@ [] (ptk/reify ::watch-component-changes ptk/WatchEvent - (watch [_ _ stream] + (watch [_ state stream] (let [stopper-s (->> stream (rx/map ptk/type) @@ -1450,7 +1450,7 @@ (rx/tap #(log/trc :hint "buffer initialized")))] (when (or (contains? cf/flags :component-thumbnails) - (features/active-feature? @st/state "render-wasm/v1")) + (features/active-feature? state "render-wasm/v1")) (->> (rx/merge changes-s @@ -1458,7 +1458,7 @@ ;; change so single edits (fill, etc.) update instantly. ;; Non-WASM persists on every render, so it stays on the ;; debounced path below to avoid per-edit backend posts. - (if (features/active-feature? @st/state "render-wasm/v1") + (if (features/active-feature? state "render-wasm/v1") (->> changes-s (rx/filter (ptk/type? ::component-changed)) (rx/map deref) diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index d8be9688ae..5ccf4c2a31 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -343,20 +343,23 @@ (dch/commit-changes changes)))))) (defn duplicate-token-set - [id] - (ptk/reify ::duplicate-token-set - ptk/WatchEvent - (watch [it state _] - (let [data (dsh/lookup-file-data state) - tokens-lib (get data :tokens-lib) - suffix (tr "workspace.tokens.duplicate-suffix")] + ([id] + (duplicate-token-set id nil)) + ([id {:keys [id-ref]}] + (ptk/reify ::duplicate-token-set + ptk/WatchEvent + (watch [it state _] + (let [data (dsh/lookup-file-data state) + tokens-lib (get data :tokens-lib) + suffix (tr "workspace.tokens.duplicate-suffix")] - (when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})] - (let [changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/set-token-set (ctob/get-id token-set) token-set))] - (rx/of (set-selected-token-set-id (ctob/get-id token-set)) - (dch/commit-changes changes)))))))) + (when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})] + (when id-ref (reset! id-ref (ctob/get-id token-set))) + (let [changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/set-token-set (ctob/get-id token-set) token-set))] + (rx/of (set-selected-token-set-id (ctob/get-id token-set)) + (dch/commit-changes changes))))))))) (defn set-enabled-token-set [name enabled?] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index bff7170b1b..bc3a9f24ab 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -327,12 +327,21 @@ (dwm/create-modif-tree shape-ids %) :ignore-constraints (contains? layout :scale-text))))) - (->> resize-events-stream - (rx/mapcat + (let [emit-modifiers (fn [modifiers] (let [modif-tree (dwm/create-modif-tree shape-ids modifiers)] - (rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text)))))) - (rx/take-until stopper)))] + (rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text)))))] + ;; Throttle the live preview to limit re-renders; the trailing + ;; rx/last applies the exact final frame. + (rx/merge + (->> resize-events-stream + (rx/sample 16) + (rx/mapcat emit-modifiers) + (rx/take-until stopper)) + (->> resize-events-stream + (rx/take-until stopper) + (rx/last) + (rx/mapcat emit-modifiers)))))] (rx/concat ;; This initial stream waits for some pixels to be move before making the resize @@ -523,14 +532,22 @@ (rx/of (finish-transform))) - (rx/concat - (rx/merge - (->> angle-stream - (rx/map - #(dwm/set-rotation-modifiers % shapes group-center)) - (rx/take-until stopper))) - (rx/of (dwm/apply-modifiers) - (finish-transform)))))))) + (let [emit-modifiers + (fn [angle] (dwm/set-rotation-modifiers angle shapes group-center))] + ;; Throttle the live preview to limit re-renders; the trailing + ;; rx/last applies the exact final frame. + (rx/concat + (rx/merge + (->> angle-stream + (rx/sample 16) + (rx/map emit-modifiers) + (rx/take-until stopper)) + (->> angle-stream + (rx/take-until stopper) + (rx/last) + (rx/map emit-modifiers))) + (rx/of (dwm/apply-modifiers) + (finish-transform))))))))) (defn increase-rotation "Rotate shapes a fixed angle, from a keyboard action." @@ -822,6 +839,8 @@ (rx/merge (->> modifiers-stream + ;; Throttle the live preview to limit re-renders. + (rx/sample 16) (rx/map (fn [[modifiers snap-ignore-axis]] (dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis})))) @@ -843,10 +862,13 @@ ;; Last event will write the modifiers creating the changes (->> move-stream (rx/last) + (rx/with-latest-from modifiers-stream) (rx/mapcat - (fn [[_ target-frame drop-index drop-cell]] + (fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) + ;; Apply the exact final modifiers; the preview may drop the last frame. + (dwm/set-modifiers modifiers false false {:snap-ignore-axis snap-ignore-axis}) (dwm/apply-modifiers {:undo-transation? false}) (move-shapes-to-frame ids target-frame drop-index drop-cell) (finish-transform) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 85a9b3ac2f..c3bd8044fa 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -1164,6 +1164,9 @@ test-id (str/join "-" (map :seqn (sort-by :seqn thread-group))) + ;; Click-through while transforming a shape, so it doesn't capture the drag + dragging? (some? (mf/deref refs/current-transform)) + on-click (mf/use-fn (mf/deps thread-group position zoom) @@ -1176,7 +1179,8 @@ (dwz/set-zoom position scale-zoom)))))] [:div {:style {:top (dm/str pos-y "px") - :left (dm/str pos-x "px")} + :left (dm/str pos-x "px") + :pointer-events (when dragging? "none")} :on-click on-click :class (stl/css :floating-preview-wrapper :floating-preview-bubble)} [:> comment-avatar* @@ -1198,6 +1202,9 @@ frame-id (:frame-id thread) + ;; Click-through while transforming a shape, so it doesn't capture the drag + dragging? (some? (mf/deref refs/current-transform)) + state (mf/use-state #(do {:is-hover false :is-grabbing false @@ -1290,7 +1297,8 @@ (on-click thread))))] [:div {:style {:top (dm/str pos-y "px") - :left (dm/str pos-x "px")} + :left (dm/str pos-x "px") + :pointer-events (when dragging? "none")} :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-pointer-move on-pointer-move diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 0d4a5fc55f..89a1bcd42d 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -459,7 +459,7 @@ (into [] (distinct) (conj coll item))) (mf/defc multi-input - [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}] + [{:keys [form label class trim valid-item-fn caution-item-fn on-submit] :as props}] (let [form (or form (mf/use-ctx form-ctx)) input-name (get props :name) touched? (get-in @form [:touched input-name]) @@ -610,6 +610,7 @@ [:div {:class klass} [:input {:id (name input-name) + :name (name input-name) :class in-klass :type "text" :auto-focus auto-focus? diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 56df6cc5f1..cb73367c4f 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -168,10 +168,13 @@ ::mf/register-as :invite-members ::mf/props :obj} [{:keys [team origin invite-email]}] - (let [members (get team :members) + (let [teams (mf/deref refs/teams) + perms (get team :permissions) team-id (get team :id) + members (get-in teams [team-id :members]) + roles (mf/with-memo [perms] (get-available-roles perms)) @@ -824,6 +827,7 @@ [:div {:class (stl/css :empty-invitations-buttons)} [:a {:class (stl/css :btn-empty-invitations) + :role "button" :on-click on-invite-member :data-testid "invite-member"} (tr "dashboard.invite-profile")]] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 51ca68d105..394864c22a 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -959,32 +959,17 @@ ev-name (if (= next-renderer :wasm) "enable-webgl-rendering" "disable-webgl-rendering")] - - (if (cf/external-feature-flag "renderer-hard-reload" "test") - ;; Bare RPC + hard reload: skips `du/update-profile-props`, so - ;; `features/recompute-features` is not run here; bootstrap - ;; after reload resolves render-wasm/v1 from the saved profile. - (do - (->> (rx/zip - (rp/cmd! :update-profile-props {:props {:renderer next-renderer}}) - (rx/filter (ptk/type? ::ev/chunk-persisted) st/stream)) - (rx/timeout 2000 (rx/of :timeout)) - (rx/subs! (fn [_] - (dom/reload-current-window true)) - (fn [_] - (st/emit! (ntf/error (tr "errors.generic")))))) - (st/emit! (ev/event {::ev/name ev-name - ::ev/origin "workspace:menu"}) - (ptk/data-event ::ev/force-persist {}))) - - ;; `update-profile-props` WatchEvent calls - ;; `features/recompute-features`. - (st/emit! (ev/event {::ev/name ev-name - ::ev/origin "workspace:menu"}) - (du/update-profile-props {:renderer next-renderer}) - (ntf/success (tr (if (= next-renderer :wasm) - "webgl.toast.webgl-render-enabled" - "webgl.toast.webgl-render-disabled")))))))) + (->> (rx/zip + (rp/cmd! :update-profile-props {:props {:renderer next-renderer}}) + (rx/filter (ptk/type? ::ev/chunk-persisted) st/stream)) + (rx/timeout 2000 (rx/of :timeout)) + (rx/subs! (fn [_] + (dom/reload-current-window true)) + (fn [_] + (st/emit! (ntf/error (tr "errors.generic")))))) + (st/emit! (ev/event {::ev/name ev-name + ::ev/origin "workspace:menu"}) + (ptk/data-event ::ev/force-persist {}))))) open-plugins-manager (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index d3c6eb1320..8334acbebb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -48,7 +48,12 @@ :selrect :points :show-content - :hide-in-viewer]) + :hide-in-viewer + + ;; Needed to disable/enable width/height + ;; otherwise the memo will not detect changes + :layout-item-h-sizing + :layout-item-v-sizing]) (def ^:private generic-options #{:size :position :rotation}) @@ -130,7 +135,7 @@ acc)))) acc))))) -(defn- check-measures-menu-props +(defn check-measures-menu-props [old-props new-props] (let [o-values (unchecked-get old-props "values") n-values (unchecked-get new-props "values")] @@ -150,10 +155,12 @@ (get n-values :hide-in-viewer)) (identical? (get o-values :width) (get n-values :width)) - (identical? (get o-values :width) - (get n-values :width)) (identical? (get o-values :height) (get n-values :height)) + (identical? (get o-values :layout-item-h-sizing) + (get n-values :layout-item-h-sizing)) + (identical? (get o-values :layout-item-v-sizing) + (get n-values :layout-item-v-sizing)) (identical? (get o-values :points) (get n-values :points)) (identical? (get o-values :selrect) diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index 8468b29b4b..8fc476751c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -8,9 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] [app.main.data.comments :as dcm] [app.main.data.workspace.comments :as dwcm] [app.main.refs :as refs] @@ -18,6 +15,47 @@ [app.main.ui.comments :as cmt] [rumext.v2 :as mf])) +;; Pin transform for the bubble's frame so it follows the frame during a drag, +;; scoped per frame to avoid re-rendering the whole layer each tick. +(defn- use-frame-position-modifier + [frame-id] + (let [modifiers (mf/deref refs/workspace-modifiers) + wasm-mods (mf/deref refs/workspace-wasm-modifiers) + objects (mf/deref refs/workspace-page-objects)] + (dwcm/frame-pin-transform (get objects frame-id) + (get-in modifiers [frame-id :modifiers]) + (get wasm-mods frame-id)))) + +(mf/defc comment-floating-bubble-wrapper* + {::mf/private true} + [{:keys [thread zoom is-open]}] + (let [position-modifier (use-frame-position-modifier (:frame-id thread))] + [:> cmt/comment-floating-bubble* + {:thread thread + :zoom zoom + :position-modifier position-modifier + :is-open is-open}])) + +(mf/defc comment-floating-group-wrapper* + {::mf/private true} + [{:keys [thread-group zoom]}] + (let [thread (first thread-group) + position-modifier (use-frame-position-modifier (:frame-id thread))] + [:> cmt/comment-floating-group* + {:thread-group thread-group + :zoom zoom + :position-modifier position-modifier}])) + +(mf/defc comment-floating-thread-wrapper* + {::mf/private true} + [{:keys [thread viewport zoom]}] + (let [position-modifier (use-frame-position-modifier (:frame-id thread))] + [:> cmt/comment-floating-thread* + {:thread thread + :viewport viewport + :position-modifier position-modifier + :zoom zoom}])) + (mf/defc comments-layer* {::mf/wrap [mf/memo]} [{:keys [vbox vport zoom file-id page-id]}] @@ -34,38 +72,12 @@ threads-map (mf/deref refs/threads) - ;; Active transform modifiers (e.g. while dragging a board). We use - ;; them to move comment bubbles live alongside their frame, instead of - ;; only repositioning them at drop time. The SVG (legacy) renderer keeps - ;; them in `:workspace-modifiers`, while the WASM renderer pushes them - ;; through the `wasm-modifiers` stream as plain transform matrices. - modifiers (mf/deref refs/workspace-modifiers) - wasm-mods (into {} (mf/deref refs/workspace-wasm-modifiers)) - objects (mf/deref refs/workspace-page-objects) - threads (mf/with-memo [threads-map local profile page-id] (->> (vals threads-map) (filter #(= (:page-id %) page-id)) (dcm/apply-filters local profile))) - ;; Returns the position translation matrix for a frame that is being - ;; transformed, or nil when the frame has no active modifier. The delta - ;; matches `move-frame-comment-threads` (frame top-left displacement) so - ;; the bubble does not jump when the modifier is committed. - frame-position-modifier - (fn [frame-id] - (when-let [frame (get objects frame-id)] - (let [frame' - (if-let [modifier (get-in modifiers [frame-id :modifiers])] - (gsh/transform-shape frame modifier) - (when-let [transform (get wasm-mods frame-id)] - (gsh/apply-transform frame transform)))] - (when (some? frame') - (let [delta (gpt/to-vec (gpt/point (:x frame) (:y frame)) - (gpt/point (:x frame') (:y frame')))] - (gmt/translate-matrix delta)))))) - viewport (assoc vport :offset-x pos-x :offset-y pos-y) @@ -94,23 +106,20 @@ (let [group? (> (count thread-group) 1) thread (first thread-group)] (if group? - [:> cmt/comment-floating-group* {:thread-group thread-group - :zoom zoom - :position-modifier (frame-position-modifier (:frame-id thread)) - :key (:seqn thread)}] - [:> cmt/comment-floating-bubble* {:thread thread - :zoom zoom - :position-modifier (frame-position-modifier (:frame-id thread)) - :is-open (= (:id thread) (:open local)) - :key (:seqn thread)}]))) + [:> comment-floating-group-wrapper* {:thread-group thread-group + :zoom zoom + :key (:seqn thread)}] + [:> comment-floating-bubble-wrapper* {:thread thread + :zoom zoom + :is-open (= (:id thread) (:open local)) + :key (:seqn thread)}]))) (when-let [id (:open local)] (when-let [thread (get threads-map id)] (when (seq (dcm/apply-filters local profile [thread])) - [:> cmt/comment-floating-thread* + [:> comment-floating-thread-wrapper* {:thread thread :viewport viewport - :position-modifier (frame-position-modifier (:frame-id thread)) :zoom zoom}]))) (when-let [draft (:draft local)] diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index a9b1e4d323..3d3796a14e 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -317,6 +317,11 @@ (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) (u/not-valid plugin-id :group-shapes shapes) + ;; A group cannot be created from no shapes; per the documented contract + ;; return null instead of a proxy pointing at a shape that never exists. + (zero? (alength shapes)) + nil + (some #(not (u/page-active? (obj/get % "$page"))) shapes) (u/not-valid plugin-id :group "Cannot modify a page that is not currently active") @@ -664,8 +669,13 @@ (u/not-valid plugin-id :flatten-shapes "Not valid shapes") :else - (let [ids (into #{} (map #(obj/get % "$id")) shapes)] - (st/emit! (dw/convert-selected-to-path ids))))) + ;; convert-selected-to-path converts the shapes in place (keeping their + ;; ids), so return proxies for the same ids, now resolving as paths. + (let [file-id (:current-file-id @st/state) + page-id (:current-page-id @st/state) + ids (mapv #(obj/get % "$id") shapes)] + (st/emit! (dw/convert-selected-to-path (into #{} ids))) + (apply array (map #(shape/shape-proxy plugin-id file-id page-id %) ids))))) :createVariantFromComponents (fn [shapes] diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs index 629f445c40..71e8a0311b 100644 --- a/frontend/src/app/plugins/comments.cljs +++ b/frontend/src/app/plugins/comments.cljs @@ -9,7 +9,6 @@ [app.common.geom.point :as gpt] [app.common.schema :as sm] [app.main.data.comments :as dc] - [app.main.data.helpers :as dsh] [app.main.data.workspace.comments :as dwc] [app.main.repo :as rp] [app.main.store :as st] @@ -203,13 +202,12 @@ :remove (fn [] - (let [profile (:profile @st/state) - owner (dsh/lookup-profile @st/state (:owner-id data))] + (let [profile (:profile @st/state)] (cond (not (r/check-permission plugin-id "comment:write")) (u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission") - (not= (:id profile) owner) + (not= (:id profile) (:owner-id data)) (u/not-valid plugin-id :remove "Cannot change content from another user's comments") :else diff --git a/frontend/src/app/plugins/file.cljs b/frontend/src/app/plugins/file.cljs index 69e794d2d0..ac6ef089e5 100644 --- a/frontend/src/app/plugins/file.cljs +++ b/frontend/src/app/plugins/file.cljs @@ -64,7 +64,7 @@ (user/user-proxy plugin-id user-data)))} :createdAt - {:get #(.toJSDate ^js (:created-at @data))} + {:get #(:created-at @data)} :isAutosave {:get #(= "system" (:created-by @data))} @@ -136,6 +136,9 @@ :name {:get #(-> (u/locate-file id) :name)} + :revn + {:get #(-> (u/locate-file id) :revn)} + :pages {:this true :get #(.getPages ^js %)} diff --git a/frontend/src/app/plugins/flags.cljs b/frontend/src/app/plugins/flags.cljs index ef0ca57ab6..b804df4d9c 100644 --- a/frontend/src/app/plugins/flags.cljs +++ b/frontend/src/app/plugins/flags.cljs @@ -6,17 +6,24 @@ (ns app.plugins.flags (:require + [app.common.data :as d] [app.main.store :as st] [app.plugins.utils :as u] [app.util.object :as obj] [potok.v2.core :as ptk])) -(defn clear - [id] - (ptk/reify ::reset +(defn initialize + "Initialize flags values for plugins" + [id version] + (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] - (update-in state [:plugins :flags] assoc id {})))) + (let [version (d/nilv version 1)] + (update-in state [:plugins :flags] assoc id + {:natural-child-ordering false + ;; For version >= 2 harden the contract by throwing errors + ;; on validation failures + :throw-validation-errors (>= version 2)}))))) (defn- set-flag [id key value] diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index 306db90698..2ae009ce9d 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -64,13 +64,13 @@ (u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active") :else - (let [id (obj/get text "$id") + (let [text-id (obj/get text "$id") values {:font-id id :font-family family :font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant)) :font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant)) :font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}] - (st/emit! (dwt/update-attrs id values))))) + (st/emit! (dwt/update-attrs text-id values))))) :applyToRange (fn [range variant] @@ -85,15 +85,15 @@ (u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active") :else - (let [id (obj/get range "$id") - start (obj/get range "start") - end (obj/get range "end") + (let [range-id (obj/get range "$id") + start (obj/get range "$start") + end (obj/get range "$end") values {:font-id id :font-family family :font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant)) :font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant)) :font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}] - (st/emit! (dwt/update-text-range id start end values))))))))) + (st/emit! (dwt/update-text-range range-id start end values))))))))) (defn fonts-subcontext [plugin-id] diff --git a/frontend/src/app/plugins/format.cljs b/frontend/src/app/plugins/format.cljs index 36137f1307..e920793434 100644 --- a/frontend/src/app/plugins/format.cljs +++ b/frontend/src/app/plugins/format.cljs @@ -47,6 +47,7 @@ :frame "board" :rect "rectangle" :circle "ellipse" + :bool "boolean" (d/name type))) ;;export type Bounds = { @@ -146,7 +147,7 @@ [[color attrs]] (let [shapes-info (apply array (map format-shape-info attrs)) color (format-color color)] - (obj/set! color "shapeInfo" shapes-info) + (obj/set! color "shapesInfo" shapes-info) color)) diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index 5582b80d0c..6964a034dd 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -301,11 +301,15 @@ :addRowAtIndex (fn [index type value] - (let [type (keyword type)] + (let [type (keyword type) + num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)] (cond (not (sm/valid-safe-int? index)) (u/not-valid plugin-id :addRowAtIndex-index index) + (or (< index 0) (> index num-rows)) + (u/not-valid plugin-id :addRowAtIndex-index index) + (not (contains? ctl/grid-track-types type)) (u/not-valid plugin-id :addRowAtIndex-type type) @@ -344,64 +348,80 @@ :addColumnAtIndex (fn [index type value] - (cond - (not (sm/valid-safe-int? index)) - (u/not-valid plugin-id :addColumnAtIndex-index index) + (let [type (keyword type) + num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)] + (cond + (not (sm/valid-safe-int? index)) + (u/not-valid plugin-id :addColumnAtIndex-index index) - (not (contains? ctl/grid-track-types type)) - (u/not-valid plugin-id :addColumnAtIndex-type type) + (or (< index 0) (> index num-columns)) + (u/not-valid plugin-id :addColumnAtIndex-index index) - (and (or (= :percent type) (= :flex type) (= :fixed type)) - (not (sm/valid-safe-number? value))) - (u/not-valid plugin-id :addColumnAtIndex-value value) + (not (contains? ctl/grid-track-types type)) + (u/not-valid plugin-id :addColumnAtIndex-type type) - (not (r/check-permission plugin-id "content:write")) - (u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission") + (and (or (= :percent type) (= :flex type) (= :fixed type)) + (not (sm/valid-safe-number? value))) + (u/not-valid plugin-id :addColumnAtIndex-value value) - (not (u/page-active? page-id)) - (u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active") + (not (r/check-permission plugin-id "content:write")) + (u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission") - :else - (let [type (keyword type)] + (not (u/page-active? page-id)) + (u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active") + + :else (st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index))))) :removeRow (fn [index] - (cond - (not (sm/valid-safe-int? index)) - (u/not-valid plugin-id :removeRow index) + (let [num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)] + (cond + (not (sm/valid-safe-int? index)) + (u/not-valid plugin-id :removeRow index) - (not (r/check-permission plugin-id "content:write")) - (u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission") + (or (< index 0) (>= index num-rows)) + (u/not-valid plugin-id :removeRow index) - (not (u/page-active? page-id)) - (u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active") + (not (r/check-permission plugin-id "content:write")) + (u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission") - :else - (st/emit! (dwsl/remove-layout-track #{id} :row index)))) + (not (u/page-active? page-id)) + (u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active") + + :else + (st/emit! (dwsl/remove-layout-track #{id} :row index))))) :removeColumn (fn [index] - (cond - (not (sm/valid-safe-int? index)) - (u/not-valid plugin-id :removeColumn index) + (let [num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)] + (cond + (not (sm/valid-safe-int? index)) + (u/not-valid plugin-id :removeColumn index) - (not (r/check-permission plugin-id "content:write")) - (u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission") + (or (< index 0) (>= index num-columns)) + (u/not-valid plugin-id :removeColumn index) - (not (u/page-active? page-id)) - (u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active") + (not (r/check-permission plugin-id "content:write")) + (u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission") - :else - (st/emit! (dwsl/remove-layout-track #{id} :column index)))) + (not (u/page-active? page-id)) + (u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active") + + :else + (st/emit! (dwsl/remove-layout-track #{id} :column index))))) :setColumn (fn [index type value] - (let [type (keyword type)] + (let [type (keyword type) + num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)] (cond (not (sm/valid-safe-int? index)) (u/not-valid plugin-id :setColumn-index index) + (or (< index 0) (>= index num-columns)) + (u/not-valid plugin-id :setColumn-index index) + (not (contains? ctl/grid-track-types type)) (u/not-valid plugin-id :setColumn-type type) @@ -420,11 +440,15 @@ :setRow (fn [index type value] - (let [type (keyword type)] + (let [type (keyword type) + num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)] (cond (not (sm/valid-safe-int? index)) (u/not-valid plugin-id :setRow-index index) + (or (< index 0) (>= index num-rows)) + (u/not-valid plugin-id :setRow-index index) + (not (contains? ctl/grid-track-types type)) (u/not-valid plugin-id :setRow-type type) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 2e5b5c434c..dccce39b97 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -51,6 +51,7 @@ :$file {:enumerable false :get (constantly file-id)} :id {:get (fn [] (dm/str id))} + :libraryId {:get (fn [] (dm/str file-id))} :fileId {:get #(dm/str file-id)} :name @@ -101,7 +102,8 @@ :else (let [color (-> (u/proxy->library-color self) - (assoc :color value))] + (assoc :color value) + (dissoc :gradient :image))] (st/emit! (dwl/update-color-data color file-id)))))} :opacity @@ -136,7 +138,8 @@ :else (let [color (-> (u/proxy->library-color self) - (assoc :gradient value))] + (assoc :gradient value) + (dissoc :color :image))] (st/emit! (dwl/update-color-data color file-id))))))} :image @@ -154,7 +157,8 @@ :else (let [color (-> (u/proxy->library-color self) - (assoc :image value))] + (assoc :image value) + (dissoc :color :gradient))] (st/emit! (dwl/update-color-data color file-id))))))} :remove @@ -295,6 +299,7 @@ :$id {:enumerable false :get (constantly id)} :$file {:enumerable false :get (constantly file-id)} :id {:get (fn [] (dm/str id))} + :libraryId {:get (fn [] (dm/str file-id))} :name {:this true @@ -484,6 +489,27 @@ (assoc :text-transform value))] (st/emit! (dwl/update-typography typo file-id)))))} + :setFont + (fn [font variant] + (cond + (not (obj/type-of? font "FontProxy")) + (u/not-valid plugin-id :setFont font) + + (not (r/check-permission plugin-id "library:write")) + (u/not-valid plugin-id :setFont "Plugin doesn't have 'library:write' permission") + + :else + ;; When a variant is given read the variant-specific fields from it; + ;; otherwise the FontProxy exposes the font's default variant fields. + (let [source (if (obj/type-of? variant "FontVariantProxy") variant font) + typo (-> (u/locate-library-typography file-id id) + (assoc :font-id (obj/get font "fontId") + :font-family (obj/get font "fontFamily") + :font-variant-id (obj/get source "fontVariantId") + :font-style (obj/get source "fontStyle") + :font-weight (obj/get source "fontWeight")))] + (st/emit! (dwl/update-typography typo file-id))))) + :remove (fn [] (cond @@ -539,8 +565,8 @@ :else (let [shape-id (obj/get range "$id") - start (obj/get range "start") - end (obj/get range "end") + start (obj/get range "$start") + end (obj/get range "$end") typography (u/locate-library-typography file-id id) attrs (-> typography (assoc :typography-ref-file file-id) @@ -718,6 +744,7 @@ :$id {:enumerable false :get (constantly id)} :$file {:enumerable false :get (constantly file-id)} :id {:get (fn [] (dm/str id))} + :libraryId {:get (fn [] (dm/str file-id))} :name {:this true diff --git a/frontend/src/app/plugins/local_storage.cljs b/frontend/src/app/plugins/local_storage.cljs index 4a9a4967a8..1b24d520ed 100644 --- a/frontend/src/app/plugins/local_storage.cljs +++ b/frontend/src/app/plugins/local_storage.cljs @@ -60,7 +60,7 @@ (u/not-valid plugin-id :removeItem "The key must be a string") :else - (.getItem ^js local-storage (prefix-key plugin-id key)))) + (.removeItem ^js local-storage (prefix-key plugin-id key)))) :getKeys (fn [] diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index 27755844ad..9ab02e3b6c 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -412,8 +412,7 @@ (js/Promise. (fn [resolve] (let [thread-id (obj/get thread "$id")] - (js/Promise. - (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))) + (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve)))))))) :findCommentThreads (fn [criteria] diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 3d6b481b50..0c3c2af32a 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -343,10 +343,10 @@ (when (some? guide) (case (obj/get guide "type") "column" - parse-frame-guide-column + (parse-frame-guide-column guide) "row" - parse-frame-guide-row + (parse-frame-guide-row guide) "square" (parse-frame-guide-square guide)))) @@ -489,7 +489,7 @@ :destination (-> (obj/get action "destination") (obj/get "$id")) :relative-to (-> (obj/get action "relativeTo") (obj/get "$id")) :overlay-pos-type (-> (obj/get action "position") parse-keyword) - :overlay-position (-> (obj/get action "manualPositionLocation") parse-point) + :overlay-position (-> (obj/get action "manualPositionLocation") parse-point (d/nilv (gpt/point 0 0))) :close-click-outside (obj/get action "closeWhenClickOutside") :background-overlay (obj/get action "addBackgroundOverlay") :animation (-> (obj/get action "animation") parse-animation)} diff --git a/frontend/src/app/plugins/public_utils.cljs b/frontend/src/app/plugins/public_utils.cljs index 1f7c92f8cd..3a303fe888 100644 --- a/frontend/src/app/plugins/public_utils.cljs +++ b/frontend/src/app/plugins/public_utils.cljs @@ -14,10 +14,15 @@ [app.plugins.utils :as u])) (defn ^:export centerShapes - [plugin-id shapes] + [shapes] (cond (not (every? shape/shape-proxy? shapes)) - (u/not-valid plugin-id :centerShapes shapes) + (u/not-valid nil :centerShapes shapes) + + ;; The documented contract returns null for an empty array; without this + ;; guard `shapes->rect` yields a non-rect and `rect->center` asserts. + (empty? shapes) + nil :else (let [shapes (->> shapes (map u/proxy->shape))] diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 882f8eca05..cccc32c625 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -101,7 +101,7 @@ :else (st/emit! (dwi/update-interaction - {:id shape-id} + (u/locate-shape file-id page-id shape-id) index #(assoc % :event-type value) {:page-id page-id})))))} @@ -117,7 +117,7 @@ :else (st/emit! (dwi/update-interaction - {:id shape-id} + (u/locate-shape file-id page-id shape-id) index #(assoc % :delay value) {:page-id page-id}))))} @@ -137,7 +137,7 @@ :else (st/emit! (dwi/update-interaction - {:id shape-id} + (u/locate-shape file-id page-id shape-id) index #(d/patch-object % params) {:page-id page-id})))))} @@ -592,7 +592,7 @@ :else (st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))} - :background-blur + :backgroundBlur {:this true :get #(-> % u/proxy->shape :background-blur format/format-blur) :set @@ -1249,6 +1249,11 @@ :else (st/emit! (dwg/unmask-group #{id}))))) + :isMask + (fn [] + (let [shape (u/locate-shape file-id page-id id)] + (boolean (cfh/mask-shape? shape)))) + ;; Only for path and bool shapes :toD (fn [] @@ -1315,19 +1320,19 @@ :bringForward (fn [] - (st/emit! (dw/vertical-order-selected :up))) + (st/emit! (dw/vertical-order-selected :up [id]))) :sendBackward (fn [] - (st/emit! (dw/vertical-order-selected :down))) + (st/emit! (dw/vertical-order-selected :down [id]))) :bringToFront (fn [] - (st/emit! (dw/vertical-order-selected :top))) + (st/emit! (dw/vertical-order-selected :top [id]))) :sendToBack (fn [] - (st/emit! (dw/vertical-order-selected :bottom))) + (st/emit! (dw/vertical-order-selected :bottom [id]))) ;; COMPONENTS :isComponentInstance @@ -1402,6 +1407,28 @@ :else (st/emit! (dwl/detach-component id)))) + :swapComponent + (fn [component] + (let [shape (u/locate-shape file-id page-id id)] + (cond + (not (u/page-active? page-id)) + (u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active") + + (not (r/check-permission plugin-id "content:write")) + (u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission") + + (not (obj/type-of? component "LibraryComponentProxy")) + (u/not-valid plugin-id :swapComponent "Component not valid") + + (not (ctk/in-component-copy? shape)) + (u/not-valid plugin-id :swapComponent "The shape is not a component copy instance") + + :else + (st/emit! (dwl/component-swap shape + (obj/get component "$file") + (obj/get component "$id") + true))))) + ;; Export :export (fn [value] @@ -1536,7 +1563,7 @@ (rg/ruler-guide-proxy plugin-id file-id page-id ruler-id))))) :removeRulerGuide - (fn [_ value] + (fn [value] (cond (not (rg/ruler-guide-proxy? value)) (u/not-valid plugin-id :removeRulerGuide "Guide not provided") @@ -1618,7 +1645,7 @@ :else (let [ids - (into #{id} (keep uuid/parse*) id) + (into #{id} (keep uuid/parse*) ids) valid? (every? @@ -1740,7 +1767,7 @@ (let [id (obj/get self "$id") value (parser/parse-frame-guides value)] (cond - (not (sm/validate [:vector ::ctg/grid] value)) + (not (sm/validate [:vector ctg/schema:grid] value)) (u/not-valid plugin-id :guides value) (not (r/check-permission plugin-id "content:write")) diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs index 597833619b..ffe3ec0a91 100644 --- a/frontend/src/app/plugins/text.cljs +++ b/frontend/src/app/plugins/text.cljs @@ -78,7 +78,7 @@ taking? (or taking? (and (<= from start) (< start to))) text (subs text (max 0 (- start acc)) (- end acc)) result (cond-> result - (and taking? (d/not-empty? text)) + (and taking? (seq text)) (conj (assoc node-style :text text))) continue? (or (> from end) (>= end to))] (recur (when continue? (rest styles)) taking? to result)) @@ -95,10 +95,11 @@ :$id {:enumerable false :get (constantly id)} :$file {:enumerable false :get (constantly file-id)} :$page {:enumerable false :get (constantly page-id)} + :$start {:enumerable false :get (constantly start)} + :$end {:enumerable false :get (constantly end)} :shape - {:this true - :get #(-> % u/proxy->shape)} + {:get (fn [] (format/shape-proxy plugin-id file-id page-id id))} :characters {:this true diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index e18ef5f5b5..a9c27c9649 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -96,13 +96,83 @@ :expand-with-children false}) (se/add-event plugin-id)))))) +(defn- typography-resolved-value->js + "Converts a resolved typography composite (a Clojure map keyed by the + tokenscript field names) into the plugin's `TokenTypographyValue[]` shape: a + JS array with a single object using the public camelCase member names." + [m] + (when (map? m) + #js [#js {"fontFamilies" (clj->js (:font-family m)) + "fontSizes" (:font-size m) + "fontWeights" (some-> (:font-weight m) str) + "letterSpacing" (:letter-spacing m) + "lineHeight" (:line-height m) + "textCase" (:text-case m) + "textDecoration" (:text-decoration m)}])) + +(defn- shadow-key->camel + "Renames a shadow composite field name (kebab string) to its public camelCase + member name. The shadow schema is closed; offset-x/offset-y are its only + multi-word fields, so the rest (blur, spread, color, inset) pass through." + [k] + (case k + "offset-x" "offsetX" + "offset-y" "offsetY" + k)) + +(defn- shadow-entry->js + "Converts one resolved shadow entry (a JS Map of field name -> tokenscript + symbol) into a plain JS object using the public member names and the + unit-converted values." + [^js m] + (let [out #js {}] + (.forEach m (fn [sym k] + (obj/set! out (shadow-key->camel k) + (ts/tokenscript-symbols->penpot-unit sym)))) + out)) + +(defn- shadow-resolved-value->js + "Converts a resolved shadow composite (a sequence of shadow entries) into the + plugin's `TokenShadowValue[]` shape." + [entries] + (when (some? entries) + (into-array (map shadow-entry->js entries)))) + +(defn- font-families-resolved-value->js + "Converts a resolved fontFamilies value (a tokenscript list symbol) into the + documented `string[]` shape rather than leaking the raw tokenscript structure." + [resolved-value] + (let [v (ts/tokenscript-symbols->penpot-unit resolved-value)] + (cond + (nil? v) nil + (sequential? v) (clj->js v) + :else #js [v]))) + (defn- get-resolved-value [token tokens-tree] (let [resolved-tokens (ts/resolve-tokens tokens-tree) - resolved-value (-> resolved-tokens - (dm/get-in [(:name token) :resolved-value]) - (ts/tokenscript-symbols->penpot-unit))] - resolved-value)) + resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])] + (cond + (= :font-family (:type token)) + ;; A fontFamilies token resolves to a list of families; expose it as the + ;; documented `string[]` rather than the raw tokenscript list symbol. + (font-families-resolved-value->js resolved-value) + + (= :typography (:type token)) + ;; A typography token resolves to a composite; expose it as the documented + ;; `TokenTypographyValue[]` rather than the raw tokenscript structure. + (typography-resolved-value->js + (ts/tokenscript-symbols->penpot-unit resolved-value)) + + (= :shadow (:type token)) + ;; A shadow token resolves to a list of composites whose entries the + ;; tokenscript unit conversion leaves as raw symbols; expose them as the + ;; documented `TokenShadowValue[]`. + (shadow-resolved-value->js + (ts/tokenscript-symbols->penpot-unit resolved-value)) + + :else + (ts/tokenscript-symbols->penpot-unit resolved-value)))) (defn token-proxy? [p] (obj/type-of? p "TokenProxy")) @@ -150,11 +220,21 @@ (fn [_] (let [token (u/locate-token file-id set-id id)] (json/->js (:value token)))) - :schema (let [token (u/locate-token file-id set-id id)] - (cfo/make-token-value-schema (:type token))) + :schema (let [token (u/locate-token file-id set-id id) + base (cfo/make-token-value-schema (:type token))] + ;; plugin-types declares the fontFamilies value as + ;; `string | string[]`, but the core schema only accepts a + ;; vector/ref; also accept a plain string (normalized in :set). + (if (= :font-family (:type token)) + [:or :string base] + base)) :set (fn [_ value] - (st/emit! (dwtl/update-token set-id id {:value value})))} + (let [token (u/locate-token file-id set-id id) + value (cond-> value + (= :font-family (:type token)) + (ctob/convert-dtcg-font-family))] + (st/emit! (dwtl/update-token set-id id {:value value}))))} :resolvedValue {:this true @@ -361,7 +441,10 @@ :duplicate (fn [] - (st/emit! (dwtl/duplicate-token-set id))) + (let [id-ref (atom nil)] + (st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref})) + (when (some? @id-ref) + (token-set-proxy plugin-id file-id @id-ref)))) :remove (fn [] @@ -460,7 +543,7 @@ ;; Guard against nil to prevent `enable-set` from conj'ing nil ;; into the theme's :sets — which would send `:sets #{nil}` to the ;; backend and crash the workspace. - (let [set-name (obj/get token-set :name) + (let [set-name (obj/get token-set "name") theme (u/locate-token-theme file-id id)] (when (and (some? set-name) (some? theme)) (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))} @@ -470,7 +553,7 @@ :schema [:tuple [:fn token-set-proxy?]] :fn (fn [token-set] ;; Same nil guard as addSet — see comment above. - (let [set-name (obj/get token-set :name) + (let [set-name (obj/get token-set "name") theme (u/locate-token-theme file-id id)] (when (and (some? set-name) (some? theme)) (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))} diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index e08e792ada..2e0db5d324 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -323,14 +323,27 @@ message to the console." [plugin-id] (fn [cause] - (let [message - (if-let [explain (-> cause ex-data ::sm/explain)] - (do - (js/console.error (sm/humanize-explain explain)) - (error-messages explain)) - (ex-data cause))] - (js/console.log (.-stack cause)) - (not-valid plugin-id :error message)))) + (let [explain (-> cause ex-data ::sm/explain) + throw? (throw-validation-errors? plugin-id)] + (cond + ;; If it's a clojure error we throw as a validation error + (and throw? explain) + (throw-not-valid :error (error-messages explain)) + + ;; Unexpected errors we just propagate them + throw? + (throw cause) + + ;; If not throw is active we log the caught error + :else + (let [message + (if explain + (do + (js/console.error (sm/humanize-explain explain)) + (error-messages explain)) + (ex-data cause))] + (js/console.log (.-stack cause)) + (not-valid plugin-id :error message)))))) (defn is-main-component-proxy? [p] diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index d99324647c..3ddf63d3d9 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1073,66 +1073,75 @@ (defn set-grid-layout-rows [entries] - (let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE) - offset (mem/alloc size) - dview (mem/get-data-view)] + ;; Only allocate when there are entries; an empty list would alloc 0 bytes. + ;; The wasm side reads an empty buffer as zero rows. + (when (seq entries) + (let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE) + offset (mem/alloc size) + dview (mem/get-data-view)] - (reduce (fn [offset {:keys [type value]}] - (-> offset - (mem/write-u8 dview (sr/translate-grid-track-type type)) - (+ 3) ;; padding - (mem/write-f32 dview value) - (mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE))) + (reduce (fn [offset {:keys [type value]}] + (-> offset + (mem/write-u8 dview (sr/translate-grid-track-type type)) + (+ 3) ;; padding + (mem/write-f32 dview value) + (mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE))) - offset - entries) + offset + entries))) - (h/call wasm/internal-module "_set_grid_rows"))) + (h/call wasm/internal-module "_set_grid_rows")) (defn set-grid-layout-columns [entries] - (let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE) - offset (mem/alloc size) - dview (mem/get-data-view)] + ;; Only allocate when there are entries; an empty list would alloc 0 bytes. + ;; The wasm side reads an empty buffer as zero columns. + (when (seq entries) + (let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE) + offset (mem/alloc size) + dview (mem/get-data-view)] - (reduce (fn [offset {:keys [type value]}] - (-> offset - (mem/write-u8 dview (sr/translate-grid-track-type type)) - (+ 3) ;; padding - (mem/write-f32 dview value) - (mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE))) - offset - entries) + (reduce (fn [offset {:keys [type value]}] + (-> offset + (mem/write-u8 dview (sr/translate-grid-track-type type)) + (+ 3) ;; padding + (mem/write-f32 dview value) + (mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE))) + offset + entries))) - (h/call wasm/internal-module "_set_grid_columns"))) + (h/call wasm/internal-module "_set_grid_columns")) (defn set-grid-layout-cells [cells] - (let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE) - offset (mem/alloc size) - dview (mem/get-data-view)] + ;; Only allocate when there are cells; an empty collection would alloc 0 + ;; bytes. The wasm side reads an empty buffer as zero cells. + (when (seq cells) + (let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE) + offset (mem/alloc size) + dview (mem/get-data-view)] - (reduce-kv (fn [offset _ cell] - (let [shape-id (-> (get cell :shapes) first)] - (-> offset - (mem/write-i32 dview (get cell :row)) - (mem/write-i32 dview (get cell :row-span)) - (mem/write-i32 dview (get cell :column)) - (mem/write-i32 dview (get cell :column-span)) + (reduce-kv (fn [offset _ cell] + (let [shape-id (-> (get cell :shapes) first)] + (-> offset + (mem/write-i32 dview (get cell :row)) + (mem/write-i32 dview (get cell :row-span)) + (mem/write-i32 dview (get cell :column)) + (mem/write-i32 dview (get cell :column-span)) - (mem/write-u8 dview (sr/translate-align-self (get cell :align-self))) - (mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self))) + (mem/write-u8 dview (sr/translate-align-self (get cell :align-self))) + (mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self))) - ;; padding - (+ 2) + ;; padding + (+ 2) - (mem/write-uuid dview (d/nilv shape-id uuid/zero)) - (mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE)))) + (mem/write-uuid dview (d/nilv shape-id uuid/zero)) + (mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE)))) - offset - cells) + offset + cells))) - (h/call wasm/internal-module "_set_grid_cells"))) + (h/call wasm/internal-module "_set_grid_cells")) (defn set-grid-layout [shape] diff --git a/frontend/test/frontend_tests/helpers/mock.cljc b/frontend/test/frontend_tests/helpers/mock.cljc index fad0e8c3d1..50f4d49d29 100644 --- a/frontend/test/frontend_tests/helpers/mock.cljc +++ b/frontend/test/frontend_tests/helpers/mock.cljc @@ -131,6 +131,39 @@ [_ms] (rx/of :immediate)) + ;; Static-dispatch-safe stubs + ;; ═══════════════════════════════════════════════════════════════ + ;; + ;; The `:esm` test build compiles calls to a *multi-arity* var as + ;; `f.cljs$core$IFn$_invoke$arity$N(...)`. A plain single-arity `fn` + ;; (including `identity`) does not expose that property, so using one + ;; to redefine such a var throws "arity$N is not a function". Multi-arity + ;; fns do expose the property, hence the helpers below. + + (defn noop + "Multi-arity no-op. Use to stub static-dispatched multi-arity vars + such as `st/emit!` (replacing `identity`, which is single-arity)." + ([] nil) + ([_] nil) + ([_ _] nil) + ([_ _ _] nil) + ([_ _ _ _] nil) + ([_ _ _ _ & _] nil)) + + (defn stub + "Wraps `f` in a multi-arity fn (arities 0-6) delegating to `f`, so the + result exposes `cljs$core$IFn$_invoke$arity$N`. Required when replacing + a multi-arity var in a `with-redefs`/`set!` mock with a capturing fn." + [f] + (fn + ([] (f)) + ([a] (f a)) + ([a b] (f a b)) + ([a b c] (f a b c)) + ([a b c d] (f a b c d)) + ([a b c d e] (f a b c d e)) + ([a b c d e g] (f a b c d e g)))) + ;; Lifecycle ;; ═══════════════════════════════════════════════════════════════ diff --git a/frontend/test/frontend_tests/plugins/comments_test.cljs b/frontend/test/frontend_tests/plugins/comments_test.cljs new file mode 100644 index 0000000000..ee19153a73 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/comments_test.cljs @@ -0,0 +1,60 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.comments-test + (:require + [app.main.data.comments :as dc] + [app.main.store :as st] + [app.plugins.comments :as comments] + [app.plugins.page :as page] + [app.plugins.register :as r] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock])) + +(def ^:private plugin-id "00000000-0000-0000-0000-000000000000") + +(t/deftest comment-thread-remove-allows-the-owner + (let [owner-id (random-uuid) + file-id (random-uuid) + page-id (random-uuid) + thread-id (random-uuid) + emitted (atom nil) + thread (comments/comment-thread-proxy + plugin-id + file-id + page-id + {:id thread-id :owner-id owner-id})] + (set! st/state (atom {:profile {:id owner-id}})) + (with-redefs [r/check-permission (constantly true) + dc/delete-comment-thread-on-workspace + (mock/stub (fn [params callback] + (callback) + [:delete-thread params])) + st/emit! (mock/stub (fn [event] (reset! emitted event)))] + (let [result (.remove thread)] + (t/is (instance? js/Promise result)) + (t/is (= [:delete-thread {:id thread-id}] @emitted)))))) + +(t/deftest page-remove-comment-thread-emits-delete-event + (let [file-id (random-uuid) + page-id (random-uuid) + thread-id (random-uuid) + emitted (atom nil) + page (page/page-proxy plugin-id file-id page-id) + thread (comments/comment-thread-proxy + plugin-id + file-id + page-id + {:id thread-id :owner-id (random-uuid)})] + (with-redefs [r/check-permission (constantly true) + dc/delete-comment-thread-on-workspace + (mock/stub (fn [params callback] + (callback) + [:delete-thread params])) + st/emit! (mock/stub (fn [event] (reset! emitted event)))] + (let [result (.removeCommentThread page thread)] + (t/is (instance? js/Promise result)) + (t/is (= [:delete-thread {:id thread-id}] @emitted)))))) diff --git a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs index dd1a7e3634..0bba39cd40 100644 --- a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs +++ b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs @@ -14,7 +14,8 @@ [app.util.object :as obj] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths] - [frontend-tests.helpers.wasm :as thw])) + [frontend-tests.helpers.wasm :as thw] + [potok.v2.core :as ptk])) (t/deftest test-common-shape-properties (thw/with-wasm-mocks* @@ -25,6 +26,7 @@ ^js context (api/create-context "00000000-0000-0000-0000-000000000000") _ (set! st/state store) + _ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true)) ^js file (. context -currentFile) ^js page (. context -currentPage) @@ -65,7 +67,7 @@ (t/is (= (.-x shape) 10)) (t/is (= (get-in @store (get-shape-path :x)) 10)) - (set! (.-x shape) "fail") + (t/is (thrown? js/Error (set! (.-x shape) "fail"))) (t/is (= (.-x shape) 10)) (t/is (= (get-in @store (get-shape-path :x)) 10))) @@ -74,7 +76,7 @@ (t/is (= (.-y shape) 50)) (t/is (= (get-in @store (get-shape-path :y)) 50)) - (set! (.-y shape) "fail") + (t/is (thrown? js/Error (set! (.-y shape) "fail"))) (t/is (= (.-y shape) 50)) (t/is (= (get-in @store (get-shape-path :y)) 50))) @@ -85,7 +87,7 @@ (t/is (= (get-in @store (get-shape-path :width)) 250)) (t/is (= (get-in @store (get-shape-path :height)) 300)) - (.resize shape 0 0) + (t/is (thrown? js/Error (.resize shape 0 0))) (t/is (= (.-width shape) 250)) (t/is (= (.-height shape) 300)) (t/is (= (get-in @store (get-shape-path :width)) 250)) @@ -115,7 +117,7 @@ (t/is (= (get-in @store (get-shape-path :proportion-lock)) true))) (t/testing " - constraintsHorizontal" - (set! (.-constraintsHorizontal shape) "fail") + (t/is (thrown? js/Error (set! (.-constraintsHorizontal shape) "fail"))) (t/is (not= (.-constraintsHorizontal shape) "fail")) (t/is (not= (get-in @store (get-shape-path :constraints-h)) "fail")) @@ -124,7 +126,7 @@ (t/is (= (get-in @store (get-shape-path :constraints-h)) :right))) (t/testing " - constraintsVertical" - (set! (.-constraintsVertical shape) "fail") + (t/is (thrown? js/Error (set! (.-constraintsVertical shape) "fail"))) (t/is (not= (.-constraintsVertical shape) "fail")) (t/is (not= (get-in @store (get-shape-path :constraints-v)) "fail")) @@ -175,7 +177,7 @@ (t/is (= (.-blendMode shape) "multiply")) (t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply)) - (set! (.-blendMode shape) "fail") + (t/is (thrown? js/Error (set! (.-blendMode shape) "fail"))) (t/is (= (.-blendMode shape) "multiply")) (t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply))) @@ -194,7 +196,7 @@ :color {:color "#fabada" :opacity 1} :hidden false}])))) (let [shadow #js {:style "fail"}] - (set! (.-shadows shape) #js [shadow]) + (t/is (thrown? js/Error (set! (.-shadows shape) #js [shadow]))) (t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow")))) (t/testing " - blur" @@ -211,7 +213,7 @@ (t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test")) (t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])) - (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}]) + (t/is (thrown? js/Error (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}]))) (t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))) (t/testing " - flipX" @@ -234,7 +236,7 @@ (t/is (= (get-in @store (get-shape-path :rotation)) 0))) (t/testing " - fills" - (set! (.-fills shape) #js [#js {:fillColor 100}]) + (t/is (thrown? js/Error (set! (.-fills shape) #js [#js {:fillColor 100}]))) (t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}])) (t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5")) diff --git a/frontend/test/frontend_tests/plugins/file_test.cljs b/frontend/test/frontend_tests/plugins/file_test.cljs new file mode 100644 index 0000000000..8d7c6b0d12 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/file_test.cljs @@ -0,0 +1,21 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.file-test + (:require + [app.plugins.file :as file] + [cljs.test :as t :include-macros true])) + +(t/deftest file-version-created-at-returns-stored-date + (let [created-at (js/Date.) + version (file/file-version-proxy + "00000000-0000-0000-0000-000000000000" + (random-uuid) + {} + {:id (random-uuid) + :label "Version" + :created-at created-at})] + (t/is (identical? created-at (.-createdAt version))))) diff --git a/frontend/test/frontend_tests/plugins/format_test.cljs b/frontend/test/frontend_tests/plugins/format_test.cljs index 2bc525b224..fd52f1f3fe 100644 --- a/frontend/test/frontend_tests/plugins/format_test.cljs +++ b/frontend/test/frontend_tests/plugins/format_test.cljs @@ -38,3 +38,17 @@ (format/format-frame-guides nil) (format/format-tracks nil) (format/format-path-content nil))) + +(t/deftest test-format-color-result-uses-shapes-info-key + (let [shape-id (random-uuid) + result (format/format-color-result + [{:color "#fabada"} + [{:prop :fill :shape-id shape-id :index 0}]]) + info (aget result "shapesInfo")] + (t/is (array? info)) + (t/is (nil? (aget result "shapesColors"))) + (t/is (= "fill" (aget (aget info 0) "property"))) + (t/is (= (str shape-id) (aget (aget info 0) "shapeId"))))) + +(t/deftest test-shape-type-reports-boolean + (t/is (= "boolean" (format/shape-type :bool)))) diff --git a/frontend/test/frontend_tests/plugins/grid_test.cljs b/frontend/test/frontend_tests/plugins/grid_test.cljs new file mode 100644 index 0000000000..035153ccde --- /dev/null +++ b/frontend/test/frontend_tests/plugins/grid_test.cljs @@ -0,0 +1,49 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.grid-test + (:require + [app.common.test-helpers.files :as cthf] + [app.main.store :as st] + [app.plugins.api :as api] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.wasm :as thw] + [potok.v2.core :as ptk])) + +(def ^:private plugin-id "00000000-0000-0000-0000-000000000000") + +(defn- setup-grid [] + (let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) + _ (set! st/state store) + _ (set! st/stream (ptk/input-stream store)) + context (api/create-context plugin-id) + board (.createBoard ^js context) + grid (.addGridLayout ^js board)] + {:store store :context context :board board :grid grid})) + +(t/deftest add-column-at-index-accepts-fixed-track-type + (thw/with-wasm-mocks* + (fn [] + (let [{:keys [^js grid]} (setup-grid)] + (.addColumn grid "flex" 1) + (.addColumnAtIndex grid 0 "fixed" 100) + (t/is (= "fixed" (aget (aget (.-columns grid) 0) "type"))) + (t/is (= 100 (aget (aget (.-columns grid) 0) "value"))))))) + +(t/deftest grid-track-methods-reject-out-of-range-indices + (thw/with-wasm-mocks* + (fn [] + (let [{:keys [store ^js grid]} (setup-grid)] + (swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true) + (.addRow grid "flex" 1) + (.addColumn grid "flex" 1) + (t/is (thrown? js/Error (.addRowAtIndex grid -1 "fixed" 10))) + (t/is (thrown? js/Error (.addColumnAtIndex grid 2 "fixed" 10))) + (t/is (thrown? js/Error (.setRow grid 1 "fixed" 10))) + (t/is (thrown? js/Error (.setColumn grid 1 "fixed" 10))) + (t/is (thrown? js/Error (.removeRow grid 1))) + (t/is (thrown? js/Error (.removeColumn grid 1))))))) diff --git a/frontend/test/frontend_tests/plugins/library_test.cljs b/frontend/test/frontend_tests/plugins/library_test.cljs new file mode 100644 index 0000000000..47d5869b1a --- /dev/null +++ b/frontend/test/frontend_tests/plugins/library_test.cljs @@ -0,0 +1,95 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.library-test + (:require + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.texts :as dwt] + [app.main.store :as st] + [app.plugins.library :as library] + [app.plugins.register :as r] + [app.plugins.text :as text] + [app.plugins.utils :as u] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock])) + +(def ^:private plugin-id "00000000-0000-0000-0000-000000000000") + +(t/deftest library-asset-proxies-expose-library-id + (let [file-id (random-uuid) + id (random-uuid)] + (t/is (= (str file-id) (.-libraryId (library/lib-color-proxy plugin-id file-id id)))) + (t/is (= (str file-id) (.-libraryId (library/lib-typography-proxy plugin-id file-id id)))) + (t/is (= (str file-id) (.-libraryId (library/lib-component-proxy plugin-id file-id id)))))) + +(t/deftest typography-apply-to-text-range-uses-hidden-range-bounds + (let [file-id (random-uuid) + page-id (random-uuid) + shape-id (random-uuid) + typography-id (random-uuid) + typography (library/lib-typography-proxy plugin-id file-id typography-id) + text-range (text/text-range-proxy plugin-id file-id page-id shape-id 2 5) + captured (atom nil)] + (with-redefs [r/check-permission (constantly true) + u/page-active? (constantly true) + u/locate-library-typography + (constantly {:id typography-id + :name "Body" + :font-size "14"}) + dwt/update-text-range + (fn [shape-id start end attrs] + (reset! captured {:shape-id shape-id + :start start + :end end + :attrs attrs}) + :update-text-range) + st/emit! mock/noop] + (.applyToTextRange typography text-range) + (t/is (= shape-id (:shape-id @captured))) + (t/is (= 2 (:start @captured))) + (t/is (= 5 (:end @captured))) + (t/is (= file-id (get-in @captured [:attrs :typography-ref-file]))) + (t/is (= typography-id (get-in @captured [:attrs :typography-ref-id])))))) + +(t/deftest library-color-gradient-and-image-clear-exclusive-representations + (let [file-id (random-uuid) + color-id (random-uuid) + proxy (library/lib-color-proxy plugin-id file-id color-id) + captured (atom nil) + base {:id color-id + :name "Brand" + :color "#fabada" + :opacity 1 + :gradient {:type :linear} + :image {:id (random-uuid) :width 1 :height 1}}] + (with-redefs [r/check-permission (constantly true) + u/proxy->library-color (constantly base) + dwl/update-color-data (fn [color file-id] + (reset! captured {:color color :file-id file-id}) + :update-color-data) + st/emit! mock/noop] + (set! (.-gradient proxy) + #js {:type "linear" + :startX 0 + :startY 0 + :endX 1 + :endY 1 + :width 1 + :stops #js [#js {:color "#000000" + :opacity 1 + :offset 0}]}) + (t/is (contains? (:color @captured) :gradient)) + (t/is (not (contains? (:color @captured) :color))) + (t/is (not (contains? (:color @captured) :image))) + + (set! (.-image proxy) + #js {:id (str (random-uuid)) + :width 10 + :height 20 + :mtype "image/png"}) + (t/is (contains? (:color @captured) :image)) + (t/is (not (contains? (:color @captured) :color))) + (t/is (not (contains? (:color @captured) :gradient)))))) diff --git a/frontend/test/frontend_tests/plugins/local_storage_test.cljs b/frontend/test/frontend_tests/plugins/local_storage_test.cljs new file mode 100644 index 0000000000..f1f5117ed2 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/local_storage_test.cljs @@ -0,0 +1,28 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.local-storage-test + (:require + [app.plugins.local-storage :as storage] + [app.plugins.register :as r] + [cljs.test :as t :include-macros true])) + +(t/deftest remove-item-removes-the-prefixed-key + (let [data (atom {}) + fake #js {} + plugin-id "plugin-a"] + (set! (.-getItem fake) (fn [key] (get @data key))) + (set! (.-setItem fake) (fn [key value] (swap! data assoc key value))) + (set! (.-removeItem fake) (fn [key] (swap! data dissoc key))) + (set! (.-keys fake) (fn [] (to-array (keys @data)))) + (with-redefs [r/check-permission (constantly true) + storage/local-storage fake] + (let [proxy (storage/local-storage-proxy plugin-id)] + (.setItem proxy "key" "value") + (t/is (= "value" (.getItem proxy "key"))) + (.removeItem proxy "key") + (t/is (nil? (.getItem proxy "key"))) + (t/is (empty? @data)))))) diff --git a/frontend/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs index bcc59c4ce8..d29149e846 100644 --- a/frontend/test/frontend_tests/plugins/page_test.cljs +++ b/frontend/test/frontend_tests/plugins/page_test.cljs @@ -32,6 +32,7 @@ store (ths/setup-store file) _ (set! st/state store) _ (set! st/stream (ptk/input-stream store)) + _ (ptk/emit! store #(assoc-in % [:plugins :flags "00000000-0000-0000-0000-000000000000" :throw-validation-errors] true)) context (api/create-context "00000000-0000-0000-0000-000000000000")] {:file file :store store :context context})) @@ -89,9 +90,11 @@ ^js page2 (aget pages 1)] (t/is (instance? js/Promise (.openPage context page2 true))))) -(t/deftest test-open-page-invalid-arg-returns-nil +(t/deftest test-open-page-invalid-arg-throws + ;; With throwValidationErrors enabled an invalid argument surfaces as an + ;; exception instead of being silently logged. (let [^js context (:context (setup))] - (t/is (nil? (.openPage context "not-a-page"))))) + (t/is (thrown? js/Error (.openPage context "not-a-page"))))) (t/deftest test-open-page-resolves-when-page-changes (t/async done diff --git a/frontend/test/frontend_tests/plugins/parser_test.cljs b/frontend/test/frontend_tests/plugins/parser_test.cljs index 4b257b2023..44abb8ebbd 100644 --- a/frontend/test/frontend_tests/plugins/parser_test.cljs +++ b/frontend/test/frontend_tests/plugins/parser_test.cljs @@ -31,3 +31,33 @@ (t/is (gpt/point? result)) (t/is (= 0 (:x result))) (t/is (= 0 (:y result)))))) + +(t/deftest test-parse-overlay-action-defaults-manual-position + (let [destination #js {"$id" (random-uuid)} + action (parser/parse-action + #js {:type "open-overlay" + :destination destination + :position "center"})] + (t/is (= :open-overlay (:action-type action))) + (t/is (= :center (:overlay-pos-type action))) + (t/is (gpt/point? (:overlay-position action))) + (t/is (= 0 (:x (:overlay-position action)))) + (t/is (= 0 (:y (:overlay-position action)))))) + +(t/deftest test-parse-frame-guide-calls-guide-parser + (let [column (parser/parse-frame-guide + #js {:type "column" + :display true + :params #js {:type "stretch" + :size 12}}) + row (parser/parse-frame-guide + #js {:type "row" + :display false + :params #js {:type "center" + :margin 4}})] + (t/is (= :column (:type column))) + (t/is (= true (:display column))) + (t/is (= :stretch (get-in column [:params :type]))) + (t/is (= :row (:type row))) + (t/is (= false (:display row))) + (t/is (= :center (get-in row [:params :type]))))) diff --git a/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs b/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs new file mode 100644 index 0000000000..bf9e77f325 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs @@ -0,0 +1,202 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.plugins.shape-bugfixes-test + (:require + [app.common.data :as d] + [app.common.test-helpers.files :as cthf] + [app.common.types.component :as ctk] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] + [app.main.data.workspace.variants :as dwv] + [app.main.store :as st] + [app.plugins.api :as api] + [app.plugins.public-utils :as public-utils] + [app.plugins.shape :as shape] + [app.plugins.utils :as u] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock] + [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.wasm :as thw])) + +(def ^:private plugin-id "00000000-0000-0000-0000-000000000000") + +;; --------------------------------------------------------------------------- +;; Helpers +;; --------------------------------------------------------------------------- + +(defn- child-shapes + "Ordered child shape ids of `board`, read back from the live store + (the observable result of a z-order operation)." + [store ^js context ^js board] + (let [file-id (aget (. context -currentFile) "$id") + page-id (aget (. context -currentPage) "$id") + board-id (aget board "$id")] + (get-in @store [:files file-id :data :pages-index page-id + :objects board-id :shapes]))) + +(defn- page-guides + "The guides map of the current page, read back from the live store." + [store ^js context] + (let [file-id (aget (. context -currentFile) "$id") + page-id (aget (. context -currentPage) "$id")] + (get-in @store [:files file-id :data :pages-index page-id :guides]))) + +;; --------------------------------------------------------------------------- +;; Tests +;; --------------------------------------------------------------------------- + +(t/deftest trigger-setter-updates-the-interaction-event-type + ;; Regression: the `trigger` setter must update the interaction of the + ;; located shape. Asserting on the observable interaction (read back through + ;; the proxy from the live store) covers that without coupling to which + ;; internal action gets emitted. + (thw/with-wasm-mocks* + (fn [] + (let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) + ^js context (api/create-context plugin-id) + _ (set! st/state store) + ^js board (.createBoard context)] + (.addInteraction board "click" #js {:type "open-url" :url "https://example.com"}) + (let [^js interaction (aget (.-interactions board) 0)] + (t/is (= "click" (.-trigger interaction)) + "the interaction starts with the click trigger") + (set! (.-trigger interaction) "mouse-over") + (t/is (= "mouse-over" (.-trigger interaction)) + "the trigger setter updates the interaction event-type")))))) + +(t/deftest center-shapes-empty-input-returns-nil + (t/is (nil? (public-utils/centerShapes #js [])))) + +(t/deftest background-blur-reads-background-blur-key + (let [file-id (uuid/next) + page-id (uuid/next) + shape-id (uuid/next) + blur-id (uuid/next) + proxy (shape/shape-proxy plugin-id file-id page-id shape-id)] + (with-redefs [u/proxy->shape (constantly {:background-blur {:id blur-id + :value 12 + :hidden false}})] + (let [blur (.-backgroundBlur proxy)] + (t/is (= (str blur-id) (aget blur "id"))) + (t/is (= 12 (aget blur "value"))))))) + +(t/deftest flatten-returns-proxies-for-converted-shapes + ;; `convert-selected-to-path` runs the WASM boolean/path pipeline, so this + ;; test stays at the proxy boundary: it verifies `flatten` forwards the + ;; selected ids to the conversion and wraps the result back into proxies. + (let [file-id (uuid/next) + page-id (uuid/next) + shape-id (uuid/next) + input (shape/shape-proxy plugin-id file-id page-id shape-id) + emitted (atom nil) + context (api/create-context plugin-id)] + (set! st/state (atom {:current-file-id file-id + :current-page-id page-id})) + (with-redefs [dw/convert-selected-to-path + (mock/stub (fn [ids] + (reset! emitted ids) + :convert-selected-to-path)) + st/emit! mock/noop + shape/shape-proxy + (mock/stub (fn [_plugin file page id] + #js {"$file" file "$page" page "$id" id}))] + (let [result (.flatten context #js [input])] + (t/is (= #{shape-id} @emitted)) + (t/is (array? result)) + (t/is (= shape-id (aget result 0 "$id"))) + (t/is (= file-id (aget result 0 "$file"))) + (t/is (= page-id (aget result 0 "$page"))))))) + +(t/deftest z-order-methods-reorder-the-shape-within-its-parent + ;; Asserts the observable child order in the parent after each z-order + ;; method, instead of merely checking which location keyword was emitted. + ;; The assertions are independent of the parent's `:shapes` ordering + ;; convention: a reorder is verified by relative movement and extremes. + (thw/with-wasm-mocks* + (fn [] + (let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) + ^js context (api/create-context plugin-id) + _ (set! st/state store) + ^js board (.createBoard context) + children (mapv (fn [_] (.createRectangle context)) (range 4)) + ids (mapv #(aget % "$id") children) + order #(child-shapes store context board)] + (doseq [^js c children] (.appendChild board c)) + + ;; Operate on a shape that is currently interior (so both a forward + ;; and a backward step are observable). + (let [mid-id (nth (order) 1) + ^js mid (nth children (d/index-of ids mid-id))] + + (t/testing "bringForward and sendBackward move in opposite directions" + (let [i0 (d/index-of (order) mid-id) + _ (.bringForward mid) + i1 (d/index-of (order) mid-id) + _ (.sendBackward mid) + i2 (d/index-of (order) mid-id)] + (t/is (not= i0 i1) "bringForward changes the order") + (t/is (not= i1 i2) "sendBackward changes the order") + (t/is (= (pos? (- i1 i0)) (neg? (- i2 i1))) + "the two steps move the shape in opposite directions"))) + + (t/testing "bringToFront and sendToBack move to opposite extremes" + (let [n (count (order)) + _ (.bringToFront mid) + p1 (d/index-of (order) mid-id) + _ (.sendToBack mid) + p2 (d/index-of (order) mid-id)] + (t/is (contains? #{0 (dec n)} p1) "bringToFront moves to an extreme") + (t/is (contains? #{0 (dec n)} p2) "sendToBack moves to an extreme") + (t/is (not= p1 p2) "front and back are opposite extremes")))))))) + +(t/deftest is-variant-container-predicate-returns-boolean + (t/is (false? (ctk/is-variant-container? {}))) + (t/is (true? (ctk/is-variant-container? {:is-variant-container true})))) + +(t/deftest combine-as-variants-uses-the-passed-component-ids + ;; `combine-as-variants` needs real main components and the variant pipeline, + ;; so this stays at the proxy boundary and verifies the component ids that + ;; the head proxy collects from its argument before delegating. + (let [file-id (uuid/next) + page-id (uuid/next) + head-id (uuid/next) + other-id (uuid/next) + proxy (shape/shape-proxy plugin-id file-id page-id head-id) + captured (atom nil)] + (with-redefs [u/locate-shape (fn [_file _page id] {:id id :component-id id}) + u/locate-library-component (constantly {:id (uuid/next)}) + ctk/is-variant? (constantly false) + dwv/combine-as-variants + (fn [ids opts] + (reset! captured {:ids ids :opts opts}) + ;; return value flows through `se/add-event` (which + ;; calls `with-meta`), so it must support metadata + {:event :combine-as-variants}) + st/emit! mock/noop + shape/shape-proxy (mock/stub (fn [& _] #js {}))] + (.combineAsVariants proxy #js [(str other-id)]) + (t/is (= #{head-id other-id} (:ids @captured)))))) + +(t/deftest remove-ruler-guide-deletes-the-guide-from-the-page + ;; Adds a real ruler guide through the API and asserts it is gone from the + ;; page guides after removeRulerGuide, rather than checking the removal call. + (thw/with-wasm-mocks* + (fn [] + (let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) + ^js context (api/create-context plugin-id) + _ (set! st/state store) + ^js board (.createBoard context) + ^js guide (.addRulerGuide board "horizontal" 10)] + (t/is (= 1 (count (page-guides store context))) + "addRulerGuide stores one guide on the page") + (.removeRulerGuide board guide) + (t/is (empty? (page-guides store context)) + "removeRulerGuide deletes the guide from the page"))))) + +(t/deftest group-empty-input-returns-nil + (let [context (api/create-context plugin-id)] + (t/is (nil? (.group context #js []))))) diff --git a/frontend/test/frontend_tests/plugins/text_test.cljs b/frontend/test/frontend_tests/plugins/text_test.cljs index edcfbf853d..1b1decdcdb 100644 --- a/frontend/test/frontend_tests/plugins/text_test.cljs +++ b/frontend/test/frontend_tests/plugins/text_test.cljs @@ -6,8 +6,18 @@ (ns frontend-tests.plugins.text-test (:require + [app.main.data.workspace.texts :as dwt] + [app.main.store :as st] + [app.plugins.fonts :as fonts] + [app.plugins.format :as format] + [app.plugins.register :as r] + [app.plugins.shape :as shape] [app.plugins.text :as plugins.text] - [cljs.test :as t :include-macros true])) + [app.plugins.utils :as u] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock])) + +(def ^:private plugin-id "00000000-0000-0000-0000-000000000000") ;; Regression coverage for issue #9780. ;; @@ -35,3 +45,71 @@ (t/is (not (valid? "abc"))) (t/is (not (valid? "1-2"))) (t/is (not (valid? "--1")))) + + +(t/deftest font-apply-to-text-uses-font-id-not-shape-id + (let [file-id (random-uuid) + page-id (random-uuid) + shape-id (random-uuid) + font (fonts/font-proxy + plugin-id + {:id "font-id" + :family "Inter" + :name "Inter" + :variants [{:id "regular" + :name "Regular" + :weight "400" + :style "normal"}]}) + text (shape/shape-proxy plugin-id file-id page-id shape-id) + captured (atom nil)] + (with-redefs [r/check-permission (constantly true) + u/page-active? (constantly true) + dwt/update-attrs + (fn [id attrs] + (reset! captured {:id id :attrs attrs}) + :update-attrs) + st/emit! mock/noop] + (.applyToText font text nil) + (t/is (= shape-id (:id @captured))) + (t/is (= "font-id" (get-in @captured [:attrs :font-id])))))) + +(t/deftest font-apply-to-range-uses-hidden-range-bounds + (let [file-id (random-uuid) + page-id (random-uuid) + shape-id (random-uuid) + font (fonts/font-proxy + plugin-id + {:id "font-id" + :family "Inter" + :name "Inter" + :variants [{:id "regular" + :name "Regular" + :weight "400" + :style "normal"}]}) + range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 1 4) + captured (atom nil)] + (with-redefs [r/check-permission (constantly true) + u/page-active? (constantly true) + dwt/update-text-range + (fn [id start end attrs] + (reset! captured {:id id + :start start + :end end + :attrs attrs}) + :update-text-range) + st/emit! mock/noop] + (.applyToRange font range nil) + (t/is (= shape-id (:id @captured))) + (t/is (= 1 (:start @captured))) + (t/is (= 4 (:end @captured))) + (t/is (= "font-id" (get-in @captured [:attrs :font-id])))))) + +(t/deftest text-range-shape-returns-a-shape-proxy + (let [file-id (random-uuid) + page-id (random-uuid) + shape-id (random-uuid) + range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 0 3)] + (with-redefs [format/shape-proxy shape/shape-proxy] + (let [text-shape (.-shape range)] + (t/is (shape/shape-proxy? text-shape)) + (t/is (= shape-id (aget text-shape "$id"))))))) diff --git a/frontend/test/frontend_tests/plugins/tokens_test.cljs b/frontend/test/frontend_tests/plugins/tokens_test.cljs index c45789b6ca..f95ba1d811 100644 --- a/frontend/test/frontend_tests/plugins/tokens_test.cljs +++ b/frontend/test/frontend_tests/plugins/tokens_test.cljs @@ -12,15 +12,20 @@ [app.common.test-helpers.tokens :as ctht] [app.common.types.tokens-lib :as ctob] [app.main.data.tokenscript :as ts] + [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.store :as st] [app.plugins.api :as api] [app.plugins.tokens :as ptok] + [app.plugins.utils :as u] [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock] [frontend-tests.helpers.state :as ths] [potok.v2.core :as ptk])) (t/use-fixtures :each {:before cthi/reset-idmap!}) +(def ^:private get-resolved-value @#'ptok/get-resolved-value) + ;; Regression coverage for issue #9162. ;; ;; Plugin code calling `shape.applyToken(token, ["fill"])` or @@ -226,3 +231,110 @@ {:keys [errors resolved-value]} (get resolved (:name token))] (t/is (nil? resolved-value)) (t/is (seq errors)))) + +(t/deftest token-set-duplicate-returns-the-duplicated-set + (let [file-id (cthi/new-id! :file) + set-id (cthi/new-id! :set) + dup-id (cthi/new-id! :dup) + proxy (ptok/token-set-proxy "plugin-id" file-id set-id)] + (with-redefs [dwtl/duplicate-token-set + (mock/stub (fn [id {:keys [id-ref]}] + (t/is (= set-id id)) + (reset! id-ref dup-id) + :duplicate-token-set)) + st/emit! mock/noop] + (let [dup (.duplicate proxy)] + (t/is (ptok/token-set-proxy? dup)) + (t/is (= (str dup-id) (.-id dup))))))) + +(t/deftest theme-add-set-and-remove-set-use-the-set-name + (let [file-id (cthi/new-id! :file) + theme-id (cthi/new-id! :theme) + set-id (cthi/new-id! :set) + set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives") + theme (ptok/token-theme-proxy "plugin-id" file-id theme-id) + captured (atom [])] + (with-redefs [u/locate-token-theme + (fn [_file _theme] + (ctob/make-token-theme :id theme-id + :name "Theme" + :sets #{"Primitives"})) + dwtl/update-token-theme + (fn [id theme] + (swap! captured conj {:id id :theme theme}) + :update-token-theme) + st/emit! identity] + (.addSet theme set) + (.removeSet theme set) + (t/is (= [theme-id theme-id] (mapv :id @captured))) + (t/is (contains? (-> @captured first :theme :sets) "Primitives")) + (t/is (not (contains? (-> @captured second :theme :sets) "Primitives")))))) + +(t/deftest font-family-token-value-accepts-a-string + (let [file-id (cthi/new-id! :file) + set-id (cthi/new-id! :set) + token-id (cthi/new-id! :token) + captured (atom nil)] + (with-redefs [u/locate-token (constantly {:id token-id + :name "font.primary" + :type :font-family + :value ["Inter"]}) + dwtl/update-token (mock/stub (fn [set-id token-id attrs] + (reset! captured {:set-id set-id + :token-id token-id + :attrs attrs}) + :update-token)) + st/emit! mock/noop] + (let [token (ptok/token-proxy "plugin-id" file-id set-id token-id)] + (set! (.-value token) "Inter, Arial") + (t/is (= set-id (:set-id @captured))) + (t/is (= token-id (:token-id @captured))) + (t/is (= ["Inter" "Arial"] (get-in @captured [:attrs :value]))))))) + +(t/deftest typography-token-resolved-value-is-plugin-array-shape + (let [token (ctob/make-token + {:name "type.body" + :type :typography + :value {:font-family ["Inter" "Arial"] + :font-size "16px" + :font-weight "600" + :line-height "20px" + :letter-spacing "1" + :text-case "uppercase" + :text-decoration "underline"}}) + result (get-resolved-value token {(:name token) token}) + entry (aget result 0)] + (t/is (array? result)) + (t/is (= ["Inter" "Arial"] (vec (aget entry "fontFamilies")))) + (t/is (= 16 (aget entry "fontSizes"))) + (t/is (= "600" (aget entry "fontWeights"))) + (t/is (= 20 (aget entry "lineHeight"))) + (t/is (= "uppercase" (aget entry "textCase"))) + (t/is (= "underline" (aget entry "textDecoration"))))) + +(t/deftest shadow-token-resolved-value-is-plugin-array-shape + (let [token (ctob/make-token + {:name "shadow.card" + :type :shadow + :value [{:offset-x "1px" + :offset-y "2px" + :blur "3px" + :spread "4px" + :color "#000000" + :inset false}]}) + result (get-resolved-value token {(:name token) token}) + entry (aget result 0)] + (t/is (array? result)) + (t/is (= 1 (aget entry "offsetX"))) + (t/is (= 2 (aget entry "offsetY"))) + (t/is (= 3 (aget entry "blur"))) + (t/is (= 4 (aget entry "spread"))))) + +(t/deftest font-family-token-resolved-value-is-string-array + (let [token (ctob/make-token + {:name "font.primary" + :type :font-family + :value ["Inter" "Arial"]}) + result (get-resolved-value token {(:name token) token})] + (t/is (array? result)) + (t/is (= ["Inter" "Arial"] (vec result))))) diff --git a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs index e20aa2e1bc..740029a3ca 100644 --- a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs @@ -16,6 +16,8 @@ in :fetching) and are permanently stuck with fallback-font layout metrics." (:require [app.render-wasm.api :as wasm.api] + [app.render-wasm.mem :as mem] + [app.render-wasm.wasm :as wasm] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true])) @@ -108,3 +110,21 @@ ;; process-pending fires update-text-layouts, it covers shape-b too. (t/is (= 2 (count (:shapes @captured))) "Both shapes are in process-pending so font-load covers all of them"))) + +(t/deftest empty-grid-tracks-do-not-allocate-zero-bytes + (let [calls (atom []) + ;; `h/call` is a macro that resolves the wasm function off the module + ;; via `unchecked-get`, so it cannot be redefined. Mock the module + ;; itself with recording stubs and let the real macro expansion run. + module #js {"_set_grid_rows" (fn [& _] (swap! calls conj [:call "_set_grid_rows"]) nil) + "_set_grid_columns" (fn [& _] (swap! calls conj [:call "_set_grid_columns"]) nil)}] + (with-redefs [mem/alloc (fn [size] + (swap! calls conj [:alloc size]) + 0) + wasm/internal-module module] + (wasm.api/set-grid-layout-rows []) + (wasm.api/set-grid-layout-columns [])) + (t/is (not-any? #(= :alloc (first %)) @calls)) + (t/is (= [[:call "_set_grid_rows"] + [:call "_set_grid_columns"]] + @calls)))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 57f755389a..2296fe8566 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -27,12 +27,18 @@ [frontend-tests.logic.groups-test] [frontend-tests.logic.pasting-in-containers-test] [frontend-tests.main-errors-test] + [frontend-tests.plugins.comments-test] [frontend-tests.plugins.context-shapes-test] + [frontend-tests.plugins.file-test] [frontend-tests.plugins.format-test] + [frontend-tests.plugins.grid-test] [frontend-tests.plugins.interactions-test] + [frontend-tests.plugins.library-test] + [frontend-tests.plugins.local-storage-test] [frontend-tests.plugins.page-active-validation-test] [frontend-tests.plugins.page-test] [frontend-tests.plugins.parser-test] + [frontend-tests.plugins.shape-bugfixes-test] [frontend-tests.plugins.text-test] [frontend-tests.plugins.tokens-test] [frontend-tests.plugins.utils-test] @@ -45,7 +51,9 @@ [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-errors-test] [frontend-tests.tokens.workspace-tokens-remap-test] + [frontend-tests.ui.comments-position-modifier-test] [frontend-tests.ui.ds-controls-numeric-input-test] + [frontend-tests.ui.measures-menu-props-test] [frontend-tests.util-object-test] [frontend-tests.util-range-tree-test] [frontend-tests.util-simple-math-test] @@ -65,7 +73,8 @@ (.exit js/process 1))) (def test-namespaces - ['frontend-tests.code-gen-style-test + ['frontend-tests.basic-shapes-test + 'frontend-tests.code-gen-style-test 'frontend-tests.copy-as-svg-test 'frontend-tests.data.nitrate-test 'frontend-tests.data.repo-test @@ -87,12 +96,18 @@ 'frontend-tests.logic.groups-test 'frontend-tests.logic.pasting-in-containers-test 'frontend-tests.main-errors-test + 'frontend-tests.plugins.comments-test 'frontend-tests.plugins.context-shapes-test + 'frontend-tests.plugins.file-test 'frontend-tests.plugins.format-test + 'frontend-tests.plugins.grid-test 'frontend-tests.plugins.interactions-test + 'frontend-tests.plugins.library-test + 'frontend-tests.plugins.local-storage-test 'frontend-tests.plugins.page-active-validation-test 'frontend-tests.plugins.page-test 'frontend-tests.plugins.parser-test + 'frontend-tests.plugins.shape-bugfixes-test 'frontend-tests.plugins.text-test 'frontend-tests.plugins.tokens-test 'frontend-tests.plugins.utils-test @@ -105,13 +120,14 @@ 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-errors-test 'frontend-tests.tokens.workspace-tokens-remap-test + 'frontend-tests.ui.comments-position-modifier-test 'frontend-tests.ui.ds-controls-numeric-input-test + 'frontend-tests.ui.measures-menu-props-test 'frontend-tests.util-object-test 'frontend-tests.util-range-tree-test 'frontend-tests.util-simple-math-test 'frontend-tests.util-webapi-test - 'frontend-tests.worker-snap-test - 'frontend-tests.basic-shapes-test]) + 'frontend-tests.worker-snap-test]) (assert (every? find-ns-obj test-namespaces) "test-namespaces contains a namespace that isn't required in runner.cljs") diff --git a/frontend/test/frontend_tests/ui/comments_position_modifier_test.cljs b/frontend/test/frontend_tests/ui/comments_position_modifier_test.cljs new file mode 100644 index 0000000000..3f842df31a --- /dev/null +++ b/frontend/test/frontend_tests/ui/comments_position_modifier_test.cljs @@ -0,0 +1,60 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.ui.comments-position-modifier-test + (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.common.types.modifiers :as ctm] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [app.main.data.workspace.comments :as dwcm] + [cljs.test :as t :include-macros true])) + +(defn- frame + [id] + (cts/setup-shape {:id id :type :frame :name "Board" + :x 100 :y 100 :width 200 :height 150})) + +(defn- close-point? + [a b] + (and (mth/close? (:x a) (:x b)) + (mth/close? (:y a) (:y b)))) + +(t/deftest frame-pin-transform-move + (let [f (frame (uuid/next)) + mods (ctm/move-modifiers (gpt/point 10 20)) + m (dwcm/frame-pin-transform f mods nil)] + (t/testing "the comment follows the frame translation" + (t/is (close-point? (gpt/point 160 170) + (gpt/transform (gpt/point 150 150) m)))))) + +(t/deftest frame-pin-transform-rotation + (let [f (frame (uuid/next)) + center (gsh/shape->center f) + mods (ctm/rotation (ctm/empty) center 90) + m (dwcm/frame-pin-transform f mods nil) + p (gpt/point 150 150)] + (t/testing "the comment rotates around the frame center" + (t/is (close-point? (gpt/transform p (ctm/modifiers->transform mods)) + (gpt/transform p m)))))) + +(t/deftest frame-pin-transform-resize + (let [f (frame (uuid/next)) + mods (ctm/resize (ctm/empty) (gpt/point 2 2) (gpt/point 100 100)) + m (dwcm/frame-pin-transform f mods nil) + p (gpt/point 150 150)] + (t/testing "the comment keeps its position without scaling" + (t/is (close-point? p (gpt/transform p m)))) + (t/testing "the comment is not scaled along with the frame" + (t/is (not (close-point? (gpt/transform p (ctm/modifiers->transform mods)) + (gpt/transform p m))))))) + +(t/deftest frame-pin-transform-without-transform + (let [f (frame (uuid/next))] + (t/testing "no active transform yields no matrix" + (t/is (nil? (dwcm/frame-pin-transform f nil nil)))))) diff --git a/frontend/test/frontend_tests/ui/measures_menu_props_test.cljs b/frontend/test/frontend_tests/ui/measures_menu_props_test.cljs new file mode 100644 index 0000000000..8214572f59 --- /dev/null +++ b/frontend/test/frontend_tests/ui/measures_menu_props_test.cljs @@ -0,0 +1,51 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.ui.measures-menu-props-test + (:require + [app.main.ui.workspace.sidebar.options.menus.measures :refer [check-measures-menu-props]] + [cljs.test :as t :include-macros true])) + +;; Shared, identical-by-reference props so the comparator only reacts to the +;; `values` differences we are testing. +(def ^:private ids #js ["id-1"]) +(def ^:private shape-type :rect) +(def ^:private tokens #js {}) + +(defn- props + [values] + #js {"ids" ids "type" shape-type "appliedTokens" tokens "values" values}) + +(def ^:private base-values + {:width 100 + :height 200 + :layout-item-h-sizing :fix + :layout-item-v-sizing :fix}) + +(t/deftest test-check-measures-menu-props + (t/testing "skips re-render when nothing relevant changed" + ;; Different map instances with identical scalar content must be treated + ;; as equal (returns true => memoized, no re-render). + (t/is (true? (check-measures-menu-props + (props base-values) + (props (into {} base-values)))))) + + (t/testing "re-renders when horizontal sizing changes but width does not" + ;; Regression test: toggling fix <-> auto without changing the width value + ;; must force a re-render so the width input enabled/disabled state updates. + (t/is (false? (check-measures-menu-props + (props base-values) + (props (assoc base-values :layout-item-h-sizing :auto)))))) + + (t/testing "re-renders when vertical sizing changes but height does not" + (t/is (false? (check-measures-menu-props + (props base-values) + (props (assoc base-values :layout-item-v-sizing :auto)))))) + + (t/testing "re-renders when width changes" + (t/is (false? (check-measures-menu-props + (props base-values) + (props (assoc base-values :width 150))))))) diff --git a/frontend/text-editor/package.json b/frontend/text-editor/package.json index 24355d1b7a..8a9bcaa5ea 100644 --- a/frontend/text-editor/package.json +++ b/frontend/text-editor/package.json @@ -15,18 +15,18 @@ "test:watch:e2e": "vitest --browser" }, "devDependencies": { - "@playwright/test": "1.61.0", - "@types/node": "^25.9.2", + "@playwright/test": "1.61.1", + "@types/node": "^26.0.1", "@vitest/browser": "^4.1.9", "@vitest/coverage-v8": "^4.1.9", "@vitest/ui": "^4.1.9", "canvas": "^3.2.3", "esbuild": "^0.28.0", "jsdom": "^29.1.1", - "playwright": "1.61.0", - "prettier": "^3.8.4", - "vite": "^8.0.16", + "playwright": "1.61.1", + "prettier": "^3.9.4", + "vite": "^8.1.1", "vitest": "^4.1.9" }, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620" + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b" } diff --git a/library/package.json b/library/package.json index ea08e64218..17616cd7a8 100644 --- a/library/package.json +++ b/library/package.json @@ -3,7 +3,7 @@ "version": "1.2.0-RC1", "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "type": "module", "repository": { "type": "git", @@ -34,11 +34,11 @@ "watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library" }, "devDependencies": { - "@types/node": "^22.18.12", - "@zip.js/zip.js": "2.8.11", - "concurrently": "^9.2.1", - "date-fns": "^4.1.0", - "nodemon": "^3.1.10", + "@types/node": "^26.0.1", + "@zip.js/zip.js": "2.8.26", + "concurrently": "^10.0.3", + "date-fns": "^4.4.0", + "nodemon": "^3.1.14", "source-map-support": "^0.5.21" } } diff --git a/library/pnpm-lock.yaml b/library/pnpm-lock.yaml index 019a867960..9e978ca88f 100644 --- a/library/pnpm-lock.yaml +++ b/library/pnpm-lock.yaml @@ -5,63 +5,63 @@ settings: excludeLinksFromLockfile: false patchedDependencies: - '@zip.js/zip.js@2.8.11': - hash: 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95 - path: patches/@zip.js__zip.js@2.8.11.patch + '@zip.js/zip.js@2.8.26': 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95 importers: .: devDependencies: '@types/node': - specifier: ^22.18.12 - version: 22.19.3 + specifier: ^26.0.1 + version: 26.0.1 '@zip.js/zip.js': - specifier: 2.8.11 - version: 2.8.11(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) + specifier: 2.8.26 + version: 2.8.26(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) concurrently: - specifier: ^9.2.1 - version: 9.2.1 + specifier: ^10.0.3 + version: 10.0.3 date-fns: - specifier: ^4.1.0 - version: 4.1.0 + specifier: ^4.4.0 + version: 4.4.0 nodemon: - specifier: ^3.1.10 - version: 3.1.11 + specifier: ^3.1.14 + version: 3.1.14 source-map-support: specifier: ^0.5.21 version: 0.5.21 packages: - '@types/node@22.19.3': - resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + '@types/node@26.0.1': + resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} - '@zip.js/zip.js@2.8.11': - resolution: {integrity: sha512-0fztsk/0ryJ+2PPr9EyXS5/Co7OK8q3zY/xOoozEWaUsL5x+C0cyZ4YyMuUffOO2Dx/rAdq4JMPqW0VUtm+vzA==} + '@zip.js/zip.js@2.8.26': + resolution: {integrity: sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -70,35 +70,25 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - concurrently@9.2.1: - resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} - engines: {node: '>=18'} + concurrently@10.0.3: + resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} + engines: {node: '>=22'} hasBin: true - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -109,8 +99,8 @@ packages: supports-color: optional: true - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -129,6 +119,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -137,10 +131,6 @@ packages: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -152,10 +142,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -164,14 +150,15 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nodemon@3.1.11: - resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} engines: {node: '>=10'} hasBin: true @@ -179,8 +166,8 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} pstree.remy@1.1.8: @@ -190,20 +177,16 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} simple-update-notifier@2.0.0: @@ -217,26 +200,22 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -255,52 +234,49 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} snapshots: - '@types/node@22.19.3': + '@types/node@26.0.1': dependencies: - undici-types: 6.21.0 + undici-types: 8.3.0 - '@zip.js/zip.js@2.8.11(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {} + '@zip.js/zip.js@2.8.26(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {} - ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 - balanced-match@1.0.2: {} + balanced-match@4.0.4: {} binary-extensions@2.3.0: {} - brace-expansion@1.1.12: + brace-expansion@5.0.7: dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -308,10 +284,7 @@ snapshots: buffer-from@1.1.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + chalk@5.6.2: {} chokidar@3.6.0: dependencies: @@ -325,30 +298,22 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - cliui@8.0.1: + cliui@9.0.1: dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 - color-convert@2.0.1: + concurrently@10.0.3: dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - concurrently@9.2.1: - dependencies: - chalk: 4.1.2 + chalk: 5.6.2 rxjs: 7.8.2 - shell-quote: 1.8.3 - supports-color: 8.1.1 + shell-quote: 1.8.4 + supports-color: 10.2.2 tree-kill: 1.2.2 - yargs: 17.7.2 + yargs: 18.0.0 - date-fns@4.1.0: {} + date-fns@4.4.0: {} debug@4.4.3(supports-color@5.5.0): dependencies: @@ -356,7 +321,7 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - emoji-regex@8.0.0: {} + emoji-regex@10.6.0: {} escalade@3.2.0: {} @@ -369,14 +334,14 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 has-flag@3.0.0: {} - has-flag@4.0.0: {} - ignore-by-default@1.0.1: {} is-binary-path@2.1.0: @@ -385,28 +350,26 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-number@7.0.0: {} - minimatch@3.1.2: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 5.0.7 ms@2.1.3: {} - nodemon@3.1.11: + nodemon@3.1.14: dependencies: chokidar: 3.6.0 debug: 4.4.3(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 3.1.2 + minimatch: 10.2.5 pstree.remy: 1.1.8 - semver: 7.7.3 + semver: 7.8.5 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.1 @@ -414,27 +377,25 @@ snapshots: normalize-path@3.0.0: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} pstree.remy@1.1.8: {} readdirp@3.6.0: dependencies: - picomatch: 2.3.1 - - require-directory@2.1.1: {} + picomatch: 2.3.2 rxjs@7.8.2: dependencies: tslib: 2.8.1 - semver@7.7.3: {} + semver@7.8.5: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} simple-update-notifier@2.0.0: dependencies: - semver: 7.7.3 + semver: 7.8.5 source-map-support@0.5.21: dependencies: @@ -443,28 +404,22 @@ snapshots: source-map@0.6.1: {} - string-width@4.2.3: + string-width@7.2.0: dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 - strip-ansi@6.0.1: + strip-ansi@7.2.0: dependencies: - ansi-regex: 5.0.1 + ansi-regex: 6.2.2 + + supports-color@10.2.2: {} supports-color@5.5.0: dependencies: has-flag: 3.0.0 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -477,24 +432,23 @@ snapshots: undefsafe@2.0.5: {} - undici-types@6.21.0: {} + undici-types@8.3.0: {} - wrap-ansi@7.0.0: + wrap-ansi@9.0.2: dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 y18n@5.0.8: {} - yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} - yargs@17.7.2: + yargs@18.0.0: dependencies: - cliui: 8.0.1 + cliui: 9.0.1 escalade: 3.2.0 get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 + string-width: 7.2.0 y18n: 5.0.8 - yargs-parser: 21.1.1 + yargs-parser: 22.0.0 diff --git a/library/pnpm-workspace.yaml b/library/pnpm-workspace.yaml index 1cb0550888..fad1ba32af 100644 --- a/library/pnpm-workspace.yaml +++ b/library/pnpm-workspace.yaml @@ -1,2 +1,2 @@ patchedDependencies: - '@zip.js/zip.js@2.8.11': patches/@zip.js__zip.js@2.8.11.patch + '@zip.js/zip.js@2.8.26': patches/@zip.js__zip.js@2.8.11.patch diff --git a/mcp/package.json b/mcp/package.json index 2054f5c79f..955340d53f 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -22,9 +22,9 @@ "type": "git", "url": "https://github.com/penpot/penpot.git" }, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "devDependencies": { "concurrently": "^10.0.3", - "prettier": "^3.8.4" + "prettier": "^3.9.1" } } diff --git a/mcp/packages/common/package.json b/mcp/packages/common/package.json index fc6d9c9cfd..01f7af6711 100644 --- a/mcp/packages/common/package.json +++ b/mcp/packages/common/package.json @@ -4,7 +4,7 @@ "description": "Shared type definitions and interfaces for Penpot MCP", "main": "dist/index.js", "types": "dist/index.d.ts", - "packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "scripts": { "build": "tsc --build --clean && tsc --build", "watch": "tsc --watch", @@ -12,7 +12,7 @@ "clean": "rm -rf dist/" }, "devDependencies": { - "typescript": "^5.0.0" + "typescript": "^6.0.3" }, "files": [ "dist/**/*" diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index 9dddf112fd..c5df6328f0 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -12,13 +12,13 @@ "clean": "rm -rf dist/" }, "dependencies": { - "@penpot/plugin-styles": "1.4.1", - "@penpot/plugin-types": "1.4.1" + "@penpot/plugin-styles": "1.4.2", + "@penpot/plugin-types": "1.4.2" }, "devDependencies": { - "cross-env": "^7.0.3", - "typescript": "^5.8.3", - "vite": "^7.0.8", - "vite-live-preview": "^0.3.2" + "cross-env": "^10.1.0", + "typescript": "^6.0.3", + "vite": "^8.1.0", + "vite-live-preview": "^0.4.0" } } diff --git a/mcp/packages/server/package.json b/mcp/packages/server/package.json index 29f1ec467b..62476f19a9 100644 --- a/mcp/packages/server/package.json +++ b/mcp/packages/server/package.json @@ -24,35 +24,35 @@ ], "author": "", "license": "MIT", - "packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "dependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", + "class-validator": "^0.15.1", "express": "^5.1.0", - "ioredis": "^5.6.0", - "js-yaml": "^4.1.1", + "ioredis": "^5.11.1", + "js-yaml": "^5.2.0", "nrepl-client": "^0.3.0", "penpot-mcp": "file:..", - "pino": "^9.10.0", - "pino-loki": "^2.6.0", + "pino": "^10.3.1", + "pino-loki": "^3.0.0", "pino-pretty": "^13.1.1", - "reflect-metadata": "^0.1.13", - "sharp": "^0.34.5", - "ws": "^8.18.0", - "zod": "^4.3.6" + "reflect-metadata": "^0.2.2", + "sharp": "^0.35.2", + "ws": "^8.21.0", + "zod": "^4.4.3" }, "devDependencies": { "@penpot/mcp-common": "workspace:../common", - "@types/express": "^4.17.0", + "@types/express": "^5.0.6", "@types/js-yaml": "^4.0.9", - "@types/node": "^20.0.0", + "@types/node": "^26.0.1", "@types/ws": "^8.5.10", - "cross-env": "^7.0.3", - "esbuild": "^0.25.0", + "cross-env": "^10.1.0", + "esbuild": "^0.28.1", "ts-node": "^10.9.2", "tsx": "^4.22.3", - "typescript": "^5.0.0" + "typescript": "^6.0.3" }, "ts-node": { "esm": true diff --git a/mcp/packages/server/tsconfig.json b/mcp/packages/server/tsconfig.json index f61bd28a33..5362935d9d 100644 --- a/mcp/packages/server/tsconfig.json +++ b/mcp/packages/server/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2022", "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", diff --git a/mcp/pnpm-lock.yaml b/mcp/pnpm-lock.yaml index 5eac2b0d0c..259e153b28 100644 --- a/mcp/pnpm-lock.yaml +++ b/mcp/pnpm-lock.yaml @@ -12,57 +12,57 @@ importers: specifier: ^10.0.3 version: 10.0.3 prettier: - specifier: ^3.8.4 - version: 3.8.4 + specifier: ^3.9.1 + version: 3.9.1 packages/common: devDependencies: typescript: - specifier: ^5.0.0 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 packages/plugin: dependencies: '@penpot/plugin-styles': - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.2 + version: 1.4.2 '@penpot/plugin-types': - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.2 + version: 1.4.2 devDependencies: cross-env: - specifier: ^7.0.3 - version: 7.0.3 + specifier: ^10.1.0 + version: 10.1.0 typescript: - specifier: ^5.8.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 vite: - specifier: ^7.0.8 - version: 7.3.1(@types/node@20.19.30)(tsx@4.22.3) + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(tsx@4.22.4) vite-live-preview: - specifier: ^0.3.2 - version: 0.3.2(vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3)) + specifier: ^0.4.0 + version: 0.4.0(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(tsx@4.22.4)) packages/server: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.24.0 - version: 1.25.3(hono@4.11.7)(zod@4.3.6) + specifier: ^1.29.0 + version: 1.29.0(zod@4.4.3) class-transformer: specifier: ^0.5.1 version: 0.5.1 class-validator: - specifier: ^0.14.3 - version: 0.14.3 + specifier: ^0.15.1 + version: 0.15.1 express: specifier: ^5.1.0 version: 5.2.1 ioredis: - specifier: ^5.6.0 - version: 5.11.0 + specifier: ^5.11.1 + version: 5.11.1 js-yaml: - specifier: ^4.1.1 - version: 4.1.1 + specifier: ^5.2.0 + version: 5.2.0 nrepl-client: specifier: ^0.3.0 version: 0.3.0 @@ -70,696 +70,397 @@ importers: specifier: file:.. version: file:packages pino: - specifier: ^9.10.0 - version: 9.14.0 + specifier: ^10.3.1 + version: 10.3.1 pino-loki: - specifier: ^2.6.0 - version: 2.6.0 + specifier: ^3.0.0 + version: 3.0.0 pino-pretty: specifier: ^13.1.1 version: 13.1.3 reflect-metadata: - specifier: ^0.1.13 - version: 0.1.14 + specifier: ^0.2.2 + version: 0.2.2 sharp: - specifier: ^0.34.5 - version: 0.34.5 + specifier: ^0.35.2 + version: 0.35.2 ws: - specifier: ^8.18.0 - version: 8.19.0 + specifier: ^8.21.0 + version: 8.21.0 zod: - specifier: ^4.3.6 - version: 4.3.6 + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@penpot/mcp-common': specifier: workspace:../common version: link:../common '@types/express': - specifier: ^4.17.0 - version: 4.17.25 + specifier: ^5.0.6 + version: 5.0.6 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^20.0.0 - version: 20.19.30 + specifier: ^26.0.1 + version: 26.0.1 '@types/ws': specifier: ^8.5.10 version: 8.18.1 cross-env: - specifier: ^7.0.3 - version: 7.0.3 + specifier: ^10.1.0 + version: 10.1.0 esbuild: - specifier: ^0.25.0 - version: 0.25.12 + specifier: ^0.28.1 + version: 0.28.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.19.30)(typescript@5.9.3) + version: 10.9.2(@types/node@26.0.1)(typescript@6.0.3) tsx: specifier: ^4.22.3 - version: 4.22.3 + version: 4.22.4 typescript: - specifier: ^5.0.0 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 packages: - '@commander-js/extra-typings@12.1.0': - resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} - peerDependencies: - commander: ~12.1.0 - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -776,8 +477,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/sdk@1.25.3': - resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -786,152 +487,127 @@ packages: '@cfworker/json-schema': optional: true - '@penpot/plugin-styles@1.4.1': - resolution: {integrity: sha512-6TuJqKQsq1Xmhn2A02R+kCOzIzIdqgFg5z6ncLH2PlAflKIX6aYsGiOF7yFx4RYgCegRVMFPnVis6/hwO+YGQg==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@penpot/plugin-types@1.4.1': - resolution: {integrity: sha512-pHE2B3GI8M5JR03S/NdBoN+z6e1R1IEh3vpFbLG9LN0EZpQE6nEbmCo5jWAWI73Jqlg6CHG/RWVJNmWECnkDTA==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@penpot/plugin-styles@1.4.2': + resolution: {integrity: sha512-/Rn6xy80W+mxAi6j5/SOiNY8P0qHMB1WW+j+nyZajecFSnVGPzSWOiMcIDH+Jtsz7Xpmd5AICpgVM0xjechQig==} + + '@penpot/plugin-types@1.4.2': + resolution: {integrity: sha512-O8wU6RSYE8bIVU7g8cSTYi32ppxs3R13dq7X3Nn9tmDaJjBOKOBpVLuoRPIp3fJC65fv8/7om0sdrtFoL5v19g==} '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@rollup/rollup-android-arm-eabi@4.57.0': - resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.57.0': - resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==} + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.57.0': - resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==} + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.57.0': - resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==} + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.57.0': - resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.57.0': - resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==} + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.57.0': - resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.57.0': - resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.57.0': - resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.57.0': - resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.57.0': - resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.57.0': - resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.57.0': - resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.57.0': - resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.57.0': - resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.57.0': - resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.57.0': - resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.57.0': - resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.57.0': - resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.57.0': - resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.57.0': - resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==} + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.57.0': - resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==} + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.57.0': - resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.57.0': - resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==} + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.57.0': - resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@seahax/deep-copy@0.1.0': + resolution: {integrity: sha512-Ux88qw9ypPbqszDGwj0JvP8FP017MV3ck7jnkit+0i3mecTkqhYiCUqy3exbxhGFBu07zrUS6yPRUWVct1eSMQ==} + + '@seahax/semaphore@0.5.1': + resolution: {integrity: sha512-q6SXYYbE6X+LDcq2h2yCgE+pCWJumNP3XCZkztdG4S4tiig9akMZGp8TsfU/EIRcHWPdnQ3BA8/NAvdDYdF/NQ==} '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -945,8 +621,8 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@types/ansi-html@0.0.0': - resolution: {integrity: sha512-PEBpUlteD0VW02udY7UjjgjxHwVXmkdanhmRIMkzatGmORJGjzqKylrXVxz1G5xRTEECMxIkwTHpPmZ9Jb7ANQ==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -954,17 +630,11 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -972,29 +642,20 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@26.0.1': + resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@20.19.30': - resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} - - '@types/qs@6.14.0': - resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -1023,13 +684,8 @@ packages: ajv: optional: true - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - ansi-html@0.0.9: - resolution: {integrity: sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==} - engines: {'0': node >= 0.8.0} - hasBin: true + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} @@ -1075,8 +731,8 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - class-validator@0.14.3: - resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} + class-validator@0.15.1: + resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} @@ -1089,10 +745,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - concurrently@10.0.3: resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} engines: {node: '>=22'} @@ -1121,9 +773,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} hasBin: true cross-spawn@7.0.6: @@ -1187,18 +839,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -1217,16 +859,16 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -1244,8 +886,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1307,8 +949,8 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} - hono@4.11.7: - resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + hono@4.12.27: + resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} http-errors@2.0.1: @@ -1322,10 +964,14 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ioredis@5.11.0: - resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1336,15 +982,15 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@5.2.0: + resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} hasBin: true json-schema-traverse@1.0.0: @@ -1353,8 +999,82 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - libphonenumber-js@1.12.35: - resolution: {integrity: sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==} + libphonenumber-js@1.13.7: + resolution: {integrity: sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -1385,8 +1105,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1416,10 +1136,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - p-defer@4.0.1: - resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} - engines: {node: '>=12'} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1437,18 +1153,16 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} - pino-loki@2.6.0: - resolution: {integrity: sha512-Qy+NeIdb0YmZe/M5mgnO5aGaAyVaeqgwn45T6VajhRXZlZVfGe1YNYhFa9UZyCeNFAPGaUkD2e9yPGjx+2BBYA==} + pino-loki@3.0.0: + resolution: {integrity: sha512-9TyUW5syTjp2nT70QcijJtIWUzdYUj+olQ7+fWNfm1/HrDGEWt86Q4ACzClH6DM6GBwtQimRDgneNczP+p4ypA==} + engines: {node: '>=20'} hasBin: true pino-pretty@13.1.3: @@ -1458,20 +1172,20 @@ packages: pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@9.14.0: - resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.9.1: + resolution: {integrity: sha512-ppiDo2CSwexck1eyZUwJHg/N3nf1+6IRCv7W/VJ5vaLnVCmB7+3CdRfMwoCHBBX6xTrREDTksZ4OZl5SSf4zXA==} engines: {node: '>=14'} hasBin: true @@ -1485,6 +1199,9 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -1504,6 +1221,9 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -1512,16 +1232,16 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - reflect-metadata@0.1.14: - resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - rollup@4.57.0: - resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true router@2.2.0: @@ -1541,8 +1261,8 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -1557,9 +1277,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -1592,6 +1312,9 @@ packages: sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1623,11 +1346,12 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} toidentifier@1.0.1: @@ -1655,8 +1379,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.22.3: - resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} engines: {node: '>=18.0.0'} hasBin: true @@ -1664,13 +1388,13 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -1679,29 +1403,30 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - validator@13.15.26: - resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-live-preview@0.3.2: - resolution: {integrity: sha512-NrmGaAc85qvkx/+6FluiTo9rLnoY+/NOYnuUvcW5Yb5tSJzUxuloXYrCSS1dtxQB9YKUbpQ95JCb0GRuF//JEQ==} + vite-live-preview@0.4.0: + resolution: {integrity: sha512-Qz8kr0kixXwnQl+zLPZX66OjajN4jnVnDwhNToJsO6TTboUtBo8pEmRuc0iBmkwW9lXR8mOeMu+QtxFkXBcHYg==} hasBin: true peerDependencies: - vite: '>=5.2.13' + vite: '>=5.4.0' - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -1712,12 +1437,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -1745,8 +1472,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1773,361 +1500,224 @@ packages: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: - zod: ^3.25 || ^4 + zod: ^3.25.28 || ^4 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: - '@commander-js/extra-typings@12.1.0(commander@12.1.0)': - dependencies: - commander: 12.1.0 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/runtime@1.8.1': + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.2': - optional: true - - '@esbuild/aix-ppc64@0.28.0': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.27.2': - optional: true - - '@esbuild/android-arm64@0.28.0': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-arm@0.27.2': - optional: true - - '@esbuild/android-arm@0.28.0': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/android-x64@0.27.2': - optional: true - - '@esbuild/android-x64@0.28.0': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.27.2': - optional: true - - '@esbuild/darwin-arm64@0.28.0': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.27.2': - optional: true - - '@esbuild/darwin-x64@0.28.0': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.27.2': - optional: true - - '@esbuild/freebsd-arm64@0.28.0': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.27.2': - optional: true - - '@esbuild/freebsd-x64@0.28.0': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.27.2': - optional: true - - '@esbuild/linux-arm64@0.28.0': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-arm@0.27.2': - optional: true - - '@esbuild/linux-arm@0.28.0': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.27.2': - optional: true - - '@esbuild/linux-ia32@0.28.0': - optional: true - - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.27.2': - optional: true - - '@esbuild/linux-loong64@0.28.0': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.27.2': - optional: true - - '@esbuild/linux-mips64el@0.28.0': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.27.2': - optional: true - - '@esbuild/linux-ppc64@0.28.0': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.27.2': - optional: true - - '@esbuild/linux-riscv64@0.28.0': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.27.2': - optional: true - - '@esbuild/linux-s390x@0.28.0': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.2': - optional: true - - '@esbuild/linux-x64@0.28.0': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.2': - optional: true - - '@esbuild/netbsd-arm64@0.28.0': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.2': - optional: true - - '@esbuild/netbsd-x64@0.28.0': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.2': - optional: true - - '@esbuild/openbsd-arm64@0.28.0': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.2': - optional: true - - '@esbuild/openbsd-x64@0.28.0': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.2': - optional: true - - '@esbuild/openharmony-arm64@0.28.0': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.2': - optional: true - - '@esbuild/sunos-x64@0.28.0': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.2': - optional: true - - '@esbuild/win32-arm64@0.28.0': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.2': - optional: true - - '@esbuild/win32-ia32@0.28.0': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.2': - optional: true - - '@esbuild/win32-x64@0.28.0': - optional: true - - '@hono/node-server@1.19.9(hono@4.11.7)': + '@emnapi/wasi-threads@1.2.2': dependencies: - hono: 4.11.7 - - '@img/colour@1.0.0': {} - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + tslib: 2.8.1 optional: true - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@epic-web/invariant@1.0.0': {} + + '@esbuild/aix-ppc64@0.28.1': optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@esbuild/android-arm64@0.28.1': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@esbuild/android-arm@0.28.1': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@esbuild/android-x64@0.28.1': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@esbuild/darwin-x64@0.28.1': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@esbuild/linux-arm64@0.28.1': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@esbuild/linux-arm@0.28.1': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@esbuild/linux-ia32@0.28.1': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@esbuild/linux-loong64@0.28.1': optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@esbuild/linux-mips64el@0.28.1': optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@esbuild/linux-ppc64@0.28.1': optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@esbuild/linux-riscv64@0.28.1': optional: true - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@esbuild/linux-s390x@0.28.1': optional: true - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@esbuild/linux-x64@0.28.1': optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@esbuild/netbsd-x64@0.28.1': optional: true - '@img/sharp-wasm32@0.34.5': + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@hono/node-server@1.19.14(hono@4.12.27)': dependencies: - '@emnapi/runtime': 1.8.1 + hono: 4.12.27 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-win32-arm64@0.35.2': + optional: true + + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': optional: true '@ioredis/commands@1.10.0': {} @@ -2141,108 +1731,97 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)': + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.7) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) + '@hono/node-server': 1.19.14(hono@4.12.27) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.6 + eventsource-parser: 3.1.0 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.27 + jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - - hono - supports-color - '@penpot/plugin-styles@1.4.1': {} + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true - '@penpot/plugin-types@1.4.1': {} + '@oxc-project/types@0.137.0': {} + + '@penpot/plugin-styles@1.4.2': {} + + '@penpot/plugin-types@1.4.2': {} '@pinojs/redact@0.4.0': {} - '@rollup/rollup-android-arm-eabi@4.57.0': + '@rolldown/binding-android-arm64@1.1.3': optional: true - '@rollup/rollup-android-arm64@4.57.0': + '@rolldown/binding-darwin-arm64@1.1.3': optional: true - '@rollup/rollup-darwin-arm64@4.57.0': + '@rolldown/binding-darwin-x64@1.1.3': optional: true - '@rollup/rollup-darwin-x64@4.57.0': + '@rolldown/binding-freebsd-x64@1.1.3': optional: true - '@rollup/rollup-freebsd-arm64@4.57.0': + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': optional: true - '@rollup/rollup-freebsd-x64@4.57.0': + '@rolldown/binding-linux-arm64-gnu@1.1.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + '@rolldown/binding-linux-arm64-musl@1.1.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.57.0': + '@rolldown/binding-linux-ppc64-gnu@1.1.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.57.0': + '@rolldown/binding-linux-s390x-gnu@1.1.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.57.0': + '@rolldown/binding-linux-x64-gnu@1.1.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.57.0': + '@rolldown/binding-linux-x64-musl@1.1.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.57.0': + '@rolldown/binding-openharmony-arm64@1.1.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.57.0': + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rollup/rollup-linux-ppc64-musl@4.57.0': + '@rolldown/binding-win32-arm64-msvc@1.1.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.57.0': + '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.57.0': - optional: true + '@rolldown/pluginutils@1.0.1': {} - '@rollup/rollup-linux-s390x-gnu@4.57.0': - optional: true + '@seahax/deep-copy@0.1.0': {} - '@rollup/rollup-linux-x64-gnu@4.57.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.57.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.57.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.57.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.57.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.57.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.57.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.57.0': - optional: true + '@seahax/semaphore@0.5.1': {} '@tsconfig/node10@1.0.12': {} @@ -2252,73 +1831,59 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@types/ansi-html@0.0.0': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.19.30 + '@types/node': 26.0.1 '@types/connect@3.4.38': dependencies: - '@types/node': 20.19.30 + '@types/node': 26.0.1 - '@types/debug@4.1.12': + '@types/express-serve-static-core@5.1.1': dependencies: - '@types/ms': 2.1.0 - - '@types/estree@1.0.8': {} - - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 20.19.30 - '@types/qs': 6.14.0 + '@types/node': 26.0.1 + '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@4.17.25': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.14.0 - '@types/serve-static': 1.15.10 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 '@types/http-errors@2.0.5': {} '@types/js-yaml@4.0.9': {} - '@types/mime@1.3.5': {} - - '@types/ms@2.1.0': {} - - '@types/node@20.19.30': + '@types/node@26.0.1': dependencies: - undici-types: 6.21.0 + undici-types: 8.3.0 - '@types/qs@6.14.0': {} + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.19.30 - '@types/send@1.2.1': dependencies: - '@types/node': 20.19.30 + '@types/node': 26.0.1 - '@types/serve-static@1.15.10': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 20.19.30 - '@types/send': 0.17.6 + '@types/node': 26.0.1 '@types/validator@13.15.10': {} '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.30 + '@types/node': 26.0.1 accepts@2.0.0: dependencies: @@ -2331,19 +1896,17 @@ snapshots: acorn@8.15.0: {} - ajv-formats@3.0.1(ajv@8.17.1): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.20.0 - ajv@8.17.1: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-html@0.0.9: {} - ansi-regex@6.2.2: {} ansi-styles@6.2.3: {} @@ -2386,11 +1949,11 @@ snapshots: class-transformer@0.5.1: {} - class-validator@0.14.3: + class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.12.35 - validator: 13.15.26 + libphonenumber-js: 1.13.7 + validator: 13.15.35 cliui@9.0.1: dependencies: @@ -2402,8 +1965,6 @@ snapshots: colorette@2.0.20: {} - commander@12.1.0: {} - concurrently@10.0.3: dependencies: chalk: 5.6.2 @@ -2428,8 +1989,9 @@ snapshots: create-require@1.1.1: {} - cross-env@7.0.3: + cross-env@10.1.0: dependencies: + '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 cross-spawn@7.0.6: @@ -2476,92 +2038,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.25.12: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 - - esbuild@0.28.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -2571,15 +2075,16 @@ snapshots: etag@1.8.1: {} - eventsource-parser@3.0.6: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.6 + eventsource-parser: 3.1.0 - express-rate-limit@7.5.1(express@5.2.1): + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 + ip-address: 10.2.0 express@5.2.1: dependencies: @@ -2620,11 +2125,11 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 finalhandler@2.1.1: dependencies: @@ -2678,7 +2183,7 @@ snapshots: help-me@5.0.0: {} - hono@4.11.7: {} + hono@4.12.27: {} http-errors@2.0.1: dependencies: @@ -2694,7 +2199,7 @@ snapshots: inherits@2.0.4: {} - ioredis@5.11.0: + ioredis@5.11.1: dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 @@ -2706,17 +2211,19 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} is-promise@4.0.0: {} isexe@2.0.0: {} - jose@6.1.3: {} + jose@6.2.3: {} joycon@3.1.1: {} - js-yaml@4.1.1: + js-yaml@5.2.0: dependencies: argparse: 2.0.1 @@ -2724,7 +2231,56 @@ snapshots: json-schema-typed@8.0.2: {} - libphonenumber-js@1.12.35: {} + libphonenumber-js@1.13.7: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 make-error@1.3.6: {} @@ -2744,7 +2300,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.15: {} negotiator@1.0.0: {} @@ -2767,8 +2323,6 @@ snapshots: dependencies: wrappy: 1.0.2 - p-defer@4.0.1: {} - parseurl@1.3.3: {} path-key@3.1.1: {} @@ -2779,20 +2333,16 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.3: {} - - pino-abstract-transport@2.0.0: - dependencies: - split2: 4.2.0 + picomatch@4.0.4: {} pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 - pino-loki@2.6.0: + pino-loki@3.0.0: dependencies: - pino-abstract-transport: 2.0.0 - pump: 3.0.3 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 pino-pretty@13.1.3: dependencies: @@ -2812,29 +2362,29 @@ snapshots: pino-std-serializers@7.1.0: {} - pino@9.14.0: + pino@10.3.1: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 + pino-abstract-transport: 3.0.0 pino-std-serializers: 7.1.0 process-warning: 5.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.0 - thread-stream: 3.1.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 pkce-challenge@5.0.1: {} - postcss@8.5.6: + postcss@8.5.16: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.4: {} + prettier@3.9.1: {} process-warning@5.0.0: {} @@ -2848,6 +2398,11 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -2865,46 +2420,38 @@ snapshots: real-require@0.2.0: {} + real-require@1.0.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - reflect-metadata@0.1.14: {} + reflect-metadata@0.2.2: {} require-from-string@2.0.2: {} - rollup@4.57.0: + rolldown@1.1.3: dependencies: - '@types/estree': 1.0.8 + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.57.0 - '@rollup/rollup-android-arm64': 4.57.0 - '@rollup/rollup-darwin-arm64': 4.57.0 - '@rollup/rollup-darwin-x64': 4.57.0 - '@rollup/rollup-freebsd-arm64': 4.57.0 - '@rollup/rollup-freebsd-x64': 4.57.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.57.0 - '@rollup/rollup-linux-arm-musleabihf': 4.57.0 - '@rollup/rollup-linux-arm64-gnu': 4.57.0 - '@rollup/rollup-linux-arm64-musl': 4.57.0 - '@rollup/rollup-linux-loong64-gnu': 4.57.0 - '@rollup/rollup-linux-loong64-musl': 4.57.0 - '@rollup/rollup-linux-ppc64-gnu': 4.57.0 - '@rollup/rollup-linux-ppc64-musl': 4.57.0 - '@rollup/rollup-linux-riscv64-gnu': 4.57.0 - '@rollup/rollup-linux-riscv64-musl': 4.57.0 - '@rollup/rollup-linux-s390x-gnu': 4.57.0 - '@rollup/rollup-linux-x64-gnu': 4.57.0 - '@rollup/rollup-linux-x64-musl': 4.57.0 - '@rollup/rollup-openbsd-x64': 4.57.0 - '@rollup/rollup-openharmony-arm64': 4.57.0 - '@rollup/rollup-win32-arm64-msvc': 4.57.0 - '@rollup/rollup-win32-ia32-msvc': 4.57.0 - '@rollup/rollup-win32-x64-gnu': 4.57.0 - '@rollup/rollup-win32-x64-msvc': 4.57.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 router@2.2.0: dependencies: @@ -2926,7 +2473,7 @@ snapshots: secure-json-parse@4.1.0: {} - semver@7.7.3: {} + semver@7.8.5: {} send@1.2.1: dependencies: @@ -2955,36 +2502,37 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.34.5: + sharp@0.35.2: dependencies: - '@img/colour': 1.0.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.3 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 shebang-command@2.0.0: dependencies: @@ -3026,6 +2574,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} split2@4.2.0: {} @@ -3048,42 +2600,42 @@ snapshots: supports-color@10.2.2: {} - thread-stream@3.1.0: + thread-stream@4.2.0: dependencies: - real-require: 0.2.0 + real-require: 1.0.0 - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 toidentifier@1.0.1: {} tree-kill@1.2.2: {} - ts-node@10.9.2(@types/node@20.19.30)(typescript@5.9.3): + ts-node@10.9.2(@types/node@26.0.1)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.19.30 + '@types/node': 26.0.1 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.4 make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 tslib@2.8.1: {} - tsx@4.22.3: + tsx@4.22.4: dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -3093,49 +2645,43 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript@5.9.3: {} + typescript@6.0.3: {} - undici-types@6.21.0: {} + undici-types@8.3.0: {} unpipe@1.0.0: {} v8-compile-cache-lib@3.0.1: {} - validator@13.15.26: {} + validator@13.15.35: {} vary@1.1.2: {} - vite-live-preview@0.3.2(vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3)): + vite-live-preview@0.4.0(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(tsx@4.22.4)): dependencies: - '@commander-js/extra-typings': 12.1.0(commander@12.1.0) - '@types/ansi-html': 0.0.0 - '@types/debug': 4.1.12 + '@seahax/deep-copy': 0.1.0 + '@seahax/semaphore': 0.5.1 '@types/ws': 8.18.1 - ansi-html: 0.0.9 - chalk: 5.6.2 - commander: 12.1.0 - debug: 4.4.3 escape-goat: 4.0.0 - p-defer: 4.0.1 - vite: 7.3.1(@types/node@20.19.30)(tsx@4.22.3) - ws: 8.19.0 + strip-ansi: 7.2.0 + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(tsx@4.22.4) + ws: 8.21.0 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate - vite@7.3.1(@types/node@20.19.30)(tsx@4.22.3): + vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(tsx@4.22.4): dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.0 - tinyglobby: 0.2.15 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 20.19.30 + '@types/node': 26.0.1 + esbuild: 0.28.1 fsevents: 2.3.3 - tsx: 4.22.3 + tsx: 4.22.4 which@2.0.2: dependencies: @@ -3149,7 +2695,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.19.0: {} + ws@8.21.0: {} y18n@5.0.8: {} @@ -3166,8 +2712,8 @@ snapshots: yn@3.1.1: {} - zod-to-json-schema@3.25.1(zod@4.3.6): + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: - zod: 4.3.6 + zod: 4.4.3 - zod@4.3.6: {} + zod@4.4.3: {} diff --git a/package.json b/package.json index 373949e3e7..77800eaca6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" @@ -16,10 +16,10 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@types/node": "^26.0.0", + "@types/node": "^26.0.1", "esbuild": "^0.28.1", "mdts": "^0.20.3", "nrepl-client": "^0.3.0", - "opencode-ai": "^1.17.9" + "opencode-ai": "^1.17.11" } } diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 64163dbd42..c3138b0659 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -1,22 +1,36 @@ ## 1.5.0 (Unreleased) +### 💣 Breaking changes & Deprecations + - **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page. -- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`. -- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items +- **plugin-types**: Change return type of `combineAsVariants` +- **plugin-types:** Deprecate the legacy `Image` shape interface — image shapes exist only for backward compatibility with old files; new images are embedded in a `Fill` via its `fillImage` (an `ImageData`). +- We've solved several inconsistencies accross the API, if you relied on an undocumented property or method be aware that might have changed. + +### 🚀 Features + - **plugins-runtime**: Added `version` field that returns the current version - **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible) - **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering` +- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation - **plugin-types**: `penpot.openPage()` now returns `Promise` and should be awaited before performing operations on the new page -- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default - **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent` - **plugin-types**: Added `createVariantFromComponents` -- **plugin-types**: Change return type of `combineAsVariants` - **plugin-types**: Added `textBounds` property for text shapes -- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation - **plugin-types**: Fix missing `webp` export format in `Export.type` - **plugin-types**: Added `fixedWhenScrolling` property for shapes - **plugin-runtime:** `addToken` now resolves references against all token sets, allowing references to tokens in inactive sets - **plugin-types:** `TokenCatalog.addSet` now accepts an optional `active` flag to create an already-active set (sets are inactive by default) +- **plugin-runtime:** A `fontFamilies` token's `resolvedValue` now returns the documented `string[]` (the resolved family list) instead of leaking the raw tokenscript list symbol + +### 🩹 Fixes + +- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`. +- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items +- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default +- **plugin-types**: Rename `LibraryTypography.fontFamilies` to `fontFamily` to match the runtime (it holds a single font family, not an array) +- **plugin-runtime:** Setting a `LibraryColor`'s `gradient` or `image` now clears the other color representations (solid/gradient/image are mutually exclusive), so the result is a valid color instead of being rejected with "expected valid color" +- **plugin-types:** Mark members that have no runtime setter as `readonly`, fixing a mismatch where they were typed as writable: font metadata (`Font.*`, `FontVariant.*`, `FontsContext.all`), the `Ellipse`/`Image`/`SvgRaw` `type` discriminants (now consistent with the other shapes), `File.name`/`pages`/`revn`, `Page.root`, `TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, the board guide value objects (`GuideColumn`/`GuideRow`/`GuideSquare` and their params — `board.guides` returns a formatted snapshot, so reconfiguring means reassigning the whole array), the `Point` and `Bounds` value objects, the `Penpot.ui`/`Penpot.utils` subcontexts, the derived `Boolean` path data (`d`/`content`/`commands` are computed from the operands; `Boolean` is not editable like a `Path`), and the `EventsMap` event entries (a type-only event→callback map, never assigned). Members that do expose a setter stay writable: `Board.children`, `Path.d`/`content`/`commands` and `FileVersion.label`. ## 1.4.2 (2026-01-21) diff --git a/plugins/angular.json b/plugins/angular.json index 10e455165a..84c54ea0a5 100644 --- a/plugins/angular.json +++ b/plugins/angular.json @@ -10,7 +10,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/contrast-plugin", @@ -62,7 +62,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "contrast-plugin:build:production" }, "development": { @@ -82,7 +82,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/icons-plugin", @@ -134,7 +134,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "icons-plugin:build:production" }, "development": { @@ -154,7 +154,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/lorem-ipsum-plugin", @@ -206,7 +206,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "lorem-ipsum-plugin:build:production" @@ -228,7 +228,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/table-plugin", @@ -280,7 +280,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "table-plugin:build:production" }, "development": { @@ -300,7 +300,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/rename-layers-plugin", @@ -352,7 +352,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "rename-layers-plugin:build:production" @@ -374,7 +374,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/colors-to-tokens-plugin", @@ -426,7 +426,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "colors-to-tokens-plugin:build:production" @@ -448,7 +448,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/poc-state-plugin", @@ -499,7 +499,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "poc-state-plugin:build:production" @@ -521,7 +521,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "dist/apps/poc-tokens-plugin", @@ -573,7 +573,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "poc-tokens-plugin:build:production" diff --git a/plugins/apps/colors-to-tokens-plugin/src/model.ts b/plugins/apps/colors-to-tokens-plugin/src/model.ts index b4b44c5548..1aa750720d 100644 --- a/plugins/apps/colors-to-tokens-plugin/src/model.ts +++ b/plugins/apps/colors-to-tokens-plugin/src/model.ts @@ -35,9 +35,7 @@ export interface ResizePluginUIEvent { } export type PluginUIEvent = - | GETColorsPluginUIEvent - | ResizePluginUIEvent - | ResetPluginUIEvent; + GETColorsPluginUIEvent | ResizePluginUIEvent | ResetPluginUIEvent; export interface ThemePluginEvent { type: 'theme'; diff --git a/plugins/apps/contrast-plugin/src/model.ts b/plugins/apps/contrast-plugin/src/model.ts index 37de33b748..41742d4a56 100644 --- a/plugins/apps/contrast-plugin/src/model.ts +++ b/plugins/apps/contrast-plugin/src/model.ts @@ -24,6 +24,4 @@ export interface ThemePluginEvent { } export type PluginMessageEvent = - | InitPluginEvent - | SelectionPluginEvent - | ThemePluginEvent; + InitPluginEvent | SelectionPluginEvent | ThemePluginEvent; diff --git a/plugins/apps/lorem-ipsum-plugin/src/model.ts b/plugins/apps/lorem-ipsum-plugin/src/model.ts index dce13e4f73..d726d0ec3f 100644 --- a/plugins/apps/lorem-ipsum-plugin/src/model.ts +++ b/plugins/apps/lorem-ipsum-plugin/src/model.ts @@ -1,8 +1,5 @@ export type GenerationTypes = - | 'paragraphs' - | 'sentences' - | 'words' - | 'characters'; + 'paragraphs' | 'sentences' | 'words' | 'characters'; export interface InitPluginUIEvent { type: 'ready'; @@ -35,6 +32,4 @@ export interface ThemePluginEvent { } export type PluginMessageEvent = - | InitPluginEvent - | SelectionPluginEvent - | ThemePluginEvent; + InitPluginEvent | SelectionPluginEvent | ThemePluginEvent; diff --git a/plugins/apps/plugin-api-test-suite/README.md b/plugins/apps/plugin-api-test-suite/README.md new file mode 100644 index 0000000000..82313a1060 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/README.md @@ -0,0 +1,391 @@ +# Plugin API Test Suite + +A Penpot plugin that is a launcher + runner for a battery of tests exercising the +Penpot **Plugin API** against a live Penpot instance. It doubles as living +documentation of what the public API actually does at runtime. + +- A plain TypeScript + Vite Penpot plugin living in `plugins/apps/plugin-api-test-suite`. +- The UI (an iframe) lists auto-discovered tests and lets you run all / a subset / + one. Each test shows green (pass) or red (fail, with the error message). +- It reports **API coverage**: which members of the public Plugin API the tests + exercised, measured against `libs/plugin-types/index.d.ts`. +- The same test files run both in the plugin UI and in a headless CI runner, so a + test is never written twice. + +This document is the context a developer (or agent) needs to add tests. Read it +fully before writing any test. + +## The one rule that matters most + +> **Always call the API through `ctx.penpot`, never the global `penpot`.** + +`ctx.penpot` is a recording proxy. Calls made through it are what count towards +coverage and are correctly attributed to the right interface. Calls on the global +`penpot` still work but are invisible to coverage. Same for shapes: operate on the +objects returned by `ctx.penpot.*` (and on `ctx.board`), not on objects obtained +some other way. + +## Running and iterating + +From `plugins/`: + +- Dev server: `pnpm run start:plugin:api-test-suite` (serves on port 4202). +- In Penpot: open the Plugin Manager (Ctrl+Alt+P) and install + `http://localhost:4202/manifest.json`. +- **Hot-reloading tests:** after editing a `*.test.ts`, click **Reload** in the + plugin UI. It fetches the freshly built test bundle and swaps in your changes — + no need to close/reopen the plugin. (The dev server rebuilds the bundle on save.) +- **Adding a _new_ test file:** tests are discovered via `import.meta.glob` at + build time, and `vite build --watch` does not reliably pick up a brand-new file + (only edits to files already in its graph). After creating a new `*.test.ts`, + **restart the watch process** (`pnpm run watch` or `pnpm run init`) and then + click **Reload** (or reopen the plugin). Editing an existing test file does not + need this. +- The UI: tests are shown in **collapsible groups** (from `describe`) with per-group + passed/failed/total counts. Run with **Run all**, **Run selected** (per-test or + per-group checkboxes), the per-group **Run group**, or the per-row **Run** button. + Failures expand to show the error. The coverage panel shows the percentage, a + progress bar, and per-interface get/set/call targets. + +## Running in CI + +A headless runner executes the same tests against a live instance via Playwright: + +``` +E2E_LOGIN_EMAIL=… E2E_LOGIN_PASSWORD=… \ + pnpm --filter plugin-api-test-suite run test:ci +``` + +- It builds `headless.js`, logs in, creates a scratch file, injects the test + bundle, and prints per-test results + the coverage report. +- Exit code is non-zero iff any test failed (coverage does not affect it). +- Optional env: `PENPOT_BASE_URL` (default `https://localhost:3449`). Against a + local devenv with a self-signed certificate, prefix the command with + `NODE_TLS_REJECT_UNAUTHORIZED=0` to avoid a `fetch failed` TLS error. +- `PRINT_UNCOVERED=1` dumps the uncovered targets per interface; `PRINT_STATIC=1` + dumps the statically-covered ones (see [Coverage](#how-coverage-works-and-how-to-write-tests-that-move-it)). + +CI entry points reuse the exact same test files (`src/ci/headless.ts` discovers +them the same way the plugin does). + +### Mocked-backend mode + +The same runner can run without a live instance — it serves the prebuilt +frontend via the frontend e2e static server and intercepts every backend RPC +with Playwright `page.route`, reusing the frontend e2e mock fixtures: + +``` +pnpm --filter plugin-api-test-suite run test:ci:mocked +``` + +(equivalently `MOCK_BACKEND=1 … run test:ci`). No login or backend is needed. +This validates the frontend Plugin API binding + in-memory store only, so it +can't faithfully reproduce results that depend on real backend behaviour +(validation, persistence, generated ids, …). Tests that need the real backend +opt out of this mode by tagging themselves `skipIfMocked`: + +```ts +test.skipIfMocked('depends on backend validation', (ctx) => { + /* … */ +}); + +// or a whole group: +describe.skipIfMocked('Backend-dependent', () => { + /* … */ +}); +``` + +Skipped tests are listed in the runner output. The wiring (fixtures, RPC mocks, +WebSocket mock) lives in `ci/run-ci.ts`; mocked-mode fidelity is its main +limitation, so prefer the live `test:ci` for anything backend-sensitive. + +## Anatomy of a test + +Tests live in `src/tests/*.test.ts` and are **auto-discovered** (via +`import.meta.glob`) — just create a file matching that glob, no registration list +to update. A file registers one or more tests by calling `test(name, fn)`. + +```ts +import { expect } from '../framework/expect'; +import { test } from '../framework/registry'; + +test('creates a rectangle', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + expect(rect.type).toBe('rectangle'); + rect.name = 'sample-rect'; + expect(rect.name).toBe('sample-rect'); +}); +``` + +### Grouping tests + +Wrap related tests in `describe(groupName, fn)` to group them. In the UI each group +is a **collapsible section** showing its own passed / failed / total counts, with a +"Run group" button and a select-all checkbox. Tests not inside any `describe` fall +into the `General` group. + +```ts +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +describe('Shapes', () => { + test('creates a rectangle', (ctx) => { + /* … */ + }); + + test('creates an ellipse', (ctx) => { + /* … */ + }); +}); +``` + +`describe` blocks may be nested in a file. Nested names are **joined into a single +group path** with `" / "`, so the group reveals the file/area it lives in — e.g. +`describe('Layout', () => describe('Flex', …))` produces the group `Layout / Flex`. +Wrap each file's tests in a top-level `describe` named after its area so every +group is recognizable. Several files may contribute to the same group path (they +merge in the UI). Prefer one clear group per feature area. + +In the UI each group header shows an aggregate **status dot** rolled up from its +tests: it turns purple while any test in the group is running, red if any failed, +green only once every test passed, and grey until then. + +### The test context (`ctx`) + +`fn` receives a `TestContext` (`src/framework/types.ts`): + +- `ctx.penpot` — the recording proxy over the real `penpot` global. Use it for + every API call. +- `ctx.board` — a **fresh scratch `Board`** created for this test and + **removed automatically afterwards**. Append shapes you create to it + (`ctx.board.appendChild(shape)`) so the user's canvas is left clean. Do not rely + on it persisting between tests. + +The runner also resets shared state between tests: the selection is cleared and the +active page is restored to whatever was active when the run started (both through +the raw `penpot`, so they aren't credited toward coverage). A test that changes the +active page therefore won't leak into later tests. + +### Sync or async + +`fn` may be `void` or `Promise`; async tests are awaited. Use `async (ctx) =>` +and `await` when the API call is asynchronous (e.g. `uploadMediaUrl`, +`library.availableLibraries()`, token application — see notes below). + +### Naming + +The test name becomes its id (slugified) and is shown in the UI. Keep names unique +and descriptive; duplicates are de-duplicated automatically but that's confusing. + +## Assertions + +Import `expect` from `../framework/expect`. It is a small, dependency-free, +jest-like matcher set (it must stay dependency-free — it runs inside the SES +sandbox). Available matchers: + +- `toBe(expected)` — `Object.is` equality +- `toEqual(expected)` — deep structural equality +- `toBeTruthy()` / `toBeFalsy()` +- `toBeNull()` / `toBeUndefined()` / `toBeDefined()` +- `toContain(item)` — substring or array membership +- `toHaveLength(n)` +- `toBeGreaterThan(n)` / `toBeLessThan(n)` +- `toBeCloseTo(n, numDigits?)` — for floats +- `toThrow(expected?)` — `expected` is a substring or `RegExp` matched against the + error message; pass a function as the value: `expect(() => …).toThrow('msg')` +- `.not` negates any matcher: `expect(x).not.toBeNull()` + +For asynchronous failures use `expectReject(promiseOrThunk, expected?)`: `toThrow` +calls its argument synchronously, so it can't catch a rejected promise, whereas +`expectReject` awaits and asserts the rejection (string includes / RegExp on the +message). + +A failing matcher throws; the runner turns that into a red test with the message. +You can also just `throw new Error('…')` to fail a test. + +> Do not add other assertion libraries. Anything imported here is bundled into the +> sandbox and must be SES-safe and dependency-free. + +## How coverage works (and how to write tests that move it) + +Coverage is **type-aware** and tracks three separate targets per member: + +- **`name (get)`** — reading a property (`const n = shape.name`) +- **`name (set)`** — writing a property (`shape.name = 'x'`) +- **`appendChild()`** — calling a method (credited only when actually **called**, + not when merely referenced) + +Implications when writing tests: + +- A property has independent get/set targets. To cover both, read it _and_ write + it. Read-only properties (declared `readonly` in the d.ts) only have a get + target; methods only have a call target. +- Accessing a member through a value you got from `ctx.penpot` is what counts. + Reaching a nested object also counts: e.g. `ctx.board.children[0].type` records + `Board.children (get)` and then the element's `type` get, resolved to the + concrete shape type at runtime. +- Coverage **accumulates across a run**. Running all tests aggregates every test's + accesses. Running a single test shows only that test's accesses. + +### Recorded vs. effective coverage + +The report distinguishes three states per target: + +- **Covered (recorded)** — credited by the recording proxy (green). +- **Statically covered** — exercised behaviourally by the tests but the proxy + _structurally cannot_ credit it (shown in a distinct colour). These come from a + curated allowlist in `src/framework/static-coverage.ts`, keyed by + `Interface.member#mode`. See [Coverage notes](#coverage-notes) for which members + and why. +- **Uncovered** — neither. + +The header shows two numbers: the **recorded** percentage (what the proxy actually +credited) and the **effective** percentage (recorded + statically covered). +Recorded coverage always wins, so listing a target in the static allowlist that +turns out to be recorded is harmless — it simply never shows as static. Coverage is +report-only; it never fails a run or the build. + +The denominator comes from `src/generated/api-surface.json`, generated from +`libs/plugin-types/index.d.ts`. If the Plugin API types change, regenerate it: + +``` +pnpm --filter plugin-api-test-suite run gen:api +``` + +## Runtime details you need to know + +- **Shape `type` values** returned at runtime: `Board` → `'board'`, + `Rectangle` → `'rectangle'`, `Ellipse` → `'ellipse'`, plus `'text'`, `'path'`, + `'group'`, `'image'`, `'svg-raw'`. (`createRectangle().type === 'rectangle'`.) +- `createText(str)` returns `Text | null` — guard the result (`if (text) { … }`). +- `width`/`height` are read-only; use `resize(w, h)`. `x`/`y` are writable. +- The plugin manifest already requests broad permissions (`content:*`, + `library:*`, `user:read`, `comment:*`, `allow:downloads`, `allow:localstorage`), + so most of the API is callable from tests without changes. +- The runner sets `throwValidationErrors = true` and `naturalChildOrdering = true`, + so invalid API usage throws (surfacing as a red test) and `children` is always in + z-index order. +- The runtime is SES-sandboxed: no Node APIs, no DOM, no extra npm deps inside + tests. Stick to the Plugin API, `expect`, and plain JS. + +## Coverage notes + +The suite covers a large majority of the type surface. The remaining members are +uncovered or only _statically_ covered for the reasons below — **not** missing +tests. Note these notes can drift as the API is fixed: when in doubt, write the +test asserting the documented correct behaviour and run `test:ci` to see what +actually happens. + +### Exercised behaviourally but not creditable by the recorder (statically covered) + +Listed in `src/framework/static-coverage.ts`: + +- **`ContextTypesUtils.*` and `ContextGeometryUtils.center`** — `penpot.utils.types` + and `penpot.utils.geometry` are frozen (SES) data properties, so the recording + proxy must return them raw and cannot wrap their members. Both are exercised + behaviourally in `platform.test.ts`. +- **`ColorShapeInfo.shapesInfo`, `ColorShapeInfoEntry.*`** — `shapesColors()` has an + unresolved return type in the generated surface (`type: null`), so the recorder + hands the result back raw and can't attribute nested access. Exercised in + `colors.test.ts`. (Alternatively, resolving the return type in + `tools/gen-api-surface.ts` would make these genuinely recorded.) +- **`EventsMap.*`** — a type map, not a runtime object. `on`/`off` are credited on + `Penpot`, never as `EventsMap` members. The deterministic events + (`selectionchange`, `shapechange`) are exercised in `events.test.ts`. +- **`ShapeBase.fills`** — every concrete shape redeclares `fills`, so accesses are + attributed to the concrete type (`Rectangle.fills`, …); the base-interface target + is never the attribution. +- **`LibraryVariantComponent.*`** — the recorder types a component as + `LibraryComponent` and can't narrow to `LibraryVariantComponent` via the + `isVariant()` type-guard. The behaviour is exercised via `VariantContainer.variants` + in `variants.test.ts`. + +### Read-only at runtime + +Members that have no setter in the runtime binding (`frontend/src/app/plugins/*.cljs`) +are now marked `readonly` in the Plugin API d.ts (`Font.*`, `FontVariant.*`, +`FontsContext.all`, `Image/Ellipse/SvgRaw.type`, `File.name/pages/revn`, `Page.root`, +`TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, and the board guide +value objects `GuideColumn/GuideRow/GuideSquare` and their params — `board.guides` +returns a formatted snapshot, so guides are reconfigured by reassigning the whole +array, not by mutating a returned guide), the `Point`/`Bounds` value objects, the +`Penpot.ui`/`Penpot.utils` subcontexts, and the derived `Boolean` path data +(`d`/`content`/`commands` are computed from the operands — a `Boolean` isn't editable +like a `Path`). They therefore have only a `(get)` target and need no runtime +assertion — the type system enforces the contract. + +Members that **do** have a runtime setter stay writable, even when the setter +rejects some inputs (that's input validation, not read-only-ness): `Board.children` +(assigning a reordered array reorders the children), `Path.d/content/commands` +(editing the path), and `FileVersion.label` (relabels the version). + +### Excluded from coverage + +`tools/gen-api-surface.ts` drops two categories from the denominator so they never +count: + +- **`@deprecated` interfaces and members** — the legacy `Image` shape interface + (images live in a `Fill` via `fillImage`), `Color.refId`/`refFile`, and the + `Boolean`/`Path` `toD()`/`content` path accessors. +- **Members removed by the public interface via `Omit`** — `Context` is the + internal interface and the public `Penpot` is `Omit` (those are superseded by `on`/`off`). The generator honors the + `Omit`, so `Context.addListener`/`removeListener` aren't reachable surface and + don't count. + +### Red tests pinning confirmed API bugs + +When a member is confirmed broken, add a test that asserts its **correct** behaviour +and comment it as blocked-by-bug; it stays red until the API is fixed and then turns +green (at which point drop the "API bug" framing). There are currently no such red +tests — e.g. the `fontFamilies` token `resolvedValue` bug (it used to leak the raw +tokenscript structure instead of `string[]`) has since been fixed. + +### d.ts / runtime mismatches + +`strokeStyle: 'none'` is listed in the d.ts but rejected at runtime ("Value not +valid"); `fills-strokes.test.ts` pins this with a `toThrow`. + +### External state / not reachable headless + +- **`ActiveUser.position/zoom`** — needs a second collaborator in the file. +- **`LibrarySummary.*`, `LibraryContext.connectLibrary`** — need a published shared + library. +- **`FileVersion.restore`, `Penpot.closePlugin`, `Penpot.ui`, `Context.openViewer`** — + tear down or navigate away from the running plugin/workspace. +- **`FileVersion.pin`** — only converts a _system_ autosave to a permanent version; + a plugin can only create manual versions (`saveVersion`), so `pin()` always + rejects. +- **`Context.addListener/removeListener`** — omitted from the `penpot` global + (`Omit`), so unreachable via `penpot`. +- **`EventsMap` events `pagechange/filechange/themechange/contentsave/finish`** — + can't be triggered deterministically in the headless runner. + +## Checklist before finishing + +- [ ] Test file is `src/tests/.test.ts` and uses `test(...)` + `expect`, + ideally wrapped in a `describe('', …)`. +- [ ] All API calls go through `ctx.penpot`; shapes are appended to `ctx.board`. +- [ ] Created shapes don't leak (rely on the scratch board cleanup; don't touch the + user's existing content). +- [ ] Lint/format/typecheck pass: + `pnpm --filter plugin-api-test-suite run lint` and, from `plugins/`, + `pnpm exec prettier --check "apps/plugin-api-test-suite/**/*.{ts,css,json}"`. +- [ ] If you relied on new API members, `gen:api` was re-run so coverage reflects + them. + +## Where things live (for deeper changes) + +- `src/framework/registry.ts` — `test()`, `describe()`, `getTests()`, `setTests()` (reload). +- `src/framework/runner.ts` — runs tests, scratch board lifecycle, per-test state reset, coverage. +- `src/framework/coverage.ts` — the recording proxy + coverage computation. +- `src/framework/static-coverage.ts` — the statically-covered allowlist. +- `src/framework/expect.ts` — the assertion library. +- `src/framework/types.ts` — `TestContext`, `TestResult`, `CoverageReport`, etc. +- `tools/gen-api-surface.ts` — generates `src/generated/api-surface.json`. +- `src/plugin.ts` (sandbox), `src/ui.ts` (iframe), `src/model.ts` (messages). +- `src/ci/headless.ts` + `ci/run-ci.ts` — CI path. + +Writing tests should only ever require touching `src/tests/`. diff --git a/plugins/apps/plugin-api-test-suite/ci/fixtures/get-file.json b/plugins/apps/plugin-api-test-suite/ci/fixtures/get-file.json new file mode 100644 index 0000000000..dcc331badd --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/ci/fixtures/get-file.json @@ -0,0 +1,60 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "fdata/shape-data-type", + "fdata/path-data", + "components/v2", + "design-tokens/v1", + "variants/v1", + "plugins/runtime" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 1", + "~:revn": 11, + "~:modified-at": "~m1713873823633", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:is-shared": false, + "~:version": 46, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713536343369", + "~:data": { + "~:pages": ["~u66697432-c33d-8055-8006-2c62cc084cad"], + "~:pages-index": { + "~u66697432-c33d-8055-8006-2c62cc084cad": { + "~#penpot/pointer": [ + "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + { + "~:created-at": "~m1713873823636" + } + ] + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:options": { + "~:components-v2": true + }, + "~:recent-colors": [ + { + "~:color": "#0000ff", + "~:opacity": 1, + "~:id": null, + "~:file-id": null, + "~:image": null + } + ] + } +} diff --git a/plugins/apps/plugin-api-test-suite/ci/run-ci.ts b/plugins/apps/plugin-api-test-suite/ci/run-ci.ts new file mode 100644 index 0000000000..5b03d30386 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/ci/run-ci.ts @@ -0,0 +1,475 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { chromium, type Page } from 'playwright'; +import type { CoverageReport, TestResult } from '../src/framework/types'; + +// Out-of-sandbox CI driver (Node + Playwright). Injects the prebuilt +// `headless.js` bundle (built from the in-sandbox entry `src/ci/headless.ts` — +// note: a different `ci/` directory) into the plugin sandbox via +// `globalThis.ɵloadPlugin` and captures results/coverage from the page console. +// Two modes: +// +// - LIVE (default): logs into a real Penpot instance (devenv), creates a scratch +// file, and drives the real backend + frontend end-to-end. +// Required env: E2E_LOGIN_EMAIL, E2E_LOGIN_PASSWORD. +// Optional env: PENPOT_BASE_URL (default https://localhost:3449). +// +// - MOCKED (`MOCK_BACKEND=1`): serves the prebuilt frontend bundle via the e2e +// static server and intercepts every backend RPC with Playwright `page.route`, +// reusing the frontend e2e mock fixtures. No backend/login needed. Validates +// the frontend Plugin API binding + in-memory store only; results that depend +// on real backend behaviour are not faithfully reproduced, so those tests are +// skipped via the `skipIfMocked` tag. + +const here = dirname(fileURLToPath(import.meta.url)); +// here = /plugins/apps/plugin-api-test-suite/ci +const repoRoot = resolve(here, '../../../../'); +const frontendDir = resolve(repoRoot, 'frontend'); +const e2eDataDir = resolve(frontendDir, 'playwright/data'); + +const MOCKED = !!process.env['MOCK_BACKEND']; +const MOCK_BASE_URL = 'http://localhost:3000'; +const apiUrl = MOCKED + ? MOCK_BASE_URL + : (process.env['PENPOT_BASE_URL'] ?? 'https://localhost:3449'); + +const headlessBundlePath = resolve( + here, + '../../../dist/apps/plugin-api-test-suite/headless.js', +); + +// Source the permissions from the same manifest the real plugin ships with, so +// the CI sandbox never drifts from what users actually grant. +const manifestPath = resolve(here, '../public/manifest.json'); +const PERMISSIONS: string[] = ( + JSON.parse(readFileSync(manifestPath, 'utf-8')) as { permissions: string[] } +).permissions; + +function cleanId(id: string): string { + return id.replace('~u', ''); +} + +interface FileRpc { + '~:id': string; + '~:project-id': string; + '~:data': { '~:pages': string[] }; +} + +async function login() { + const email = process.env['E2E_LOGIN_EMAIL']; + const password = process.env['E2E_LOGIN_PASSWORD']; + if (!email || !password) { + throw new Error('E2E_LOGIN_EMAIL / E2E_LOGIN_PASSWORD must be set'); + } + + const response = await fetch( + `${apiUrl}/api/main/methods/login-with-password`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }, + ); + + const loginData = await response.json(); + const authToken = response.headers + .getSetCookie() + .find((cookie) => cookie.startsWith('auth-token=')) + ?.split(';')[0]; + + if (!authToken) + throw new Error('Login failed: no auth-token cookie returned'); + + return { authToken, defaultProjectId: loginData['~:default-project-id'] }; +} + +async function createFile( + authToken: string, + projectId: string, +): Promise { + const response = await fetch(`${apiUrl}/api/main/methods/create-file`, { + method: 'POST', + headers: { + 'Content-Type': 'application/transit+json', + cookie: authToken, + }, + body: JSON.stringify({ + '~:name': `api-test-suite ${new Date().toISOString()}`, + '~:project-id': projectId, + '~:features': { + '~#set': [ + 'fdata/objects-map', + 'fdata/pointer-map', + 'fdata/shape-data-type', + 'fdata/path-data', + 'design-tokens/v1', + 'variants/v1', + 'components/v2', + 'styles/v2', + 'layout/grid', + 'plugins/runtime', + ], + }, + }), + }); + + return (await response.json()) as FileRpc; +} + +function getFileUrl(file: FileRpc): string { + const projectId = cleanId(file['~:project-id']); + const fileId = cleanId(file['~:id']); + const pageId = cleanId(file['~:data']['~:pages'][0]); + return `${apiUrl}/#/workspace/${projectId}/${fileId}?page-id=${pageId}`; +} + +// --- Mocked mode setup ------------------------------------------------------- + +// Ids of the mocked full-feature file fixture (`ci/fixtures/get-file.json`), +// kept in sync with the frontend e2e fixtures. +const MOCK_TEAM_ID = 'c7ce0794-0992-8105-8004-38e630f7920a'; +const MOCK_FILE_ID = 'c7ce0794-0992-8105-8004-38f280443849'; +const MOCK_PAGE_ID = '66697432-c33d-8055-8006-2c62cc084cad'; + +// Workspace-load RPCs mirrored from the frontend e2e harness +// (WorkspacePage.init + setupEmptyFile). Maps RPC glob -> fixture file relative +// to frontend/playwright/data. +const MOCK_RPCS: Record = { + 'get-profile': 'logged-in-user/get-profile-logged-in.json', + 'get-teams': 'get-teams.json', + 'get-team?id=*': 'workspace/get-team-default.json', + 'get-team-members?team-id=*': + 'logged-in-user/get-team-members-your-penpot.json', + 'get-team-users?file-id=*': 'logged-in-user/get-team-users-single-user.json', + 'get-project?id=*': 'workspace/get-project-default.json', + 'get-comment-threads?file-id=*': 'workspace/get-comment-threads-empty.json', + 'get-profiles-for-file-comments?file-id=*': + 'workspace/get-profile-for-file-comments.json', + 'get-file-object-thumbnails?file-id=*': + 'workspace/get-file-object-thumbnails-blank.json', + 'get-font-variants?team-id=*': 'workspace/get-font-variants-empty.json', + 'get-file-fragment?file-id=*': 'workspace/get-file-fragment-blank.json', + 'get-file-libraries?file-id=*': 'workspace/get-file-libraries-empty.json', + 'update-profile-props': 'workspace/update-profile-empty.json', +}; + +// Persistence (`update-file`) response shape the frontend expects: it reads +// `revn`/`lagged` (persistence.cljs `update-file-revn`). `revn` is merged with +// `max`, so a low value is harmless. +const UPDATE_FILE_RESPONSE = JSON.stringify({ '~:revn': 1, '~:lagged': [] }); + +async function waitForServer(url: string, timeoutMs = 30000): Promise { + const start = Date.now(); + for (;;) { + try { + const res = await fetch(url); + if (res.ok || res.status === 404) return; // static server is up + } catch { + /* not up yet */ + } + if (Date.now() - start > timeoutMs) { + throw new Error(`Timed out waiting for server at ${url}`); + } + await new Promise((r) => setTimeout(r, 250)); + } +} + +function startE2eServer(): ChildProcess { + // Reuse the frontend e2e static server: it serves frontend/resources/public + // on port 3000, which is also the host the app opens its notifications + // WebSocket against (ws://localhost:3000/ws/notifications) — so the WS mock + // below matches without extra config. + const child = spawn('node', ['scripts/e2e-server.js'], { + cwd: frontendDir, + stdio: 'inherit', + }); + return child; +} + +// Install the frontend e2e WebSocket mock so the workspace's notifications +// socket can be "opened" without a backend. +async function installWebSocketMock(page: Page): Promise { + const created = new Set(); + await page.exposeFunction('onMockWebSocketConstructor', (url: string) => { + created.add(url); + }); + await page.addInitScript({ + path: resolve(frontendDir, 'playwright/scripts/MockWebSocket.js'), + }); + // Stash the helper on the page object for later use. + (page as unknown as { __wsCreated: Set }).__wsCreated = created; +} + +async function openNotificationsWebSocket(page: Page): Promise { + const created = (page as unknown as { __wsCreated: Set }).__wsCreated; + const start = Date.now(); + let wsUrl: string | undefined; + while (!wsUrl) { + wsUrl = [...created].find((u) => u.includes('ws/notifications')); + if (wsUrl) break; + if (Date.now() - start > 30000) { + throw new Error('Timed out waiting for notifications WebSocket'); + } + await new Promise((r) => setTimeout(r, 50)); + } + await page.evaluate((url) => { + ( + WebSocket as unknown as { + getByURL: (u: string) => { mockOpen: () => void } | undefined; + } + ) + .getByURL(url) + ?.mockOpen(); + }, wsUrl); +} + +async function setupMockedRoutes(page: Page): Promise { + // Config flags: deterministic empty flags (mirror BasePage.mockConfigFlags). + await page.route('**/js/config.js*', (route) => + route.fulfill({ + status: 200, + contentType: 'application/javascript', + body: 'var penpotFlags = "";\n', + }), + ); + + // Workspace-load RPCs from fixtures. + for (const [rpc, fixture] of Object.entries(MOCK_RPCS)) { + await page.route(`**/api/main/methods/${rpc}`, (route) => + route.fulfill({ + status: 200, + contentType: 'application/transit+json', + path: resolve(e2eDataDir, fixture), + }), + ); + } + + // get-file: the custom full-feature fixture (enables plugins/runtime, + // design-tokens/v1, variants/v1, ...). Without these features active the + // plugin runtime never initialises. + await page.route(/\/api\/main\/methods\/get-file\?/, (route) => + route.fulfill({ + status: 200, + contentType: 'application/transit+json', + path: resolve(here, 'fixtures/get-file.json'), + }), + ); + + // Blanket no-op persistence: most of the Plugin API mutates the in-memory + // store optimistically, so a 200 `update-file` mock is enough for the bulk of + // the suite to run against in-memory state. + await page.route(/\/api\/main\/methods\/update-file\b/, (route) => + route.fulfill({ + status: 200, + contentType: 'application/transit+json', + body: UPDATE_FILE_RESPONSE, + }), + ); +} + +function mockedFileUrl(): string { + return `${MOCK_BASE_URL}/#/workspace?team-id=${MOCK_TEAM_ID}&file-id=${MOCK_FILE_ID}&page-id=${MOCK_PAGE_ID}`; +} + +// --- Reporting --------------------------------------------------------------- + +function printReport( + results: TestResult[], + coverage: CoverageReport | null, + skipped: string[], +) { + // Each result is already printed live as it streams in; here we only recap the + // failures so they're easy to find at the bottom of a long run. + const failures = results.filter((r) => r.status === 'fail'); + if (failures.length > 0) { + console.log('\nFailures:'); + for (const r of failures) { + console.log(` ✗ ${r.name} (${r.durationMs}ms)`); + if (r.error) { + console.log(` ${r.error}`); + } + } + } + + if (skipped.length > 0) { + console.log(`\nSkipped (mocked mode): ${skipped.length}`); + for (const name of skipped) { + console.log(` - ${name}`); + } + } + + if (coverage) { + console.log( + `\nAPI coverage (report-only): ${coverage.percent}% recorded ` + + `(${coverage.covered}/${coverage.total}), ` + + `${coverage.effectivePercent}% effective ` + + `(+${coverage.staticallyCovered} statically covered)`, + ); + + // Opt-in dump of the uncovered targets per interface, to drive test writing. + if (process.env['PRINT_UNCOVERED']) { + console.log('\nUncovered targets by interface:'); + for (const [iface, info] of Object.entries(coverage.byInterface)) { + if (info.uncovered.length > 0) { + console.log(` ${iface}: ${info.uncovered.join(', ')}`); + } + } + } + + // Opt-in dump of the statically-covered targets (exercised behaviourally but + // not creditable through the recording proxy). + if (process.env['PRINT_STATIC']) { + console.log('\nStatically covered targets by interface:'); + for (const [iface, info] of Object.entries(coverage.byInterface)) { + if (info.staticallyCovered.length > 0) { + console.log(` ${iface}: ${info.staticallyCovered.join(', ')}`); + } + } + } + } +} + +async function main() { + const bundle = readFileSync(headlessBundlePath, 'utf-8'); + + let server: ChildProcess | undefined; + let fileUrl: string; + let authToken: string | undefined; + + if (MOCKED) { + server = startE2eServer(); + await waitForServer(MOCK_BASE_URL); + fileUrl = mockedFileUrl(); + } else { + const session = await login(); + authToken = session.authToken; + const file = await createFile(authToken, session.defaultProjectId); + fileUrl = getFileUrl(file); + } + + const browser = await chromium.launch({ + args: ['--ignore-certificate-errors'], + }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + if (authToken) { + await context.addCookies([ + { name: 'auth-token', value: authToken.split('=')[1], url: apiUrl }, + ]); + } + + const page = await context.newPage(); + + if (MOCKED) { + await installWebSocketMock(page); + await setupMockedRoutes(page); + } + + // The bundle runs inside an SES Compartment (its own `globalThis`), so a page + // `addInitScript` global can't reach it. Prepend the mocked flag straight into + // the evaluated code so the bundle's `runTests` excludes `skipIfMocked` tests. + const injectedCode = MOCKED + ? `globalThis.__PLUGIN_SUITE_MOCKED__ = true;\n${bundle}` + : bundle; + + const results: TestResult[] = []; + let coverage: CoverageReport | null = null; + let skipped: string[] = []; + let fatal: string | null = null; + + console.log('\nRunning tests:'); + const done = new Promise((resolvePromise) => { + page.on('console', (msg) => { + const text = msg.text(); + if (text.startsWith('__TEST_RESULT__ ')) { + const result: TestResult = JSON.parse( + text.slice('__TEST_RESULT__ '.length), + ); + results.push(result); + // Print each result as it streams in so the run shows live progress + // instead of staying silent until it finishes. + const icon = result.status === 'pass' ? '✓' : '✗'; + console.log(` ${icon} ${result.name} (${result.durationMs}ms)`); + if (result.status === 'fail' && result.error) { + console.log(` ${result.error}`); + } + } else if (text.startsWith('__TEST_COVERAGE__ ')) { + coverage = JSON.parse(text.slice('__TEST_COVERAGE__ '.length)); + } else if (text.startsWith('__TEST_SKIPPED__ ')) { + skipped = JSON.parse(text.slice('__TEST_SKIPPED__ '.length)); + } else if (text.startsWith('__TEST_DONE__ ')) { + resolvePromise(); + } else if (text.startsWith('__TEST_FATAL__ ')) { + fatal = JSON.parse(text.slice('__TEST_FATAL__ '.length)).message; + resolvePromise(); + } + }); + }); + + await page.goto(fileUrl); + + if (MOCKED) { + await openNotificationsWebSocket(page); + } + + await page.waitForSelector('[data-testid="viewport"]'); + // The plugin runtime initialises asynchronously after the file's features are + // active; wait for the loader to be exposed before injecting the bundle. + await page.waitForFunction( + () => + typeof (globalThis as unknown as { ɵloadPlugin?: unknown }) + .ɵloadPlugin === 'function', + { timeout: 30000 }, + ); + + await page.evaluate( + ({ code, permissions }) => { + ( + globalThis as unknown as { ɵloadPlugin: (m: unknown) => void } + ).ɵloadPlugin({ + pluginId: '00000000-0000-0000-0000-000000000000', + name: 'Plugin API Test Suite (CI)', + code, + icon: '', + description: '', + permissions, + }); + }, + { code: injectedCode, permissions: PERMISSIONS }, + ); + + await Promise.race([ + done, + new Promise((_, reject) => + setTimeout( + () => reject(new Error('Timed out waiting for test results')), + 120000, + ), + ), + ]); + + await browser.close(); + server?.kill(); + + printReport(results, coverage, skipped); + + if (fatal) { + console.error(`\nFatal error while running tests: ${fatal}`); + process.exit(1); + } + + const failed = results.filter((r) => r.status === 'fail').length; + const passed = results.filter((r) => r.status === 'pass').length; + console.log( + `\n${passed} passed, ${failed} failed${ + skipped.length ? `, ${skipped.length} skipped` : '' + }.`, + ); + process.exit(failed > 0 ? 1 : 0); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/plugins/apps/plugin-api-test-suite/eslint.config.js b/plugins/apps/plugin-api-test-suite/eslint.config.js new file mode 100644 index 0000000000..961ac200ec --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/eslint.config.js @@ -0,0 +1,27 @@ +import baseConfig from '../../eslint.config.js'; + +export default [ + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + project: './tsconfig.*?.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: {}, + }, + { + ignores: [ + '**/assets/*.js', + 'vite.config.ts', + 'vite.config.headless.ts', + 'vite.config.tests.ts', + 'vite.config.iife.ts', + ], + }, +]; diff --git a/plugins/apps/plugin-api-test-suite/index.html b/plugins/apps/plugin-api-test-suite/index.html new file mode 100644 index 0000000000..22b70e5f1f --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/index.html @@ -0,0 +1,12 @@ + + + + + Plugin API Test Suite + + + +
+ + + diff --git a/plugins/apps/plugin-api-test-suite/package.json b/plugins/apps/plugin-api-test-suite/package.json new file mode 100644 index 0000000000..43bf67412c --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/package.json @@ -0,0 +1,22 @@ +{ + "name": "plugin-api-test-suite", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --emptyOutDir && pnpm run build:headless && pnpm run build:tests", + "build:headless": "vite build --config vite.config.headless.ts", + "build:tests": "vite build --config vite.config.tests.ts", + "watch": "concurrently --kill-others --names app,tests \"vite build --watch --mode development\" \"vite build --watch --mode development --config vite.config.tests.ts\"", + "serve": "vite preview", + "init": "concurrently --kill-others --names build,serve \"pnpm run watch\" \"pnpm run serve\"", + "lint": "eslint .", + "gen:api": "tsx tools/gen-api-surface.ts", + "test:ci": "pnpm run build:headless && tsx ci/run-ci.ts", + "test:ci:mocked": "pnpm run build:headless && MOCK_BACKEND=1 tsx ci/run-ci.ts" + }, + "devDependencies": { + "playwright": "^1.61.0" + } +} diff --git a/plugins/apps/plugin-api-test-suite/public/_headers b/plugins/apps/plugin-api-test-suite/public/_headers new file mode 100644 index 0000000000..cdb4e7ed20 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/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/plugin-api-test-suite/public/assets/icon.png b/plugins/apps/plugin-api-test-suite/public/assets/icon.png new file mode 100644 index 0000000000..4f010b502f Binary files /dev/null and b/plugins/apps/plugin-api-test-suite/public/assets/icon.png differ diff --git a/plugins/apps/plugin-api-test-suite/public/manifest.json b/plugins/apps/plugin-api-test-suite/public/manifest.json new file mode 100644 index 0000000000..a56ee76498 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/public/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Plugin API Test Suite", + "description": "Launcher for a battery of Penpot Plugin API tests", + "code": "plugin.js", + "version": 2, + "icon": "assets/icon.png", + "permissions": [ + "content:read", + "content:write", + "library:read", + "library:write", + "user:read", + "comment:read", + "comment:write", + "allow:downloads", + "allow:localstorage" + ] +} diff --git a/plugins/apps/plugin-api-test-suite/src/ci/headless.ts b/plugins/apps/plugin-api-test-suite/src/ci/headless.ts new file mode 100644 index 0000000000..9374ab4e48 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/ci/headless.ts @@ -0,0 +1,39 @@ +import { runTests } from '../framework/runner'; + +// In-sandbox CI entry point. Built as a standalone IIFE bundle (headless.js) and +// evaluated inside a real Penpot plugin sandbox by the out-of-sandbox driver +// `ci/run-ci.ts` (note: distinct from this `src/ci/` directory). It runs every +// test and reports results + coverage through `console.log` markers that the +// Playwright driver parses. It has no UI. + +// Auto-discover the same tests used by the UI plugin. +import.meta.glob('../tests/*.test.ts', { eager: true }); + +async function main() { + // Set by the mocked-backend runner (MOCK_BACKEND=1) before this bundle loads, + // so backend-result-dependent tests tagged `skipIfMocked` are excluded. + const skipMocked = !!( + globalThis as unknown as { __PLUGIN_SUITE_MOCKED__?: boolean } + ).__PLUGIN_SUITE_MOCKED__; + + // Stream each result as it completes (not just at the end) so the runner sees + // progress and partial output survives if a later test hangs to its timeout. + const { summary, coverage, skipped } = await runTests( + 'all', + (result) => { + if (result.status !== 'running') { + console.log('__TEST_RESULT__ ' + JSON.stringify(result)); + } + }, + { skipMocked }, + ); + + console.log('__TEST_COVERAGE__ ' + JSON.stringify(coverage)); + console.log('__TEST_SKIPPED__ ' + JSON.stringify(skipped)); + console.log('__TEST_DONE__ ' + JSON.stringify(summary)); +} + +main().catch((err) => { + const message = err instanceof Error ? err.message : String(err); + console.log('__TEST_FATAL__ ' + JSON.stringify({ message })); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/framework/coverage.ts b/plugins/apps/plugin-api-test-suite/src/framework/coverage.ts new file mode 100644 index 0000000000..cbf36aa048 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/coverage.ts @@ -0,0 +1,287 @@ +import { STATIC_COVERAGE } from './static-coverage'; +import type { ApiSurface, CoverageReport, InterfaceCoverage } from './types'; + +export interface Recorder { + /** Proxy to hand to tests; mirrors `root` but records member access. */ + proxy: T; + /** Every `Interface.member` pair touched through the proxy. */ + accessed: Set; + /** + * Wraps an already-obtained value as a given interface so subsequent access + * through it is recorded, without crediting how it was obtained. Used for the + * scratch board, whose creation is harness bookkeeping, not test coverage. + */ + wrap(value: V, typeName: string): V; +} + +function isWrappable(value: unknown): value is object { + return ( + value !== null && (typeof value === 'object' || typeof value === 'function') + ); +} + +/** + * True when `prop` is a non-configurable, non-writable data property of `target`. + * The Proxy `get` invariant requires returning that exact value, so wrapping it + * is not allowed. + */ +function nonConfigurableData(target: object, prop: PropertyKey): boolean { + const desc = Reflect.getOwnPropertyDescriptor(target, prop); + return ( + !!desc && + desc.configurable === false && + desc.writable === false && + !desc.get && + !desc.set + ); +} + +/** + * Wraps `root` (the real `penpot` API) in a recursive Proxy that records member + * access in a *type-aware* way. Each proxy is tagged with the interface (or union + * alias) name the underlying value has, derived from the API type graph. When a + * member is accessed we record `Interface.member` against the interface that + * actually declares it, and we tag the returned value with the member's declared + * type so nested access is attributed correctly too. + * + * This avoids the false positives of name-only matching, where e.g. reading + * `shape.id` would wrongly credit every interface that happens to have an `id` + * member. Unknown/primitive types are returned unwrapped and never recorded. + */ +export function createRecorder( + root: T, + surface: ApiSurface, +): Recorder { + const accessed = new Set(); + const toOriginal = new WeakMap(); + // Cache proxies per (target, typeName) so identity is stable and cycles end. + const cache = new WeakMap>(); + + function unwrap(value: unknown): unknown { + if (isWrappable(value) && toOriginal.has(value)) { + return toOriginal.get(value); + } + return value; + } + + /** Resolves the concrete interface name for a tagged value (handles unions). */ + function concreteType(target: object, typeName: string): string | null { + if (surface.graph[typeName]) return typeName; + + const union = surface.unions[typeName]; + if (union?.discriminant) { + const disc = Reflect.get(target, union.discriminant.field) as unknown; + if (typeof disc === 'string') { + return union.discriminant.map[disc] ?? null; + } + } + return null; + } + + function wrapValue( + value: unknown, + typeName: string | null, + array: boolean, + ): unknown { + if (!isWrappable(value) || !typeName) return value; + if (array) { + return Array.isArray(value) ? wrapArray(value, typeName) : value; + } + if (surface.graph[typeName] || surface.unions[typeName]) { + return wrapObject(value, typeName); + } + return value; + } + + function wrapArray(arr: unknown[], elementType: string): unknown[] { + const proxy = new Proxy(arr, { + get(tgt, prop, receiver): unknown { + const value = Reflect.get(tgt, prop, receiver); + if (typeof prop === 'string' && /^\d+$/.test(prop)) { + // A frozen array (e.g. the selection array, sealed by SES) has + // non-configurable, non-writable elements. The Proxy invariant then + // forbids returning a wrapped value that differs from the target's, + // so return the raw element (it just isn't credited for coverage). + if (nonConfigurableData(tgt, prop)) return value; + return wrapValue(value, elementType, false); + } + return value; + }, + }); + toOriginal.set(proxy, arr); + return proxy; + } + + function wrapObject(target: object, typeName: string): object { + let byType = cache.get(target); + if (!byType) { + byType = new Map(); + cache.set(target, byType); + } + const cached = byType.get(typeName); + if (cached) return cached; + + const proxy: object = new Proxy(target, { + get(tgt, prop, receiver): unknown { + const concrete = concreteType(tgt, typeName); + const entry = + concrete && typeof prop === 'string' + ? surface.graph[concrete]?.[prop] + : undefined; + + const raw = Reflect.get(tgt, prop, receiver === proxy ? tgt : receiver); + + // Methods are credited on call (see wrapMethod), not on access. Property + // reads are credited here as `#get`. + if (entry && entry.kind === 'method') { + return wrapMethod(raw as (...a: unknown[]) => unknown, tgt, { + ...entry, + member: String(prop), + }); + } + if (entry) accessed.add(`${entry.decl}.${String(prop)}#get`); + + // Don't wrap a frozen own property (Proxy invariant would be violated). + if (typeof prop === 'string' && nonConfigurableData(tgt, prop)) { + return raw; + } + + return entry ? wrapValue(raw, entry.type, entry.array) : raw; + }, + set(tgt, prop, value, receiver): boolean { + const concrete = concreteType(tgt, typeName); + const entry = + concrete && typeof prop === 'string' + ? surface.graph[concrete]?.[prop] + : undefined; + if (entry) accessed.add(`${entry.decl}.${String(prop)}#set`); + + return Reflect.set( + tgt, + prop, + unwrap(value), + receiver === proxy ? tgt : receiver, + ); + }, + }); + + toOriginal.set(proxy, target); + byType.set(typeName, proxy); + return proxy; + } + + function wrapMethod( + fn: (...a: unknown[]) => unknown, + self: object, + entry: { + decl: string; + member: string; + type: string | null; + array: boolean; + }, + ): (...a: unknown[]) => unknown { + return (...args: unknown[]) => { + // Credit the call only once it returns without throwing, so coverage + // means "successfully exercised" rather than "merely invoked". + const result = fn.apply(self, args.map(unwrap)); + accessed.add(`${entry.decl}.${entry.member}#call`); + + // Async API methods (e.g. uploadMediaUrl, createShapeFromSvgWithImages) + // return a Promise. Wrapping the Promise itself as the declared type would + // break `await` (then() called on the proxy is an incompatible receiver), + // so resolve it first and wrap the resolved value instead. + if ( + isWrappable(result) && + typeof (result as { then?: unknown }).then === 'function' + ) { + return Promise.resolve(result as Promise).then((value) => + wrapValue(value, entry.type, entry.array), + ); + } + + return wrapValue(result, entry.type, entry.array); + }; + } + + return { + proxy: wrapObject(root, 'Penpot') as T, + accessed, + wrap: (value: V, typeName: string) => + wrapValue(value, typeName, false) as V, + }; +} + +/** + * Compares the recorded `Interface.member` pairs against the public API surface + * and produces a report grouped by interface. The denominator is each + * interface's own declared members. + */ +export function computeCoverage( + accessed: Set, + surface: ApiSurface, +): CoverageReport { + const byInterface: Record = {}; + let total = 0; + let coveredCount = 0; + let staticCount = 0; + + for (const [iface, members] of Object.entries(surface.interfaces)) { + const all: string[] = []; + const covered: string[] = []; + const staticallyCovered: string[] = []; + const uncovered: string[] = []; + + for (const member of members) { + // Each writable property contributes separate get/set targets; read-only + // properties only get; methods only call. + const kind = surface.graph[iface]?.[member]?.kind ?? 'getset'; + const targets: { mode: string; label: string }[] = + kind === 'method' + ? [{ mode: 'call', label: `${member}()` }] + : kind === 'get' + ? [{ mode: 'get', label: member }] + : [ + { mode: 'get', label: `${member} (get)` }, + { mode: 'set', label: `${member} (set)` }, + ]; + + for (const { mode, label } of targets) { + all.push(label); + total += 1; + const key = `${iface}.${member}#${mode}`; + if (accessed.has(key)) { + covered.push(label); + coveredCount += 1; + } else if (STATIC_COVERAGE.has(key)) { + staticallyCovered.push(label); + staticCount += 1; + } else { + uncovered.push(label); + } + } + } + + byInterface[iface] = { + members: all, + covered, + staticallyCovered, + uncovered, + }; + } + + const percent = + total === 0 ? 100 : Math.round((coveredCount / total) * 1000) / 10; + const effectivePercent = + total === 0 + ? 100 + : Math.round(((coveredCount + staticCount) / total) * 1000) / 10; + + return { + total, + covered: coveredCount, + staticallyCovered: staticCount, + percent, + effectivePercent, + byInterface, + }; +} diff --git a/plugins/apps/plugin-api-test-suite/src/framework/expect.ts b/plugins/apps/plugin-api-test-suite/src/framework/expect.ts new file mode 100644 index 0000000000..e194317e6b --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/expect.ts @@ -0,0 +1,285 @@ +/** + * Minimal, dependency-free jest-like assertion library. It must not rely on any + * Node/browser globals beyond the basics because it runs inside the SES plugin + * sandbox. Every failed matcher throws an {@link AssertionError}; the runner + * turns that into a red test with the message attached. + * + * Two properties matter for coverage correctness: + * - Failure messages are built lazily (only when an assertion fails), so passing + * assertions never touch the value's members. + * - `stringify` never enumerates non-plain objects (e.g. the recording proxies + * used for API coverage). Otherwise `JSON.stringify` would walk every property + * of a shape and inflate coverage with members the test never used. + */ + +export class AssertionError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssertionError'; + } +} + +function isPlainObject(value: object): boolean { + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function stringify(value: unknown): string { + if (typeof value === 'string') return JSON.stringify(value); + if (typeof value === 'bigint') return `${value}n`; + if (typeof value === 'function') + return `[Function ${value.name || 'anonymous'}]`; + if (value === undefined) return 'undefined'; + if (value === null) return 'null'; + // Only enumerate plain objects/arrays. Host/proxy objects (e.g. Penpot shape + // proxies) are rendered opaquely so stringifying never reads their members. + if ( + typeof value === 'object' && + !Array.isArray(value) && + !isPlainObject(value) + ) { + return '[object]'; + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function deepEqual(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true; + if ( + typeof a !== 'object' || + typeof b !== 'object' || + a === null || + b === null + ) { + return false; + } + if (Array.isArray(a) !== Array.isArray(b)) return false; + + const aKeys = Object.keys(a as Record); + const bKeys = Object.keys(b as Record); + if (aKeys.length !== bKeys.length) return false; + + return aKeys.every( + (key) => + Object.prototype.hasOwnProperty.call(b, key) && + deepEqual( + (a as Record)[key], + (b as Record)[key], + ), + ); +} + +export interface Matchers { + toBe(expected: unknown): void; + toEqual(expected: unknown): void; + toBeTruthy(): void; + toBeFalsy(): void; + toBeNull(): void; + toBeUndefined(): void; + toBeDefined(): void; + toContain(item: unknown): void; + toHaveLength(length: number): void; + toBeGreaterThan(n: number): void; + toBeLessThan(n: number): void; + toBeCloseTo(n: number, numDigits?: number): void; + toThrow(expected?: string | RegExp): void; +} + +export interface Expectation extends Matchers { + not: Matchers; +} + +type Message = () => string; + +function errorMessage(thrown: unknown): string { + return thrown instanceof Error ? thrown.message : String(thrown); +} + +function messageMatches(message: string, expected?: string | RegExp): boolean { + if (typeof expected === 'string') return message.includes(expected); + if (expected instanceof RegExp) return expected.test(message); + return true; +} + +function makeMatchers(actual: unknown, negate: boolean): Matchers { + // Message factories are only invoked on failure, so passing assertions never + // stringify `actual` (which would enumerate proxies and skew coverage). + const check = (pass: boolean, message: Message, negatedMessage: Message) => { + if (negate ? pass : !pass) { + throw new AssertionError((negate ? negatedMessage : message)()); + } + }; + + return { + toBe(expected) { + check( + Object.is(actual, expected), + () => `Expected ${stringify(actual)} to be ${stringify(expected)}`, + () => `Expected ${stringify(actual)} not to be ${stringify(expected)}`, + ); + }, + toEqual(expected) { + check( + deepEqual(actual, expected), + () => `Expected ${stringify(actual)} to equal ${stringify(expected)}`, + () => + `Expected ${stringify(actual)} not to equal ${stringify(expected)}`, + ); + }, + toBeTruthy() { + check( + !!actual, + () => `Expected ${stringify(actual)} to be truthy`, + () => `Expected ${stringify(actual)} not to be truthy`, + ); + }, + toBeFalsy() { + check( + !actual, + () => `Expected ${stringify(actual)} to be falsy`, + () => `Expected ${stringify(actual)} not to be falsy`, + ); + }, + toBeNull() { + check( + actual === null, + () => `Expected ${stringify(actual)} to be null`, + () => `Expected ${stringify(actual)} not to be null`, + ); + }, + toBeUndefined() { + check( + actual === undefined, + () => `Expected ${stringify(actual)} to be undefined`, + () => `Expected ${stringify(actual)} not to be undefined`, + ); + }, + toBeDefined() { + check( + actual !== undefined, + () => 'Expected value to be defined', + () => 'Expected value not to be defined', + ); + }, + toContain(item) { + const pass = + (typeof actual === 'string' && actual.includes(String(item))) || + (Array.isArray(actual) && actual.includes(item)); + check( + pass, + () => `Expected ${stringify(actual)} to contain ${stringify(item)}`, + () => `Expected ${stringify(actual)} not to contain ${stringify(item)}`, + ); + }, + toHaveLength(length) { + const actualLength = (actual as { length?: number })?.length; + check( + actualLength === length, + () => + `Expected ${stringify(actual)} to have length ${length} but got ${actualLength}`, + () => `Expected ${stringify(actual)} not to have length ${length}`, + ); + }, + toBeGreaterThan(n) { + check( + typeof actual === 'number' && actual > n, + () => `Expected ${stringify(actual)} to be greater than ${n}`, + () => `Expected ${stringify(actual)} not to be greater than ${n}`, + ); + }, + toBeLessThan(n) { + check( + typeof actual === 'number' && actual < n, + () => `Expected ${stringify(actual)} to be less than ${n}`, + () => `Expected ${stringify(actual)} not to be less than ${n}`, + ); + }, + toBeCloseTo(n, numDigits = 2) { + const pass = + typeof actual === 'number' && + Math.abs(actual - n) < Math.pow(10, -numDigits) / 2; + check( + pass, + () => + `Expected ${stringify(actual)} to be close to ${n} (${numDigits} digits)`, + () => + `Expected ${stringify(actual)} not to be close to ${n} (${numDigits} digits)`, + ); + }, + toThrow(expected) { + if (typeof actual !== 'function') { + throw new AssertionError( + `Expected a function to call but got ${stringify(actual)}`, + ); + } + let thrown: unknown; + let didThrow = false; + try { + (actual as () => unknown)(); + } catch (err) { + didThrow = true; + thrown = err; + } + if (!didThrow) { + check( + false, + () => 'Expected function to throw', + () => 'Expected function not to throw', + ); + return; + } + const message = errorMessage(thrown); + const matches = messageMatches(message, expected); + check( + matches, + () => + `Expected function to throw matching ${stringify(expected)} but threw ${stringify(message)}`, + () => `Expected function not to throw matching ${stringify(expected)}`, + ); + }, + }; +} + +export function expect(actual: unknown): Expectation { + const matchers = makeMatchers(actual, false) as Expectation; + matchers.not = makeMatchers(actual, true); + return matchers; +} + +/** + * Async counterpart to {@link Matchers.toThrow}: awaits a promise (or a 0-arg + * thunk returning one) and asserts that it REJECTS. `toThrow` can't cover this + * because it calls its argument synchronously, but a large share of edge cases + * are async (uploads, exports, version/comment/library ops). + * + * A thunk that throws synchronously also counts as a rejection, so callers can + * pass `() => ctx.penpot.someAsyncCall(badArgs)` regardless of whether the + * failure surfaces before or after the first await. The optional `expected` + * matches the error message exactly like `toThrow` (string includes / RegExp). + */ +export async function expectReject( + actual: Promise | (() => Promise | unknown), + expected?: string | RegExp, +): Promise { + let thrown: unknown; + let didReject = false; + try { + await (typeof actual === 'function' ? actual() : actual); + } catch (err) { + didReject = true; + thrown = err; + } + if (!didReject) { + throw new AssertionError('Expected promise to reject but it resolved'); + } + const message = errorMessage(thrown); + if (!messageMatches(message, expected)) { + throw new AssertionError( + `Expected promise to reject matching ${stringify(expected)} but rejected with ${stringify(message)}`, + ); + } +} diff --git a/plugins/apps/plugin-api-test-suite/src/framework/registry.ts b/plugins/apps/plugin-api-test-suite/src/framework/registry.ts new file mode 100644 index 0000000000..da01c57430 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/registry.ts @@ -0,0 +1,115 @@ +import type { TestCase, TestFn, TestMeta } from './types'; + +export const DEFAULT_GROUP = 'General'; + +let registry: TestCase[] = []; +let seenIds = new Set(); +const groupStack: string[] = []; +// >0 while inside a `describe.skipIfMocked` block; every test registered while +// it is positive is tagged `mockedSkip`. +let skipMockedDepth = 0; + +/** Separator used to join nested `describe` names into a single group path. */ +export const GROUP_SEPARATOR = ' / '; + +function slugify(name: string): string { + return name + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +/** + * Groups the tests registered inside `fn` under `name`. Groups are collapsible in + * the UI and show their own pass/fail counts. Calls may be nested in a file; the + * nested names are joined into a single hierarchical path (e.g. `Layout / Flex`) + * so a group always reveals the file/area it belongs to. Tests registered outside + * any `describe` fall into the {@link DEFAULT_GROUP}. + */ +function describeImpl(name: string, fn: () => void): void { + groupStack.push(name); + try { + fn(); + } finally { + groupStack.pop(); + } +} + +/** + * Groups the tests registered inside `fn` under `name`. + * + * `describe.skipIfMocked(name, fn)` additionally tags every test registered in + * the block as {@link TestCase.mockedSkip} — use it for a whole group of + * backend-dependent tests. + */ +export const describe: { + (name: string, fn: () => void): void; + skipIfMocked(name: string, fn: () => void): void; +} = Object.assign(describeImpl, { + skipIfMocked(name: string, fn: () => void): void { + skipMockedDepth++; + try { + describeImpl(name, fn); + } finally { + skipMockedDepth--; + } + }, +}); + +function registerTest(name: string, fn: TestFn, mockedSkip: boolean): void { + const base = slugify(name) || 'test'; + let id = base; + let n = 2; + while (seenIds.has(id)) { + id = `${base}-${n++}`; + } + seenIds.add(id); + const group = groupStack.length + ? groupStack.join(GROUP_SEPARATOR) + : DEFAULT_GROUP; + registry.push({ id, name, group, fn, mockedSkip }); +} + +/** + * Registers a test. Called at module load time from the auto-discovered + * `tests/*.test.ts` files. Ids are derived from the name and de-duplicated so + * the UI and runner can address each test unambiguously. + * + * `test.skipIfMocked(name, fn)` registers a single test that is excluded when + * running against a mocked backend (see {@link TestCase.mockedSkip}). + */ +export const test: { + (name: string, fn: TestFn): void; + skipIfMocked(name: string, fn: TestFn): void; +} = Object.assign( + (name: string, fn: TestFn): void => + registerTest(name, fn, skipMockedDepth > 0), + { + skipIfMocked(name: string, fn: TestFn): void { + registerTest(name, fn, true); + }, + }, +); + +export function getTests(): TestCase[] { + return registry.slice(); +} + +export function getTestMetas(): TestMeta[] { + return registry.map(({ id, name, group }) => ({ id, name, group })); +} + +/** + * Replaces the whole registry. Used by the reload mechanism, which evaluates a + * freshly built test bundle and hands back the discovered {@link TestCase}s. + */ +export function setTests(tests: TestCase[]): void { + registry = tests.slice(); + seenIds = new Set(registry.map((t) => t.id)); +} + +export function clearTests(): void { + registry = []; + seenIds = new Set(); +} diff --git a/plugins/apps/plugin-api-test-suite/src/framework/runner.ts b/plugins/apps/plugin-api-test-suite/src/framework/runner.ts new file mode 100644 index 0000000000..218a95e2f8 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/runner.ts @@ -0,0 +1,179 @@ +import apiSurface from '../generated/api-surface.json'; +import { computeCoverage, createRecorder } from './coverage'; +import { getTests } from './registry'; +import type { + ApiSurface, + CoverageReport, + RunSummary, + TestResult, +} from './types'; + +const SCRATCH_NAME = '__api_test_scratch__'; + +// A single test must never freeze the whole run. Some plugin API calls can hang +// indefinitely (e.g. an async op whose completion event never fires), so each +// test is raced against this timeout and turned into a failure if it exceeds it. +const TEST_TIMEOUT_MS = 15000; + +export interface RunOutput { + results: TestResult[]; + summary: RunSummary; + coverage: CoverageReport; + /** Names of tests excluded because of {@link RunOptions.skipMocked}. */ + skipped: string[]; +} + +export interface RunOptions { + /** + * When true, tests tagged {@link TestCase.mockedSkip} are excluded from the + * run (used by the mocked-backend CI mode). Their names are returned in + * {@link RunOutput.skipped}. + */ + skipMocked?: boolean; +} + +export type ResultReporter = (result: TestResult) => void; + +function withTimeout(promise: void | Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + let settled = false; + const timer = setTimeout(() => { + if (!settled) { + settled = true; + reject(new Error(`Test timed out after ${ms}ms`)); + } + }, ms); + Promise.resolve(promise).then( + () => { + if (!settled) { + settled = true; + clearTimeout(timer); + resolve(); + } + }, + (err) => { + if (!settled) { + settled = true; + clearTimeout(timer); + reject(err); + } + }, + ); + }); +} + +/** + * Runs the selected tests (or all of them) in the plugin sandbox. Each test gets + * a fresh scratch board through the recording proxy and that board is removed + * afterwards, so the user's file is left clean. API usage across the whole run is + * accumulated and turned into a coverage report. + */ +export async function runTests( + ids: string[] | 'all', + onResult?: ResultReporter, + options?: RunOptions, +): Promise { + const all = getTests(); + const requested = ids === 'all' ? all : all.filter((t) => ids.includes(t.id)); + const skipped = options?.skipMocked + ? requested.filter((t) => t.mockedSkip).map((t) => t.name) + : []; + const selected = options?.skipMocked + ? requested.filter((t) => !t.mockedSkip) + : requested; + + const recorder = createRecorder(penpot, apiSurface as ApiSurface); + + // Run every test with strict, deterministic API behavior. Set through the + // recording proxy so the flags also count towards coverage: + // - throwValidationErrors: invalid API usage throws instead of only logging, + // so it surfaces as a red test rather than passing silently. + // - naturalChildOrdering: `children` is always in z-index order and + // appendChild/insertChild respect it, making ordering assertions stable. + recorder.proxy.flags.throwValidationErrors = true; + recorder.proxy.flags.naturalChildOrdering = true; + + // Remember the page that was active when the run started. Tests share global + // state (selection, the active page) with no per-test reset, so a test that + // changes the active page — or fails before restoring it — would silently make + // every later test run on the wrong page. After each test we clear the + // selection and restore this page, all through the *raw* penpot so the cleanup + // isn't credited toward coverage. + const homePage = penpot.currentPage; + + const results: TestResult[] = []; + + for (const testCase of selected) { + onResult?.({ + id: testCase.id, + name: testCase.name, + status: 'running', + durationMs: 0, + }); + + const start = Date.now(); + // Create/name/remove the scratch board through the *raw* penpot so this + // harness bookkeeping isn't credited toward coverage. The test still gets a + // recording-wrapped board, so its own access to it is counted. + let rawBoard: ReturnType | undefined; + let result: TestResult; + + try { + rawBoard = penpot.createBoard(); + rawBoard.name = SCRATCH_NAME; + const board = recorder.wrap(rawBoard, 'Board'); + await withTimeout( + testCase.fn({ penpot: recorder.proxy, board }), + TEST_TIMEOUT_MS, + ); + result = { + id: testCase.id, + name: testCase.name, + status: 'pass', + durationMs: Date.now() - start, + }; + } catch (err) { + result = { + id: testCase.id, + name: testCase.name, + status: 'fail', + error: err instanceof Error ? err.message : String(err), + durationMs: Date.now() - start, + }; + } finally { + try { + rawBoard?.remove(); + } catch { + // best-effort cleanup; never fail a test because teardown failed + } + // Reset shared state so the next test starts clean. All best-effort: a + // teardown failure must never turn into a test failure. + try { + penpot.selection = []; + } catch { + /* ignore */ + } + try { + const active = penpot.currentPage; + if (homePage && active && active.id !== homePage.id) { + await penpot.openPage(homePage); + } + } catch { + /* ignore */ + } + } + + results.push(result); + onResult?.(result); + } + + const summary: RunSummary = { + total: results.length, + passed: results.filter((r) => r.status === 'pass').length, + failed: results.filter((r) => r.status === 'fail').length, + }; + + const coverage = computeCoverage(recorder.accessed, apiSurface as ApiSurface); + + return { results, summary, coverage, skipped }; +} diff --git a/plugins/apps/plugin-api-test-suite/src/framework/static-coverage.ts b/plugins/apps/plugin-api-test-suite/src/framework/static-coverage.ts new file mode 100644 index 0000000000..ecc1e86736 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/static-coverage.ts @@ -0,0 +1,70 @@ +/** + * "Statically covered" coverage targets. + * + * These members ARE exercised behaviourally by the test suite, but the recording + * proxy structurally cannot credit them, so they would otherwise show as + * uncovered. The reasons are all recorder limitations: frozen SES values (the + * proxy must return them raw), base-interface attribution (members redeclared on + * concrete types are credited there), type maps (events are credited on + * `Penpot.on/off`), type-guard narrowing the recorder can't perform, and methods + * whose return type the surface generator couldn't resolve (the result is handed + * back raw). See README.md "Coverage notes". + * + * Keys are `Interface.member#mode` (mode ∈ get/set/call), exactly matching the + * recorder's accessed-set keys and the targets in `computeCoverage`. Only add a + * target here when a named test genuinely exercises it — this set feeds the + * "effective" coverage number, so over-claiming makes that number dishonest. + * + * Recorder-credited (recorded) coverage always wins over this set: a target that + * turns out to be recorded simply never shows as static. + */ +export const STATIC_COVERAGE: ReadonlySet = new Set([ + // ShapeBase.fills — every concrete shape redeclares `fills`, so accesses are + // attributed to the concrete type (Rectangle.fills, …); exercised pervasively + // (fills-strokes.test.ts, misc.test.ts). + 'ShapeBase.fills#get', + 'ShapeBase.fills#set', + + // utils.types predicates — `penpot.utils.types` is a frozen data property, so + // its members can't be wrapped. Exercised in platform.test.ts. + 'ContextTypesUtils.isBoard#call', + 'ContextTypesUtils.isBool#call', + 'ContextTypesUtils.isEllipse#call', + 'ContextTypesUtils.isGroup#call', + 'ContextTypesUtils.isMask#call', + 'ContextTypesUtils.isPath#call', + 'ContextTypesUtils.isRectangle#call', + 'ContextTypesUtils.isSVG#call', + 'ContextTypesUtils.isText#call', + 'ContextTypesUtils.isVariantComponent#call', + 'ContextTypesUtils.isVariantContainer#call', + + // utils.geometry.center — `penpot.utils.geometry` is likewise a frozen data + // property, so the call can't be wrapped. Exercised (and verified) in + // platform.test.ts. + 'ContextGeometryUtils.center#call', + + // shapesColors() returns objects whose declared type the surface generator + // couldn't resolve (it records as `type: null`), so the recorder hands the + // result back raw and cannot credit nested access. The members are exercised + // in colors.test.ts (entry.shapesInfo[0].property / .shapeId). + 'ColorShapeInfo.shapesInfo#get', + 'ColorShapeInfoEntry.index#get', + 'ColorShapeInfoEntry.property#get', + 'ColorShapeInfoEntry.shapeId#get', + + // Deterministic events — `on`/`off` are credited on `Penpot`, never as + // `EventsMap` members. Exercised in events.test.ts. The remaining events + // (pagechange/filechange/themechange/contentsave/finish) are not triggered + // deterministically headless and stay genuinely uncovered. + 'EventsMap.selectionchange#get', + 'EventsMap.shapechange#get', + + // LibraryVariantComponent — the recorder types a component as LibraryComponent + // and can't narrow via the isVariant() type-guard; the behaviour is exercised + // via VariantContainer.variants in variants.test.ts. + 'LibraryVariantComponent.variants#get', + 'LibraryVariantComponent.variantProps#get', + 'LibraryVariantComponent.addVariant#call', + 'LibraryVariantComponent.setVariantProperty#call', +]); diff --git a/plugins/apps/plugin-api-test-suite/src/framework/types.ts b/plugins/apps/plugin-api-test-suite/src/framework/types.ts new file mode 100644 index 0000000000..94b19cbe8b --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/framework/types.ts @@ -0,0 +1,118 @@ +import type { Board, Penpot } from '@penpot/plugin-types'; + +export type TestStatus = 'pending' | 'running' | 'pass' | 'fail'; + +/** + * The context handed to every test. Tests MUST use `ctx.penpot` (the recording + * proxy) rather than the global `penpot` so their API usage is counted towards + * coverage. A fresh scratch `board` is provided per test and removed afterwards. + */ +export interface TestContext { + penpot: Penpot; + board: Board; +} + +export type TestFn = (ctx: TestContext) => void | Promise; + +export interface TestCase { + id: string; + name: string; + /** Group the test belongs to (set via `describe`, defaults to "General"). */ + group: string; + fn: TestFn; + /** + * When true, the test is excluded from runs against a mocked backend + * (`MOCK_BACKEND=1`): it depends on real backend results/validation that a + * `page.route` mock cannot faithfully reproduce. Set via `test.skipIfMocked` + * or `describe.skipIfMocked`. + */ + mockedSkip?: boolean; +} + +/** Lightweight test description sent to the UI (no function). */ +export interface TestMeta { + id: string; + name: string; + group: string; +} + +export interface TestResult { + id: string; + name: string; + status: TestStatus; + error?: string; + durationMs: number; +} + +export interface RunSummary { + total: number; + passed: number; + failed: number; +} + +/** Per-interface coverage detail derived from the public Plugin API types. */ +export interface InterfaceCoverage { + members: string[]; + covered: string[]; + /** + * Targets exercised behaviourally by the tests but not creditable through the + * recording proxy (see {@link ../framework/static-coverage}). Listed + * separately from `covered` and `uncovered`. + */ + staticallyCovered: string[]; + uncovered: string[]; +} + +export interface CoverageReport { + total: number; + /** Targets credited by the recording proxy. */ + covered: number; + /** Targets covered only via the static allowlist (not recorder-credited). */ + staticallyCovered: number; + /** Recorder-credited coverage: `covered / total`. */ + percent: number; + /** Effective coverage including static targets: `(covered + static) / total`. */ + effectivePercent: number; + byInterface: Record; +} + +/** + * How a member is exercised, which determines the coverage targets it has: + * - `method`: callable -> a single `call` target. + * - `get`: read-only property -> a single `get` target. + * - `getset`: writable property -> separate `get` and `set` targets. + */ +export type MemberKind = 'method' | 'get' | 'getset'; + +/** A single member in the API type graph. */ +export interface ApiMemberInfo { + /** Interface that actually declares this member (may be a base interface). */ + decl: string; + /** Whether the member is a method, a read-only, or a writable property. */ + kind: MemberKind; + /** + * The interface/union name the member yields (return type for methods, + * property type otherwise), or `null` when it is a primitive/untracked type. + */ + type: string | null; + /** True when the member yields an array of `type`. */ + array: boolean; +} + +/** A union alias (e.g. `Shape`) and how to resolve it at runtime. */ +export interface UnionInfo { + variants: string[]; + /** Discriminant used to pick the concrete variant from a runtime value. */ + discriminant: { field: string; map: Record } | null; +} + +/** + * Shape of the generated `api-surface.json`. Coverage is type-aware: `interfaces` + * is the denominator (own members per interface) and `graph`/`unions` let the + * recorder attribute each access to the interface the value actually is. + */ +export interface ApiSurface { + interfaces: Record; + graph: Record>; + unions: Record; +} diff --git a/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json b/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json new file mode 100644 index 0000000000..783f45d044 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/generated/api-surface.json @@ -0,0 +1,11192 @@ +{ + "interfaces": { + "ActiveUser": ["position", "zoom"], + "Blur": ["hidden", "id", "value"], + "Board": [ + "addFlexLayout", + "addGridLayout", + "addRulerGuide", + "appendChild", + "children", + "clipContent", + "fills", + "flex", + "grid", + "guides", + "horizontalSizing", + "insertChild", + "isVariantContainer", + "removeRulerGuide", + "rulerGuides", + "showInViewMode", + "type", + "verticalSizing" + ], + "Boolean": [ + "appendChild", + "children", + "commands", + "d", + "fills", + "insertChild", + "type" + ], + "Bounds": ["height", "width", "x", "y"], + "CloseOverlay": ["animation", "destination", "type"], + "Color": [ + "color", + "fileId", + "gradient", + "id", + "image", + "name", + "opacity", + "path" + ], + "ColorShapeInfo": ["shapesInfo"], + "ColorShapeInfoEntry": ["index", "property", "shapeId"], + "Comment": ["content", "date", "remove", "user"], + "CommentThread": [ + "board", + "findComments", + "owner", + "position", + "remove", + "reply", + "resolved", + "seqNumber" + ], + "CommonLayout": [ + "alignContent", + "alignItems", + "bottomPadding", + "columnGap", + "horizontalPadding", + "horizontalSizing", + "justifyContent", + "justifyItems", + "leftPadding", + "remove", + "rightPadding", + "rowGap", + "topPadding", + "verticalPadding", + "verticalSizing" + ], + "Context": [ + "activeUsers", + "alignHorizontal", + "alignVertical", + "createBoard", + "createBoolean", + "createEllipse", + "createPage", + "createPath", + "createRectangle", + "createShapeFromSvg", + "createShapeFromSvgWithImages", + "createText", + "createVariantFromComponents", + "currentFile", + "currentPage", + "currentUser", + "distributeHorizontal", + "distributeVertical", + "flags", + "flatten", + "fonts", + "generateFontFaces", + "generateMarkup", + "generateStyle", + "group", + "history", + "library", + "localStorage", + "openPage", + "openViewer", + "replaceColor", + "root", + "selection", + "shapesColors", + "theme", + "ungroup", + "uploadMediaData", + "uploadMediaUrl", + "version", + "viewport" + ], + "ContextGeometryUtils": ["center"], + "ContextTypesUtils": [ + "isBoard", + "isBool", + "isEllipse", + "isGroup", + "isMask", + "isPath", + "isRectangle", + "isSVG", + "isText", + "isVariantComponent", + "isVariantContainer" + ], + "ContextUtils": ["geometry", "types"], + "Dissolve": ["duration", "easing", "type"], + "Ellipse": ["fills", "type"], + "EventsMap": [ + "contentsave", + "filechange", + "finish", + "pagechange", + "selectionchange", + "shapechange", + "themechange" + ], + "Export": ["scale", "skipChildren", "suffix", "type"], + "File": [ + "export", + "findVersions", + "id", + "name", + "pages", + "revn", + "saveVersion" + ], + "FileVersion": [ + "createdAt", + "createdBy", + "isAutosave", + "label", + "pin", + "remove", + "restore" + ], + "Fill": [ + "fillColor", + "fillColorGradient", + "fillColorRefFile", + "fillColorRefId", + "fillImage", + "fillOpacity" + ], + "Flags": ["naturalChildOrdering", "throwValidationErrors"], + "FlexLayout": ["appendChild", "dir", "wrap"], + "Flow": ["name", "page", "remove", "startingBoard"], + "Font": [ + "applyToRange", + "applyToText", + "fontFamily", + "fontId", + "fontStyle", + "fontVariantId", + "fontWeight", + "name", + "variants" + ], + "FontsContext": [ + "all", + "findAllById", + "findAllByName", + "findById", + "findByName" + ], + "FontVariant": ["fontStyle", "fontVariantId", "fontWeight", "name"], + "Gradient": ["endX", "endY", "startX", "startY", "stops", "type", "width"], + "GridLayout": [ + "addColumn", + "addColumnAtIndex", + "addRow", + "addRowAtIndex", + "appendChild", + "columns", + "dir", + "removeColumn", + "removeRow", + "rows", + "setColumn", + "setRow" + ], + "Group": [ + "appendChild", + "children", + "insertChild", + "isMask", + "makeMask", + "removeMask", + "type" + ], + "GuideColumn": ["display", "params", "type"], + "GuideColumnParams": [ + "color", + "gutter", + "itemLength", + "margin", + "size", + "type" + ], + "GuideRow": ["display", "params", "type"], + "GuideSquare": ["display", "params", "type"], + "GuideSquareParams": ["color", "size"], + "HistoryContext": ["undoBlockBegin", "undoBlockFinish"], + "ImageData": [ + "data", + "height", + "id", + "keepAspectRatio", + "mtype", + "name", + "width" + ], + "Interaction": ["action", "delay", "remove", "shape", "trigger"], + "LayoutCellProperties": [ + "areaName", + "column", + "columnSpan", + "position", + "row", + "rowSpan" + ], + "LayoutChildProperties": [ + "absolute", + "alignSelf", + "bottomMargin", + "horizontalMargin", + "horizontalSizing", + "leftMargin", + "maxHeight", + "maxWidth", + "minHeight", + "minWidth", + "rightMargin", + "topMargin", + "verticalMargin", + "verticalSizing", + "zIndex" + ], + "Library": [ + "colors", + "components", + "createColor", + "createComponent", + "createTypography", + "id", + "name", + "tokens", + "typographies" + ], + "LibraryColor": [ + "asFill", + "asStroke", + "color", + "gradient", + "image", + "opacity" + ], + "LibraryComponent": [ + "instance", + "isVariant", + "mainInstance", + "transformInVariant" + ], + "LibraryContext": [ + "availableLibraries", + "connectLibrary", + "connected", + "local" + ], + "LibraryElement": ["id", "libraryId", "name", "path"], + "LibrarySummary": [ + "id", + "name", + "numColors", + "numComponents", + "numTypographies" + ], + "LibraryTypography": [ + "applyToText", + "applyToTextRange", + "fontFamily", + "fontId", + "fontSize", + "fontStyle", + "fontVariantId", + "fontWeight", + "letterSpacing", + "lineHeight", + "setFont", + "textTransform" + ], + "LibraryVariantComponent": [ + "addVariant", + "setVariantProperty", + "variantError", + "variantProps", + "variants" + ], + "LocalStorage": ["getItem", "getKeys", "removeItem", "setItem"], + "NavigateTo": [ + "animation", + "destination", + "preserveScrollPosition", + "type" + ], + "OpenOverlay": ["type"], + "OpenUrl": ["type", "url"], + "OverlayAction": [ + "addBackgroundOverlay", + "animation", + "closeWhenClickOutside", + "destination", + "manualPositionLocation", + "position", + "relativeTo" + ], + "Page": [ + "addCommentThread", + "addRulerGuide", + "createFlow", + "findCommentThreads", + "findShapes", + "flows", + "getShapeById", + "id", + "name", + "removeCommentThread", + "removeFlow", + "removeRulerGuide", + "root", + "rulerGuides" + ], + "Path": ["commands", "d", "fills", "type"], + "PathCommand": ["command", "params"], + "Penpot": ["closePlugin", "off", "on", "ui", "utils"], + "PluginData": [ + "getPluginData", + "getPluginDataKeys", + "getSharedPluginData", + "getSharedPluginDataKeys", + "setPluginData", + "setSharedPluginData" + ], + "Point": ["x", "y"], + "PreviousScreen": ["type"], + "Push": ["direction", "duration", "easing", "type"], + "Rectangle": ["fills", "type"], + "RulerGuide": ["board", "orientation", "position"], + "Shadow": [ + "blur", + "color", + "hidden", + "id", + "offsetX", + "offsetY", + "spread", + "style" + ], + "ShapeBase": [ + "addInteraction", + "applyToken", + "backgroundBlur", + "blendMode", + "blocked", + "blur", + "boardX", + "boardY", + "borderRadius", + "borderRadiusBottomLeft", + "borderRadiusBottomRight", + "borderRadiusTopLeft", + "borderRadiusTopRight", + "bounds", + "bringForward", + "bringToFront", + "center", + "clone", + "combineAsVariants", + "component", + "componentHead", + "componentRefShape", + "componentRoot", + "constraintsHorizontal", + "constraintsVertical", + "detach", + "export", + "exports", + "fills", + "fixedWhenScrolling", + "flipX", + "flipY", + "height", + "hidden", + "id", + "interactions", + "isComponentCopyInstance", + "isComponentHead", + "isComponentInstance", + "isComponentMainInstance", + "isComponentRoot", + "isVariantHead", + "layoutCell", + "layoutChild", + "name", + "opacity", + "parent", + "parentIndex", + "parentX", + "parentY", + "proportionLock", + "remove", + "removeInteraction", + "resize", + "rotate", + "rotation", + "sendBackward", + "sendToBack", + "setParentIndex", + "shadows", + "strokes", + "swapComponent", + "switchVariant", + "tokens", + "visible", + "width", + "x", + "y" + ], + "Slide": ["direction", "duration", "easing", "offsetEffect", "type", "way"], + "Stroke": [ + "strokeAlignment", + "strokeCapEnd", + "strokeCapStart", + "strokeColor", + "strokeColorGradient", + "strokeColorRefFile", + "strokeColorRefId", + "strokeOpacity", + "strokeStyle", + "strokeWidth" + ], + "SvgRaw": ["type"], + "Text": [ + "align", + "applyTypography", + "characters", + "direction", + "fontFamily", + "fontId", + "fontSize", + "fontStyle", + "fontVariantId", + "fontWeight", + "getRange", + "growType", + "letterSpacing", + "lineHeight", + "textBounds", + "textDecoration", + "textTransform", + "type", + "verticalAlign" + ], + "TextRange": [ + "align", + "applyTypography", + "characters", + "direction", + "fills", + "fontFamily", + "fontId", + "fontSize", + "fontStyle", + "fontVariantId", + "fontWeight", + "letterSpacing", + "lineHeight", + "shape", + "textDecoration", + "textTransform", + "verticalAlign" + ], + "ToggleOverlay": ["type"], + "TokenBase": [ + "applyToSelected", + "applyToShapes", + "description", + "duplicate", + "id", + "name", + "remove", + "resolvedValueString" + ], + "TokenBorderRadius": ["resolvedValue", "type", "value"], + "TokenBorderWidth": ["resolvedValue", "type", "value"], + "TokenCatalog": [ + "addSet", + "addTheme", + "getSetById", + "getThemeById", + "sets", + "themes" + ], + "TokenColor": ["resolvedValue", "type", "value"], + "TokenDimension": ["resolvedValue", "type", "value"], + "TokenFontFamilies": ["resolvedValue", "type", "value"], + "TokenFontSizes": ["resolvedValue", "type", "value"], + "TokenFontWeights": ["resolvedValue", "type", "value"], + "TokenLetterSpacing": ["resolvedValue", "type", "value"], + "TokenNumber": ["resolvedValue", "type", "value"], + "TokenOpacity": ["resolvedValue", "type", "value"], + "TokenRotation": ["resolvedValue", "type", "value"], + "TokenSet": [ + "active", + "addToken", + "duplicate", + "getTokenById", + "id", + "name", + "remove", + "toggleActive", + "tokens", + "tokensByType" + ], + "TokenShadow": ["resolvedValue", "type", "value"], + "TokenShadowValue": [ + "blur", + "color", + "inset", + "offsetX", + "offsetY", + "spread" + ], + "TokenShadowValueString": [ + "blur", + "color", + "inset", + "offsetX", + "offsetY", + "spread" + ], + "TokenSizing": ["resolvedValue", "type", "value"], + "TokenSpacing": ["resolvedValue", "type", "value"], + "TokenTextCase": ["resolvedValue", "type", "value"], + "TokenTextDecoration": ["resolvedValue", "type", "value"], + "TokenTheme": [ + "active", + "activeSets", + "addSet", + "duplicate", + "externalId", + "group", + "id", + "name", + "remove", + "removeSet", + "toggleActive" + ], + "TokenTypography": ["resolvedValue", "type", "value"], + "TokenTypographyValue": [ + "fontFamilies", + "fontSizes", + "fontWeights", + "letterSpacing", + "lineHeight", + "textCase", + "textDecoration" + ], + "TokenTypographyValueString": [ + "fontFamilies", + "fontSizes", + "fontWeight", + "letterSpacing", + "lineHeight", + "textCase", + "textDecoration" + ], + "Track": ["type", "value"], + "User": ["avatarUrl", "color", "id", "name", "sessionId"], + "VariantContainer": ["variants"], + "Variants": [ + "addProperty", + "addVariant", + "currentValues", + "id", + "libraryId", + "properties", + "removeProperty", + "renameProperty", + "variantComponents" + ], + "Viewport": [ + "bounds", + "center", + "zoom", + "zoomIntoView", + "zoomReset", + "zoomToFitAll" + ] + }, + "graph": { + "ActiveUser": { + "position": { + "decl": "ActiveUser", + "kind": "getset", + "type": null, + "array": false + }, + "zoom": { + "decl": "ActiveUser", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "avatarUrl": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "color": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "sessionId": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + } + }, + "Blur": { + "id": { + "decl": "Blur", + "kind": "getset", + "type": null, + "array": false + }, + "value": { + "decl": "Blur", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "Blur", + "kind": "getset", + "type": null, + "array": false + } + }, + "Board": { + "type": { + "decl": "Board", + "kind": "get", + "type": null, + "array": false + }, + "clipContent": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "showInViewMode": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "grid": { + "decl": "Board", + "kind": "get", + "type": "GridLayout", + "array": false + }, + "flex": { + "decl": "Board", + "kind": "get", + "type": "FlexLayout", + "array": false + }, + "guides": { + "decl": "Board", + "kind": "getset", + "type": "Guide", + "array": true + }, + "rulerGuides": { + "decl": "Board", + "kind": "get", + "type": "RulerGuide", + "array": true + }, + "horizontalSizing": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "Board", + "kind": "getset", + "type": "Fill", + "array": true + }, + "children": { + "decl": "Board", + "kind": "getset", + "type": "Shape", + "array": true + }, + "appendChild": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "insertChild": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "addFlexLayout": { + "decl": "Board", + "kind": "method", + "type": "FlexLayout", + "array": false + }, + "addGridLayout": { + "decl": "Board", + "kind": "method", + "type": "GridLayout", + "array": false + }, + "addRulerGuide": { + "decl": "Board", + "kind": "method", + "type": "RulerGuide", + "array": false + }, + "removeRulerGuide": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "isVariantContainer": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Boolean": { + "type": { + "decl": "Boolean", + "kind": "get", + "type": null, + "array": false + }, + "toD": { + "decl": "Boolean", + "kind": "method", + "type": null, + "array": false + }, + "content": { + "decl": "Boolean", + "kind": "get", + "type": null, + "array": false + }, + "d": { + "decl": "Boolean", + "kind": "get", + "type": null, + "array": false + }, + "commands": { + "decl": "Boolean", + "kind": "get", + "type": "PathCommand", + "array": true + }, + "fills": { + "decl": "Boolean", + "kind": "getset", + "type": "Fill", + "array": true + }, + "children": { + "decl": "Boolean", + "kind": "get", + "type": "Shape", + "array": true + }, + "appendChild": { + "decl": "Boolean", + "kind": "method", + "type": null, + "array": false + }, + "insertChild": { + "decl": "Boolean", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Bounds": { + "x": { + "decl": "Bounds", + "kind": "get", + "type": null, + "array": false + }, + "y": { + "decl": "Bounds", + "kind": "get", + "type": null, + "array": false + }, + "width": { + "decl": "Bounds", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "Bounds", + "kind": "get", + "type": null, + "array": false + } + }, + "CloseOverlay": { + "type": { + "decl": "CloseOverlay", + "kind": "get", + "type": null, + "array": false + }, + "destination": { + "decl": "CloseOverlay", + "kind": "get", + "type": "Board", + "array": false + }, + "animation": { + "decl": "CloseOverlay", + "kind": "get", + "type": "Animation", + "array": false + } + }, + "Color": { + "id": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "fileId": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "name": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "color": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "refId": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "refFile": { + "decl": "Color", + "kind": "getset", + "type": null, + "array": false + }, + "gradient": { + "decl": "Color", + "kind": "getset", + "type": "Gradient", + "array": false + }, + "image": { + "decl": "Color", + "kind": "getset", + "type": "ImageData", + "array": false + } + }, + "ColorShapeInfo": { + "shapesInfo": { + "decl": "ColorShapeInfo", + "kind": "get", + "type": "ColorShapeInfoEntry", + "array": true + } + }, + "ColorShapeInfoEntry": { + "property": { + "decl": "ColorShapeInfoEntry", + "kind": "get", + "type": null, + "array": false + }, + "index": { + "decl": "ColorShapeInfoEntry", + "kind": "get", + "type": null, + "array": false + }, + "shapeId": { + "decl": "ColorShapeInfoEntry", + "kind": "get", + "type": null, + "array": false + } + }, + "Comment": { + "user": { + "decl": "Comment", + "kind": "get", + "type": "User", + "array": false + }, + "date": { + "decl": "Comment", + "kind": "get", + "type": null, + "array": false + }, + "content": { + "decl": "Comment", + "kind": "getset", + "type": null, + "array": false + }, + "remove": { + "decl": "Comment", + "kind": "method", + "type": null, + "array": false + } + }, + "CommentThread": { + "seqNumber": { + "decl": "CommentThread", + "kind": "get", + "type": null, + "array": false + }, + "board": { + "decl": "CommentThread", + "kind": "get", + "type": "Board", + "array": false + }, + "owner": { + "decl": "CommentThread", + "kind": "get", + "type": "User", + "array": false + }, + "position": { + "decl": "CommentThread", + "kind": "getset", + "type": "Point", + "array": false + }, + "resolved": { + "decl": "CommentThread", + "kind": "getset", + "type": null, + "array": false + }, + "findComments": { + "decl": "CommentThread", + "kind": "method", + "type": "Comment", + "array": true + }, + "reply": { + "decl": "CommentThread", + "kind": "method", + "type": "Comment", + "array": false + }, + "remove": { + "decl": "CommentThread", + "kind": "method", + "type": null, + "array": false + } + }, + "CommonLayout": { + "alignItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "alignContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rowGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "columnGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "topPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rightPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "bottomPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "leftPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "remove": { + "decl": "CommonLayout", + "kind": "method", + "type": null, + "array": false + } + }, + "Context": { + "version": { + "decl": "Context", + "kind": "get", + "type": null, + "array": false + }, + "root": { + "decl": "Context", + "kind": "get", + "type": "Shape", + "array": false + }, + "currentFile": { + "decl": "Context", + "kind": "get", + "type": "File", + "array": false + }, + "currentPage": { + "decl": "Context", + "kind": "get", + "type": "Page", + "array": false + }, + "viewport": { + "decl": "Context", + "kind": "get", + "type": "Viewport", + "array": false + }, + "flags": { + "decl": "Context", + "kind": "get", + "type": "Flags", + "array": false + }, + "history": { + "decl": "Context", + "kind": "get", + "type": "HistoryContext", + "array": false + }, + "library": { + "decl": "Context", + "kind": "get", + "type": "LibraryContext", + "array": false + }, + "fonts": { + "decl": "Context", + "kind": "get", + "type": "FontsContext", + "array": false + }, + "currentUser": { + "decl": "Context", + "kind": "get", + "type": "User", + "array": false + }, + "activeUsers": { + "decl": "Context", + "kind": "get", + "type": "ActiveUser", + "array": true + }, + "theme": { + "decl": "Context", + "kind": "get", + "type": "Theme", + "array": false + }, + "localStorage": { + "decl": "Context", + "kind": "get", + "type": "LocalStorage", + "array": false + }, + "selection": { + "decl": "Context", + "kind": "getset", + "type": "Shape", + "array": true + }, + "shapesColors": { + "decl": "Context", + "kind": "method", + "type": null, + "array": true + }, + "replaceColor": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "uploadMediaUrl": { + "decl": "Context", + "kind": "method", + "type": "ImageData", + "array": false + }, + "uploadMediaData": { + "decl": "Context", + "kind": "method", + "type": "ImageData", + "array": false + }, + "group": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "ungroup": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "createRectangle": { + "decl": "Context", + "kind": "method", + "type": "Rectangle", + "array": false + }, + "createBoard": { + "decl": "Context", + "kind": "method", + "type": "Board", + "array": false + }, + "createEllipse": { + "decl": "Context", + "kind": "method", + "type": "Ellipse", + "array": false + }, + "createPath": { + "decl": "Context", + "kind": "method", + "type": "Path", + "array": false + }, + "createBoolean": { + "decl": "Context", + "kind": "method", + "type": "Boolean", + "array": false + }, + "createShapeFromSvg": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "createShapeFromSvgWithImages": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "createText": { + "decl": "Context", + "kind": "method", + "type": "Text", + "array": false + }, + "generateMarkup": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "generateStyle": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "generateFontFaces": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "addListener": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "removeListener": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "openViewer": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "createPage": { + "decl": "Context", + "kind": "method", + "type": "Page", + "array": false + }, + "openPage": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "alignHorizontal": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "alignVertical": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "distributeHorizontal": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "distributeVertical": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "flatten": { + "decl": "Context", + "kind": "method", + "type": "Path", + "array": true + }, + "createVariantFromComponents": { + "decl": "Context", + "kind": "method", + "type": "VariantContainer", + "array": false + } + }, + "ContextGeometryUtils": { + "center": { + "decl": "ContextGeometryUtils", + "kind": "method", + "type": null, + "array": false + } + }, + "ContextTypesUtils": { + "isBoard": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isGroup": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isMask": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isBool": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isRectangle": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isPath": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isText": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isEllipse": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isSVG": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isVariantContainer": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + }, + "isVariantComponent": { + "decl": "ContextTypesUtils", + "kind": "method", + "type": null, + "array": false + } + }, + "ContextUtils": { + "geometry": { + "decl": "ContextUtils", + "kind": "get", + "type": "ContextGeometryUtils", + "array": false + }, + "types": { + "decl": "ContextUtils", + "kind": "get", + "type": "ContextTypesUtils", + "array": false + } + }, + "Dissolve": { + "type": { + "decl": "Dissolve", + "kind": "get", + "type": null, + "array": false + }, + "duration": { + "decl": "Dissolve", + "kind": "get", + "type": null, + "array": false + }, + "easing": { + "decl": "Dissolve", + "kind": "get", + "type": null, + "array": false + } + }, + "Ellipse": { + "type": { + "decl": "Ellipse", + "kind": "get", + "type": null, + "array": false + }, + "fills": { + "decl": "Ellipse", + "kind": "getset", + "type": "Fill", + "array": true + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "EventsMap": { + "pagechange": { + "decl": "EventsMap", + "kind": "get", + "type": "Page", + "array": false + }, + "filechange": { + "decl": "EventsMap", + "kind": "get", + "type": "File", + "array": false + }, + "selectionchange": { + "decl": "EventsMap", + "kind": "get", + "type": null, + "array": true + }, + "themechange": { + "decl": "EventsMap", + "kind": "get", + "type": "Theme", + "array": false + }, + "finish": { + "decl": "EventsMap", + "kind": "get", + "type": null, + "array": false + }, + "shapechange": { + "decl": "EventsMap", + "kind": "get", + "type": "Shape", + "array": false + }, + "contentsave": { + "decl": "EventsMap", + "kind": "get", + "type": null, + "array": false + } + }, + "Export": { + "type": { + "decl": "Export", + "kind": "getset", + "type": null, + "array": false + }, + "scale": { + "decl": "Export", + "kind": "getset", + "type": null, + "array": false + }, + "suffix": { + "decl": "Export", + "kind": "getset", + "type": null, + "array": false + }, + "skipChildren": { + "decl": "Export", + "kind": "getset", + "type": null, + "array": false + } + }, + "File": { + "id": { + "decl": "File", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "File", + "kind": "get", + "type": null, + "array": false + }, + "revn": { + "decl": "File", + "kind": "get", + "type": null, + "array": false + }, + "pages": { + "decl": "File", + "kind": "get", + "type": "Page", + "array": true + }, + "export": { + "decl": "File", + "kind": "method", + "type": null, + "array": false + }, + "findVersions": { + "decl": "File", + "kind": "method", + "type": "FileVersion", + "array": true + }, + "saveVersion": { + "decl": "File", + "kind": "method", + "type": "FileVersion", + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "FileVersion": { + "label": { + "decl": "FileVersion", + "kind": "getset", + "type": null, + "array": false + }, + "createdBy": { + "decl": "FileVersion", + "kind": "get", + "type": "User", + "array": false + }, + "createdAt": { + "decl": "FileVersion", + "kind": "get", + "type": null, + "array": false + }, + "isAutosave": { + "decl": "FileVersion", + "kind": "get", + "type": null, + "array": false + }, + "restore": { + "decl": "FileVersion", + "kind": "method", + "type": null, + "array": false + }, + "remove": { + "decl": "FileVersion", + "kind": "method", + "type": null, + "array": false + }, + "pin": { + "decl": "FileVersion", + "kind": "method", + "type": "FileVersion", + "array": false + } + }, + "Fill": { + "fillColor": { + "decl": "Fill", + "kind": "getset", + "type": null, + "array": false + }, + "fillOpacity": { + "decl": "Fill", + "kind": "getset", + "type": null, + "array": false + }, + "fillColorGradient": { + "decl": "Fill", + "kind": "getset", + "type": "Gradient", + "array": false + }, + "fillColorRefFile": { + "decl": "Fill", + "kind": "getset", + "type": null, + "array": false + }, + "fillColorRefId": { + "decl": "Fill", + "kind": "getset", + "type": null, + "array": false + }, + "fillImage": { + "decl": "Fill", + "kind": "getset", + "type": "ImageData", + "array": false + } + }, + "Flags": { + "naturalChildOrdering": { + "decl": "Flags", + "kind": "getset", + "type": null, + "array": false + }, + "throwValidationErrors": { + "decl": "Flags", + "kind": "getset", + "type": null, + "array": false + } + }, + "FlexLayout": { + "dir": { + "decl": "FlexLayout", + "kind": "getset", + "type": null, + "array": false + }, + "wrap": { + "decl": "FlexLayout", + "kind": "getset", + "type": null, + "array": false + }, + "appendChild": { + "decl": "FlexLayout", + "kind": "method", + "type": null, + "array": false + }, + "alignItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "alignContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rowGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "columnGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "topPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rightPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "bottomPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "leftPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "remove": { + "decl": "CommonLayout", + "kind": "method", + "type": null, + "array": false + } + }, + "Flow": { + "page": { + "decl": "Flow", + "kind": "get", + "type": "Page", + "array": false + }, + "name": { + "decl": "Flow", + "kind": "getset", + "type": null, + "array": false + }, + "startingBoard": { + "decl": "Flow", + "kind": "getset", + "type": "Board", + "array": false + }, + "remove": { + "decl": "Flow", + "kind": "method", + "type": null, + "array": false + } + }, + "Font": { + "name": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "fontId": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "fontFamily": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "fontStyle": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "fontVariantId": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "Font", + "kind": "get", + "type": null, + "array": false + }, + "variants": { + "decl": "Font", + "kind": "get", + "type": "FontVariant", + "array": true + }, + "applyToText": { + "decl": "Font", + "kind": "method", + "type": null, + "array": false + }, + "applyToRange": { + "decl": "Font", + "kind": "method", + "type": null, + "array": false + } + }, + "FontsContext": { + "all": { + "decl": "FontsContext", + "kind": "get", + "type": "Font", + "array": true + }, + "findById": { + "decl": "FontsContext", + "kind": "method", + "type": "Font", + "array": false + }, + "findByName": { + "decl": "FontsContext", + "kind": "method", + "type": "Font", + "array": false + }, + "findAllById": { + "decl": "FontsContext", + "kind": "method", + "type": "Font", + "array": true + }, + "findAllByName": { + "decl": "FontsContext", + "kind": "method", + "type": "Font", + "array": true + } + }, + "FontVariant": { + "name": { + "decl": "FontVariant", + "kind": "get", + "type": null, + "array": false + }, + "fontVariantId": { + "decl": "FontVariant", + "kind": "get", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "FontVariant", + "kind": "get", + "type": null, + "array": false + }, + "fontStyle": { + "decl": "FontVariant", + "kind": "get", + "type": null, + "array": false + } + }, + "Gradient": { + "type": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "startX": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "startY": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "endX": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "endY": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": false + }, + "stops": { + "decl": "Gradient", + "kind": "getset", + "type": null, + "array": true + } + }, + "GridLayout": { + "dir": { + "decl": "GridLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rows": { + "decl": "GridLayout", + "kind": "get", + "type": "Track", + "array": true + }, + "columns": { + "decl": "GridLayout", + "kind": "get", + "type": "Track", + "array": true + }, + "addRow": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "addRowAtIndex": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "addColumn": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "addColumnAtIndex": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "removeRow": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "removeColumn": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "setColumn": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "setRow": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "appendChild": { + "decl": "GridLayout", + "kind": "method", + "type": null, + "array": false + }, + "alignItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "alignContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyItems": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "justifyContent": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rowGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "columnGap": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "topPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "rightPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "bottomPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "leftPadding": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "CommonLayout", + "kind": "getset", + "type": null, + "array": false + }, + "remove": { + "decl": "CommonLayout", + "kind": "method", + "type": null, + "array": false + } + }, + "Group": { + "type": { + "decl": "Group", + "kind": "get", + "type": null, + "array": false + }, + "children": { + "decl": "Group", + "kind": "get", + "type": "Shape", + "array": true + }, + "appendChild": { + "decl": "Group", + "kind": "method", + "type": null, + "array": false + }, + "insertChild": { + "decl": "Group", + "kind": "method", + "type": null, + "array": false + }, + "isMask": { + "decl": "Group", + "kind": "method", + "type": null, + "array": false + }, + "makeMask": { + "decl": "Group", + "kind": "method", + "type": null, + "array": false + }, + "removeMask": { + "decl": "Group", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Fill", + "array": true + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "GuideColumn": { + "type": { + "decl": "GuideColumn", + "kind": "get", + "type": null, + "array": false + }, + "display": { + "decl": "GuideColumn", + "kind": "get", + "type": null, + "array": false + }, + "params": { + "decl": "GuideColumn", + "kind": "get", + "type": "GuideColumnParams", + "array": false + } + }, + "GuideColumnParams": { + "color": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + }, + "type": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + }, + "size": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + }, + "margin": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + }, + "itemLength": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + }, + "gutter": { + "decl": "GuideColumnParams", + "kind": "get", + "type": null, + "array": false + } + }, + "GuideRow": { + "type": { + "decl": "GuideRow", + "kind": "get", + "type": null, + "array": false + }, + "display": { + "decl": "GuideRow", + "kind": "get", + "type": null, + "array": false + }, + "params": { + "decl": "GuideRow", + "kind": "get", + "type": "GuideColumnParams", + "array": false + } + }, + "GuideSquare": { + "type": { + "decl": "GuideSquare", + "kind": "get", + "type": null, + "array": false + }, + "display": { + "decl": "GuideSquare", + "kind": "get", + "type": null, + "array": false + }, + "params": { + "decl": "GuideSquare", + "kind": "get", + "type": "GuideSquareParams", + "array": false + } + }, + "GuideSquareParams": { + "color": { + "decl": "GuideSquareParams", + "kind": "get", + "type": null, + "array": false + }, + "size": { + "decl": "GuideSquareParams", + "kind": "get", + "type": null, + "array": false + } + }, + "HistoryContext": { + "undoBlockBegin": { + "decl": "HistoryContext", + "kind": "method", + "type": null, + "array": false + }, + "undoBlockFinish": { + "decl": "HistoryContext", + "kind": "method", + "type": null, + "array": false + } + }, + "Image": { + "type": { + "decl": "Image", + "kind": "get", + "type": null, + "array": false + }, + "fills": { + "decl": "Image", + "kind": "getset", + "type": "Fill", + "array": true + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "ImageData": { + "name": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "width": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "mtype": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "keepAspectRatio": { + "decl": "ImageData", + "kind": "get", + "type": null, + "array": false + }, + "data": { + "decl": "ImageData", + "kind": "method", + "type": null, + "array": false + } + }, + "Interaction": { + "shape": { + "decl": "Interaction", + "kind": "get", + "type": "Shape", + "array": false + }, + "trigger": { + "decl": "Interaction", + "kind": "getset", + "type": "Trigger", + "array": false + }, + "delay": { + "decl": "Interaction", + "kind": "getset", + "type": null, + "array": false + }, + "action": { + "decl": "Interaction", + "kind": "getset", + "type": "Action", + "array": false + }, + "remove": { + "decl": "Interaction", + "kind": "method", + "type": null, + "array": false + } + }, + "LayoutCellProperties": { + "row": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + }, + "rowSpan": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + }, + "column": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + }, + "columnSpan": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + }, + "areaName": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + }, + "position": { + "decl": "LayoutCellProperties", + "kind": "getset", + "type": null, + "array": false + } + }, + "LayoutChildProperties": { + "absolute": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "zIndex": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalSizing": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "alignSelf": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "horizontalMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "verticalMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "topMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "rightMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "bottomMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "leftMargin": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "maxWidth": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "maxHeight": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "minWidth": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + }, + "minHeight": { + "decl": "LayoutChildProperties", + "kind": "getset", + "type": null, + "array": false + } + }, + "Library": { + "id": { + "decl": "Library", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "Library", + "kind": "get", + "type": null, + "array": false + }, + "colors": { + "decl": "Library", + "kind": "get", + "type": "LibraryColor", + "array": true + }, + "typographies": { + "decl": "Library", + "kind": "get", + "type": "LibraryTypography", + "array": true + }, + "components": { + "decl": "Library", + "kind": "get", + "type": "LibraryComponent", + "array": true + }, + "tokens": { + "decl": "Library", + "kind": "get", + "type": "TokenCatalog", + "array": false + }, + "createColor": { + "decl": "Library", + "kind": "method", + "type": "LibraryColor", + "array": false + }, + "createTypography": { + "decl": "Library", + "kind": "method", + "type": "LibraryTypography", + "array": false + }, + "createComponent": { + "decl": "Library", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LibraryColor": { + "color": { + "decl": "LibraryColor", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "LibraryColor", + "kind": "getset", + "type": null, + "array": false + }, + "gradient": { + "decl": "LibraryColor", + "kind": "getset", + "type": "Gradient", + "array": false + }, + "image": { + "decl": "LibraryColor", + "kind": "getset", + "type": "ImageData", + "array": false + }, + "asFill": { + "decl": "LibraryColor", + "kind": "method", + "type": "Fill", + "array": false + }, + "asStroke": { + "decl": "LibraryColor", + "kind": "method", + "type": "Stroke", + "array": false + }, + "id": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LibraryComponent": { + "instance": { + "decl": "LibraryComponent", + "kind": "method", + "type": "Shape", + "array": false + }, + "mainInstance": { + "decl": "LibraryComponent", + "kind": "method", + "type": "Shape", + "array": false + }, + "isVariant": { + "decl": "LibraryComponent", + "kind": "method", + "type": null, + "array": false + }, + "transformInVariant": { + "decl": "LibraryComponent", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LibraryContext": { + "local": { + "decl": "LibraryContext", + "kind": "get", + "type": "Library", + "array": false + }, + "connected": { + "decl": "LibraryContext", + "kind": "get", + "type": "Library", + "array": true + }, + "availableLibraries": { + "decl": "LibraryContext", + "kind": "method", + "type": "LibrarySummary", + "array": true + }, + "connectLibrary": { + "decl": "LibraryContext", + "kind": "method", + "type": "Library", + "array": false + } + }, + "LibraryElement": { + "id": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LibrarySummary": { + "id": { + "decl": "LibrarySummary", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibrarySummary", + "kind": "get", + "type": null, + "array": false + }, + "numColors": { + "decl": "LibrarySummary", + "kind": "get", + "type": null, + "array": false + }, + "numComponents": { + "decl": "LibrarySummary", + "kind": "get", + "type": null, + "array": false + }, + "numTypographies": { + "decl": "LibrarySummary", + "kind": "get", + "type": null, + "array": false + } + }, + "LibraryTypography": { + "fontId": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "fontFamily": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "fontVariantId": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "fontSize": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "fontStyle": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "lineHeight": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "letterSpacing": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "textTransform": { + "decl": "LibraryTypography", + "kind": "getset", + "type": null, + "array": false + }, + "applyToText": { + "decl": "LibraryTypography", + "kind": "method", + "type": null, + "array": false + }, + "applyToTextRange": { + "decl": "LibraryTypography", + "kind": "method", + "type": null, + "array": false + }, + "setFont": { + "decl": "LibraryTypography", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LibraryVariantComponent": { + "variants": { + "decl": "LibraryVariantComponent", + "kind": "get", + "type": "Variants", + "array": false + }, + "variantProps": { + "decl": "LibraryVariantComponent", + "kind": "get", + "type": null, + "array": false + }, + "variantError": { + "decl": "LibraryVariantComponent", + "kind": "getset", + "type": null, + "array": false + }, + "addVariant": { + "decl": "LibraryVariantComponent", + "kind": "method", + "type": null, + "array": false + }, + "setVariantProperty": { + "decl": "LibraryVariantComponent", + "kind": "method", + "type": null, + "array": false + }, + "instance": { + "decl": "LibraryComponent", + "kind": "method", + "type": "Shape", + "array": false + }, + "mainInstance": { + "decl": "LibraryComponent", + "kind": "method", + "type": "Shape", + "array": false + }, + "isVariant": { + "decl": "LibraryComponent", + "kind": "method", + "type": null, + "array": false + }, + "transformInVariant": { + "decl": "LibraryComponent", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "LibraryElement", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "path": { + "decl": "LibraryElement", + "kind": "getset", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "LocalStorage": { + "getItem": { + "decl": "LocalStorage", + "kind": "method", + "type": null, + "array": false + }, + "setItem": { + "decl": "LocalStorage", + "kind": "method", + "type": null, + "array": false + }, + "removeItem": { + "decl": "LocalStorage", + "kind": "method", + "type": null, + "array": false + }, + "getKeys": { + "decl": "LocalStorage", + "kind": "method", + "type": null, + "array": true + } + }, + "NavigateTo": { + "type": { + "decl": "NavigateTo", + "kind": "get", + "type": null, + "array": false + }, + "destination": { + "decl": "NavigateTo", + "kind": "get", + "type": "Board", + "array": false + }, + "preserveScrollPosition": { + "decl": "NavigateTo", + "kind": "get", + "type": null, + "array": false + }, + "animation": { + "decl": "NavigateTo", + "kind": "get", + "type": "Animation", + "array": false + } + }, + "OpenOverlay": { + "type": { + "decl": "OpenOverlay", + "kind": "get", + "type": null, + "array": false + }, + "destination": { + "decl": "OverlayAction", + "kind": "get", + "type": "Board", + "array": false + }, + "relativeTo": { + "decl": "OverlayAction", + "kind": "get", + "type": "Shape", + "array": false + }, + "position": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "manualPositionLocation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Point", + "array": false + }, + "closeWhenClickOutside": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "addBackgroundOverlay": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "animation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Animation", + "array": false + } + }, + "OpenUrl": { + "type": { + "decl": "OpenUrl", + "kind": "get", + "type": null, + "array": false + }, + "url": { + "decl": "OpenUrl", + "kind": "get", + "type": null, + "array": false + } + }, + "OverlayAction": { + "destination": { + "decl": "OverlayAction", + "kind": "get", + "type": "Board", + "array": false + }, + "relativeTo": { + "decl": "OverlayAction", + "kind": "get", + "type": "Shape", + "array": false + }, + "position": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "manualPositionLocation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Point", + "array": false + }, + "closeWhenClickOutside": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "addBackgroundOverlay": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "animation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Animation", + "array": false + } + }, + "Page": { + "id": { + "decl": "Page", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "Page", + "kind": "getset", + "type": null, + "array": false + }, + "rulerGuides": { + "decl": "Page", + "kind": "get", + "type": "RulerGuide", + "array": true + }, + "root": { + "decl": "Page", + "kind": "get", + "type": "Shape", + "array": false + }, + "getShapeById": { + "decl": "Page", + "kind": "method", + "type": "Shape", + "array": false + }, + "findShapes": { + "decl": "Page", + "kind": "method", + "type": "Shape", + "array": true + }, + "flows": { + "decl": "Page", + "kind": "get", + "type": "Flow", + "array": true + }, + "createFlow": { + "decl": "Page", + "kind": "method", + "type": "Flow", + "array": false + }, + "removeFlow": { + "decl": "Page", + "kind": "method", + "type": null, + "array": false + }, + "addRulerGuide": { + "decl": "Page", + "kind": "method", + "type": "RulerGuide", + "array": false + }, + "removeRulerGuide": { + "decl": "Page", + "kind": "method", + "type": null, + "array": false + }, + "addCommentThread": { + "decl": "Page", + "kind": "method", + "type": "CommentThread", + "array": false + }, + "removeCommentThread": { + "decl": "Page", + "kind": "method", + "type": null, + "array": false + }, + "findCommentThreads": { + "decl": "Page", + "kind": "method", + "type": "CommentThread", + "array": true + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Path": { + "type": { + "decl": "Path", + "kind": "get", + "type": null, + "array": false + }, + "toD": { + "decl": "Path", + "kind": "method", + "type": null, + "array": false + }, + "content": { + "decl": "Path", + "kind": "getset", + "type": null, + "array": false + }, + "d": { + "decl": "Path", + "kind": "getset", + "type": null, + "array": false + }, + "commands": { + "decl": "Path", + "kind": "getset", + "type": "PathCommand", + "array": true + }, + "fills": { + "decl": "Path", + "kind": "getset", + "type": "Fill", + "array": true + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "PathCommand": { + "command": { + "decl": "PathCommand", + "kind": "getset", + "type": null, + "array": false + }, + "params": { + "decl": "PathCommand", + "kind": "getset", + "type": null, + "array": false + } + }, + "Penpot": { + "ui": { + "decl": "Penpot", + "kind": "get", + "type": null, + "array": false + }, + "utils": { + "decl": "Penpot", + "kind": "get", + "type": "ContextUtils", + "array": false + }, + "closePlugin": { + "decl": "Penpot", + "kind": "method", + "type": null, + "array": false + }, + "on": { + "decl": "Penpot", + "kind": "method", + "type": null, + "array": false + }, + "off": { + "decl": "Penpot", + "kind": "method", + "type": null, + "array": false + }, + "version": { + "decl": "Context", + "kind": "get", + "type": null, + "array": false + }, + "root": { + "decl": "Context", + "kind": "get", + "type": "Shape", + "array": false + }, + "currentFile": { + "decl": "Context", + "kind": "get", + "type": "File", + "array": false + }, + "currentPage": { + "decl": "Context", + "kind": "get", + "type": "Page", + "array": false + }, + "viewport": { + "decl": "Context", + "kind": "get", + "type": "Viewport", + "array": false + }, + "flags": { + "decl": "Context", + "kind": "get", + "type": "Flags", + "array": false + }, + "history": { + "decl": "Context", + "kind": "get", + "type": "HistoryContext", + "array": false + }, + "library": { + "decl": "Context", + "kind": "get", + "type": "LibraryContext", + "array": false + }, + "fonts": { + "decl": "Context", + "kind": "get", + "type": "FontsContext", + "array": false + }, + "currentUser": { + "decl": "Context", + "kind": "get", + "type": "User", + "array": false + }, + "activeUsers": { + "decl": "Context", + "kind": "get", + "type": "ActiveUser", + "array": true + }, + "theme": { + "decl": "Context", + "kind": "get", + "type": "Theme", + "array": false + }, + "localStorage": { + "decl": "Context", + "kind": "get", + "type": "LocalStorage", + "array": false + }, + "selection": { + "decl": "Context", + "kind": "getset", + "type": "Shape", + "array": true + }, + "shapesColors": { + "decl": "Context", + "kind": "method", + "type": null, + "array": true + }, + "replaceColor": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "uploadMediaUrl": { + "decl": "Context", + "kind": "method", + "type": "ImageData", + "array": false + }, + "uploadMediaData": { + "decl": "Context", + "kind": "method", + "type": "ImageData", + "array": false + }, + "group": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "ungroup": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "createRectangle": { + "decl": "Context", + "kind": "method", + "type": "Rectangle", + "array": false + }, + "createBoard": { + "decl": "Context", + "kind": "method", + "type": "Board", + "array": false + }, + "createEllipse": { + "decl": "Context", + "kind": "method", + "type": "Ellipse", + "array": false + }, + "createPath": { + "decl": "Context", + "kind": "method", + "type": "Path", + "array": false + }, + "createBoolean": { + "decl": "Context", + "kind": "method", + "type": "Boolean", + "array": false + }, + "createShapeFromSvg": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "createShapeFromSvgWithImages": { + "decl": "Context", + "kind": "method", + "type": "Group", + "array": false + }, + "createText": { + "decl": "Context", + "kind": "method", + "type": "Text", + "array": false + }, + "generateMarkup": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "generateStyle": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "generateFontFaces": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "openViewer": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "createPage": { + "decl": "Context", + "kind": "method", + "type": "Page", + "array": false + }, + "openPage": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "alignHorizontal": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "alignVertical": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "distributeHorizontal": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "distributeVertical": { + "decl": "Context", + "kind": "method", + "type": null, + "array": false + }, + "flatten": { + "decl": "Context", + "kind": "method", + "type": "Path", + "array": true + }, + "createVariantFromComponents": { + "decl": "Context", + "kind": "method", + "type": "VariantContainer", + "array": false + } + }, + "PluginData": { + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Point": { + "x": { + "decl": "Point", + "kind": "get", + "type": null, + "array": false + }, + "y": { + "decl": "Point", + "kind": "get", + "type": null, + "array": false + } + }, + "PreviousScreen": { + "type": { + "decl": "PreviousScreen", + "kind": "get", + "type": null, + "array": false + } + }, + "Push": { + "type": { + "decl": "Push", + "kind": "get", + "type": null, + "array": false + }, + "direction": { + "decl": "Push", + "kind": "get", + "type": null, + "array": false + }, + "duration": { + "decl": "Push", + "kind": "get", + "type": null, + "array": false + }, + "easing": { + "decl": "Push", + "kind": "get", + "type": null, + "array": false + } + }, + "Rectangle": { + "type": { + "decl": "Rectangle", + "kind": "get", + "type": null, + "array": false + }, + "fills": { + "decl": "Rectangle", + "kind": "getset", + "type": "Fill", + "array": true + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "RulerGuide": { + "orientation": { + "decl": "RulerGuide", + "kind": "get", + "type": "RulerGuideOrientation", + "array": false + }, + "position": { + "decl": "RulerGuide", + "kind": "getset", + "type": null, + "array": false + }, + "board": { + "decl": "RulerGuide", + "kind": "getset", + "type": "Board", + "array": false + } + }, + "Shadow": { + "id": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "style": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "offsetX": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "offsetY": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "blur": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "spread": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "Shadow", + "kind": "getset", + "type": null, + "array": false + }, + "color": { + "decl": "Shadow", + "kind": "getset", + "type": "Color", + "array": false + } + }, + "ShapeBase": { + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Fill", + "array": true + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Slide": { + "type": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + }, + "way": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + }, + "direction": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + }, + "duration": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + }, + "offsetEffect": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + }, + "easing": { + "decl": "Slide", + "kind": "get", + "type": null, + "array": false + } + }, + "Stroke": { + "strokeColor": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeColorRefFile": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeColorRefId": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeOpacity": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeStyle": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeWidth": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeAlignment": { + "decl": "Stroke", + "kind": "getset", + "type": null, + "array": false + }, + "strokeCapStart": { + "decl": "Stroke", + "kind": "getset", + "type": "StrokeCap", + "array": false + }, + "strokeCapEnd": { + "decl": "Stroke", + "kind": "getset", + "type": "StrokeCap", + "array": false + }, + "strokeColorGradient": { + "decl": "Stroke", + "kind": "getset", + "type": "Gradient", + "array": false + } + }, + "SvgRaw": { + "type": { + "decl": "SvgRaw", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Fill", + "array": true + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Text": { + "type": { + "decl": "Text", + "kind": "get", + "type": null, + "array": false + }, + "characters": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "growType": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontId": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontFamily": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontVariantId": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontSize": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "fontStyle": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "lineHeight": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "letterSpacing": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "textTransform": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "textDecoration": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "direction": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "align": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "verticalAlign": { + "decl": "Text", + "kind": "getset", + "type": null, + "array": false + }, + "textBounds": { + "decl": "Text", + "kind": "get", + "type": null, + "array": false + }, + "getRange": { + "decl": "Text", + "kind": "method", + "type": "TextRange", + "array": false + }, + "applyTypography": { + "decl": "Text", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Fill", + "array": true + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "TextRange": { + "shape": { + "decl": "TextRange", + "kind": "get", + "type": "Text", + "array": false + }, + "characters": { + "decl": "TextRange", + "kind": "get", + "type": null, + "array": false + }, + "fontId": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fontFamily": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fontVariantId": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fontSize": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fontStyle": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "lineHeight": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "letterSpacing": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "textTransform": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "textDecoration": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "direction": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "TextRange", + "kind": "getset", + "type": "Fill", + "array": true + }, + "align": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "verticalAlign": { + "decl": "TextRange", + "kind": "getset", + "type": null, + "array": false + }, + "applyTypography": { + "decl": "TextRange", + "kind": "method", + "type": null, + "array": false + } + }, + "ToggleOverlay": { + "type": { + "decl": "ToggleOverlay", + "kind": "get", + "type": null, + "array": false + }, + "destination": { + "decl": "OverlayAction", + "kind": "get", + "type": "Board", + "array": false + }, + "relativeTo": { + "decl": "OverlayAction", + "kind": "get", + "type": "Shape", + "array": false + }, + "position": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "manualPositionLocation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Point", + "array": false + }, + "closeWhenClickOutside": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "addBackgroundOverlay": { + "decl": "OverlayAction", + "kind": "get", + "type": null, + "array": false + }, + "animation": { + "decl": "OverlayAction", + "kind": "get", + "type": "Animation", + "array": false + } + }, + "TokenBase": { + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenBorderRadius": { + "type": { + "decl": "TokenBorderRadius", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenBorderRadius", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenBorderRadius", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenBorderWidth": { + "type": { + "decl": "TokenBorderWidth", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenBorderWidth", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenBorderWidth", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenCatalog": { + "themes": { + "decl": "TokenCatalog", + "kind": "get", + "type": "TokenTheme", + "array": true + }, + "sets": { + "decl": "TokenCatalog", + "kind": "get", + "type": "TokenSet", + "array": true + }, + "addTheme": { + "decl": "TokenCatalog", + "kind": "method", + "type": "TokenTheme", + "array": false + }, + "addSet": { + "decl": "TokenCatalog", + "kind": "method", + "type": "TokenSet", + "array": false + }, + "getThemeById": { + "decl": "TokenCatalog", + "kind": "method", + "type": "TokenTheme", + "array": false + }, + "getSetById": { + "decl": "TokenCatalog", + "kind": "method", + "type": "TokenSet", + "array": false + } + }, + "TokenColor": { + "type": { + "decl": "TokenColor", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenColor", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenColor", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenDimension": { + "type": { + "decl": "TokenDimension", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenDimension", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenDimension", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenFontFamilies": { + "type": { + "decl": "TokenFontFamilies", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenFontFamilies", + "kind": "getset", + "type": null, + "array": true + }, + "resolvedValue": { + "decl": "TokenFontFamilies", + "kind": "get", + "type": null, + "array": true + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenFontSizes": { + "type": { + "decl": "TokenFontSizes", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenFontSizes", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenFontSizes", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenFontWeights": { + "type": { + "decl": "TokenFontWeights", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenFontWeights", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenFontWeights", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenLetterSpacing": { + "type": { + "decl": "TokenLetterSpacing", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenLetterSpacing", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenLetterSpacing", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenNumber": { + "type": { + "decl": "TokenNumber", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenNumber", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenNumber", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenOpacity": { + "type": { + "decl": "TokenOpacity", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenOpacity", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenOpacity", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenRotation": { + "type": { + "decl": "TokenRotation", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenRotation", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenRotation", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenSet": { + "id": { + "decl": "TokenSet", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenSet", + "kind": "getset", + "type": null, + "array": false + }, + "active": { + "decl": "TokenSet", + "kind": "getset", + "type": null, + "array": false + }, + "tokens": { + "decl": "TokenSet", + "kind": "get", + "type": "Token", + "array": true + }, + "tokensByType": { + "decl": "TokenSet", + "kind": "get", + "type": null, + "array": true + }, + "toggleActive": { + "decl": "TokenSet", + "kind": "method", + "type": null, + "array": false + }, + "getTokenById": { + "decl": "TokenSet", + "kind": "method", + "type": "Token", + "array": false + }, + "addToken": { + "decl": "TokenSet", + "kind": "method", + "type": "Token", + "array": false + }, + "duplicate": { + "decl": "TokenSet", + "kind": "method", + "type": "TokenSet", + "array": false + }, + "remove": { + "decl": "TokenSet", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenShadow": { + "type": { + "decl": "TokenShadow", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenShadow", + "kind": "getset", + "type": "TokenShadowValueString", + "array": true + }, + "resolvedValue": { + "decl": "TokenShadow", + "kind": "get", + "type": "TokenShadowValue", + "array": true + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenShadowValue": { + "color": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + }, + "inset": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + }, + "offsetX": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + }, + "offsetY": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + }, + "spread": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + }, + "blur": { + "decl": "TokenShadowValue", + "kind": "getset", + "type": null, + "array": false + } + }, + "TokenShadowValueString": { + "color": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + }, + "inset": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + }, + "offsetX": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + }, + "offsetY": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + }, + "spread": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + }, + "blur": { + "decl": "TokenShadowValueString", + "kind": "getset", + "type": null, + "array": false + } + }, + "TokenSizing": { + "type": { + "decl": "TokenSizing", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenSizing", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenSizing", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenSpacing": { + "type": { + "decl": "TokenSpacing", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenSpacing", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenSpacing", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenTextCase": { + "type": { + "decl": "TokenTextCase", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenTextCase", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenTextCase", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenTextDecoration": { + "type": { + "decl": "TokenTextDecoration", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenTextDecoration", + "kind": "getset", + "type": null, + "array": false + }, + "resolvedValue": { + "decl": "TokenTextDecoration", + "kind": "get", + "type": null, + "array": false + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenTheme": { + "id": { + "decl": "TokenTheme", + "kind": "get", + "type": null, + "array": false + }, + "externalId": { + "decl": "TokenTheme", + "kind": "get", + "type": null, + "array": false + }, + "group": { + "decl": "TokenTheme", + "kind": "getset", + "type": null, + "array": false + }, + "name": { + "decl": "TokenTheme", + "kind": "getset", + "type": null, + "array": false + }, + "active": { + "decl": "TokenTheme", + "kind": "getset", + "type": null, + "array": false + }, + "toggleActive": { + "decl": "TokenTheme", + "kind": "method", + "type": null, + "array": false + }, + "activeSets": { + "decl": "TokenTheme", + "kind": "get", + "type": "TokenSet", + "array": true + }, + "addSet": { + "decl": "TokenTheme", + "kind": "method", + "type": null, + "array": false + }, + "removeSet": { + "decl": "TokenTheme", + "kind": "method", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenTheme", + "kind": "method", + "type": "TokenTheme", + "array": false + }, + "remove": { + "decl": "TokenTheme", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenTypography": { + "type": { + "decl": "TokenTypography", + "kind": "get", + "type": null, + "array": false + }, + "value": { + "decl": "TokenTypography", + "kind": "getset", + "type": "TokenTypographyValueString", + "array": false + }, + "resolvedValue": { + "decl": "TokenTypography", + "kind": "get", + "type": "TokenTypographyValue", + "array": true + }, + "id": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "description": { + "decl": "TokenBase", + "kind": "getset", + "type": null, + "array": false + }, + "duplicate": { + "decl": "TokenBase", + "kind": "method", + "type": "Token", + "array": false + }, + "remove": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "resolvedValueString": { + "decl": "TokenBase", + "kind": "get", + "type": null, + "array": false + }, + "applyToShapes": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToSelected": { + "decl": "TokenBase", + "kind": "method", + "type": null, + "array": false + } + }, + "TokenTypographyValue": { + "letterSpacing": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + }, + "fontFamilies": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": true + }, + "fontSizes": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + }, + "fontWeights": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + }, + "lineHeight": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + }, + "textCase": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + }, + "textDecoration": { + "decl": "TokenTypographyValue", + "kind": "getset", + "type": null, + "array": false + } + }, + "TokenTypographyValueString": { + "letterSpacing": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + }, + "fontFamilies": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": true + }, + "fontSizes": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + }, + "fontWeight": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + }, + "lineHeight": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + }, + "textCase": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + }, + "textDecoration": { + "decl": "TokenTypographyValueString", + "kind": "getset", + "type": null, + "array": false + } + }, + "Track": { + "type": { + "decl": "Track", + "kind": "getset", + "type": "TrackType", + "array": false + }, + "value": { + "decl": "Track", + "kind": "getset", + "type": null, + "array": false + } + }, + "User": { + "id": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "avatarUrl": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "color": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + }, + "sessionId": { + "decl": "User", + "kind": "get", + "type": null, + "array": false + } + }, + "VariantContainer": { + "variants": { + "decl": "VariantContainer", + "kind": "get", + "type": "Variants", + "array": false + }, + "type": { + "decl": "Board", + "kind": "get", + "type": null, + "array": false + }, + "clipContent": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "showInViewMode": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "grid": { + "decl": "Board", + "kind": "get", + "type": "GridLayout", + "array": false + }, + "flex": { + "decl": "Board", + "kind": "get", + "type": "FlexLayout", + "array": false + }, + "guides": { + "decl": "Board", + "kind": "getset", + "type": "Guide", + "array": true + }, + "rulerGuides": { + "decl": "Board", + "kind": "get", + "type": "RulerGuide", + "array": true + }, + "horizontalSizing": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "verticalSizing": { + "decl": "Board", + "kind": "getset", + "type": null, + "array": false + }, + "fills": { + "decl": "Board", + "kind": "getset", + "type": "Fill", + "array": true + }, + "children": { + "decl": "Board", + "kind": "getset", + "type": "Shape", + "array": true + }, + "appendChild": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "insertChild": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "addFlexLayout": { + "decl": "Board", + "kind": "method", + "type": "FlexLayout", + "array": false + }, + "addGridLayout": { + "decl": "Board", + "kind": "method", + "type": "GridLayout", + "array": false + }, + "addRulerGuide": { + "decl": "Board", + "kind": "method", + "type": "RulerGuide", + "array": false + }, + "removeRulerGuide": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "isVariantContainer": { + "decl": "Board", + "kind": "method", + "type": null, + "array": false + }, + "id": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "name": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parent": { + "decl": "ShapeBase", + "kind": "get", + "type": "Shape", + "array": false + }, + "parentIndex": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "x": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "y": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "width": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "height": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "bounds": { + "decl": "ShapeBase", + "kind": "get", + "type": "Bounds", + "array": false + }, + "center": { + "decl": "ShapeBase", + "kind": "get", + "type": "Point", + "array": false + }, + "blocked": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "hidden": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "visible": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "proportionLock": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsHorizontal": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "constraintsVertical": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "fixedWhenScrolling": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadius": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusTopRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomRight": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "borderRadiusBottomLeft": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "opacity": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "blendMode": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "shadows": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Shadow", + "array": true + }, + "blur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "backgroundBlur": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Blur", + "array": false + }, + "exports": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Export", + "array": true + }, + "boardX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "boardY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "parentY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipX": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "flipY": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "rotation": { + "decl": "ShapeBase", + "kind": "getset", + "type": null, + "array": false + }, + "strokes": { + "decl": "ShapeBase", + "kind": "getset", + "type": "Stroke", + "array": true + }, + "layoutChild": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutChildProperties", + "array": false + }, + "layoutCell": { + "decl": "ShapeBase", + "kind": "get", + "type": "LayoutCellProperties", + "array": false + }, + "setParentIndex": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "tokens": { + "decl": "ShapeBase", + "kind": "get", + "type": null, + "array": false + }, + "isComponentInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentMainInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentCopyInstance": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "isComponentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "componentRefShape": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentRoot": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "componentHead": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "component": { + "decl": "ShapeBase", + "kind": "method", + "type": "LibraryComponent", + "array": false + }, + "detach": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "swapComponent": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "switchVariant": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "combineAsVariants": { + "decl": "ShapeBase", + "kind": "method", + "type": "VariantContainer", + "array": false + }, + "isVariantHead": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "resize": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "rotate": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringToFront": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "bringForward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendToBack": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "sendBackward": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "export": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "interactions": { + "decl": "ShapeBase", + "kind": "get", + "type": "Interaction", + "array": true + }, + "addInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": "Interaction", + "array": false + }, + "removeInteraction": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "applyToken": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "clone": { + "decl": "ShapeBase", + "kind": "method", + "type": "Shape", + "array": false + }, + "remove": { + "decl": "ShapeBase", + "kind": "method", + "type": null, + "array": false + }, + "getPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + }, + "getSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "setSharedPluginData": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": false + }, + "getSharedPluginDataKeys": { + "decl": "PluginData", + "kind": "method", + "type": null, + "array": true + } + }, + "Variants": { + "id": { + "decl": "Variants", + "kind": "get", + "type": null, + "array": false + }, + "libraryId": { + "decl": "Variants", + "kind": "get", + "type": null, + "array": false + }, + "properties": { + "decl": "Variants", + "kind": "get", + "type": null, + "array": true + }, + "currentValues": { + "decl": "Variants", + "kind": "method", + "type": null, + "array": true + }, + "removeProperty": { + "decl": "Variants", + "kind": "method", + "type": null, + "array": false + }, + "renameProperty": { + "decl": "Variants", + "kind": "method", + "type": null, + "array": false + }, + "variantComponents": { + "decl": "Variants", + "kind": "method", + "type": "LibraryComponent", + "array": true + }, + "addVariant": { + "decl": "Variants", + "kind": "method", + "type": null, + "array": false + }, + "addProperty": { + "decl": "Variants", + "kind": "method", + "type": null, + "array": false + } + }, + "Viewport": { + "center": { + "decl": "Viewport", + "kind": "getset", + "type": "Point", + "array": false + }, + "zoom": { + "decl": "Viewport", + "kind": "getset", + "type": null, + "array": false + }, + "bounds": { + "decl": "Viewport", + "kind": "get", + "type": "Bounds", + "array": false + }, + "zoomReset": { + "decl": "Viewport", + "kind": "method", + "type": null, + "array": false + }, + "zoomToFitAll": { + "decl": "Viewport", + "kind": "method", + "type": null, + "array": false + }, + "zoomIntoView": { + "decl": "Viewport", + "kind": "method", + "type": null, + "array": false + } + } + }, + "unions": { + "Action": { + "variants": [ + "NavigateTo", + "OpenOverlay", + "ToggleOverlay", + "CloseOverlay", + "PreviousScreen", + "OpenUrl" + ], + "discriminant": { + "field": "type", + "map": { + "navigate-to": "NavigateTo", + "open-overlay": "OpenOverlay", + "toggle-overlay": "ToggleOverlay", + "close-overlay": "CloseOverlay", + "previous-screen": "PreviousScreen", + "open-url": "OpenUrl" + } + } + }, + "Animation": { + "variants": ["Dissolve", "Slide", "Push"], + "discriminant": { + "field": "type", + "map": { + "dissolve": "Dissolve", + "slide": "Slide", + "push": "Push" + } + } + }, + "Guide": { + "variants": ["GuideColumn", "GuideRow", "GuideSquare"], + "discriminant": { + "field": "type", + "map": { + "column": "GuideColumn", + "row": "GuideRow", + "square": "GuideSquare" + } + } + }, + "Shape": { + "variants": [ + "Board", + "Group", + "Boolean", + "Rectangle", + "Path", + "Text", + "Ellipse", + "SvgRaw", + "Image" + ], + "discriminant": { + "field": "type", + "map": { + "board": "Board", + "group": "Group", + "boolean": "Boolean", + "rectangle": "Rectangle", + "path": "Path", + "text": "Text", + "ellipse": "Ellipse", + "svg-raw": "SvgRaw", + "image": "Image" + } + } + }, + "Token": { + "variants": [ + "TokenBorderRadius", + "TokenShadow", + "TokenColor", + "TokenDimension", + "TokenFontFamilies", + "TokenFontSizes", + "TokenFontWeights", + "TokenLetterSpacing", + "TokenNumber", + "TokenOpacity", + "TokenRotation", + "TokenSizing", + "TokenSpacing", + "TokenBorderWidth", + "TokenTextCase", + "TokenTextDecoration", + "TokenTypography" + ], + "discriminant": { + "field": "type", + "map": { + "borderRadius": "TokenBorderRadius", + "shadow": "TokenShadow", + "color": "TokenColor", + "dimension": "TokenDimension", + "fontFamilies": "TokenFontFamilies", + "fontSizes": "TokenFontSizes", + "fontWeights": "TokenFontWeights", + "letterSpacing": "TokenLetterSpacing", + "number": "TokenNumber", + "opacity": "TokenOpacity", + "rotation": "TokenRotation", + "sizing": "TokenSizing", + "spacing": "TokenSpacing", + "borderWidth": "TokenBorderWidth", + "textCase": "TokenTextCase", + "textDecoration": "TokenTextDecoration", + "typography": "TokenTypography" + } + } + }, + "TokenValueString": { + "variants": ["TokenShadowValueString", "TokenTypographyValueString"], + "discriminant": null + } + } +} diff --git a/plugins/apps/plugin-api-test-suite/src/model.ts b/plugins/apps/plugin-api-test-suite/src/model.ts new file mode 100644 index 0000000000..a86cb3eb19 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/model.ts @@ -0,0 +1,60 @@ +import type { + CoverageReport, + RunSummary, + TestMeta, + TestResult, +} from './framework/types'; + +// Messages sent from the UI iframe to the plugin sandbox. +export interface ReadyMessage { + type: 'ready'; +} + +export interface RunMessage { + type: 'run'; + ids: string[] | 'all'; +} + +/** Carries the freshly built tests bundle source to be evaluated in the sandbox. */ +export interface ReloadTestsMessage { + type: 'reloadTests'; + code: string; +} + +export type UIToPluginMessage = ReadyMessage | RunMessage | ReloadTestsMessage; + +// Messages sent from the plugin sandbox to the UI iframe. +export interface TestsMessage { + type: 'tests'; + tests: TestMeta[]; +} + +export interface ResultMessage { + type: 'result'; + result: TestResult; +} + +export interface RunCompleteMessage { + type: 'runComplete'; + summary: RunSummary; + coverage: CoverageReport; +} + +export interface ThemeMessage { + type: 'theme'; + theme: string; +} + +/** Sent after a reload attempt so the UI can surface success/failure. */ +export interface ReloadedMessage { + type: 'reloaded'; + ok: boolean; + error?: string; +} + +export type PluginToUIMessage = + | TestsMessage + | ResultMessage + | RunCompleteMessage + | ThemeMessage + | ReloadedMessage; diff --git a/plugins/apps/plugin-api-test-suite/src/plugin.ts b/plugins/apps/plugin-api-test-suite/src/plugin.ts new file mode 100644 index 0000000000..db4f548bbb --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/plugin.ts @@ -0,0 +1,63 @@ +import { getTestMetas, setTests } from './framework/registry'; +import type { TestCase } from './framework/types'; +import { runTests } from './framework/runner'; +import type { PluginToUIMessage, UIToPluginMessage } from './model'; + +// Auto-discover every test. Importing the modules eagerly runs their top-level +// `test(...)` calls, which register them into the shared registry. +import.meta.glob('./tests/*.test.ts', { eager: true }); + +penpot.ui.open('Plugin API Test Suite', `?theme=${penpot.theme}`, { + width: 400, + height: 600, +}); + +function send(message: PluginToUIMessage) { + penpot.ui.sendMessage(message); +} + +penpot.ui.onMessage(async (message) => { + if (message.type === 'ready') { + send({ type: 'tests', tests: getTestMetas() }); + return; + } + + if (message.type === 'run') { + const { summary, coverage } = await runTests(message.ids, (result) => + send({ type: 'result', result }), + ); + send({ type: 'runComplete', summary, coverage }); + return; + } + + if (message.type === 'reloadTests') { + try { + // The runtime is configured with `evalTaming: 'unsafeEval'`, so evaluating + // the freshly built IIFE bundle is allowed. It publishes the discovered + // tests on `globalThis.__penpotReloadedTests`, which we swap into the + // registry so the next run uses the edited code. + const globals = globalThis as unknown as { + __penpotReloadedTests?: TestCase[]; + }; + globals.__penpotReloadedTests = undefined; + (0, eval)(message.code); + const reloaded = globals.__penpotReloadedTests; + if (!reloaded) { + throw new Error('Reloaded bundle did not expose any tests'); + } + setTests(reloaded); + send({ type: 'tests', tests: getTestMetas() }); + send({ type: 'reloaded', ok: true }); + } catch (err) { + send({ + type: 'reloaded', + ok: false, + error: err instanceof Error ? err.message : String(err), + }); + } + } +}); + +penpot.on('themechange', () => { + send({ type: 'theme', theme: penpot.theme }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests-bundle.ts b/plugins/apps/plugin-api-test-suite/src/tests-bundle.ts new file mode 100644 index 0000000000..cdba097903 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests-bundle.ts @@ -0,0 +1,15 @@ +import { getTests } from './framework/registry'; + +// Standalone bundle of all test cases, built as a single self-executing (IIFE) +// chunk by `vite.config.tests.ts` and rebuilt on every save by `watch`. +// +// The reload flow (see `src/plugin.ts`) fetches the freshly built bundle and +// `eval`s it inside the plugin sandbox. Importing the test modules registers them +// into this bundle's own registry; we then publish the discovered tests on +// `globalThis` so the sandbox can pick them up and swap them in without the user +// having to close and reopen the plugin. +import.meta.glob('./tests/*.test.ts', { eager: true }); + +( + globalThis as unknown as { __penpotReloadedTests?: unknown } +).__penpotReloadedTests = getTests(); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/colors.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/colors.test.ts new file mode 100644 index 0000000000..d61a34234d --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/colors.test.ts @@ -0,0 +1,50 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { TestContext } from '../framework/types'; + +// Colors. +// Exercises the context-level color helpers shapesColors() and replaceColor(), +// plus the ColorShapeInfo metadata they expose. + +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Colors', () => { + test('shapesColors lists the colors used by shapes', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#abcdef', fillOpacity: 1 }]; + + const colors = ctx.penpot.shapesColors([r]); + expect(colors.length).toBeGreaterThan(0); + + const entry = colors.find((c) => c.color === '#abcdef'); + expect(entry).toBeDefined(); + if (entry) { + expect(entry.shapesInfo).toBeDefined(); + expect(entry.shapesInfo.length).toBeGreaterThan(0); + expect(entry.shapesInfo[0].property).toBe('fill'); + expect(entry.shapesInfo[0].shapeId).toBe(r.id); + } + }); + + test('replaceColor swaps a solid fill color', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#111111', fillOpacity: 1 }]; + + // replaceColor matches by exact color-attrs equality, so the old color must + // include the same opacity the fill has. + ctx.penpot.replaceColor( + [r], + { color: '#111111', opacity: 1 }, + { color: '#222222', opacity: 1 }, + ); + + const fills = r.fills; + if (Array.isArray(fills)) { + expect(fills[0].fillColor).toBe('#222222'); + } + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/comments.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/comments.test.ts new file mode 100644 index 0000000000..b6ba1745ad --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/comments.test.ts @@ -0,0 +1,158 @@ +import { expect, expectReject } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { CommentThread, Page } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Comments. +// Comment threads are created on the current page. Both thread removal APIs are +// currently broken (see the dedicated red tests), so cleanup is best-effort to +// keep the other assertions meaningful. + +function page(ctx: TestContext): Page { + const p = ctx.penpot.currentPage; + if (!p) throw new Error('no current page'); + return p; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function cleanup(thread: CommentThread): void { + try { + thread.remove(); + } catch (err) { + void err; // thread.remove is currently broken; ignore for cleanup + } +} + +// Skipped under MOCK_BACKEND: comments assert backend-shaped responses +// (seqNumber, etc.) and pin real backend behaviour that a mock won't reproduce. +describe.skipIfMocked('Comments', () => { + test('addCommentThread creates a thread', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Hello comment', { + x: 100, + y: 120, + }); + try { + expect(typeof thread.seqNumber).toBe('number'); + expect(thread.position.x).toBeCloseTo(100, 0); + expect(thread.position.y).toBeCloseTo(120, 0); + expect(thread.resolved).toBe(false); + expect(thread.owner).toBeDefined(); + // A page-level thread has no board; reading it still exercises the getter. + void thread.board; + } finally { + cleanup(thread); + } + }); + + test('findCommentThreads lists threads', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Find me', { x: 50, y: 50 }); + try { + const threads = await p.findCommentThreads(); + expect(threads.length).toBeGreaterThan(0); + } finally { + cleanup(thread); + } + }); + + test('reply adds a comment and findComments lists them', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('First comment', { x: 10, y: 10 }); + try { + const reply = await thread.reply('A reply'); + expect(reply.content).toBe('A reply'); + expect(reply.user).toBeDefined(); + expect(reply.date).toBeDefined(); + + const comments = await thread.findComments(); + expect(comments.length).toBeGreaterThan(1); + } finally { + cleanup(thread); + } + }); + + test('thread resolved and position round-trip', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Toggle me', { x: 30, y: 30 }); + try { + thread.resolved = true; + expect(thread.resolved).toBe(true); + thread.position = { x: 200, y: 220 }; + expect(thread.position.x).toBeCloseTo(200, 0); + expect(thread.position.y).toBeCloseTo(220, 0); + } finally { + cleanup(thread); + } + }); + + test('comment content round-trips', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Editable', { x: 0, y: 0 }); + try { + const comments = await thread.findComments(); + const comment = comments[0]; + comment.content = 'edited content'; + // The content setter persists via an async RPC before updating locally. + await sleep(300); + expect(comment.content).toBe('edited content'); + expect(comment.user).toBeDefined(); + } finally { + cleanup(thread); + } + }); + + test('a comment can be removed', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Keep', { x: 5, y: 5 }); + try { + const reply = await thread.reply('to be removed'); + await reply.remove(); + const comments = await thread.findComments(); + expect(comments.length).toBeGreaterThan(0); + } finally { + cleanup(thread); + } + }); + + test('a comment thread can be removed', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('Remove via thread', { + x: 8, + y: 8, + }); + thread.remove(); + const threads = await p.findCommentThreads(); + expect(threads.every((t) => t.seqNumber !== thread.seqNumber)).toBe(true); + }); + + test('removeCommentThread removes a thread', async (ctx) => { + const p = page(ctx); + const thread: CommentThread = await p.addCommentThread('Remove me', { + x: 70, + y: 70, + }); + await p.removeCommentThread(thread); + }); + + // --------------------------------------------------------------------------- + // Edge cases: empty comment content must be rejected. + // --------------------------------------------------------------------------- + test('addCommentThread with empty content rejects', async (ctx) => { + const p = page(ctx); + await expectReject(() => p.addCommentThread('', { x: 0, y: 0 })); + }); + + test('reply with empty content rejects', async (ctx) => { + const p = page(ctx); + const thread = await p.addCommentThread('parent', { x: 12, y: 12 }); + try { + await expectReject(() => thread.reply('')); + } finally { + cleanup(thread); + } + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/components.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/components.test.ts new file mode 100644 index 0000000000..4982bc574c --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/components.test.ts @@ -0,0 +1,135 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Board, Shape } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Component instances and the ShapeBase component methods. +// A component is built from a rectangle and instantiated; the instance exposes +// the component predicates and navigation methods. + +function makeComponent(ctx: TestContext) { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + return ctx.penpot.library.local.createComponent([rect]); +} + +function instanceOf(ctx: TestContext): Shape { + const comp = makeComponent(ctx); + const inst = comp.instance(); + ctx.board.appendChild(inst); + return inst; +} + +describe('Component instances', () => { + test('component predicates identify an instance', (ctx) => { + const inst = instanceOf(ctx); + expect(inst.isComponentInstance()).toBeTruthy(); + expect(inst.isComponentRoot()).toBeTruthy(); + expect(inst.isComponentHead()).toBeTruthy(); + // A fresh instance is a copy, not the main instance. + expect(inst.isComponentMainInstance()).toBeFalsy(); + expect(inst.isComponentCopyInstance()).toBeTruthy(); + expect(inst.isVariantHead()).toBeFalsy(); + }); + + test('component navigation methods return shapes', (ctx) => { + const inst = instanceOf(ctx); + expect(inst.componentRoot()).toBeDefined(); + expect(inst.componentHead()).toBeDefined(); + expect(inst.componentRefShape()).toBeDefined(); + }); + + test('component() returns the library component', (ctx) => { + const inst = instanceOf(ctx); + const comp = inst.component(); + expect(comp).not.toBeNull(); + if (comp) { + expect(typeof comp.id).toBe('string'); + } + }); + + test('detach turns an instance into a basic shape', (ctx) => { + const inst = instanceOf(ctx); + inst.detach(); + expect(inst.isComponentInstance()).toBeFalsy(); + }); + + test('swapComponent replaces the instance component', (ctx) => { + const inst = instanceOf(ctx); + const other = makeComponent(ctx); + inst.swapComponent(other); + const comp = inst.component(); + expect(comp).not.toBeNull(); + if (comp) { + expect(comp.id).toBe(other.id); + } + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests exercise the component methods on shapes + // that are not component instances (documented null/self returns, invalid + // swap target); the "success" test checks instance independence. + // --------------------------------------------------------------------------- + test('component() on a plain shape returns null', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(rect.component()).toBeNull(); + }); + + test('componentRoot() on a plain shape returns null', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + // componentRoot (like component(), componentHead(), componentRefShape()) + // is null for a shape that is not part of any component. The d.ts + // "returns itself" note applies to a shape that IS the root of a component. + expect(rect.componentRoot()).toBeNull(); + }); + + test('swapComponent with a non-component target throws', (ctx) => { + const inst = instanceOf(ctx); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(() => + inst.swapComponent(rect as unknown as ReturnType), + ).toThrow(); + }); + + test('two instances of one component are independent but share the source', (ctx) => { + const comp = makeComponent(ctx); + const first = comp.instance(); + const second = comp.instance(); + ctx.board.appendChild(first); + ctx.board.appendChild(second); + + first.name = 'first'; + second.name = 'second'; + expect(first.id).not.toBe(second.id); + expect(first.name).toBe('first'); + expect(second.name).toBe('second'); + + const c1 = first.component(); + const c2 = second.component(); + expect(c1).not.toBeNull(); + expect(c2).not.toBeNull(); + if (c1 && c2) { + expect(c1.id).toBe(c2.id); + } + }); +}); + +describe('Shape interactions cleanup', () => { + test('removeInteraction removes an interaction from a shape', (ctx) => { + const dest = ctx.penpot.createBoard(); + ctx.board.appendChild(dest as Board); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + const interaction = rect.addInteraction('click', { + type: 'navigate-to', + destination: dest, + }); + const before = rect.interactions.length; + rect.removeInteraction(interaction); + expect(rect.interactions.length).toBe(before - 1); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/events.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/events.test.ts new file mode 100644 index 0000000000..8d1fddf66a --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/events.test.ts @@ -0,0 +1,67 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Events. +// Listeners are registered with `on`, triggered by mutating state, and removed +// with `off`. Callbacks are debounced (~10ms), so the tests wait before asserting. + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe('Events', () => { + test('selectionchange fires with the selected ids', async (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + let received: string[] | null = null; + const listenerId = ctx.penpot.on('selectionchange', (ids) => { + received = ids; + }); + + ctx.penpot.selection = [rect]; + await sleep(150); + ctx.penpot.off(listenerId); + + expect(received).not.toBeNull(); + if (received) { + expect((received as string[]).includes(rect.id)).toBe(true); + } + }); + + test('shapechange fires when the observed shape changes', async (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + let fired = false; + const listenerId = ctx.penpot.on( + 'shapechange', + () => { + fired = true; + }, + { shapeId: rect.id }, + ); + + rect.name = 'changed-name'; + await sleep(150); + ctx.penpot.off(listenerId); + + expect(fired).toBe(true); + }); + + test('off stops further notifications', async (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + let count = 0; + const listenerId = ctx.penpot.on('selectionchange', () => { + count += 1; + }); + ctx.penpot.off(listenerId); + + ctx.penpot.selection = [rect]; + await sleep(150); + + expect(count).toBe(0); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/file.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/file.test.ts new file mode 100644 index 0000000000..5f8f4a8b11 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/file.test.ts @@ -0,0 +1,103 @@ +import { expect, expectReject } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// File & versions. +// Read-only assertions on currentFile plus the version history API. The file +// name is only read (renaming would mutate the user's file). + +describe('File', () => { + test('currentFile exposes id and name', (ctx) => { + const file = ctx.penpot.currentFile; + expect(file).not.toBeNull(); + if (file) { + expect(typeof file.id).toBe('string'); + expect(typeof file.name).toBe('string'); + } + }); + + test('currentFile exposes revn', (ctx) => { + const file = ctx.penpot.currentFile; + if (file) { + expect(typeof file.revn).toBe('number'); + } + }); + + test('file lists its pages', (ctx) => { + const file = ctx.penpot.currentFile; + if (file) { + expect(file.pages.length).toBeGreaterThan(0); + expect(typeof file.pages[0].id).toBe('string'); + } + }); + + test('export returns binary data', async (ctx) => { + const file = ctx.penpot.currentFile; + if (file) { + // The exporter service may be unavailable in the headless runner, so a + // rejection here is treated as an environment limitation; when it does + // run, the result must be a non-empty byte array. + const data = await file.export('penpot', 'detach').catch(() => null); + if (data) { + expect(data.length).toBeGreaterThan(0); + } + } + }); + + // Skipped under MOCK_BACKEND: version history is persisted/returned by the + // backend; a no-op persist mock can't reproduce saved versions. + describe.skipIfMocked('Versions', () => { + test('saveVersion and findVersions manage version history', async (ctx) => { + const file = ctx.penpot.currentFile; + expect(file).not.toBeNull(); + if (file) { + const version = await file.saveVersion('plugin-test-version'); + expect(version).toBeDefined(); + expect(version.label).toBe('plugin-test-version'); + expect(version.isAutosave).toBe(false); + + // Relabel the saved version (covers FileVersion.label set). + version.label = 'plugin-test-version-renamed'; + expect(version.label).toBe('plugin-test-version-renamed'); + + const versions = await file.findVersions(); + expect(versions.length).toBeGreaterThan(0); + + // Clean up the version we just created. + await version.remove(); + } + }); + + test('version exposes its creation date', async (ctx) => { + const file = ctx.penpot.currentFile; + if (file) { + const version = await file.saveVersion('plugin-test-version-date'); + try { + expect(version.createdAt).toBeDefined(); + } finally { + await version.remove(); + } + } + }); + + test('version createdBy is exercised', async (ctx) => { + const file = ctx.penpot.currentFile; + if (file) { + const version = await file.saveVersion('plugin-test-version-pin'); + void version.createdBy; + // `pin` is intentionally not exercised: it only converts a *system* + // autosave to a permanent version, and a plugin cannot create an + // autosave, so calling it would always reject. See README.md. + await version.remove().catch(() => undefined); + } + }); + + // Edge case: an empty version label must be rejected. + test('saveVersion with an empty label rejects', async (ctx) => { + const file = ctx.penpot.currentFile; + expect(file).not.toBeNull(); + if (file) { + await expectReject(() => file.saveVersion('')); + } + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/fills-strokes.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/fills-strokes.test.ts new file mode 100644 index 0000000000..bf4ef96cf0 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/fills-strokes.test.ts @@ -0,0 +1,280 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { TestContext } from '../framework/types'; + +// Fills & strokes. +// Fills/strokes are assigned as whole arrays of plain objects and read back +// through the shape proxy, so the Fill/Stroke getters are what coverage records +// (the per-property setters are not individually settable at runtime). +// +// Each group bundles its happy-path round-trips together with the related edge +// cases: "throws" tests assert invalid input is rejected, the "(currently +// unvalidated)" tests pin lenient behaviour, and the remaining ones cover +// non-trivial valid behaviour (ordering, type switching, multiple strokes, +// clearing). + +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Fills & strokes', () => { + describe('Fills', () => { + test('solid fill color and opacity round-trip', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 0.5 }]; + + const fills = r.fills; + expect(fills).toHaveLength(1); + if (Array.isArray(fills)) { + expect(fills[0].fillColor).toBe('#ff0000'); + expect(fills[0].fillOpacity).toBeCloseTo(0.5, 2); + } + }); + + test('gradient fill is preserved', (ctx) => { + const r = rect(ctx); + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }, + }, + ]; + + const fills = r.fills; + if (Array.isArray(fills)) { + const gradient = fills[0].fillColorGradient; + expect(gradient).toBeDefined(); + expect(gradient && gradient.type).toBe('linear'); + } + }); + + test('multiple fills can be stacked', (ctx) => { + const r = rect(ctx); + r.fills = [ + { fillColor: '#ff0000', fillOpacity: 0.5 }, + { fillColor: '#0000ff', fillOpacity: 0.5 }, + ]; + expect(r.fills).toHaveLength(2); + }); + + test('multiple fills preserve their order', (ctx) => { + const r = rect(ctx); + r.fills = [ + { fillColor: '#ff0000', fillOpacity: 1 }, + { fillColor: '#00ff00', fillOpacity: 1 }, + { fillColor: '#0000ff', fillOpacity: 1 }, + ]; + const fills = r.fills; + expect(fills).toHaveLength(3); + if (Array.isArray(fills)) { + expect(fills.map((f) => f.fillColor)).toEqual([ + '#ff0000', + '#00ff00', + '#0000ff', + ]); + } + }); + + test('a fill can switch solid -> gradient -> solid', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }, + }, + ]; + let fills = r.fills; + if (Array.isArray(fills)) { + expect(fills[0].fillColorGradient).toBeDefined(); + } + r.fills = [{ fillColor: '#00ff00', fillOpacity: 1 }]; + fills = r.fills; + if (Array.isArray(fills)) { + expect(fills[0].fillColor).toBe('#00ff00'); + // Switching back to a solid fill clears the gradient (read back as null). + expect(fills[0].fillColorGradient).toBeFalsy(); + } + }); + + test('fillOpacity above 1 throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1.5 }]; + }).toThrow(); + }); + + test('fillOpacity below 0 throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.fills = [{ fillColor: '#ff0000', fillOpacity: -0.5 }]; + }).toThrow(); + }); + + test('setting fills on a group is accepted (currently unvalidated)', (ctx) => { + // The plugin API does not block fills on groups, so the assignment is + // accepted rather than rejected. This pins the current (lenient) behaviour. + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + expect(() => { + group.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + }).not.toThrow(); + } + }); + + test('assigning empty arrays clears fills and strokes', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }]; + r.fills = []; + r.strokes = []; + expect(r.fills).toHaveLength(0); + expect(r.strokes).toHaveLength(0); + }); + }); + + describe('Strokes', () => { + test('stroke properties round-trip', (ctx) => { + const r = rect(ctx); + r.strokes = [ + { + strokeColor: '#0000ff', + strokeOpacity: 1, + strokeStyle: 'solid', + strokeWidth: 3, + strokeAlignment: 'center', + }, + ]; + + expect(r.strokes).toHaveLength(1); + const stroke = r.strokes[0]; + expect(stroke.strokeColor).toBe('#0000ff'); + expect(stroke.strokeOpacity).toBeCloseTo(1, 2); + expect(stroke.strokeStyle).toBe('solid'); + expect(stroke.strokeWidth).toBeCloseTo(3, 0); + expect(stroke.strokeAlignment).toBe('center'); + }); + + test('stroke caps round-trip on an open path', (ctx) => { + const path = ctx.penpot.createPath(); + ctx.board.appendChild(path); + path.d = 'M0 0 L40 0'; + path.strokes = [ + { + strokeColor: '#000000', + strokeWidth: 4, + strokeCapStart: 'round', + strokeCapEnd: 'triangle-arrow', + }, + ]; + + const stroke = path.strokes[0]; + expect(stroke.strokeCapStart).toBe('round'); + expect(stroke.strokeCapEnd).toBe('triangle-arrow'); + }); + + test('dashed stroke style is preserved', (ctx) => { + const r = rect(ctx); + r.strokes = [ + { strokeColor: '#00ff00', strokeWidth: 2, strokeStyle: 'dashed' }, + ]; + expect(r.strokes[0].strokeStyle).toBe('dashed'); + }); + + test('dotted stroke style is preserved', (ctx) => { + const r = rect(ctx); + r.strokes = [ + { strokeColor: '#0000ff', strokeWidth: 2, strokeStyle: 'dotted' }, + ]; + expect(r.strokes[0].strokeStyle).toBe('dotted'); + }); + + test("stroke style 'none' is rejected at runtime (d.ts lists it)", (ctx) => { + // The d.ts allows strokeStyle 'none', but the runtime rejects it as an + // invalid value ("Value not valid"), so with throwValidationErrors it + // throws. Pins the current d.ts/runtime mismatch. + const r = rect(ctx); + expect(() => { + r.strokes = [ + { strokeColor: '#0000ff', strokeWidth: 2, strokeStyle: 'none' }, + ]; + }).toThrow(); + }); + + test('two strokes with different alignment coexist', (ctx) => { + const r = rect(ctx); + r.strokes = [ + { strokeColor: '#000000', strokeWidth: 2, strokeAlignment: 'inner' }, + { strokeColor: '#ffffff', strokeWidth: 1, strokeAlignment: 'outer' }, + ]; + expect(r.strokes).toHaveLength(2); + expect(r.strokes.map((s) => s.strokeAlignment).sort()).toEqual([ + 'inner', + 'outer', + ]); + }); + + test('negative strokeWidth is accepted (currently unvalidated)', (ctx) => { + // The plugin API does not constrain strokeWidth to be non-negative, so a + // negative value is stored as-is rather than rejected. This pins the current + // (lenient) behaviour. + const r = rect(ctx); + r.strokes = [{ strokeColor: '#000000', strokeWidth: -3 }]; + expect(r.strokes).toHaveLength(1); + expect(typeof r.strokes[0].strokeWidth).toBe('number'); + }); + + test('invalid strokeStyle throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.strokes = [ + { + strokeColor: '#000000', + strokeWidth: 1, + strokeStyle: 'wavy' as unknown as 'solid', + }, + ]; + }).toThrow(); + }); + + test('invalid strokeAlignment throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.strokes = [ + { + strokeColor: '#000000', + strokeWidth: 1, + strokeAlignment: 'middle' as unknown as 'center', + }, + ]; + }).toThrow(); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/fixtures.ts b/plugins/apps/plugin-api-test-suite/src/tests/fixtures.ts new file mode 100644 index 0000000000..45c3302b4d --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/fixtures.ts @@ -0,0 +1,12 @@ +// Shared test fixtures. Not a `*.test.ts`, so the runner's glob doesn't pick it +// up as a test file; it's only imported by the tests that need it. + +// A valid 1x1 PNG (opaque red, RGBA), so uploadMediaData needs no network. The +// bytes must form a well-formed PNG — the backend processes the image with +// ImageMagick, which rejects a malformed IDAT chunk (bad CRC / extra data). +export const PNG_1X1 = new Uint8Array([ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, + 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, + 156, 99, 248, 207, 192, 240, 31, 0, 5, 0, 1, 255, 137, 153, 61, 29, 0, 0, 0, + 0, 73, 69, 78, 68, 174, 66, 96, 130, +]); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/fonts.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/fonts.test.ts new file mode 100644 index 0000000000..7340ddfa94 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/fonts.test.ts @@ -0,0 +1,122 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Text } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Fonts. +// Exercises the FontsContext lookups, the Font/FontVariant metadata, applying a +// font to a text shape / range, and generateFontFaces. Fonts are self-provided +// from `fonts.all` so the tests don't depend on a specific font being present. + +function text(ctx: TestContext, value = 'Hello Penpot'): Text { + const t = ctx.penpot.createText(value); + if (!t) throw new Error('createText returned null'); + ctx.board.appendChild(t); + return t; +} + +describe('Fonts', () => { + test('fonts.all lists available fonts', (ctx) => { + const all = ctx.penpot.fonts.all; + expect(all.length).toBeGreaterThan(0); + }); + + test('a font exposes metadata and variants', (ctx) => { + const font = ctx.penpot.fonts.all[0]; + expect(typeof font.name).toBe('string'); + expect(typeof font.fontId).toBe('string'); + expect(typeof font.fontFamily).toBe('string'); + expect(typeof font.fontVariantId).toBe('string'); + expect(typeof font.fontWeight).toBe('string'); + // fontStyle is optional (string or null). + expect(font.fontStyle == null || typeof font.fontStyle === 'string').toBe( + true, + ); + + expect(font.variants.length).toBeGreaterThan(0); + const variant = font.variants[0]; + expect(typeof variant.name).toBe('string'); + expect(typeof variant.fontVariantId).toBe('string'); + expect(typeof variant.fontWeight).toBe('string'); + expect( + variant.fontStyle === 'normal' || variant.fontStyle === 'italic', + ).toBe(true); + }); + + test('findById returns the matching font', (ctx) => { + const font = ctx.penpot.fonts.all[0]; + const found = ctx.penpot.fonts.findById(font.fontId); + expect(found).not.toBeNull(); + expect(found && found.fontId).toBe(font.fontId); + }); + + test('findByName returns the matching font', (ctx) => { + const font = ctx.penpot.fonts.all[0]; + const found = ctx.penpot.fonts.findByName(font.name); + expect(found).not.toBeNull(); + expect(found && found.name).toBe(font.name); + }); + + test('findAllById and findAllByName return arrays', (ctx) => { + const font = ctx.penpot.fonts.all[0]; + expect(ctx.penpot.fonts.findAllById(font.fontId).length).toBeGreaterThan(0); + expect(ctx.penpot.fonts.findAllByName(font.name).length).toBeGreaterThan(0); + }); + + test('applyToText sets the font on a text shape', (ctx) => { + const t = text(ctx); + const font = ctx.penpot.fonts.all[0]; + font.applyToText(t); + expect(t.fontId).toBe(font.fontId); + }); + + test('applyToRange sets the font on a text range', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const font = ctx.penpot.fonts.all[0]; + const range = t.getRange(0, 5); + font.applyToRange(range); + expect(range.fontId).toBe(font.fontId); + }); + + test('generateFontFaces returns a css string', async (ctx) => { + const t = text(ctx); + const faces = await ctx.penpot.generateFontFaces([t]); + expect(typeof faces).toBe('string'); + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert the documented null returns for + // unknown lookups; the "success" test applies a specific variant and reads it + // back. + // --------------------------------------------------------------------------- + test('findById of an unknown id returns null', (ctx) => { + const found = ctx.penpot.fonts.findById('definitely-not-a-font-id'); + expect(found).toBeNull(); + }); + + test('findByName of an unknown name returns null', (ctx) => { + const found = ctx.penpot.fonts.findByName('No Such Font Name 12345'); + expect(found).toBeNull(); + }); + + test('findAllById of an unknown id returns an empty array', (ctx) => { + expect(ctx.penpot.fonts.findAllById('definitely-not-a-font-id')).toEqual( + [], + ); + }); + + test('applying a specific variant sets the variant on the text', (ctx) => { + const t = text(ctx); + // Prefer a font that has more than one variant so the chosen variant is + // meaningful; fall back to the first font otherwise. + const font = + ctx.penpot.fonts.all.find((f) => f.variants.length > 1) ?? + ctx.penpot.fonts.all[0]; + const variant = font.variants[font.variants.length - 1]; + + font.applyToText(t, variant); + expect(t.fontId).toBe(font.fontId); + expect(t.fontVariantId).toBe(variant.fontVariantId); + expect(t.fontWeight).toBe(variant.fontWeight); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/interactions.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/interactions.test.ts new file mode 100644 index 0000000000..91f26d3dce --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/interactions.test.ts @@ -0,0 +1,333 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Board, Rectangle } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Interactions, overlays and animations. +// Interactions are added to a shape; navigate/overlay actions target boards, so +// destination boards are self-provisioned on the scratch board. + +function board(ctx: TestContext): Board { + const b = ctx.penpot.createBoard(); + ctx.board.appendChild(b); + return b; +} + +function rect(ctx: TestContext): Rectangle { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Interactions', () => { + test('navigate-to interaction round-trips', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + }); + + expect(interaction.trigger).toBe('click'); + expect(interaction.action.type).toBe('navigate-to'); + if (interaction.action.type === 'navigate-to') { + expect(interaction.action.destination.id).toBe(dest.id); + } + expect(interaction.shape && interaction.shape.id).toBe(r.id); + expect(r.interactions.length).toBeGreaterThan(0); + }); + + test('open-url interaction round-trips', (ctx) => { + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'open-url', + url: 'https://example.com', + }); + expect(interaction.action.type).toBe('open-url'); + if (interaction.action.type === 'open-url') { + expect(interaction.action.url).toBe('https://example.com'); + } + }); + + test('open-overlay interaction round-trips', (ctx) => { + const overlay = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'open-overlay', + destination: overlay, + position: 'manual', + manualPositionLocation: { x: 10, y: 20 }, + closeWhenClickOutside: true, + addBackgroundOverlay: true, + animation: { type: 'dissolve', duration: 100, easing: 'linear' }, + }); + expect(interaction.action.type).toBe('open-overlay'); + if (interaction.action.type === 'open-overlay') { + expect(interaction.action.destination.id).toBe(overlay.id); + expect(interaction.action.position).toBe('manual'); + expect(interaction.action.closeWhenClickOutside).toBe(true); + expect(interaction.action.addBackgroundOverlay).toBe(true); + } + }); + + test('open-overlay supports a non-manual position', (ctx) => { + const overlay = board(ctx); + const r = rect(ctx); + // Per the types, manualPositionLocation is only needed for 'manual'. + const interaction = r.addInteraction('click', { + type: 'open-overlay', + destination: overlay, + position: 'center', + animation: { type: 'dissolve', duration: 100, easing: 'linear' }, + }); + expect(interaction.action.type).toBe('open-overlay'); + }); + + test('toggle-overlay interaction round-trips', (ctx) => { + const overlay = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'toggle-overlay', + destination: overlay, + position: 'manual', + manualPositionLocation: { x: 0, y: 0 }, + animation: { type: 'dissolve', duration: 100, easing: 'linear' }, + }); + expect(interaction.action.type).toBe('toggle-overlay'); + if (interaction.action.type === 'toggle-overlay') { + expect(interaction.action.destination.id).toBe(overlay.id); + } + }); + + test('close-overlay interaction round-trips', (ctx) => { + const overlay = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'close-overlay', + destination: overlay, + animation: { type: 'dissolve', duration: 200, easing: 'linear' }, + }); + expect(interaction.action.type).toBe('close-overlay'); + if (interaction.action.type === 'close-overlay') { + expect( + interaction.action.destination && interaction.action.destination.id, + ).toBe(overlay.id); + expect(interaction.action.animation).toBeDefined(); + } + }); + + test('previous-screen interaction round-trips', (ctx) => { + const r = rect(ctx); + const interaction = r.addInteraction('click', { type: 'previous-screen' }); + expect(interaction.action.type).toBe('previous-screen'); + }); + + test('after-delay trigger carries a delay', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction( + 'after-delay', + { type: 'navigate-to', destination: dest }, + 1000, + ); + expect(interaction.trigger).toBe('after-delay'); + expect(interaction.delay).toBeCloseTo(1000, 0); + }); + + test('mouse-leave trigger is recorded', (ctx) => { + // click / mouse-enter / after-delay are covered above; mouse-leave is the + // remaining trigger. + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('mouse-leave', { + type: 'navigate-to', + destination: dest, + }); + expect(interaction.trigger).toBe('mouse-leave'); + }); + + // Pins persistence of the `delay` and `action` setters on an existing + // interaction (mutating after `addInteraction`). `misc.test.ts:300` exercises + // these setters' (set) coverage targets but never asserts that the new values + // stick; this fills that behavioural gap. (An older note claimed these setters + // "don't persist" — that is stale: CI confirms they do.) + test('interaction delay and action setters persist', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction( + 'after-delay', + { type: 'navigate-to', destination: dest }, + 1000, + ); + + interaction.delay = 250; + interaction.action = { type: 'previous-screen' }; + + expect(interaction.delay).toBeCloseTo(250, 0); + expect(interaction.action.type).toBe('previous-screen'); + }); + + describe('Animations', () => { + test('dissolve animation round-trips', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + animation: { type: 'dissolve', duration: 300, easing: 'ease' }, + }); + if ( + interaction.action.type === 'navigate-to' && + interaction.action.animation + ) { + expect(interaction.action.animation.type).toBe('dissolve'); + if (interaction.action.animation.type === 'dissolve') { + expect(interaction.action.animation.duration).toBeCloseTo(300, 0); + expect(interaction.action.animation.easing).toBe('ease'); + } + } + }); + + test('dissolve animation accepts every easing curve', (ctx) => { + // Only `linear` and `ease` are exercised elsewhere; cover the remaining + // easing curves so a single broken curve is caught. + for (const easing of ['ease-in', 'ease-out', 'ease-in-out'] as const) { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + animation: { type: 'dissolve', duration: 200, easing }, + }); + if ( + interaction.action.type === 'navigate-to' && + interaction.action.animation && + interaction.action.animation.type === 'dissolve' + ) { + expect(interaction.action.animation.easing).toBe(easing); + } + } + }); + + test('slide animation round-trips', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + animation: { + type: 'slide', + way: 'in', + direction: 'right', + duration: 300, + easing: 'linear', + }, + }); + if ( + interaction.action.type === 'navigate-to' && + interaction.action.animation + ) { + expect(interaction.action.animation.type).toBe('slide'); + if (interaction.action.animation.type === 'slide') { + expect(interaction.action.animation.way).toBe('in'); + expect(interaction.action.animation.direction).toBe('right'); + expect(interaction.action.animation.duration).toBeCloseTo(300, 0); + } + } + }); + + test('push animation round-trips', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + animation: { + type: 'push', + direction: 'left', + duration: 300, + easing: 'linear', + }, + }); + if ( + interaction.action.type === 'navigate-to' && + interaction.action.animation + ) { + expect(interaction.action.animation.type).toBe('push'); + if (interaction.action.animation.type === 'push') { + expect(interaction.action.animation.direction).toBe('left'); + expect(interaction.action.animation.duration).toBeCloseTo(300, 0); + } + } + }); + }); + + test('an interaction can be removed', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + }); + + const before = r.interactions.length; + interaction.remove(); + expect(r.interactions.length).toBe(before - 1); + }); + + test('interaction trigger can be changed', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + }); + + interaction.trigger = 'mouse-enter'; + expect(interaction.trigger).toBe('mouse-enter'); + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert invalid interaction input is + // rejected; the "success" test checks several triggers coexisting. + // --------------------------------------------------------------------------- + // addInteraction validates the interaction's structure (schema) but not the + // liveness of a navigate destination nor the format of an open-url string, + // so both of these are accepted rather than rejected. These pin the current + // (lenient) behaviour. + test('navigate-to a removed board is accepted (dangling destination)', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + dest.remove(); + expect(() => + r.addInteraction('click', { type: 'navigate-to', destination: dest }), + ).not.toThrow(); + }); + + test('open-url accepts an arbitrary url string', (ctx) => { + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'open-url', + url: 'not a valid url', + }); + expect(interaction.action.type).toBe('open-url'); + if (interaction.action.type === 'open-url') { + expect(interaction.action.url).toBe('not a valid url'); + } + }); + + test('several triggers on one shape coexist', (ctx) => { + const dest = board(ctx); + const r = rect(ctx); + r.addInteraction('click', { type: 'navigate-to', destination: dest }); + r.addInteraction('mouse-enter', { + type: 'navigate-to', + destination: dest, + }); + expect(r.interactions).toHaveLength(2); + expect(r.interactions.map((i) => i.trigger).sort()).toEqual([ + 'click', + 'mouse-enter', + ]); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/layout.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/layout.test.ts new file mode 100644 index 0000000000..f4f8aacc6e --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/layout.test.ts @@ -0,0 +1,402 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Board } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Layout (flex & grid). +// Layouts are created on boards via addFlexLayout/addGridLayout. Child and cell +// properties are reached through a shape that lives inside the laid-out board. +// Each group keeps its happy-path round-trips together with the related edge +// cases: "throws" tests assert invalid input is rejected (a red test surfaces a +// missing-validation bug) and the remaining ones pin non-trivial valid behaviour. +// Note: track insertion indices are 0-based (addRowAtIndex/removeRow/setRow); +// appendChild cell coordinates and layoutCell.row/column are 1-based. + +function board(ctx: TestContext): Board { + const b = ctx.penpot.createBoard(); + ctx.board.appendChild(b); + return b; +} + +describe('Layout', () => { + describe('Flex', () => { + test('addFlexLayout adds a flex layout to the board', (ctx) => { + const b = board(ctx); + const flex = b.addFlexLayout(); + expect(flex).toBeDefined(); + expect(b.flex).toBeDefined(); + }); + + test('direction and wrap round-trip', (ctx) => { + const flex = board(ctx).addFlexLayout(); + flex.dir = 'column'; + flex.wrap = 'wrap'; + expect(flex.dir).toBe('column'); + expect(flex.wrap).toBe('wrap'); + }); + + test('alignment round-trips', (ctx) => { + const flex = board(ctx).addFlexLayout(); + flex.alignItems = 'center'; + flex.alignContent = 'space-between'; + flex.justifyItems = 'center'; + flex.justifyContent = 'space-around'; + expect(flex.alignItems).toBe('center'); + expect(flex.alignContent).toBe('space-between'); + expect(flex.justifyItems).toBe('center'); + expect(flex.justifyContent).toBe('space-around'); + }); + + test('gaps and padding round-trip', (ctx) => { + const flex = board(ctx).addFlexLayout(); + flex.rowGap = 5; + flex.columnGap = 10; + flex.verticalPadding = 4; + flex.horizontalPadding = 8; + flex.topPadding = 1; + flex.rightPadding = 2; + flex.bottomPadding = 3; + flex.leftPadding = 4; + expect(flex.rowGap).toBeCloseTo(5, 0); + expect(flex.columnGap).toBeCloseTo(10, 0); + expect(flex.topPadding).toBeCloseTo(1, 0); + expect(flex.rightPadding).toBeCloseTo(2, 0); + expect(flex.bottomPadding).toBeCloseTo(3, 0); + expect(flex.leftPadding).toBeCloseTo(4, 0); + }); + + test('sizing round-trips', (ctx) => { + const flex = board(ctx).addFlexLayout(); + flex.horizontalSizing = 'fix'; + flex.verticalSizing = 'auto'; + expect(flex.horizontalSizing).toBe('fix'); + expect(flex.verticalSizing).toBe('auto'); + }); + + test('appendChild adds a child to the flex layout', (ctx) => { + const b = board(ctx); + const flex = b.addFlexLayout(); + const rect = ctx.penpot.createRectangle(); + flex.appendChild(rect); + expect(b.children.length).toBeGreaterThan(0); + }); + + test('remove deletes the flex layout', (ctx) => { + const b = board(ctx); + const flex = b.addFlexLayout(); + flex.remove(); + expect(b.flex).toBeFalsy(); + }); + }); + + describe('Grid', () => { + test('addGridLayout adds a grid layout to the board', (ctx) => { + const b = board(ctx); + const grid = b.addGridLayout(); + expect(grid).toBeDefined(); + expect(b.grid).toBeDefined(); + }); + + test('direction round-trips', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.dir = 'row'; + expect(grid.dir).toBe('row'); + }); + + test('rows and columns can be added and read as tracks', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addColumn('percent', 50); + + expect(grid.rows.length).toBeGreaterThan(0); + expect(grid.columns.length).toBeGreaterThan(0); + expect(grid.rows[0].type).toBe('flex'); + expect(grid.columns[0].type).toBe('percent'); + expect(grid.columns[0].value).toBeCloseTo(50, 0); + }); + + test('addRowAtIndex inserts a row at an index', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addRowAtIndex(0, 'fixed', 100); + expect(grid.rows[0].type).toBe('fixed'); + }); + + test('addColumnAtIndex inserts a column at an index', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addColumn('flex', 1); + grid.addColumnAtIndex(0, 'fixed', 100); + expect(grid.columns[0].type).toBe('fixed'); + }); + + test('setRow and setColumn update tracks', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + grid.setRow(0, 'fixed', 80); + grid.setColumn(0, 'percent', 25); + expect(grid.rows[0].type).toBe('fixed'); + expect(grid.rows[0].value).toBeCloseTo(80, 0); + expect(grid.columns[0].type).toBe('percent'); + }); + + test('removeRow and removeColumn drop tracks', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + grid.addColumn('flex', 1); + const rowsBefore = grid.rows.length; + const colsBefore = grid.columns.length; + grid.removeRow(0); + grid.removeColumn(0); + expect(grid.rows.length).toBe(rowsBefore - 1); + expect(grid.columns.length).toBe(colsBefore - 1); + }); + + test('appendChild places a child into a cell', (ctx) => { + const b = board(ctx); + const grid = b.addGridLayout(); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + const rect = ctx.penpot.createRectangle(); + grid.appendChild(rect, 1, 1); + expect(b.children.length).toBeGreaterThan(0); + }); + + test('alignment and gaps round-trip', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.alignItems = 'center'; + grid.justifyItems = 'start'; + grid.rowGap = 7; + grid.columnGap = 9; + expect(grid.alignItems).toBe('center'); + expect(grid.justifyItems).toBe('start'); + expect(grid.rowGap).toBeCloseTo(7, 0); + expect(grid.columnGap).toBeCloseTo(9, 0); + }); + + // Index boundaries — invalid indices must be rejected. + test('addRowAtIndex with a negative index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + expect(() => grid.addRowAtIndex(-1, 'fixed', 100)).toThrow(); + }); + + test('addRowAtIndex past the end throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + expect(() => grid.addRowAtIndex(5, 'fixed', 100)).toThrow(); + }); + + test('addColumnAtIndex with a negative index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addColumn('flex', 1); + expect(() => grid.addColumnAtIndex(-1, 'fixed', 100)).toThrow(); + }); + + test('removeRow on an empty grid throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + expect(() => grid.removeRow(0)).toThrow(); + }); + + test('removeRow with an out-of-range index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + expect(() => grid.removeRow(5)).toThrow(); + }); + + test('removeColumn with an out-of-range index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addColumn('flex', 1); + expect(() => grid.removeColumn(5)).toThrow(); + }); + + test('setRow with an out-of-range index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + expect(() => grid.setRow(5, 'fixed', 80)).toThrow(); + }); + + test('setColumn with an out-of-range index throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addColumn('flex', 1); + expect(() => grid.setColumn(5, 'fixed', 80)).toThrow(); + }); + + // Track type — invalid track types must be rejected. + test('addRow with an invalid track type throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + expect(() => grid.addRow('not-a-type' as unknown as 'flex', 1)).toThrow(); + }); + + test('addColumn with an invalid track type throws', (ctx) => { + const grid = board(ctx).addGridLayout(); + expect(() => + grid.addColumn('not-a-type' as unknown as 'flex', 1), + ).toThrow(); + }); + + // Success edges — non-trivial valid behaviour. + test('addRowAtIndex inserts at the position and shifts the rest', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('fixed', 10); + grid.addRow('percent', 20); + grid.addRowAtIndex(1, 'flex', 1); + expect(grid.rows.length).toBe(3); + expect(grid.rows[0].type).toBe('fixed'); + expect(grid.rows[1].type).toBe('flex'); + expect(grid.rows[2].type).toBe('percent'); + }); + + test('setRow updates a track in place without changing the count', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addRow('flex', 1); + grid.addRow('flex', 1); + grid.setRow(1, 'fixed', 80); + expect(grid.rows.length).toBe(3); + expect(grid.rows[0].type).toBe('flex'); + expect(grid.rows[1].type).toBe('fixed'); + expect(grid.rows[1].value).toBeCloseTo(80, 0); + expect(grid.rows[2].type).toBe('flex'); + }); + + test('mixed track types coexist and read back', (ctx) => { + const grid = board(ctx).addGridLayout(); + grid.addRow('flex', 1); + grid.addRow('fixed', 50); + grid.addRow('percent', 25); + grid.addRow('auto'); + expect(grid.rows.map((r) => r.type)).toEqual([ + 'flex', + 'fixed', + 'percent', + 'auto', + ]); + }); + + test('appendChild places children into the cells requested', (ctx) => { + const b = board(ctx); + const grid = b.addGridLayout(); + grid.addRow('flex', 1); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + grid.addColumn('flex', 1); + + const a = ctx.penpot.createRectangle(); + const c = ctx.penpot.createRectangle(); + grid.appendChild(a, 1, 1); + grid.appendChild(c, 2, 2); + + const cellA = a.layoutCell; + const cellC = c.layoutCell; + expect(cellA).toBeDefined(); + expect(cellC).toBeDefined(); + if (cellA && cellC) { + expect(cellA.row).toBeCloseTo(1, 0); + expect(cellA.column).toBeCloseTo(1, 0); + expect(cellC.row).toBeCloseTo(2, 0); + expect(cellC.column).toBeCloseTo(2, 0); + } + }); + + test('a grid board can nest a flex board as a child', (ctx) => { + const outer = board(ctx); + const grid = outer.addGridLayout(); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + + const inner = ctx.penpot.createBoard(); + const flex = inner.addFlexLayout(); + flex.dir = 'column'; + grid.appendChild(inner, 1, 1); + + expect(outer.children.length).toBeGreaterThan(0); + expect(inner.flex).toBeDefined(); + expect(inner.flex && inner.flex.dir).toBe('column'); + }); + }); + + describe('Child', () => { + test('layout child properties round-trip', (ctx) => { + const b = board(ctx); + const flex = b.addFlexLayout(); + const rect = ctx.penpot.createRectangle(); + flex.appendChild(rect); + + const child = rect.layoutChild; + expect(child).toBeDefined(); + if (child) { + child.absolute = true; + child.zIndex = 3; + child.horizontalSizing = 'fill'; + child.verticalSizing = 'fix'; + child.alignSelf = 'center'; + child.horizontalMargin = 2; + child.verticalMargin = 4; + child.topMargin = 1; + child.rightMargin = 2; + child.bottomMargin = 3; + child.leftMargin = 4; + child.maxWidth = 200; + child.maxHeight = 150; + child.minWidth = 10; + child.minHeight = 20; + + expect(child.absolute).toBe(true); + expect(child.zIndex).toBeCloseTo(3, 0); + expect(child.horizontalSizing).toBe('fill'); + expect(child.verticalSizing).toBe('fix'); + expect(child.alignSelf).toBe('center'); + expect(child.topMargin).toBeCloseTo(1, 0); + expect(child.maxWidth).toBeCloseTo(200, 0); + expect(child.minHeight).toBeCloseTo(20, 0); + } + }); + }); + + describe('Cell', () => { + test('layout cell properties round-trip', (ctx) => { + const b = board(ctx); + const grid = b.addGridLayout(); + grid.addRow('flex', 1); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + grid.addColumn('flex', 1); + const rect = ctx.penpot.createRectangle(); + grid.appendChild(rect, 1, 1); + + const cell = rect.layoutCell; + expect(cell).toBeDefined(); + if (cell) { + cell.row = 1; + cell.column = 1; + cell.rowSpan = 1; + cell.columnSpan = 2; + expect(cell.row).toBeCloseTo(1, 0); + expect(cell.column).toBeCloseTo(1, 0); + expect(cell.columnSpan).toBeCloseTo(2, 0); + } + }); + }); + + describe('Switching type', () => { + // addFlexLayout/addGridLayout do not reject a board that already has a + // layout; they create the requested layout (switching the board's type). + // These pin that behaviour. + test('adding a grid layout to a board that already has a flex layout switches to grid', (ctx) => { + const b = board(ctx); + b.addFlexLayout(); + expect(() => b.addGridLayout()).not.toThrow(); + expect(b.grid).toBeDefined(); + }); + + test('adding a flex layout to a board that already has a grid layout switches to flex', (ctx) => { + const b = board(ctx); + b.addGridLayout(); + expect(() => b.addFlexLayout()).not.toThrow(); + expect(b.flex).toBeDefined(); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/library.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/library.test.ts new file mode 100644 index 0000000000..f28e3263b2 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/library.test.ts @@ -0,0 +1,223 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Text } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; +import { PNG_1X1 } from './fixtures'; + +// Library colors, typographies and components. +// Assets are created in the local library (self-provisioned). Reached through +// `ctx.penpot.library.local` so the Library chain is recorded for coverage. + +function text(ctx: TestContext, value = 'Hello Penpot'): Text { + const t = ctx.penpot.createText(value); + if (!t) throw new Error('createText returned null'); + ctx.board.appendChild(t); + return t; +} + +describe('Library', () => { + test('local library exposes id and name', (ctx) => { + const lib = ctx.penpot.library.local; + expect(typeof lib.id).toBe('string'); + expect(typeof lib.name).toBe('string'); + }); + + test('local library lists its assets', (ctx) => { + const lib = ctx.penpot.library.local; + expect(Array.isArray(lib.colors)).toBe(true); + expect(Array.isArray(lib.typographies)).toBe(true); + expect(Array.isArray(lib.components)).toBe(true); + expect(lib.tokens).toBeDefined(); + }); + + test('library context exposes connected libraries', (ctx) => { + expect(Array.isArray(ctx.penpot.library.connected)).toBe(true); + }); + + test('library elements expose a libraryId', (ctx) => { + const color = ctx.penpot.library.local.createColor(); + expect(typeof color.libraryId).toBe('string'); + }); + + // Skipped under MOCK_BACKEND: availableLibraries() returns backend-shaped + // shared-library summaries; under a mock it would resolve vacuously. + test.skipIfMocked('availableLibraries resolves to summaries', async (ctx) => { + // The shared-libraries RPC can error in the headless team context; treat a + // rejection as an environment limitation. + const summaries = await ctx.penpot.library + .availableLibraries() + .catch(() => []); + expect(Array.isArray(summaries)).toBe(true); + if (summaries.length > 0) { + const summary = summaries[0]; + expect(typeof summary.id).toBe('string'); + expect(typeof summary.name).toBe('string'); + expect(typeof summary.numColors).toBe('number'); + expect(typeof summary.numComponents).toBe('number'); + expect(typeof summary.numTypographies).toBe('number'); + } + }); + + // NOTE: connectLibrary with an unknown id is intentionally NOT exercised here. + // Calling it with a non-existent library id crashes the plugin workspace (the + // returned promise never settles and the sandbox freezes), which would hang + // the whole CI run. This is a genuine API bug to fix at the source; until then + // the suite must not trigger it. + + describe('Colors', () => { + test('createColor adds a color asset', (ctx) => { + const color = ctx.penpot.library.local.createColor(); + color.name = 'plugin-color'; + // Use a single-segment path: Penpot normalizes `a/b` to `a / b`. + color.path = 'plugingroup'; + color.color = '#ff8800'; + color.opacity = 0.8; + + expect(typeof color.id).toBe('string'); + expect(color.name).toBe('plugin-color'); + expect(color.path).toBe('plugingroup'); + expect(color.color).toBe('#ff8800'); + expect(color.opacity).toBeCloseTo(0.8, 2); + }); + + test('library color converts to fill and stroke', (ctx) => { + const color = ctx.penpot.library.local.createColor(); + color.color = '#123456'; + color.opacity = 1; + + const fill = color.asFill(); + expect(fill.fillColor).toBe('#123456'); + const stroke = color.asStroke(); + expect(stroke.strokeColor).toBe('#123456'); + }); + + test('library color gradient round-trips', (ctx) => { + const color = ctx.penpot.library.local.createColor(); + color.gradient = { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }; + + const g = color.gradient; + expect(g).toBeDefined(); + if (g) { + expect(g.type).toBe('linear'); + expect(g.stops).toHaveLength(2); + } + }); + + // Skipped under MOCK_BACKEND: uploadMediaData needs real backend media + // processing (ImageMagick); a mock can't return usable image data. + test.skipIfMocked('library color image round-trips', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'lib-color-image', + PNG_1X1, + 'image/png', + ); + const color = ctx.penpot.library.local.createColor(); + color.image = image; + expect(color.image).toBeDefined(); + }); + }); + + describe('Typographies', () => { + test('createTypography adds a typography asset', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + typo.name = 'plugin-typo'; + typo.path = 'text'; + typo.fontSize = '18'; + typo.lineHeight = '1.4'; + typo.letterSpacing = '0.5'; + + expect(typeof typo.id).toBe('string'); + expect(typo.name).toBe('plugin-typo'); + expect(typo.fontSize).toBe('18'); + expect(typeof typo.fontId).toBe('string'); + }); + + test('typography fontFamily and fontId round-trip', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + expect(typeof typo.fontFamily).toBe('string'); + + typo.fontFamily = 'Arial'; + typo.fontId = 'gfont-arial'; + expect(typo.fontFamily).toBe('Arial'); + expect(typo.fontId).toBe('gfont-arial'); + }); + + test('typography style members round-trip', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + typo.fontStyle = 'italic'; + typo.textTransform = 'uppercase'; + typo.fontWeight = '700'; + typo.fontVariantId = 'regular'; + typo.lineHeight = '1.5'; + typo.letterSpacing = '1'; + expect(typo.fontStyle).toBe('italic'); + expect(typo.textTransform).toBe('uppercase'); + expect(typo.fontWeight).toBe('700'); + expect(typo.fontVariantId).toBe('regular'); + expect(typeof typo.lineHeight).toBe('string'); + expect(typeof typo.letterSpacing).toBe('string'); + }); + + test('typography setFont updates the font', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + const font = ctx.penpot.fonts.all[0]; + typo.setFont(font); + expect(typo.fontId).toBe(font.fontId); + }); + + test('typography applies to a text shape', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + typo.fontSize = '22'; + const t = text(ctx); + typo.applyToText(t); + expect(t.fontSize).toBe('22'); + }); + + test('typography applies to a text range', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + typo.fontSize = '28'; + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + typo.applyToTextRange(range); + expect(range.fontSize).toBe('28'); + }); + }); + + describe('Components', () => { + test('createComponent creates a component asset', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + + expect(typeof comp.id).toBe('string'); + comp.name = 'plugin-component'; + expect(comp.name).toBe('plugin-component'); + expect(comp.isVariant()).toBe(false); + }); + + test('component instance and mainInstance return shapes', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + + const main = comp.mainInstance(); + expect(main).toBeDefined(); + expect(typeof main.id).toBe('string'); + + const instance = comp.instance(); + expect(instance).toBeDefined(); + expect(typeof instance.id).toBe('string'); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts new file mode 100644 index 0000000000..c05ba336e2 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/media.test.ts @@ -0,0 +1,145 @@ +import { expect, expectReject } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import { PNG_1X1 } from './fixtures'; + +// Media uploads and exports. + +// Skipped under MOCK_BACKEND: media upload exercises real ImageMagick on the +// backend (image validation / canned upload data) that a 200 mock can't +// reproduce — the rejection tests would fail and the success tests go vacuous. +describe.skipIfMocked('Media', () => { + test('uploadMediaData uploads bytes and returns image data', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'plugin-image', + PNG_1X1, + 'image/png', + ); + expect(typeof image.id).toBe('string'); + expect(image.width).toBe(1); + expect(image.height).toBe(1); + expect(image.mtype).toBe('image/png'); + expect(typeof image.name).toBe('string'); + // keepAspectRatio is optional and may be null when not set. + expect( + image.keepAspectRatio == null || + typeof image.keepAspectRatio === 'boolean', + ).toBe(true); + + const bytes = await image.data(); + expect(bytes.length).toBeGreaterThan(0); + }); + + test('an uploaded image can be used as a fill', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'plugin-fill', + PNG_1X1, + 'image/png', + ); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.fills = [{ fillOpacity: 1, fillImage: image }]; + + const fills = rect.fills; + if (Array.isArray(fills)) { + expect(fills[0].fillImage).toBeDefined(); + } + }); + + test('Fill.fillImage can be set on a fill', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'plugin-fill-set', + PNG_1X1, + 'image/png', + ); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + + // Set fillImage directly on the fill (covers Fill.fillImage (set)). + const fill = rect.fills[0]; + fill.fillImage = image; + expect(fill.fillImage).toBeDefined(); + }); + + test('uploadMediaUrl resolves to image data', async (ctx) => { + // Needs the backend to fetch an external URL, which may be unavailable in + // the headless runner; treat a rejection as an environment limitation. + const image = await ctx.penpot + .uploadMediaUrl( + 'plugin-url-image', + 'https://design.penpot.app/images/favicon.png', + ) + .catch(() => null); + if (image) { + expect(typeof image.id).toBe('string'); + } + }); + + // --------------------------------------------------------------------------- + // Edge cases. Invalid upload input must not resolve. (These hold + // even when the backend is unreachable in the headless runner, since a + // rejection is the asserted outcome.) + // --------------------------------------------------------------------------- + test('uploadMediaData with empty bytes rejects', async (ctx) => { + await expectReject(() => + ctx.penpot.uploadMediaData('empty', new Uint8Array([]), 'image/png'), + ); + }); + + test('uploadMediaData with non-image bytes rejects', async (ctx) => { + const garbage = new Uint8Array([1, 2, 3, 4, 5]); + await expectReject(() => + ctx.penpot.uploadMediaData('garbage', garbage, 'image/png'), + ); + }); + + test('uploadMediaUrl with an invalid URL rejects', async (ctx) => { + await expectReject(() => + ctx.penpot.uploadMediaUrl('bad-url', 'not://a.valid/url'), + ); + }); +}); + +describe('Exports', () => { + test('export settings round-trip on a shape', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.exports = [ + { type: 'png', scale: 2, suffix: '@2x', skipChildren: false }, + ]; + + expect(rect.exports).toHaveLength(1); + const exp = rect.exports[0]; + expect(exp.type).toBe('png'); + expect(exp.scale).toBeCloseTo(2, 0); + expect(exp.suffix).toBe('@2x'); + // skipChildren is optional; a stored `false` reads back as undefined. + expect(exp.skipChildren).toBeFalsy(); + }); + + test('export settings accept jpeg, webp, svg and pdf types', (ctx) => { + // Only png is exercised above; pin that the other export formats round-trip + // as settings (the actual render is covered separately and may be headless- + // limited). + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + for (const type of ['jpeg', 'webp', 'svg', 'pdf'] as const) { + rect.exports = [{ type, scale: 1 }]; + expect(rect.exports).toHaveLength(1); + expect(rect.exports[0].type).toBe(type); + } + }); + + test('shape export renders to bytes', async (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.resize(20, 20); + // Rendering may not be available in the headless runner; tolerate failure. + const bytes = await rect + .export({ type: 'png', scale: 1 }) + .catch(() => null); + if (bytes) { + expect(bytes.length).toBeGreaterThan(0); + } + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/misc.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/misc.test.ts new file mode 100644 index 0000000000..cabb0869b4 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/misc.test.ts @@ -0,0 +1,389 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Board } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Misc — remaining coverable members across many interfaces. + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +// Note: penpot.utils.types / geometry are frozen (SES) data properties, so the +// recorder cannot wrap them and their members aren't recorded (see README.md +// coverage notes). The predicates are still exercised behaviourally in +// platform.test.ts. + +describe('Misc', () => { + describe('Context root', () => { + test('root is a shape', (ctx) => { + expect(ctx.penpot.root).toBeDefined(); + }); + }); + + describe('Concrete shape fills', () => { + test('fills round-trip on ellipse, path and board', (ctx) => { + const ellipse = ctx.penpot.createEllipse(); + const pathShape = ctx.penpot.createPath(); + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(ellipse); + ctx.board.appendChild(pathShape); + ctx.board.appendChild(board); + + ellipse.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + pathShape.fills = [{ fillColor: '#00ff00', fillOpacity: 1 }]; + board.fills = [{ fillColor: '#0000ff', fillOpacity: 1 }]; + + expect(ellipse.fills).toHaveLength(1); + expect(pathShape.fills).toHaveLength(1); + expect(board.fills).toHaveLength(1); + }); + }); + + describe('Boolean members', () => { + test('boolean content, path data and children round-trip', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + b.x = 40; + const bool = ctx.penpot.createBoolean('union', [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + // Boolean fills round-trip; d/content/commands are derived from the + // operands and not independently settable (see coverage notes). + bool.fills = [{ fillColor: '#abcdef', fillOpacity: 1 }]; + void bool.content; + expect(bool.fills).toHaveLength(1); + } + }); + + test('appendChild and insertChild add operands to a boolean', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + b.x = 40; + const bool = ctx.penpot.createBoolean('union', [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + const before = bool.children.length; + bool.appendChild(rect(ctx)); + bool.insertChild(0, rect(ctx)); + expect(bool.children.length).toBe(before + 2); + } + }); + }); + + describe('Export settings setters', () => { + test('export members round-trip on the returned export', (ctx) => { + const r = rect(ctx); + r.exports = [{ type: 'png', scale: 1, suffix: '', skipChildren: false }]; + const exp = r.exports[0]; + exp.type = 'jpeg'; + exp.scale = 2; + exp.suffix = '@2x'; + exp.skipChildren = true; + expect(exp.type).toBe('jpeg'); + expect(exp.scale).toBeCloseTo(2, 0); + }); + }); + + describe('Gradient and shadow leftovers', () => { + test('gradient endpoints and stops round-trip', (ctx) => { + const r = rect(ctx); + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }, + }, + ]; + const fills = r.fills; + if (Array.isArray(fills)) { + const g = fills[0].fillColorGradient; + if (g) { + void g.endX; + void g.startY; + g.stops = [ + { color: '#00ff00', opacity: 1, offset: 0 }, + { color: '#000000', opacity: 1, offset: 1 }, + ]; + expect(g.stops.length).toBeGreaterThan(0); + } + } + }); + + test('shadow color and id round-trip', (ctx) => { + const r = rect(ctx); + r.shadows = [ + { + style: 'drop-shadow', + offsetX: 1, + offsetY: 1, + blur: 2, + spread: 0, + hidden: false, + color: { color: '#000000', opacity: 1 }, + }, + ]; + const shadow = r.shadows[0]; + void shadow.id; + shadow.color = { color: '#ff00ff', opacity: 0.5 }; + const color = shadow.color; + if (color) { + void color.id; + void color.fileId; + void color.refId; + void color.refFile; + color.gradient = { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [{ color: '#ff0000', opacity: 1, offset: 0 }], + }; + void color.gradient; + } + expect(r.shadows).toHaveLength(1); + }); + }); + + describe('Bounds and Point', () => { + test('viewport bounds members are readable', (ctx) => { + // The bounds object is frozen, so only the getters are exercised. + const b = ctx.penpot.viewport.bounds; + expect(typeof b.x).toBe('number'); + expect(typeof b.y).toBe('number'); + expect(typeof b.width).toBe('number'); + expect(typeof b.height).toBe('number'); + }); + + test('viewport center point members are readable', (ctx) => { + const c = ctx.penpot.viewport.center; + expect(typeof c.x).toBe('number'); + expect(typeof c.y).toBe('number'); + }); + }); + + describe('Layout leftovers', () => { + test('flex padding and child margins are readable', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const flex = board.addFlexLayout(); + flex.horizontalPadding = 4; + flex.verticalPadding = 6; + void flex.horizontalPadding; + void flex.verticalPadding; + + const child = ctx.penpot.createRectangle(); + flex.appendChild(child); + const lc = child.layoutChild; + if (lc) { + lc.horizontalMargin = 1; + lc.verticalMargin = 2; + lc.topMargin = 3; + lc.rightMargin = 4; + lc.bottomMargin = 5; + lc.leftMargin = 6; + lc.maxHeight = 100; + lc.minWidth = 10; + void lc.horizontalMargin; + void lc.verticalMargin; + void lc.leftMargin; + void lc.rightMargin; + void lc.bottomMargin; + void lc.maxHeight; + void lc.minWidth; + } + expect(board.type).toBe('board'); + }); + + test('grid cell properties round-trip', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const grid = board.addGridLayout(); + grid.addRow('flex', 1); + grid.addColumn('flex', 1); + const child = ctx.penpot.createRectangle(); + grid.appendChild(child, 1, 1); + const cell = child.layoutCell; + if (cell) { + cell.areaName = 'header'; + cell.position = 'auto'; + void cell.areaName; + void cell.position; + void cell.rowSpan; + } + expect(board.type).toBe('board'); + }); + }); + + describe('Track', () => { + test('grid track members round-trip on the returned track', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const grid = board.addGridLayout(); + grid.addRow('flex', 1); + const track = grid.rows[0]; + track.type = 'fixed'; + track.value = 80; + expect(track.type).toBe('fixed'); + expect(track.value).toBeCloseTo(80, 0); + }); + }); + + describe('Path commands', () => { + test('path command members round-trip', (ctx) => { + const path = ctx.penpot.createPath(); + ctx.board.appendChild(path); + path.d = 'M0 0 L10 10'; + const commands = path.commands; + expect(commands.length).toBeGreaterThan(0); + const cmd = commands[0]; + void cmd.command; + void cmd.params; + cmd.command = 'line-to'; + cmd.params = { x: 5, y: 5 }; + expect(cmd.command).toBe('line-to'); + // Reassign the whole command list (Path.commands set). + path.commands = commands; + }); + }); + + describe('Shape ordering and blur', () => { + test('sendBackward and backgroundBlur are exercised', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + void b; + a.sendBackward(); + void a.backgroundBlur; + expect(a.type).toBe('rectangle'); + }); + }); + + describe('Interaction reads', () => { + test('overlay action fields are readable', (ctx) => { + const overlay = ctx.penpot.createBoard(); + ctx.board.appendChild(overlay as Board); + const relative = rect(ctx); + const r = rect(ctx); + const interaction = r.addInteraction('click', { + type: 'open-overlay', + destination: overlay, + relativeTo: relative, + position: 'manual', + manualPositionLocation: { x: 5, y: 5 }, + animation: { type: 'dissolve', duration: 100, easing: 'linear' }, + }); + if (interaction.action.type === 'open-overlay') { + void interaction.action.relativeTo; + void interaction.action.manualPositionLocation; + void interaction.action.animation; + } + // Interaction.action and delay setters (records the (set) targets; + // persistence is asserted in interactions.test.ts). + interaction.delay = 250; + interaction.action = { type: 'previous-screen' }; + expect(interaction.shape && interaction.shape.id).toBe(r.id); + }); + + test('navigate-to preserveScrollPosition and slide/push animation fields', (ctx) => { + const dest = ctx.penpot.createBoard(); + ctx.board.appendChild(dest as Board); + const r = rect(ctx); + const nav = r.addInteraction('click', { + type: 'navigate-to', + destination: dest, + preserveScrollPosition: true, + animation: { + type: 'slide', + way: 'in', + direction: 'right', + duration: 300, + offsetEffect: true, + easing: 'ease', + }, + }); + if (nav.action.type === 'navigate-to') { + void nav.action.preserveScrollPosition; + const anim = nav.action.animation; + if (anim && anim.type === 'slide') { + void anim.offsetEffect; + void anim.easing; + } + } + + const r2 = rect(ctx); + const push = r2.addInteraction('click', { + type: 'navigate-to', + destination: dest, + animation: { + type: 'push', + direction: 'left', + duration: 300, + easing: 'ease', + }, + }); + if (push.action.type === 'navigate-to') { + const anim = push.action.animation; + if (anim && anim.type === 'push') { + void anim.easing; + } + } + expect(r.type).toBe('rectangle'); + }); + }); + + describe('Variant container variants', () => { + test('Variants interface members via a variant container', async (ctx) => { + function main(): Board { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return ctx.penpot.library.local + .createComponent([r]) + .mainInstance() as Board; + } + const container = ctx.penpot.createVariantFromComponents([ + main(), + main(), + ]); + await sleep(300); + const v = container.variants; + expect(v).not.toBeNull(); + if (v) { + expect(typeof v.id).toBe('string'); + expect(typeof v.libraryId).toBe('string'); + expect(Array.isArray(v.properties)).toBe(true); + expect(Array.isArray(v.variantComponents())).toBe(true); + if (v.properties.length > 0) { + void v.currentValues(v.properties[0]); + } + v.addProperty(); + await sleep(300); + v.addVariant(); + await sleep(300); + if (v.properties.length > 0) { + v.renameProperty(0, 'Size'); + await sleep(200); + v.removeProperty(v.properties.length - 1); + } + } + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/pages.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/pages.test.ts new file mode 100644 index 0000000000..1cd3463d7f --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/pages.test.ts @@ -0,0 +1,162 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Pages, selection and flows. +// Most assertions use the active page (`currentPage`) and the scratch board so +// the user's file is left clean. createPage/openPage necessarily leave a page +// behind (the API has no removePage), so the active page is restored afterwards. + +describe('Pages', () => { + test('currentPage exposes id and name', (ctx) => { + const page = ctx.penpot.currentPage; + expect(page).not.toBeNull(); + if (page) { + expect(typeof page.id).toBe('string'); + expect(typeof page.name).toBe('string'); + } + }); + + test('createPage and openPage activate a new page', async (ctx) => { + const original = ctx.penpot.currentPage; + const page = ctx.penpot.createPage(); + page.name = 'plugin-test-page'; + expect(page.name).toBe('plugin-test-page'); + + await ctx.penpot.openPage(page); + const active = ctx.penpot.currentPage; + expect(active && active.id).toBe(page.id); + + // Restore the originally active page so other tests aren't affected. + if (original) await ctx.penpot.openPage(original); + }); + + test('getShapeById finds a shape on the page', (ctx) => { + const page = ctx.penpot.currentPage; + expect(page).not.toBeNull(); + if (page) { + const found = page.getShapeById(ctx.board.id); + expect(found).not.toBeNull(); + expect(found && found.id).toBe(ctx.board.id); + } + }); + + test('findShapes returns shapes on the page', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const page = ctx.penpot.currentPage; + if (page) { + expect(page.findShapes().length).toBeGreaterThan(0); + } + }); + + test('page root is a shape', (ctx) => { + const page = ctx.penpot.currentPage; + if (page) { + expect(page.root).toBeDefined(); + expect(typeof page.root.type).toBe('string'); + } + }); + + // Edge cases. + test('getShapeById of an unknown id returns null', (ctx) => { + const page = ctx.penpot.currentPage; + expect(page).not.toBeNull(); + if (page) { + const found = page.getShapeById('00000000-0000-0000-0000-0000000000ff'); + expect(found).toBeNull(); + } + }); + + test('getShapeById finds a just-created shape', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const page = ctx.penpot.currentPage; + if (page) { + const found = page.getShapeById(rect.id); + expect(found).not.toBeNull(); + expect(found && found.id).toBe(rect.id); + } + }); +}); + +describe('Selection', () => { + test('selection can be set and read', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + ctx.penpot.selection = [rect]; + expect(ctx.penpot.selection).toHaveLength(1); + expect(ctx.penpot.selection[0].id).toBe(rect.id); + }); + + // Edge cases. + test('assigning an empty selection clears it', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + ctx.penpot.selection = [rect]; + ctx.penpot.selection = []; + expect(ctx.penpot.selection).toHaveLength(0); + }); + + test('selecting the same shape twice keeps a single entry', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + ctx.penpot.selection = [rect, rect]; + expect(ctx.penpot.selection).toHaveLength(1); + }); +}); + +describe('Flows', () => { + test('createFlow defines a flow on a board', (ctx) => { + const targetBoard = ctx.penpot.createBoard(); + ctx.board.appendChild(targetBoard); + const page = ctx.penpot.currentPage; + expect(page).not.toBeNull(); + if (page) { + const flow = page.createFlow('plugin-flow', targetBoard); + expect(flow.name).toBe('plugin-flow'); + expect(flow.startingBoard.id).toBe(targetBoard.id); + expect(flow.page.id).toBe(page.id); + expect(page.flows.length).toBeGreaterThan(0); + } + }); + + test('flow name and starting board round-trip', (ctx) => { + const first = ctx.penpot.createBoard(); + const second = ctx.penpot.createBoard(); + ctx.board.appendChild(first); + ctx.board.appendChild(second); + const page = ctx.penpot.currentPage; + if (page) { + const flow = page.createFlow('flow-a', first); + flow.name = 'flow-b'; + flow.startingBoard = second; + expect(flow.name).toBe('flow-b'); + expect(flow.startingBoard.id).toBe(second.id); + } + }); + + test('flow can be removed', (ctx) => { + const targetBoard = ctx.penpot.createBoard(); + ctx.board.appendChild(targetBoard); + const page = ctx.penpot.currentPage; + if (page) { + const flow = page.createFlow('to-remove', targetBoard); + const before = page.flows.length; + flow.remove(); + expect(page.flows.length).toBe(before - 1); + } + }); + + test('page.removeFlow removes a flow', (ctx) => { + const targetBoard = ctx.penpot.createBoard(); + ctx.board.appendChild(targetBoard); + const page = ctx.penpot.currentPage; + if (page) { + const flow = page.createFlow('to-remove-2', targetBoard); + const before = page.flows.length; + page.removeFlow(flow); + expect(page.flows.length).toBe(before - 1); + } + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/platform.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/platform.test.ts new file mode 100644 index 0000000000..ba318d0599 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/platform.test.ts @@ -0,0 +1,164 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Platform: user/session, context info, history, utils and markup. + +describe('Platform', () => { + describe('User', () => { + test('currentUser exposes profile fields', (ctx) => { + const user = ctx.penpot.currentUser; + expect(typeof user.id).toBe('string'); + expect(typeof user.name).toBe('string'); + expect(typeof user.sessionId).toBe('string'); + // avatarUrl and color may be undefined depending on the profile. + void user.avatarUrl; + void user.color; + }); + + test('activeUsers is an array', (ctx) => { + const users = ctx.penpot.activeUsers; + expect(Array.isArray(users)).toBe(true); + if (users.length > 0) { + expect(typeof users[0].id).toBe('string'); + void users[0].position; + void users[0].zoom; + } + }); + }); + + describe('Context info', () => { + test('version is a string', (ctx) => { + expect(typeof ctx.penpot.version).toBe('string'); + }); + + test('theme is light or dark', (ctx) => { + expect(['light', 'dark']).toContain(ctx.penpot.theme); + }); + + test('flags are readable', (ctx) => { + expect(typeof ctx.penpot.flags.naturalChildOrdering).toBe('boolean'); + expect(typeof ctx.penpot.flags.throwValidationErrors).toBe('boolean'); + }); + }); + + describe('History', () => { + test('undo block begin and finish wrap operations', (ctx) => { + const block = ctx.penpot.history.undoBlockBegin(); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.name = 'in-undo-block'; + ctx.penpot.history.undoBlockFinish(block); + expect(rect.name).toBe('in-undo-block'); + }); + + // Edge cases. + test('finishing an unknown undo block is a no-op (not rejected)', (ctx) => { + // undoBlockFinish does not validate the block id; an unknown id is ignored + // rather than rejected. + expect(() => + ctx.penpot.history.undoBlockFinish(Symbol('not-a-real-block')), + ).not.toThrow(); + }); + + test('nested undo blocks begin and finish in order', (ctx) => { + const outer = ctx.penpot.history.undoBlockBegin(); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const inner = ctx.penpot.history.undoBlockBegin(); + rect.name = 'nested'; + ctx.penpot.history.undoBlockFinish(inner); + ctx.penpot.history.undoBlockFinish(outer); + expect(rect.name).toBe('nested'); + }); + }); + + describe('Utils', () => { + test('geometry center returns a point', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.x = 0; + rect.y = 0; + rect.resize(100, 100); + const center = ctx.penpot.utils.geometry.center([rect]); + expect(center).not.toBeNull(); + if (center) { + expect(center.x).toBeCloseTo(50, 0); + expect(center.y).toBeCloseTo(50, 0); + } + }); + + // Edge cases. + test('center of an empty array returns null', (ctx) => { + expect(ctx.penpot.utils.geometry.center([])).toBeNull(); + }); + + test('center of two shapes sits at their midpoint', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + a.x = 0; + a.y = 0; + a.resize(100, 100); + b.x = 200; + b.y = 100; + b.resize(100, 100); + const center = ctx.penpot.utils.geometry.center([a, b]); + expect(center).not.toBeNull(); + if (center) { + // a spans 0..100, b spans 200..300 → combined bounds 0..300 → centre 150. + expect(center.x).toBeCloseTo(150, 0); + expect(center.y).toBeCloseTo(100, 0); + } + }); + + test('types predicates identify shapes', (ctx) => { + const types = ctx.penpot.utils.types; + const rect = ctx.penpot.createRectangle(); + const ellipse = ctx.penpot.createEllipse(); + const text = ctx.penpot.createText('hi'); + const path = ctx.penpot.createPath(); + ctx.board.appendChild(rect); + ctx.board.appendChild(ellipse); + ctx.board.appendChild(path); + if (text) ctx.board.appendChild(text); + + expect(types.isRectangle(rect)).toBe(true); + expect(types.isEllipse(ellipse)).toBe(true); + expect(types.isPath(path)).toBe(true); + expect(types.isBoard(ctx.board)).toBe(true); + if (text) { + expect(types.isText(text)).toBe(true); + } + // Non-matching predicates should be falsy. + expect(types.isGroup(rect)).toBeFalsy(); + expect(types.isBool(rect)).toBeFalsy(); + expect(types.isMask(rect)).toBeFalsy(); + expect(types.isSVG(rect)).toBeFalsy(); + expect(types.isVariantContainer(rect)).toBeFalsy(); + }); + }); + + describe('Markup', () => { + test('generateMarkup returns html', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const markup = ctx.penpot.generateMarkup([rect]); + expect(typeof markup).toBe('string'); + }); + + test('generateMarkup can target svg', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const svg = ctx.penpot.generateMarkup([rect], { type: 'svg' }); + expect(typeof svg).toBe('string'); + }); + + test('generateStyle returns css', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const styles = ctx.penpot.generateStyle([rect]); + expect(typeof styles).toBe('string'); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/plugin-data.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/plugin-data.test.ts new file mode 100644 index 0000000000..e5c02be79b --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/plugin-data.test.ts @@ -0,0 +1,108 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Plugin data and local storage. + +describe('Plugin data', () => { + test('plugin data round-trips on a shape', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.setPluginData('exampleKey', 'exampleValue'); + expect(rect.getPluginData('exampleKey')).toBe('exampleValue'); + expect(rect.getPluginDataKeys()).toContain('exampleKey'); + }); + + test('shared plugin data round-trips on a shape', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.setSharedPluginData('ns', 'sharedKey', 'sharedValue'); + expect(rect.getSharedPluginData('ns', 'sharedKey')).toBe('sharedValue'); + expect(rect.getSharedPluginDataKeys('ns')).toContain('sharedKey'); + }); + + test('plugin data round-trips on the file', (ctx) => { + const file = ctx.penpot.currentFile; + expect(file).not.toBeNull(); + if (file) { + file.setPluginData('fileKey', 'fileValue'); + expect(file.getPluginData('fileKey')).toBe('fileValue'); + } + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert invalid keys/values are + // rejected; "success" tests cover multi-key listing, overwrite, large values, + // missing keys and local/shared isolation. + // --------------------------------------------------------------------------- + test('setPluginData with an empty key is accepted (currently unvalidated)', (ctx) => { + // An empty key is not rejected; this pins the current lenient behaviour + // (a candidate for future hardening). + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(() => rect.setPluginData('', 'value')).not.toThrow(); + }); + + test('setPluginData with a non-string value throws', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(() => rect.setPluginData('key', 123 as unknown as string)).toThrow(); + }); + + test('multiple keys round-trip and are all listed', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.setPluginData('a', '1'); + rect.setPluginData('b', '2'); + rect.setPluginData('c', '3'); + expect(rect.getPluginData('a')).toBe('1'); + expect(rect.getPluginData('b')).toBe('2'); + expect(rect.getPluginData('c')).toBe('3'); + const keys = rect.getPluginDataKeys(); + expect(keys).toContain('a'); + expect(keys).toContain('b'); + expect(keys).toContain('c'); + }); + + test('overwriting a key replaces its value', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.setPluginData('k', 'first'); + rect.setPluginData('k', 'second'); + expect(rect.getPluginData('k')).toBe('second'); + }); + + test('a large value round-trips', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const big = 'x'.repeat(10000); + rect.setPluginData('big', big); + expect(rect.getPluginData('big')).toBe(big); + }); + + test('reading a missing key is falsy', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(rect.getPluginData('never-set')).toBeFalsy(); + }); + + test('local and shared plugin data are isolated', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.setPluginData('k', 'local'); + rect.setSharedPluginData('ns', 'k', 'shared'); + expect(rect.getPluginData('k')).toBe('local'); + expect(rect.getSharedPluginData('ns', 'k')).toBe('shared'); + }); +}); + +describe('Local storage', () => { + test('set, get, keys and remove an item', (ctx) => { + const ls = ctx.penpot.localStorage; + ls.setItem('plugin-key', 'plugin-value'); + expect(ls.getItem('plugin-key')).toBe('plugin-value'); + expect(ls.getKeys()).toContain('plugin-key'); + + ls.removeItem('plugin-key'); + expect(ls.getItem('plugin-key')).toBeFalsy(); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/shadows-blur.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/shadows-blur.test.ts new file mode 100644 index 0000000000..7ede39799a --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/shadows-blur.test.ts @@ -0,0 +1,81 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { TestContext } from '../framework/types'; + +// Shadows & blur. +// Like fills/strokes, shadows are assigned as a whole array and read back; the +// nested Shadow.color yields a Color whose members are then exercised on read. + +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Shadows', () => { + test('drop shadow round-trips with a color', (ctx) => { + const r = rect(ctx); + r.shadows = [ + { + style: 'drop-shadow', + offsetX: 4, + offsetY: 6, + blur: 8, + spread: 1, + hidden: false, + color: { color: '#000000', opacity: 0.5 }, + }, + ]; + + expect(r.shadows).toHaveLength(1); + const shadow = r.shadows[0]; + expect(shadow.style).toBe('drop-shadow'); + expect(shadow.offsetX).toBeCloseTo(4, 0); + expect(shadow.offsetY).toBeCloseTo(6, 0); + expect(shadow.blur).toBeCloseTo(8, 0); + expect(shadow.spread).toBeCloseTo(1, 0); + expect(shadow.hidden).toBe(false); + expect(shadow.color).toBeDefined(); + expect(shadow.color && shadow.color.color).toBe('#000000'); + expect(shadow.color && shadow.color.opacity).toBeCloseTo(0.5, 2); + }); + + test('inner shadow can be hidden', (ctx) => { + const r = rect(ctx); + r.shadows = [ + { + style: 'inner-shadow', + offsetX: 0, + offsetY: 0, + blur: 4, + spread: 0, + hidden: true, + color: { color: '#ff0000', opacity: 1 }, + }, + ]; + + const shadow = r.shadows[0]; + expect(shadow.style).toBe('inner-shadow'); + expect(shadow.hidden).toBe(true); + }); +}); + +describe('Blur', () => { + test('layer blur round-trips', (ctx) => { + const r = rect(ctx); + r.blur = { value: 10 }; + + expect(r.blur).toBeDefined(); + expect(r.blur && r.blur.value).toBeCloseTo(10, 0); + // hidden defaults to false when omitted. + expect(r.blur && r.blur.hidden).toBeFalsy(); + }); + + test('background blur round-trips', (ctx) => { + const r = rect(ctx); + r.backgroundBlur = { value: 5 }; + + expect(r.backgroundBlur).toBeDefined(); + expect(r.backgroundBlur && r.backgroundBlur.value).toBeCloseTo(5, 0); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/shapes-factories.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/shapes-factories.test.ts new file mode 100644 index 0000000000..89209a0906 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/shapes-factories.test.ts @@ -0,0 +1,318 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Shapes & geometry. +// Exercises the Context shape factories and the context-level structural +// operations (group/ungroup/flatten, align/distribute). Everything created is +// appended to the scratch board `ctx.board` so the user's canvas stays clean. +// Each group keeps its happy-path tests together with the related edge cases: +// "fail" tests assert the documented null returns / rejections for degenerate +// input; the remaining ones pin non-trivial valid construction (clone +// independence, group order, boolean tree, svg tree). + +describe('Shapes', () => { + describe('Factories', () => { + test('createRectangle returns a rectangle', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + expect(rect.type).toBe('rectangle'); + }); + + test('createEllipse returns an ellipse', (ctx) => { + const ellipse = ctx.penpot.createEllipse(); + ctx.board.appendChild(ellipse); + expect(ellipse.type).toBe('ellipse'); + }); + + test('createBoard returns a board', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + expect(board.type).toBe('board'); + }); + + test('createPath returns a path', (ctx) => { + const path = ctx.penpot.createPath(); + ctx.board.appendChild(path); + expect(path.type).toBe('path'); + }); + + test('createText returns a text shape with the given content', (ctx) => { + const text = ctx.penpot.createText('Hello Penpot'); + expect(text).not.toBeNull(); + if (text) { + ctx.board.appendChild(text); + expect(text.type).toBe('text'); + expect(text.characters).toContain('Hello'); + } + }); + + test('createBoolean unions two shapes', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + b.x = 50; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + const bool = ctx.penpot.createBoolean('union', [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + expect(bool.type).toBe('boolean'); + } + }); + + test('createShapeFromSvg returns a group', (ctx) => { + const svg = + '' + + ''; + const group = ctx.penpot.createShapeFromSvg(svg); + expect(group).not.toBeNull(); + if (group) { + ctx.board.appendChild(group); + expect(group.type).toBe('group'); + } + }); + + test('createShapeFromSvgWithImages resolves to a group', async (ctx) => { + const svg = + '' + + ''; + const group = await ctx.penpot.createShapeFromSvgWithImages(svg); + expect(group).not.toBeNull(); + if (group) { + ctx.board.appendChild(group); + expect(group.type).toBe('group'); + } + }); + + // Degenerate input — documented null returns / rejections. + test('createText with an empty string returns null', (ctx) => { + // The d.ts documents: "Returns null if an empty string is provided". + const t = ctx.penpot.createText(''); + expect(t).toBeNull(); + }); + + test('group of an empty array returns null', (ctx) => { + const g = ctx.penpot.group([]); + expect(g).toBeNull(); + }); + + test('createBoolean with an empty shapes array is rejected', (ctx) => { + // createBoolean validates a non-empty shapes array, so with + // throwValidationErrors enabled it throws rather than returning null. + expect(() => ctx.penpot.createBoolean('union', [])).toThrow(); + }); + + test('createBoolean supports difference, exclude and intersection', (ctx) => { + // Only `union` is exercised elsewhere; cover the remaining boolean ops so a + // typology-specific regression in any single operation is caught. + for (const op of ['difference', 'exclude', 'intersection'] as const) { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createEllipse(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + b.x = 10; + b.y = 10; + const bool = ctx.penpot.createBoolean(op, [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + expect(typeof bool.d).toBe('string'); + expect(typeof bool.toD()).toBe('string'); + } + } + }); + + test('createShapeFromSvg is lenient with unparseable markup', (ctx) => { + // The SVG importer is permissive: it still produces a group for input + // that is not valid SVG rather than returning null. + const group = ctx.penpot.createShapeFromSvg('not svg at all'); + expect(group).not.toBeNull(); + if (group) { + ctx.board.appendChild(group); + } + }); + + // Success edges — non-trivial valid construction. + test('clone produces an independent copy', (ctx) => { + const r = ctx.penpot.createRectangle(); + r.name = 'original'; + ctx.board.appendChild(r); + + const copy = r.clone(); + ctx.board.appendChild(copy); + copy.name = 'copy'; + + expect(copy.id).not.toBe(r.id); + expect(copy.name).toBe('copy'); + // Mutating the copy must not affect the original. + expect(r.name).toBe('original'); + }); + + test('group preserves child count and order', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createEllipse(); + const c = ctx.penpot.createRectangle(); + a.name = 'a'; + b.name = 'b'; + c.name = 'c'; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + ctx.board.appendChild(c); + + const group = ctx.penpot.group([a, b, c]); + expect(group).not.toBeNull(); + if (group) { + expect(group.children).toHaveLength(3); + expect(group.children.map((s) => s.name).sort()).toEqual([ + 'a', + 'b', + 'c', + ]); + } + }); + + test('a boolean keeps its two operands as children', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + b.x = 50; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + const bool = ctx.penpot.createBoolean('union', [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + expect(bool.type).toBe('boolean'); + expect(bool.children).toHaveLength(2); + } + }); + + test('createShapeFromSvg builds a group with children', (ctx) => { + const svg = + '' + + '' + + ''; + const group = ctx.penpot.createShapeFromSvg(svg); + expect(group).not.toBeNull(); + if (group) { + ctx.board.appendChild(group); + expect(group.type).toBe('group'); + expect(group.children.length).toBeGreaterThan(0); + } + }); + }); + + describe('Grouping', () => { + test('group wraps shapes in a group', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createEllipse(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + expect(group.type).toBe('group'); + expect(group.children).toHaveLength(2); + } + }); + + test('ungroup dissolves a group', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createEllipse(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + const before = ctx.board.children.length; + ctx.penpot.ungroup(group); + // After ungroup the two shapes should be back on the board directly. + expect(ctx.board.children.length).toBeGreaterThan(before - 1); + } + }); + + test('flatten converts shapes into paths', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + + const paths = ctx.penpot.flatten([rect]); + expect(paths).toHaveLength(1); + expect(paths[0].type).toBe('path'); + }); + }); + + describe('Align & distribute', () => { + test('alignHorizontal moves shapes to a shared edge', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + a.x = 0; + b.x = 200; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + ctx.penpot.alignHorizontal([a, b], 'left'); + expect(a.x).toBeCloseTo(b.x, 0); + }); + + test('alignVertical moves shapes to a shared edge', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + a.y = 0; + b.y = 200; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + + ctx.penpot.alignVertical([a, b], 'top'); + expect(a.y).toBeCloseTo(b.y, 0); + }); + + test('distributeHorizontal runs without error', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + const c = ctx.penpot.createRectangle(); + a.x = 0; + b.x = 50; + c.x = 300; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + ctx.board.appendChild(c); + + ctx.penpot.distributeHorizontal([a, b, c]); + // Middle shape should end up between the outer two. + expect(b.x).toBeGreaterThan(a.x); + }); + + test('distributeVertical runs without error', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + const c = ctx.penpot.createRectangle(); + a.y = 0; + b.y = 50; + c.y = 300; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + ctx.board.appendChild(c); + + ctx.penpot.distributeVertical([a, b, c]); + expect(b.y).toBeGreaterThan(a.y); + }); + + // Edge cases. + test('aligning an empty array is a no-op (not rejected)', (ctx) => { + // align/distribute do not validate the shape list; an empty array is + // simply a no-op rather than an error. + expect(() => ctx.penpot.alignHorizontal([], 'left')).not.toThrow(); + }); + + test('distributing a single shape leaves it in place', (ctx) => { + const a = ctx.penpot.createRectangle(); + a.x = 10; + ctx.board.appendChild(a); + ctx.penpot.distributeHorizontal([a]); + expect(a.x).toBeCloseTo(10, 0); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/shapes-geometry.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/shapes-geometry.test.ts new file mode 100644 index 0000000000..2402f29035 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/shapes-geometry.test.ts @@ -0,0 +1,408 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { TestContext } from '../framework/types'; + +// Shapes & geometry. +// Exercises the `ShapeBase` identity / geometry / transform / ordering members +// that are common to every shape, using a rectangle on the scratch board. + +/** Creates a rectangle, appends it to the scratch board and returns it. */ +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Shapes', () => { + describe('Identity', () => { + test('exposes a stable id', (ctx) => { + const r = rect(ctx); + expect(typeof r.id).toBe('string'); + expect(r.id.length).toBeGreaterThan(0); + }); + + test('name is readable and writable', (ctx) => { + const r = rect(ctx); + r.name = 'sample-rect'; + expect(r.name).toBe('sample-rect'); + }); + + test('parent points at the containing board', (ctx) => { + const r = rect(ctx); + expect(r.parent).not.toBeNull(); + expect(r.parent && r.parent.id).toBe(ctx.board.id); + }); + + test('parentIndex is a distinct structural index per sibling', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + // Two siblings on a fresh board occupy indices 0 and 1 (direction depends + // on naturalChildOrdering, so assert the set rather than which is which). + expect([a.parentIndex, b.parentIndex].sort()).toEqual([0, 1]); + }); + }); + + describe('Geometry', () => { + test('x and y are readable and writable', (ctx) => { + const r = rect(ctx); + r.x = 120; + r.y = 80; + expect(r.x).toBeCloseTo(120, 0); + expect(r.y).toBeCloseTo(80, 0); + }); + + test('resize changes width and height', (ctx) => { + const r = rect(ctx); + r.resize(200, 100); + expect(r.width).toBeCloseTo(200, 0); + expect(r.height).toBeCloseTo(100, 0); + }); + + test('bounds describes a rectangular area', (ctx) => { + const r = rect(ctx); + r.x = 10; + r.y = 20; + r.resize(50, 40); + const b = r.bounds; + expect(b.width).toBeCloseTo(50, 0); + expect(b.height).toBeCloseTo(40, 0); + }); + + test('center sits in the middle of the shape', (ctx) => { + const r = rect(ctx); + r.x = 0; + r.y = 0; + r.resize(100, 100); + const c = r.center; + expect(c.x).toBeCloseTo(50, 0); + expect(c.y).toBeCloseTo(50, 0); + }); + + test('boardX and boardY are readable and writable', (ctx) => { + const r = rect(ctx); + r.boardX = 15; + r.boardY = 25; + expect(r.boardX).toBeCloseTo(15, 0); + expect(r.boardY).toBeCloseTo(25, 0); + }); + + test('parentX and parentY are readable and writable', (ctx) => { + const r = rect(ctx); + r.parentX = 12; + r.parentY = 22; + expect(r.parentX).toBeCloseTo(12, 0); + expect(r.parentY).toBeCloseTo(22, 0); + }); + }); + + describe('Transform', () => { + test('rotation is readable and writable', (ctx) => { + const r = rect(ctx); + r.rotation = 45; + expect(r.rotation).toBeCloseTo(45, 0); + }); + + test('rotate() applies an angle', (ctx) => { + const r = rect(ctx); + r.rotate(90); + expect(r.rotation).toBeCloseTo(90, 0); + }); + + test('flipX is readable and writable', (ctx) => { + const r = rect(ctx); + expect(r.flipX).toBe(false); + r.flipX = true; + expect(r.flipX).toBe(true); + }); + + test('flipY is readable and writable', (ctx) => { + const r = rect(ctx); + expect(r.flipY).toBe(false); + r.flipY = true; + expect(r.flipY).toBe(true); + }); + }); + + // The geometry/transform members above run on a rectangle. Re-exercise the + // core ones on other shape types so a type-specific regression is caught. + describe('Geometry across shape types', () => { + test('resize and rotate work on an ellipse', (ctx) => { + const e = ctx.penpot.createEllipse(); + ctx.board.appendChild(e); + e.resize(120, 60); + expect(e.width).toBeCloseTo(120, 0); + expect(e.height).toBeCloseTo(60, 0); + e.rotate(45); + expect(e.rotation).toBeCloseTo(45, 0); + }); + + test('flip works on an ellipse', (ctx) => { + // Kept separate from rotation: flipping an already-rotated shape does not + // round-trip through flipX, so exercise flip on an unrotated ellipse. + const e = ctx.penpot.createEllipse(); + ctx.board.appendChild(e); + expect(e.flipX).toBe(false); + e.flipX = true; + expect(e.flipX).toBe(true); + e.flipY = true; + expect(e.flipY).toBe(true); + }); + + test('resize and reposition work on a nested board', (ctx) => { + const b = ctx.penpot.createBoard(); + ctx.board.appendChild(b); + b.resize(200, 150); + b.x = 25; + b.y = 35; + expect(b.width).toBeCloseTo(200, 0); + expect(b.height).toBeCloseTo(150, 0); + expect(b.x).toBeCloseTo(25, 0); + expect(b.y).toBeCloseTo(35, 0); + }); + }); + + describe('Appearance flags', () => { + test('blocked is readable and writable', (ctx) => { + const r = rect(ctx); + r.blocked = true; + expect(r.blocked).toBe(true); + }); + + test('hidden is readable and writable', (ctx) => { + const r = rect(ctx); + r.hidden = true; + expect(r.hidden).toBe(true); + }); + + test('visible is readable and writable', (ctx) => { + const r = rect(ctx); + r.visible = false; + expect(r.visible).toBe(false); + }); + + test('proportionLock is readable and writable', (ctx) => { + const r = rect(ctx); + r.proportionLock = true; + expect(r.proportionLock).toBe(true); + }); + + test('fixedWhenScrolling is readable and writable', (ctx) => { + const r = rect(ctx); + r.fixedWhenScrolling = true; + expect(r.fixedWhenScrolling).toBe(true); + }); + + test('opacity is readable and writable', (ctx) => { + const r = rect(ctx); + r.opacity = 0.5; + expect(r.opacity).toBeCloseTo(0.5, 2); + }); + + test('blendMode is readable and writable', (ctx) => { + const r = rect(ctx); + r.blendMode = 'multiply'; + expect(r.blendMode).toBe('multiply'); + }); + }); + + describe('Constraints', () => { + test('constraintsHorizontal is readable and writable', (ctx) => { + const r = rect(ctx); + r.constraintsHorizontal = 'center'; + expect(r.constraintsHorizontal).toBe('center'); + }); + + test('constraintsVertical is readable and writable', (ctx) => { + const r = rect(ctx); + r.constraintsVertical = 'center'; + expect(r.constraintsVertical).toBe('center'); + }); + }); + + describe('Corner radius', () => { + test('borderRadius is readable and writable', (ctx) => { + const r = rect(ctx); + r.borderRadius = 8; + expect(r.borderRadius).toBeCloseTo(8, 0); + }); + + test('per-corner border radius is readable and writable', (ctx) => { + const r = rect(ctx); + r.borderRadiusTopLeft = 1; + r.borderRadiusTopRight = 2; + r.borderRadiusBottomRight = 3; + r.borderRadiusBottomLeft = 4; + expect(r.borderRadiusTopLeft).toBeCloseTo(1, 0); + expect(r.borderRadiusTopRight).toBeCloseTo(2, 0); + expect(r.borderRadiusBottomRight).toBeCloseTo(3, 0); + expect(r.borderRadiusBottomLeft).toBeCloseTo(4, 0); + }); + }); + + describe('Ordering', () => { + test('setParentIndex moves the shape to the given index', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + void a; + b.setParentIndex(0); + expect(b.parentIndex).toBe(0); + }); + + test('bringToFront / sendToBack move the shape to opposite extremes', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + void b; + const last = ctx.board.children.length - 1; + + a.bringToFront(); + const front = a.parentIndex; + expect(front === 0 || front === last).toBe(true); + + a.sendToBack(); + const back = a.parentIndex; + expect(back === 0 || back === last).toBe(true); + + // Front and back must be different extremes. + expect(front).not.toBe(back); + }); + + test('bringForward / sendBackward move the shape one step', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + const c = rect(ctx); + void b; + void c; + + const start = a.parentIndex; + a.bringForward(); + expect(Math.abs(a.parentIndex - start)).toBe(1); + + const mid = a.parentIndex; + a.sendBackward(); + expect(Math.abs(a.parentIndex - mid)).toBe(1); + }); + }); + + describe('Lifecycle', () => { + test('clone duplicates a shape', (ctx) => { + const r = rect(ctx); + r.name = 'original'; + const copy = r.clone(); + ctx.board.appendChild(copy); + expect(copy.id).not.toBe(r.id); + expect(copy.type).toBe('rectangle'); + }); + + test('remove detaches the shape from its parent', (ctx) => { + const r = rect(ctx); + const before = ctx.board.children.length; + r.remove(); + expect(ctx.board.children.length).toBe(before - 1); + }); + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert invalid numeric/enum input is + // rejected; "success" tests assert documented boundary behaviour + // (setParentIndex clamps to last, rotation about the center, opacity 0/1). + // --------------------------------------------------------------------------- + describe('Numeric & enum — invalid values (fail)', () => { + test('opacity below 0 throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.opacity = -0.1; + }).toThrow(); + }); + + test('opacity above 1 throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.opacity = 1.5; + }).toThrow(); + }); + + test('NaN opacity throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.opacity = NaN; + }).toThrow(); + }); + + test('NaN rotation is accepted (currently unvalidated)', (ctx) => { + // The rotation setter does not reject NaN; this pins the current lenient + // behaviour (a candidate for future hardening). + const r = rect(ctx); + expect(() => { + r.rotation = NaN; + }).not.toThrow(); + }); + + test('invalid blendMode throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.blendMode = 'not-a-mode' as unknown as 'normal'; + }).toThrow(); + }); + + test('negative borderRadius throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.borderRadius = -8; + }).toThrow(); + }); + + test('setParentIndex with a negative index is accepted (currently unvalidated)', (ctx) => { + // setParentIndex does not reject a negative index; this pins the current + // lenient behaviour (a candidate for future hardening). + const a = rect(ctx); + const b = rect(ctx); + void b; + expect(() => a.setParentIndex(-1)).not.toThrow(); + }); + }); + + describe('Geometry & ordering — success edges', () => { + test('opacity accepts the 0 and 1 boundaries', (ctx) => { + const r = rect(ctx); + r.opacity = 0; + expect(r.opacity).toBeCloseTo(0, 2); + r.opacity = 1; + expect(r.opacity).toBeCloseTo(1, 2); + }); + + test('setParentIndex past the end positions the shape last', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + const c = rect(ctx); + void b; + void c; + // The d.ts documents: "If the index is greater than the number of + // elements it will positioned last." + a.setParentIndex(999); + expect(a.parentIndex).toBe(ctx.board.children.length - 1); + }); + + test('setParentIndex reorders siblings while keeping a contiguous index set', (ctx) => { + const a = rect(ctx); + const b = rect(ctx); + const c = rect(ctx); + void a; + void c; + b.setParentIndex(0); + expect(b.parentIndex).toBe(0); + const indices = ctx.board.children.map((s) => s.parentIndex).sort(); + expect(indices).toEqual([0, 1, 2]); + }); + + test('rotating 360 degrees leaves the center unchanged', (ctx) => { + const r = rect(ctx); + r.x = 0; + r.y = 0; + r.resize(100, 100); + r.rotation = 360; + const c = r.center; + expect(c.x).toBeCloseTo(50, 0); + expect(c.y).toBeCloseTo(50, 0); + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/shapes-types.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/shapes-types.test.ts new file mode 100644 index 0000000000..dcd7c82dd5 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/shapes-types.test.ts @@ -0,0 +1,299 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Shape } from '@penpot/plugin-types'; + +// Shapes & geometry. +// Exercises members specific to the concrete shape types (Board, Group, Boolean, +// Path, Ellipse, SvgRaw) beyond the common `ShapeBase` surface. + +/** Depth-first search for the first descendant matching `type`. */ +function findByType(shape: Shape, type: string): Shape | null { + if (shape.type === type) return shape; + const children = 'children' in shape ? shape.children : undefined; + if (children) { + for (const child of children) { + const found = findByType(child, type); + if (found) return found; + } + } + return null; +} + +describe('Shapes', () => { + describe('Board', () => { + test('clipContent is readable and writable', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + board.clipContent = false; + expect(board.clipContent).toBe(false); + board.clipContent = true; + expect(board.clipContent).toBe(true); + }); + + test('showInViewMode is readable and writable', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + board.showInViewMode = false; + expect(board.showInViewMode).toBe(false); + }); + + test('appendChild and children reflect added shapes', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const child = ctx.penpot.createRectangle(); + board.appendChild(child); + expect(board.children).toHaveLength(1); + expect(board.children[0].id).toBe(child.id); + }); + + test('children setter accepts a reorder and rejects a different set', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + board.appendChild(a); + board.appendChild(b); + + const ids = board.children.map((c) => c.id).sort(); + + // `children` is writable only for *reordering*: assigning the same shapes + // in a new order is accepted and preserves the set. (The visible order is + // governed by the naturalChildOrdering flag, so only the set is asserted.) + board.children = [...board.children].reverse(); + expect(board.children).toHaveLength(2); + expect(board.children.map((c) => c.id).sort()).toEqual(ids); + + // Assigning a set that doesn't match the current children is rejected. + expect(() => { + board.children = [a]; + }).toThrow(); + }); + + test('insertChild places a shape at a given index', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + const first = ctx.penpot.createRectangle(); + const second = ctx.penpot.createRectangle(); + board.appendChild(first); + board.insertChild(0, second); + // Use the structural parentIndex; the children array sort direction + // depends on the naturalChildOrdering flag. + expect(second.parentIndex).toBe(0); + }); + + test('horizontalSizing and verticalSizing are readable and writable', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + board.horizontalSizing = 'fix'; + board.verticalSizing = 'fix'; + expect(board.horizontalSizing).toBe('fix'); + expect(board.verticalSizing).toBe('fix'); + }); + + test('isVariantContainer is false for a plain board', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + expect(board.isVariantContainer()).toBe(false); + }); + }); + + describe('Group', () => { + test('children and appendChild work on a group', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + expect(group.children).toHaveLength(2); + const extra = ctx.penpot.createRectangle(); + group.appendChild(extra); + expect(group.children).toHaveLength(3); + } + }); + + test('insertChild places a shape into a group', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + const extra = ctx.penpot.createRectangle(); + group.insertChild(0, extra); + expect(extra.parentIndex).toBe(0); + } + }); + + test('makeMask and removeMask run without error', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + group.makeMask(); + group.removeMask(); + } + }); + + test('isMask reports whether the group is a mask', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + expect(group.isMask()).toBe(false); + group.makeMask(); + expect(group.isMask()).toBe(true); + } + }); + }); + + describe('Boolean', () => { + test('boolean exposes path data and child shapes', (ctx) => { + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + b.x = 40; + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const bool = ctx.penpot.createBoolean('union', [a, b]); + expect(bool).not.toBeNull(); + if (bool) { + ctx.board.appendChild(bool); + expect(bool.children.length).toBeGreaterThan(1); + expect(typeof bool.d).toBe('string'); + expect(typeof bool.toD()).toBe('string'); + expect(Array.isArray(bool.commands)).toBe(true); + } + }); + }); + + describe('Path', () => { + test('d round-trips and populates commands', (ctx) => { + const path = ctx.penpot.createPath(); + ctx.board.appendChild(path); + path.d = 'M0 0 L10 0 L10 10 Z'; + expect(path.d).toContain('M'); + expect(path.commands.length).toBeGreaterThan(0); + expect(typeof path.toD()).toBe('string'); + }); + + test('content alias is readable and writable', (ctx) => { + const path = ctx.penpot.createPath(); + ctx.board.appendChild(path); + path.content = 'M0 0 L20 20'; + expect(typeof path.content).toBe('string'); + }); + }); + + describe('Ellipse', () => { + test('an ellipse reports its type', (ctx) => { + const ellipse = ctx.penpot.createEllipse(); + ctx.board.appendChild(ellipse); + expect(ellipse.type).toBe('ellipse'); + }); + }); + + describe('SvgRaw', () => { + test('an SVG import contains svg-raw descendants', (ctx) => { + // Native tags (rect/circle/path/…) import as their own shape types; only + // tags without a native mapping (e.g. ) become raw svg nodes, so the + // fixture must include one to exercise SvgRaw. + const svg = + '' + + '' + + 'hi'; + const group = ctx.penpot.createShapeFromSvg(svg); + expect(group).not.toBeNull(); + if (group) { + ctx.board.appendChild(group); + const raw = findByType(group, 'svg-raw'); + expect(raw).not.toBeNull(); + expect(raw && raw.type).toBe('svg-raw'); + } + }); + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert that building a circular shape + // hierarchy is rejected; "success" tests assert the type predicates classify + // shapes correctly and that masking round-trips (incl. nested in a board). + // --------------------------------------------------------------------------- + describe('Hierarchy — circular references', () => { + // The plugin appendChild does not explicitly reject cycle-creating moves; + // the underlying relocate handles them without throwing. These pin that the + // call is not rejected (cycle-prevention at the API boundary is a candidate + // for future hardening). + test('appending a board into itself does not throw', (ctx) => { + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(board); + expect(() => board.appendChild(board)).not.toThrow(); + }); + + test('appending an ancestor into its descendant does not throw', (ctx) => { + const outer = ctx.penpot.createBoard(); + const inner = ctx.penpot.createBoard(); + ctx.board.appendChild(outer); + outer.appendChild(inner); + expect(() => inner.appendChild(outer)).not.toThrow(); + }); + }); + + describe('Type predicates — success edges', () => { + test('utils.types classifies shapes by their concrete type', (ctx) => { + const types = ctx.penpot.utils.types; + + const rect = ctx.penpot.createRectangle(); + const ellipse = ctx.penpot.createEllipse(); + const path = ctx.penpot.createPath(); + const board = ctx.penpot.createBoard(); + ctx.board.appendChild(rect); + ctx.board.appendChild(ellipse); + ctx.board.appendChild(path); + ctx.board.appendChild(board); + + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + ctx.board.appendChild(a); + ctx.board.appendChild(b); + const group = ctx.penpot.group([a, b]); + + expect(types.isRectangle(rect)).toBe(true); + expect(types.isBoard(rect)).toBe(false); + expect(types.isEllipse(ellipse)).toBe(true); + expect(types.isPath(path)).toBe(true); + expect(types.isBoard(board)).toBe(true); + expect(types.isGroup(board)).toBe(false); + if (group) { + expect(types.isGroup(group)).toBe(true); + expect(types.isMask(group)).toBe(false); + } + }); + + test('makeMask / removeMask toggles isMask, including nested in a board', (ctx) => { + const host = ctx.penpot.createBoard(); + ctx.board.appendChild(host); + + const a = ctx.penpot.createRectangle(); + const b = ctx.penpot.createRectangle(); + host.appendChild(a); + host.appendChild(b); + const group = ctx.penpot.group([a, b]); + expect(group).not.toBeNull(); + if (group) { + expect(group.isMask()).toBe(false); + group.makeMask(); + expect(group.isMask()).toBe(true); + expect(ctx.penpot.utils.types.isMask(group)).toBe(true); + group.removeMask(); + expect(group.isMask()).toBe(false); + } + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/text.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/text.test.ts new file mode 100644 index 0000000000..21a4efbe38 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/text.test.ts @@ -0,0 +1,327 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Text } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Text & text ranges. +// Font-dependent properties (fontFamily/fontWeight/…) are only read here; they +// are set properly via the Font API in fonts.test.ts. The style properties that +// don't depend on a concrete font are set and read back. + +function text(ctx: TestContext, value = 'Hello Penpot'): Text { + const t = ctx.penpot.createText(value); + if (!t) throw new Error('createText returned null'); + ctx.board.appendChild(t); + return t; +} + +describe('Text', () => { + test('characters round-trip', (ctx) => { + const t = text(ctx); + t.characters = 'Updated content'; + expect(t.characters).toBe('Updated content'); + }); + + test('growType round-trips', (ctx) => { + const t = text(ctx); + t.growType = 'auto-height'; + expect(t.growType).toBe('auto-height'); + }); + + test('fontSize round-trips', (ctx) => { + const t = text(ctx); + t.fontSize = '24'; + expect(t.fontSize).toBe('24'); + }); + + test('lineHeight and letterSpacing round-trip', (ctx) => { + const t = text(ctx); + t.lineHeight = '1.5'; + t.letterSpacing = '2'; + expect(t.lineHeight).toBe('1.5'); + expect(t.letterSpacing).toBe('2'); + }); + + test('alignment round-trips', (ctx) => { + const t = text(ctx); + t.align = 'center'; + t.verticalAlign = 'center'; + expect(t.align).toBe('center'); + expect(t.verticalAlign).toBe('center'); + }); + + test('transform, decoration and direction round-trip', (ctx) => { + const t = text(ctx); + t.textTransform = 'uppercase'; + t.textDecoration = 'underline'; + t.direction = 'rtl'; + expect(t.textTransform).toBe('uppercase'); + expect(t.textDecoration).toBe('underline'); + expect(t.direction).toBe('rtl'); + }); + + test('font identity and variant setters accept a real font/variant', (ctx) => { + const t = text(ctx); + const font = ctx.penpot.fonts.all[0]; + const variant = font.variants[0]; + // Set the font identity first, then the variant-specific properties using + // values drawn from that same font so validation passes. + t.fontId = font.fontId; + t.fontFamily = font.fontFamily; + t.fontVariantId = variant.fontVariantId; + t.fontWeight = variant.fontWeight; + t.fontStyle = variant.fontStyle; + expect(t.fontId).toBe(font.fontId); + }); + + test('font properties are readable', (ctx) => { + const t = text(ctx); + expect(typeof t.fontId).toBe('string'); + expect(typeof t.fontFamily).toBe('string'); + expect(typeof t.fontVariantId).toBe('string'); + expect(typeof t.fontWeight).toBe('string'); + // fontStyle is 'normal' | 'italic' | 'mixed' | null + expect(t.fontStyle === null || typeof t.fontStyle === 'string').toBe(true); + }); + + test('textBounds exposes a rectangle shape', (ctx) => { + const t = text(ctx); + const b = t.textBounds; + // The numeric values depend on text layout (`:position-data`), which the + // headless runner does not compute, so width/height may be null in CI but + // are real numbers in the interactive editor. Assert the shape of the object. + expect('x' in b).toBe(true); + expect('y' in b).toBe(true); + expect('width' in b).toBe(true); + expect('height' in b).toBe(true); + }); + + test('applyTypography applies a typography to the text shape', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + typo.fontSize = '21'; + const t = text(ctx); + t.applyTypography(typo); + expect(t.fontSize).toBe('21'); + }); + + describe('Range', () => { + test('getRange returns the range characters', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + expect(range.characters.length).toBeGreaterThan(0); + }); + + test('range shape references the owning text shape', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + expect(range.shape.type).toBe('text'); + }); + + test('range font size round-trips', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.fontSize = '30'; + expect(range.fontSize).toBe('30'); + }); + + test('range line height and letter spacing round-trip', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.lineHeight = '2'; + range.letterSpacing = '1'; + expect(range.lineHeight).toBe('2'); + expect(range.letterSpacing).toBe('1'); + }); + + test('range alignment round-trips', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.align = 'right'; + range.verticalAlign = 'center'; + expect(range.align).toBe('right'); + expect(range.verticalAlign).toBe('center'); + }); + + test('range transform and decoration round-trip', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.textTransform = 'lowercase'; + range.textDecoration = 'line-through'; + expect(range.textTransform).toBe('lowercase'); + expect(range.textDecoration).toBe('line-through'); + }); + + test('range fills round-trip', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.fills = [{ fillColor: '#00ff00', fillOpacity: 1 }]; + + const fills = range.fills; + if (Array.isArray(fills)) { + expect(fills[0].fillColor).toBe('#00ff00'); + } + }); + + test('two ranges keep independent fills', (ctx) => { + // Mixed-style coverage: distinct fills on distinct sub-ranges must not + // bleed into each other. + const t = text(ctx, 'Hello Penpot'); + const first = t.getRange(0, 5); + const second = t.getRange(6, 12); + first.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + second.fills = [{ fillColor: '#0000ff', fillOpacity: 1 }]; + + const f1 = first.fills; + const f2 = second.fills; + if (Array.isArray(f1) && Array.isArray(f2)) { + expect(f1[0].fillColor).toBe('#ff0000'); + expect(f2[0].fillColor).toBe('#0000ff'); + } + }); + + test('range font properties are readable', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + expect(typeof range.fontId).toBe('string'); + expect(typeof range.fontFamily).toBe('string'); + expect(typeof range.fontVariantId).toBe('string'); + expect(typeof range.fontWeight).toBe('string'); + }); + + test('range style properties are readable', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + void range.direction; + void range.fontStyle; + void range.letterSpacing; + void range.lineHeight; + void range.textDecoration; + void range.textTransform; + void range.verticalAlign; + void range.align; + expect(range.characters.length).toBeGreaterThan(0); + }); + + test('range font properties can be set', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + const font = ctx.penpot.fonts.all[0]; + // Setting records the (set) targets; partial-range persistence is a known + // API bug covered elsewhere, so only the call is exercised here. + // (fontStyle/fontVariantId/fontWeight are validated strictly against the + // current font's variants, so they are left out to avoid fragility.) + range.fontFamily = font.fontFamily; + range.fontId = font.fontId; + range.direction = 'ltr'; + // Variant-specific setters, using values from the same font so the strict + // per-font validation passes. + const variant = font.variants[0]; + range.fontVariantId = variant.fontVariantId; + range.fontWeight = variant.fontWeight; + range.fontStyle = variant.fontStyle; + expect(range.characters.length).toBeGreaterThan(0); + }); + + test('applyTypography applies to a text range', (ctx) => { + const typo = ctx.penpot.library.local.createTypography(); + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, 5); + range.applyTypography(typo); + expect(range.characters.length).toBeGreaterThan(0); + }); + }); + + // --------------------------------------------------------------------------- + // Edge cases. "fail" tests assert invalid input is rejected; + // "success" tests assert non-trivial valid behaviour (mixed detection, + // full-span application, multi-paragraph round-trip). + // --------------------------------------------------------------------------- + test('getRange with start greater than end throws', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + expect(() => t.getRange(5, 1)).toThrow(); + }); + + test('getRange with a negative index throws', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + expect(() => t.getRange(-1, 5)).toThrow(); + }); + + test('getRange beyond the text length is clamped (not rejected)', (ctx) => { + // An end index past the text length is clamped rather than rejected. + const t = text(ctx, 'Hello Penpot'); + let range: ReturnType | null = null; + expect(() => { + range = t.getRange(0, 999); + }).not.toThrow(); + expect(range).not.toBeNull(); + }); + + test('empty fontSize throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.fontSize = ''; + }).toThrow(); + }); + + test('negative fontSize throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.fontSize = '-12'; + }).toThrow(); + }); + + test('non-numeric fontSize throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.fontSize = 'abc'; + }).toThrow(); + }); + + test('invalid align value throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.align = 'middle' as unknown as 'center'; + }).toThrow(); + }); + + test('wrong-case textTransform throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.textTransform = 'UPPERCASE' as unknown as 'uppercase'; + }).toThrow(); + }); + + test('invalid direction value throws', (ctx) => { + const t = text(ctx); + expect(() => { + t.direction = 'sideways' as unknown as 'ltr'; + }).toThrow(); + }); + + test('a uniformly-set fontSize is reported, not mixed', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + t.fontSize = '20'; + expect(t.fontSize).toBe('20'); + }); + + test('setting fontSize on a sub-range makes the shape report mixed', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + t.fontSize = '20'; + const range = t.getRange(0, 5); + range.fontSize = '40'; + expect(t.fontSize).toBe('mixed'); + }); + + test('applying a value to the full span is uniform, not mixed', (ctx) => { + const t = text(ctx, 'Hello Penpot'); + const range = t.getRange(0, t.characters.length); + range.fontSize = '33'; + expect(t.fontSize).toBe('33'); + }); + + test('multi-paragraph content round-trips', (ctx) => { + const t = text(ctx); + t.characters = 'first line\nsecond line'; + expect(t.characters).toBe('first line\nsecond line'); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/tokens.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/tokens.test.ts new file mode 100644 index 0000000000..1bfc15003e --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/tokens.test.ts @@ -0,0 +1,482 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { + TokenCatalog, + TokenColor, + TokenSet, + TokenShadow, + TokenType, + TokenTypography, +} from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Design tokens. +// The token catalog is reached through the local library. Sets/themes/tokens are +// self-provisioned; sets are created active so token references resolve. + +function catalog(ctx: TestContext): TokenCatalog { + return ctx.penpot.library.local.tokens; +} + +function activeSet(ctx: TestContext, name: string): TokenSet { + return catalog(ctx).addSet({ name, active: true }); +} + +// Names must be unique across runs too: sets/themes leak into the file (the +// API has no theme remove and set removal is best-effort), so a plain counter +// collides with leftovers from a previous run. Add a per-run random tag. +const runTag = Math.random().toString(36).slice(2, 8); +let counter = 0; +function unique(prefix: string): string { + counter += 1; + return `${prefix}-${runTag}-${counter}`; +} + +/** Token application and theme/set wiring update the store asynchronously. */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe('Tokens', () => { + describe('Catalog', () => { + test('addSet creates a token set', (ctx) => { + const cat = catalog(ctx); + const set = cat.addSet({ name: unique('set'), active: true }); + expect(typeof set.id).toBe('string'); + expect(set.active).toBe(true); + expect(cat.sets.length).toBeGreaterThan(0); + expect(cat.getSetById(set.id)).toBeDefined(); + }); + + test('addTheme creates a token theme', (ctx) => { + const cat = catalog(ctx); + const theme = cat.addTheme({ group: '', name: unique('theme') }); + expect(typeof theme.id).toBe('string'); + expect(cat.themes.length).toBeGreaterThan(0); + expect(cat.getThemeById(theme.id)).toBeDefined(); + }); + }); + + describe('Set', () => { + test('name and active round-trip', (ctx) => { + const set = activeSet(ctx, unique('set')); + const newName = unique('renamed'); + set.name = newName; + expect(set.name).toBe(newName); + set.active = false; + expect(set.active).toBe(false); + set.toggleActive(); + expect(set.active).toBe(true); + }); + + test('addToken adds a token and lists it', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'color', + name: unique('color.'), + value: '#ff0000', + }); + expect(typeof token.id).toBe('string'); + expect(set.tokens.length).toBeGreaterThan(0); + expect(Array.isArray(set.tokensByType)).toBe(true); + expect(set.getTokenById(token.id)).toBeDefined(); + }); + + test('duplicate and remove a set', (ctx) => { + const set = activeSet(ctx, unique('set')); + const dup = set.duplicate(); + expect(dup).not.toBeNull(); + expect(dup.id).not.toBe(set.id); + dup.remove(); + }); + + // Invalid input — addToken must reject bad input. + test('empty token name throws', (ctx) => { + const set = activeSet(ctx, unique('set')); + expect(() => + set.addToken({ type: 'color', name: '', value: '#ff0000' }), + ).toThrow(); + }); + + test('duplicate token name in the same set throws', (ctx) => { + const set = activeSet(ctx, unique('set')); + const name = unique('color.dup'); + set.addToken({ type: 'color', name, value: '#ff0000' }); + expect(() => + set.addToken({ type: 'color', name, value: '#0000ff' }), + ).toThrow(); + }); + + test('opacity token value outside 0..1 throws', (ctx) => { + const set = activeSet(ctx, unique('set')); + expect(() => + set.addToken({ type: 'opacity', name: unique('op.'), value: '2' }), + ).toThrow(); + }); + + test('invalid token type throws', (ctx) => { + const set = activeSet(ctx, unique('set')); + expect(() => + set.addToken({ + type: 'not-a-type' as unknown as TokenType, + name: unique('bad.'), + value: '1', + }), + ).toThrow(); + }); + }); + + describe('Theme', () => { + test('group, name and active round-trip', (ctx) => { + const theme = catalog(ctx).addTheme({ group: '', name: unique('theme') }); + theme.group = 'brand'; + theme.name = 'dark'; + expect(theme.group).toBe('brand'); + expect(theme.name).toBe('dark'); + theme.active = true; + expect(theme.active).toBe(true); + theme.toggleActive(); + }); + + test('addSet and removeSet manage the theme sets', async (ctx) => { + const cat = catalog(ctx); + const theme = cat.addTheme({ group: '', name: unique('theme') }); + const set = cat.addSet({ name: unique('set'), active: false }); + theme.addSet(set); + await sleep(300); + expect(theme.activeSets.length).toBeGreaterThan(0); + theme.removeSet(set); + }); + + test('duplicate and remove a theme', (ctx) => { + const theme = catalog(ctx).addTheme({ group: '', name: unique('theme') }); + const dup = theme.duplicate(); + expect(dup.id).not.toBe(theme.id); + dup.remove(); + }); + }); + + describe('Token', () => { + test('base properties round-trip', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'color', + name: unique('color.'), + value: '#00ff00', + }); + token.description = 'a token'; + expect(token.description).toBe('a token'); + expect(typeof token.id).toBe('string'); + // resolvedValueString resolves against active sets. + expect(token.resolvedValueString).toBeDefined(); + }); + + test('color token exposes type and value', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'color', + name: unique('color.'), + value: '#123456', + }); + expect(token.type).toBe('color'); + expect(token.value).toBe('#123456'); + }); + + test('dimension and number tokens expose resolved values', (ctx) => { + const set = activeSet(ctx, unique('set')); + const dim = set.addToken({ + type: 'dimension', + name: unique('dim.'), + value: '16', + }); + const num = set.addToken({ + type: 'rotation', + name: unique('rot.'), + value: '2', + }); + expect(dim.type).toBe('dimension'); + expect(num.type).toBe('rotation'); + if (dim.type === 'dimension') { + expect(dim.resolvedValue).toBeCloseTo(16, 0); + } + }); + + test('applyToShapes applies a token to a shape', async (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'borderRadius', + name: unique('radius.'), + value: '8', + }); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + token.applyToShapes([rect]); + await sleep(300); + expect(Object.keys(rect.tokens).length).toBeGreaterThan(0); + }); + + test('applyToSelected applies a token to the selection', async (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'opacity', + name: unique('opacity.'), + value: '0.5', + }); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + ctx.penpot.selection = [rect]; + token.applyToSelected(); + await sleep(300); + expect(Object.keys(rect.tokens).length).toBeGreaterThan(0); + }); + + test('applyToken applies a token through the shape', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'borderRadius', + name: unique('radius.'), + value: '12', + }); + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + rect.applyToken(token); + expect(rect.tokens).toBeDefined(); + }); + + test('duplicate and remove a token', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'color', + name: unique('color.'), + value: '#abcdef', + }); + const dup = token.duplicate(); + expect(dup.id).not.toBe(token.id); + dup.remove(); + }); + + // Reference resolution — a token referencing another resolves transitively. + test('a token referencing another token resolves transitively', (ctx) => { + const set = activeSet(ctx, unique('set')); + const baseName = unique('dim.base'); + set.addToken({ type: 'dimension', name: baseName, value: '16' }); + const ref = set.addToken({ + type: 'dimension', + name: unique('dim.ref'), + value: `{${baseName}}`, + }); + if (ref.type === 'dimension') { + expect(ref.resolvedValue).toBeCloseTo(16, 0); + } + }); + }); +}); + +// Every token type and the composite value types. +describe('Token types', () => { + const simpleCases: [TokenType, string, string][] = [ + ['borderRadius', '8', '12'], + ['color', '#ff0000', '#00ff00'], + ['dimension', '16', '24'], + ['fontFamilies', 'Arial', 'Helvetica'], + ['fontSizes', '14', '18'], + ['fontWeights', '700', '400'], + ['letterSpacing', '2', '3'], + ['number', '3', '4'], + ['opacity', '0.5', '0.8'], + ['rotation', '45', '90'], + ['sizing', '100', '120'], + ['spacing', '8', '12'], + ['borderWidth', '2', '3'], + ['textCase', 'uppercase', 'lowercase'], + ['textDecoration', 'underline', 'none'], + ]; + + for (const [type, value, value2] of simpleCases) { + test(`${type} token exposes type, value and resolvedValue`, (ctx) => { + const set = activeSet(ctx, unique('set')); + // Cast to a concrete variant for property access; the recorder attributes + // members to the real runtime type via the `type` discriminant. + const token = set.addToken({ + type, + name: unique(`${type}.`), + value, + }) as TokenColor; + expect(typeof token.type).toBe('string'); + void token.value; + token.value = value2; + token.name = unique('renamed.'); + expect(typeof token.name).toBe('string'); + // Record the resolvedValue (get) target for every type. fontFamilies + // returns the wrong shape (see the dedicated red test below), but reading + // it no longer throws, so a plain read is enough here. + void token.resolvedValue; + }); + } + + // A fontFamilies token's `resolvedValue` is the resolved family list + // (`string[] | undefined`, e.g. ['Arial']). The binding used to leak the raw + // tokenscript list symbol; it now returns the documented array. + test('fontFamilies token resolvedValue is the family list', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'fontFamilies', + name: unique('fontFamilies.'), + value: 'Arial', + }); + const resolved = token.resolvedValue; + expect(Array.isArray(resolved)).toBe(true); + expect(resolved as unknown as string[]).toContain('Arial'); + }); + + test('shadow token exposes its composite value', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'shadow', + name: unique('shadow.'), + value: { + color: '#000000', + inset: 'false', + offsetX: '1', + offsetY: '2', + spread: '0', + blur: '4', + }, + }) as TokenShadow; + expect(token.type).toBe('shadow'); + + // Round-trip the value (covers TokenShadow.value get + set) without changing + // it — the setter validates against the token's value schema, so assigning + // back exactly what the getter returned is guaranteed valid. + const v = token.value; + token.value = v; + + if (typeof v !== 'string' && v.length > 0) { + const sv = v[0]; + void sv.color; + void sv.inset; + void sv.offsetX; + void sv.offsetY; + void sv.spread; + void sv.blur; + sv.color = '#111111'; + sv.inset = 'true'; + sv.offsetX = '3'; + sv.offsetY = '4'; + sv.spread = '1'; + sv.blur = '5'; + } + + // resolvedValue resolves the composite into a TokenShadowValue[]; each entry + // exposes the shadow members with their resolved (unit-converted) values. + const rv = token.resolvedValue; + expect(Array.isArray(rv)).toBe(true); + expect(rv).toBeDefined(); + if (rv && rv.length > 0) { + const s = rv[0]; + expect(s.color).toBe('#000000'); + expect(s.inset).toBe(false); + expect(s.offsetX).toBeCloseTo(1, 0); + expect(s.offsetY).toBeCloseTo(2, 0); + expect(s.spread).toBeCloseTo(0, 0); + expect(s.blur).toBeCloseTo(4, 0); + // Exercise the writable members (records the set targets). + s.color = '#222222'; + s.inset = true; + s.offsetX = 9; + s.offsetY = 8; + s.spread = 2; + s.blur = 6; + } + }); + + test('typography token exposes its composite value', (ctx) => { + const set = activeSet(ctx, unique('set')); + const token = set.addToken({ + type: 'typography', + name: unique('typo.'), + value: { + letterSpacing: '1', + fontFamilies: 'Arial', + fontSizes: '14', + fontWeight: '400', + lineHeight: '1.2', + textCase: 'none', + textDecoration: 'none', + }, + }) as TokenTypography; + expect(token.type).toBe('typography'); + + const v = token.value; + if (typeof v !== 'string') { + void v.letterSpacing; + void v.fontFamilies; + void v.fontSizes; + void v.fontWeight; + void v.lineHeight; + void v.textCase; + void v.textDecoration; + v.letterSpacing = '2'; + v.fontFamilies = 'Helvetica'; + v.fontSizes = '16'; + v.fontWeight = '700'; + v.lineHeight = '1.5'; + v.textCase = 'uppercase'; + v.textDecoration = 'underline'; + } + + // resolvedValue resolves the composite into a TokenTypographyValue[]; each + // entry exposes the typography members with their resolved (unit-converted) + // values. + const rv = token.resolvedValue; + expect(Array.isArray(rv)).toBe(true); + expect(rv).toBeDefined(); + if (rv && rv.length > 0) { + const t = rv[0]; + expect(t.fontSizes).toBeCloseTo(14, 0); + expect(t.letterSpacing).toBeCloseTo(1, 0); + expect(t.lineHeight).toBeCloseTo(1.2, 1); + expect(Array.isArray(t.fontFamilies)).toBe(true); + expect(t.fontFamilies).toContain('Arial'); + expect(typeof t.fontWeights).toBe('string'); + expect(t.textCase).toBe('none'); + expect(t.textDecoration).toBe('none'); + // Exercise the writable members (records the set targets). + t.letterSpacing = 3; + t.fontFamilies = ['Helvetica']; + t.fontSizes = 18; + t.fontWeights = '500'; + t.lineHeight = 2; + t.textCase = 'lowercase'; + t.textDecoration = 'line-through'; + } + + token.value = { + letterSpacing: '2', + fontFamilies: 'Helvetica', + fontSizes: '16', + fontWeight: '700', + lineHeight: '1.5', + textCase: 'uppercase', + textDecoration: 'underline', + }; + }); + + test('a token set can be removed', (ctx) => { + const cat = catalog(ctx); + const set = cat.addSet({ name: unique('set'), active: true }); + set.remove(); + }); + + test('theme externalId, activeSets and removeSet', (ctx) => { + const cat = catalog(ctx); + const theme = cat.addTheme({ group: '', name: unique('theme') }); + const set = cat.addSet({ name: unique('set'), active: false }); + void theme.externalId; + // theme.activeSets has no runtime setter (declared writable) — API bug. + void theme.activeSets; + theme.addSet(set); + theme.removeSet(set); + expect(typeof theme.id).toBe('string'); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/value-objects.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/value-objects.test.ts new file mode 100644 index 0000000000..adcbabd52c --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/value-objects.test.ts @@ -0,0 +1,316 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { TestContext } from '../framework/types'; +import { PNG_1X1 } from './fixtures'; + +// Value-object property setters. +// Fills/strokes/gradients are returned as live proxies (their setters persist); +// shadows/blur/colors are returned as plain snapshots (setting records the +// member and round-trips on the returned object). Either way every writable +// member is exercised by reading the value object and setting each property. + +function rect(ctx: TestContext) { + const r = ctx.penpot.createRectangle(); + ctx.board.appendChild(r); + return r; +} + +describe('Value objects', () => { + describe('Fill', () => { + test('solid fill members round-trip', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + const fills = r.fills; + if (Array.isArray(fills)) { + const fill = fills[0]; + fill.fillColor = '#00ff00'; + fill.fillOpacity = 0.5; + expect(fill.fillColor).toBe('#00ff00'); + expect(fill.fillOpacity).toBeCloseTo(0.5, 2); + } + }); + + test('assigning a gradient on a live solid fill switches it', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + const fills = r.fills; + if (Array.isArray(fills)) { + fills[0].fillColorGradient = { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }; + expect(fills[0].fillColorGradient).toBeDefined(); + } + }); + + test('fill color reference members round-trip', (ctx) => { + const r = rect(ctx); + r.fills = [{ fillColor: '#ff0000', fillOpacity: 1 }]; + const fills = r.fills; + if (Array.isArray(fills)) { + const fill = fills[0]; + fill.fillColorRefId = '00000000-0000-0000-0000-000000000001'; + fill.fillColorRefFile = '00000000-0000-0000-0000-000000000002'; + expect(fill.fillColorRefId).toBe( + '00000000-0000-0000-0000-000000000001', + ); + expect(fill.fillColorRefFile).toBe( + '00000000-0000-0000-0000-000000000002', + ); + } + }); + + test('fill gradient members round-trip', (ctx) => { + const r = rect(ctx); + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }, + }, + ]; + const fills = r.fills; + if (Array.isArray(fills)) { + const gradient = fills[0].fillColorGradient; + expect(gradient).toBeDefined(); + if (gradient) { + gradient.type = 'radial'; + gradient.startX = 0.2; + gradient.startY = 0.3; + gradient.endX = 0.8; + gradient.endY = 0.9; + gradient.width = 0.5; + expect(gradient.type).toBe('radial'); + expect(gradient.startX).toBeCloseTo(0.2, 2); + expect(gradient.endY).toBeCloseTo(0.9, 2); + expect(gradient.width).toBeCloseTo(0.5, 2); + expect(gradient.stops.length).toBeGreaterThan(0); + } + } + }); + }); + + describe('Stroke', () => { + test('stroke members round-trip', (ctx) => { + const r = rect(ctx); + r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }]; + const stroke = r.strokes[0]; + stroke.strokeColor = '#112233'; + stroke.strokeOpacity = 0.7; + stroke.strokeStyle = 'dotted'; + stroke.strokeWidth = 4; + stroke.strokeAlignment = 'inner'; + stroke.strokeCapStart = 'round'; + stroke.strokeCapEnd = 'square'; + expect(stroke.strokeColor).toBe('#112233'); + expect(stroke.strokeOpacity).toBeCloseTo(0.7, 2); + expect(stroke.strokeStyle).toBe('dotted'); + expect(stroke.strokeWidth).toBeCloseTo(4, 0); + expect(stroke.strokeAlignment).toBe('inner'); + expect(stroke.strokeCapStart).toBe('round'); + expect(stroke.strokeCapEnd).toBe('square'); + }); + + test('stroke reference and gradient members round-trip', (ctx) => { + const r = rect(ctx); + r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }]; + const stroke = r.strokes[0]; + stroke.strokeColorRefId = '00000000-0000-0000-0000-000000000001'; + stroke.strokeColorRefFile = '00000000-0000-0000-0000-000000000002'; + expect(stroke.strokeColorRefId).toBe( + '00000000-0000-0000-0000-000000000001', + ); + expect(stroke.strokeColorRefFile).toBe( + '00000000-0000-0000-0000-000000000002', + ); + + stroke.strokeColorGradient = { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }; + expect(stroke.strokeColorGradient).toBeDefined(); + }); + }); + + describe('Shadow', () => { + test('shadow members round-trip on the returned shadow', (ctx) => { + const r = rect(ctx); + r.shadows = [ + { + style: 'drop-shadow', + offsetX: 1, + offsetY: 1, + blur: 2, + spread: 0, + hidden: false, + color: { color: '#000000', opacity: 1 }, + }, + ]; + const shadow = r.shadows[0]; + shadow.style = 'inner-shadow'; + shadow.offsetX = 5; + shadow.offsetY = 6; + shadow.blur = 7; + shadow.spread = 2; + shadow.hidden = true; + shadow.id = '00000000-0000-0000-0000-000000000003'; + expect(shadow.style).toBe('inner-shadow'); + expect(shadow.offsetX).toBeCloseTo(5, 0); + expect(shadow.offsetY).toBeCloseTo(6, 0); + expect(shadow.blur).toBeCloseTo(7, 0); + expect(shadow.spread).toBeCloseTo(2, 0); + expect(shadow.hidden).toBe(true); + }); + + // Skipped under MOCK_BACKEND: exercises uploadMediaData, which needs real + // backend media processing (ImageMagick) a mock can't reproduce. + test.skipIfMocked('shadow color members round-trip', async (ctx) => { + const image = await ctx.penpot.uploadMediaData( + 'shadow-color-image', + PNG_1X1, + 'image/png', + ); + const r = rect(ctx); + r.shadows = [ + { + style: 'drop-shadow', + offsetX: 1, + offsetY: 1, + blur: 2, + spread: 0, + hidden: false, + color: { color: '#000000', opacity: 1 }, + }, + ]; + const color = r.shadows[0].color; + expect(color).toBeDefined(); + if (color) { + color.color = '#abcdef'; + color.opacity = 0.4; + color.id = '00000000-0000-0000-0000-000000000004'; + color.name = 'shadow-color'; + color.path = 'group'; + color.refId = '00000000-0000-0000-0000-000000000005'; + color.refFile = '00000000-0000-0000-0000-000000000006'; + color.fileId = '00000000-0000-0000-0000-000000000007'; + // Color is a plain snapshot, so image set/read round-trips on it like + // the other members. + color.image = image; + expect(color.color).toBe('#abcdef'); + expect(color.opacity).toBeCloseTo(0.4, 2); + expect(color.name).toBe('shadow-color'); + expect(color.path).toBe('group'); + expect(color.image).toBeDefined(); + } + }); + }); + + describe('Blur', () => { + test('blur members round-trip on the returned blur', (ctx) => { + const r = rect(ctx); + r.blur = { value: 5 }; + const blur = r.blur; + expect(blur).toBeDefined(); + if (blur) { + blur.value = 12; + blur.hidden = true; + blur.id = '00000000-0000-0000-0000-000000000008'; + expect(blur.value).toBeCloseTo(12, 0); + expect(blur.hidden).toBe(true); + expect(blur.id).toBe('00000000-0000-0000-0000-000000000008'); + } + }); + + test('negative blur value is accepted (currently unvalidated)', (ctx) => { + // The blur setter does not reject a negative value; this pins the current + // lenient behaviour (a candidate for future hardening). + const r = rect(ctx); + expect(() => { + r.blur = { value: -5 }; + }).not.toThrow(); + }); + }); + + // --------------------------------------------------------------------------- + // Edge cases — gradients. + // --------------------------------------------------------------------------- + describe('Gradient — edge cases', () => { + test('a gradient stop offset outside 0..1 throws', (ctx) => { + const r = rect(ctx); + expect(() => { + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#0000ff', opacity: 1, offset: 1.5 }, + ], + }, + }, + ]; + }).toThrow(); + }); + + test('a gradient with many stops at boundary offsets round-trips', (ctx) => { + const r = rect(ctx); + r.fills = [ + { + fillColorGradient: { + type: 'linear', + startX: 0, + startY: 0, + endX: 1, + endY: 1, + width: 1, + stops: [ + { color: '#ff0000', opacity: 1, offset: 0 }, + { color: '#00ff00', opacity: 1, offset: 0.5 }, + { color: '#0000ff', opacity: 1, offset: 1 }, + ], + }, + }, + ]; + const fills = r.fills; + if (Array.isArray(fills)) { + const gradient = fills[0].fillColorGradient; + expect(gradient).toBeDefined(); + if (gradient) { + expect(gradient.stops.length).toBe(3); + expect(gradient.stops[0].offset).toBeCloseTo(0, 2); + expect(gradient.stops[2].offset).toBeCloseTo(1, 2); + } + } + }); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/variants.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/variants.test.ts new file mode 100644 index 0000000000..6afb75f495 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/variants.test.ts @@ -0,0 +1,201 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; +import type { Board, LibraryVariantComponent } from '@penpot/plugin-types'; +import type { TestContext } from '../framework/types'; + +// Variants. +// A standard component is created and transformed into a variant; the resulting +// VariantComponent exposes the Variants interface. Variant containers are also +// built from main-instance boards via createVariantFromComponents. + +function componentMain(ctx: TestContext): Board { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + return comp.mainInstance() as Board; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// transformInVariant is async, so `variants` is only populated after a tick. +async function variantComponent( + ctx: TestContext, +): Promise { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + comp.transformInVariant(); + await sleep(400); + return comp as LibraryVariantComponent; +} + +describe('Variants', () => { + test('transformInVariant turns a component into a variant', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + comp.transformInVariant(); + expect(comp.isVariant()).toBe(true); + }); + + test('createVariantFromComponents builds a variant container', (ctx) => { + const mainA = componentMain(ctx); + const mainB = componentMain(ctx); + const container = ctx.penpot.createVariantFromComponents([mainA, mainB]); + expect(container).toBeDefined(); + expect(container.isVariantContainer()).toBe(true); + expect(container.variants).not.toBeNull(); + }); + + test('combineAsVariants builds a variant container', (ctx) => { + const mainA = componentMain(ctx); + const mainB = componentMain(ctx); + const container = mainA.combineAsVariants([mainB.id]); + expect(container).toBeDefined(); + expect(container.isVariantContainer()).toBe(true); + }); + + test('variant component exposes variant props and Variants', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + comp.transformInVariant(); + expect(comp.isVariant()).toBe(true); + + const variantComp = comp as LibraryVariantComponent; + expect(variantComp.variants).not.toBeNull(); + expect(typeof variantComp.variantProps).toBe('object'); + + const variants = variantComp.variants; + if (variants) { + expect(typeof variants.id).toBe('string'); + expect(typeof variants.libraryId).toBe('string'); + expect(Array.isArray(variants.properties)).toBe(true); + expect(Array.isArray(variants.variantComponents())).toBe(true); + } + }); + + test('variant property can be added and read', (ctx) => { + const rect = ctx.penpot.createRectangle(); + ctx.board.appendChild(rect); + const comp = ctx.penpot.library.local.createComponent([rect]); + comp.transformInVariant(); + + const variants = (comp as LibraryVariantComponent).variants; + expect(variants).not.toBeNull(); + if (variants) { + const before = variants.properties.length; + variants.addProperty(); + expect(variants.properties.length).toBe(before + 1); + variants.currentValues(variants.properties[0]); + } + }); + + test('variant component exposes the Variants interface', async (ctx) => { + const vc = await variantComponent(ctx); + expect(vc.isVariant()).toBe(true); + expect(typeof vc.variantProps).toBe('object'); + void vc.variantError; // get only (no runtime setter) + + const v = vc.variants; + expect(v).not.toBeNull(); + if (v) { + expect(typeof v.id).toBe('string'); + expect(typeof v.libraryId).toBe('string'); + expect(Array.isArray(v.properties)).toBe(true); + expect(Array.isArray(v.variantComponents())).toBe(true); + if (v.properties.length > 0) { + expect(Array.isArray(v.currentValues(v.properties[0]))).toBe(true); + } + } + }); + + test('variant properties can be added, renamed and removed', async (ctx) => { + const vc = await variantComponent(ctx); + const v = vc.variants; + expect(v).not.toBeNull(); + if (v) { + v.addProperty(); + await sleep(300); + const count = v.properties.length; + expect(count).toBeGreaterThan(0); + + v.renameProperty(0, 'Size'); + await sleep(300); + v.removeProperty(count - 1); + await sleep(300); + } + }); + + test('addVariant and setVariantProperty mutate the variant', async (ctx) => { + const vc = await variantComponent(ctx); + const v = vc.variants; + expect(v).not.toBeNull(); + if (v) { + const before = v.variantComponents().length; + vc.addVariant(); + await sleep(300); + expect(v.variantComponents().length).toBeGreaterThan(before); + + if (v.properties.length > 0) { + vc.setVariantProperty(0, 'large'); + await sleep(300); + } + } + }); + + test('switchVariant on a variant instance does not throw', async (ctx) => { + const vc = await variantComponent(ctx); + // Add a second variant so there is another value to switch to. + vc.addVariant(); + await sleep(300); + + const instance = vc.instance(); + ctx.board.appendChild(instance); + // Valid args (nat-int pos, string value): switches to the nearest variant + // with that value at the property position, or no-ops — never throws. + expect(() => instance.switchVariant(0, 'large')).not.toThrow(); + }); + + test('utils.types.isVariantComponent identifies a variant component', async (ctx) => { + const vc = await variantComponent(ctx); + expect(ctx.penpot.utils.types.isVariantComponent(vc)).toBeTruthy(); + }); + + // --------------------------------------------------------------------------- + // Edge cases. Out-of-bounds property positions and degenerate + // container input should be rejected. + // --------------------------------------------------------------------------- + // createVariantFromComponents([]) is rejected (validated), but the + // positional property ops do not bounds-check `pos`; an out-of-range index + // is a no-op rather than an error. These pin the current behaviour + // (bounds-checking the position is a candidate for future hardening). + test('createVariantFromComponents of an empty array throws', (ctx) => { + expect(() => ctx.penpot.createVariantFromComponents([])).toThrow(); + }); + + test('removeProperty out of bounds is a no-op (not rejected)', async (ctx) => { + const vc = await variantComponent(ctx); + const v = vc.variants; + expect(v).not.toBeNull(); + if (v) { + expect(() => v.removeProperty(999)).not.toThrow(); + } + }); + + test('renameProperty out of bounds is a no-op (not rejected)', async (ctx) => { + const vc = await variantComponent(ctx); + const v = vc.variants; + expect(v).not.toBeNull(); + if (v) { + expect(() => v.renameProperty(999, 'Nope')).not.toThrow(); + } + }); + + test('setVariantProperty out of bounds is a no-op (not rejected)', async (ctx) => { + const vc = await variantComponent(ctx); + expect(() => vc.setVariantProperty(999, 'large')).not.toThrow(); + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/tests/viewport-guides.test.ts b/plugins/apps/plugin-api-test-suite/src/tests/viewport-guides.test.ts new file mode 100644 index 0000000000..0745629625 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/tests/viewport-guides.test.ts @@ -0,0 +1,136 @@ +import { expect } from '../framework/expect'; +import { describe, test } from '../framework/registry'; + +// Viewport and guides (ruler guides + board guides). + +describe('Viewport', () => { + test('zoom is readable and writable', (ctx) => { + const vp = ctx.penpot.viewport; + expect(typeof vp.zoom).toBe('number'); + vp.zoom = 2; + expect(vp.zoom).toBeCloseTo(2, 1); + vp.zoomReset(); + }); + + test('center is readable and writable', (ctx) => { + const vp = ctx.penpot.viewport; + vp.center = { x: 100, y: 200 }; + expect(vp.center.x).toBeCloseTo(100, 0); + expect(vp.center.y).toBeCloseTo(200, 0); + }); + + test('bounds are readable', (ctx) => { + const vp = ctx.penpot.viewport; + expect(typeof vp.bounds.width).toBe('number'); + expect(typeof vp.bounds.height).toBe('number'); + }); + + test('zoom helpers run without error', (ctx) => { + const vp = ctx.penpot.viewport; + vp.zoomToFitAll(); + vp.zoomIntoView([ctx.board]); + vp.zoomReset(); + }); +}); + +describe('Ruler guides', () => { + test('board addRulerGuide returns a guide', (ctx) => { + const guide = ctx.board.addRulerGuide('vertical', 50); + expect(guide.orientation).toBe('vertical'); + // A board-attached ruler guide exposes its board. + void guide.board; + }); + + test('board ruler guide can be reassigned to another board', (ctx) => { + const guide = ctx.board.addRulerGuide('vertical', 50); + const other = ctx.penpot.createBoard(); + ctx.board.appendChild(other); + guide.board = other; + expect(guide.board && guide.board.id).toBe(other.id); + }); + + test('board ruler guide position round-trips', (ctx) => { + const guide = ctx.board.addRulerGuide('vertical', 50); + guide.position = 60; + expect(guide.position).toBeCloseTo(60, 0); + }); + + test('board lists its ruler guides', (ctx) => { + ctx.board.addRulerGuide('horizontal', 30); + expect(ctx.board.rulerGuides.length).toBeGreaterThan(0); + }); + + test('board removeRulerGuide removes a guide', (ctx) => { + const guide = ctx.board.addRulerGuide('vertical', 50); + ctx.board.removeRulerGuide(guide); + expect(ctx.board.rulerGuides.length).toBe(0); + }); + + test('page ruler guides can be added and removed', (ctx) => { + const page = ctx.penpot.currentPage; + expect(page).not.toBeNull(); + if (page) { + const guide = page.addRulerGuide('horizontal', 120); + expect(guide.orientation).toBe('horizontal'); + expect(page.rulerGuides.length).toBeGreaterThan(0); + page.removeRulerGuide(guide); + } + }); +}); + +describe('Board guides', () => { + test('column, row and square guides round-trip', (ctx) => { + ctx.board.guides = [ + { + type: 'column', + display: true, + params: { + color: { color: '#ff0000', opacity: 1 }, + type: 'stretch', + size: 12, + gutter: 8, + }, + }, + { + type: 'row', + display: true, + params: { + color: { color: '#00ff00', opacity: 1 }, + type: 'stretch', + size: 12, + gutter: 8, + }, + }, + { + type: 'square', + display: true, + params: { color: { color: '#0000ff', opacity: 1 }, size: 16 }, + }, + ]; + + const guides = ctx.board.guides; + expect(guides).toHaveLength(3); + expect(guides[0].type).toBe('column'); + expect(guides[1].type).toBe('row'); + expect(guides[2].type).toBe('square'); + expect(guides[0].display).toBe(true); + expect(guides[0].params.color.color).toBe('#ff0000'); + + // Read every guide's display + params fields so the per-type guide and + // params getters are all exercised. + for (const g of guides) { + void g.display; + if (g.type === 'column' || g.type === 'row') { + void g.params.color; + void g.params.type; + void g.params.size; + void g.params.gutter; + void g.params.margin; + void g.params.itemLength; + } else if (g.type === 'square') { + void g.params.color; + void g.params.size; + } + } + }); +}); diff --git a/plugins/apps/plugin-api-test-suite/src/ui.css b/plugins/apps/plugin-api-test-suite/src/ui.css new file mode 100644 index 0000000000..104ac64c90 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/ui.css @@ -0,0 +1,334 @@ +:root { + font-family: var(--font-family, sans-serif); + font-size: 12px; +} + +body { + margin: 0; +} + +.wrapper { + display: flex; + flex-direction: column; + gap: var(--spacing-12, 12px); + padding: var(--spacing-12, 12px); + min-height: 100vh; + box-sizing: border-box; +} + +.header { + display: flex; + flex-direction: column; + gap: var(--spacing-4, 4px); +} + +.title { + font-size: 14px; + margin: 0; + color: var(--foreground-primary); +} + +.summary { + margin: 0; + color: var(--foreground-secondary); +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8, 8px); +} + +.toolbar .reload { + margin-inline-start: auto; +} + +.toolbar-status { + flex-basis: 100%; + color: var(--foreground-secondary); +} + +.groups { + display: flex; + flex-direction: column; + gap: var(--spacing-8, 8px); +} + +.group { + border-radius: var(--spacing-8, 8px); + background-color: var(--background-secondary); +} + +.group-summary { + display: flex; + align-items: center; + gap: var(--spacing-8, 8px); + padding: var(--spacing-8, 8px); + cursor: pointer; + user-select: none; +} + +.group-name { + color: var(--foreground-primary); +} + +.group-counts { + margin-inline-start: auto; + font-variant-numeric: tabular-nums; +} + +.count-pass { + color: #2d9d78; +} + +.count-fail { + color: #e65244; +} + +.count-sep, +.count-total { + color: var(--foreground-secondary); +} + +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--spacing-4, 4px); +} + +.icon { + display: block; +} + +.reload.is-loading .icon { + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Small rotating ring reused wherever something is in progress. */ +.spinner { + display: inline-block; + flex: 0 0 auto; + width: 10px; + height: 10px; + border: 1.5px solid var(--background-quaternary); + border-top-color: #6911d4; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +.running-badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-4, 4px); + color: #6911d4; +} + +.group .test-list { + padding: 0 var(--spacing-8, 8px) var(--spacing-8, 8px); +} + +.test-list { + display: flex; + flex-direction: column; + gap: var(--spacing-4, 4px); + list-style: none; + margin: 0; + padding: 0; +} + +.test-row { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: var(--spacing-8, 8px); + padding: var(--spacing-8, 8px); + border-radius: var(--spacing-8, 8px); + background-color: var(--background-tertiary); +} + +.test-main { + display: flex; + align-items: center; + gap: var(--spacing-8, 8px); + cursor: pointer; + min-width: 0; +} + +.test-name { + color: var(--foreground-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.test-duration { + display: inline-flex; + align-items: center; + gap: var(--spacing-4, 4px); + color: var(--foreground-secondary); + font-variant-numeric: tabular-nums; +} + +.test-duration.running { + color: #6911d4; +} + +/* Tint the whole row while its test runs so it stands out at a glance. */ +.test-row.status-running { + background-color: color-mix(in srgb, #6911d4 12%, var(--background-tertiary)); +} + +.status-dot { + flex: 0 0 auto; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--foreground-secondary); +} + +.dot-pending { + background-color: #8f9da3; +} +.dot-running { + background-color: #6911d4; + animation: pulse 1s ease-in-out infinite; +} +.dot-pass { + background-color: #2d9d78; +} +.dot-fail { + background-color: #e65244; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +.test-error { + grid-column: 1 / -1; + margin: 0; + padding: var(--spacing-8, 8px); + border-radius: var(--spacing-4, 4px); + background-color: var(--background-primary); + color: #e65244; + white-space: pre-wrap; + word-break: break-word; + font-family: monospace; +} + +.coverage { + display: flex; + flex-direction: column; + gap: var(--spacing-8, 8px); + border-top: 1px solid var(--background-quaternary); + padding-top: var(--spacing-8, 8px); + color: var(--foreground-secondary); +} + +.coverage-empty { + margin: 0; +} + +.coverage-header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.coverage-title { + color: var(--foreground-primary); + font-weight: 600; +} + +.coverage-value { + color: var(--foreground-secondary); + font-variant-numeric: tabular-nums; +} + +.progress-track { + width: 100%; + height: 8px; + border-radius: 999px; + background-color: var(--background-quaternary); + overflow: hidden; +} + +.progress-track { + position: relative; +} + +.progress-fill { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + height: 100%; + border-radius: inherit; + background-color: #2d9d78; + transition: width 0.3s ease; +} + +/* The static segment sits behind the recorded fill, in a distinct blue. */ +.progress-fill.static { + background-color: #4a8fe7; +} + +.coverage summary { + cursor: pointer; + color: var(--foreground-primary); +} + +.coverage-body { + display: flex; + flex-direction: column; + gap: var(--spacing-8, 8px); + margin-top: var(--spacing-8, 8px); +} + +.coverage-iface { + display: flex; + flex-direction: column; + gap: var(--spacing-4, 4px); +} + +.coverage-iface strong { + color: var(--foreground-primary); +} + +.coverage-members { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4, 4px); +} + +.coverage-member { + padding: 1px 6px; + border-radius: var(--spacing-4, 4px); + background-color: var(--background-tertiary); + word-break: break-word; +} + +.coverage-member.covered { + color: #2d9d78; +} + +.coverage-member.static { + color: #4a8fe7; +} + +.coverage-member.uncovered { + color: var(--foreground-secondary); +} diff --git a/plugins/apps/plugin-api-test-suite/src/ui.ts b/plugins/apps/plugin-api-test-suite/src/ui.ts new file mode 100644 index 0000000000..64dc79b486 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/src/ui.ts @@ -0,0 +1,558 @@ +import 'plugins-styles/lib/styles.css'; +import './ui.css'; + +import type { PluginToUIMessage, UIToPluginMessage } from './model'; +import type { + CoverageReport, + TestMeta, + TestResult, + TestStatus, +} from './framework/types'; + +const root = document.getElementById('app') as HTMLElement; + +let tests: TestMeta[] = []; +const results = new Map(); +const selected = new Set(); +const expandedGroups = new Set(); +let running = false; +let reloading = false; +let statusText = ''; + +/** Groups tests by their `group`, preserving first-seen order. */ +function groupTests(): { name: string; tests: TestMeta[] }[] { + const order: string[] = []; + const byGroup = new Map(); + for (const test of tests) { + let bucket = byGroup.get(test.group); + if (!bucket) { + bucket = []; + byGroup.set(test.group, bucket); + order.push(test.group); + } + bucket.push(test); + } + return order.map((name) => ({ name, tests: byGroup.get(name)! })); +} + +function applyTheme(theme: string | null) { + document.documentElement.setAttribute( + 'data-theme', + theme === 'light' ? 'light' : 'dark', + ); +} + +function sendToPlugin(message: UIToPluginMessage) { + // `'*'` is intentional: the plugin host controls this iframe's parent and the + // exact embedding origin isn't known ahead of time. Standard for Penpot + // plugin iframes; nothing sensitive crosses this channel. + parent.postMessage(message, '*'); +} + +/** + * Rolls a group's leaf statuses up into a single status for its header dot: + * running if any test is running, otherwise failed if any failed, otherwise + * passed only when every test passed, and pending until then. + */ +function aggregateStatus(statuses: TestStatus[]): TestStatus { + if (statuses.some((s) => s === 'running')) return 'running'; + if (statuses.some((s) => s === 'fail')) return 'fail'; + if (statuses.length > 0 && statuses.every((s) => s === 'pass')) return 'pass'; + return 'pending'; +} + +function statusLabel(status: TestStatus): string { + switch (status) { + case 'pass': + return 'Passed'; + case 'fail': + return 'Failed'; + case 'running': + return 'Running…'; + default: + return 'Not run'; + } +} + +function el( + tag: K, + props: Partial = {}, + children: (Node | string)[] = [], +): HTMLElementTagNameMap[K] { + const node = document.createElement(tag); + Object.assign(node, props); + for (const child of children) { + node.append(child); + } + return node; +} + +function svgIcon(paths: string[], fill: boolean): SVGSVGElement { + const ns = 'http://www.w3.org/2000/svg'; + const svg = document.createElementNS(ns, 'svg'); + svg.setAttribute('viewBox', '0 0 16 16'); + svg.setAttribute('width', '12'); + svg.setAttribute('height', '12'); + svg.setAttribute('aria-hidden', 'true'); + svg.classList.add('icon'); + for (const d of paths) { + const path = document.createElementNS(ns, 'path'); + path.setAttribute('d', d); + if (fill) { + path.setAttribute('fill', 'currentColor'); + } else { + path.setAttribute('fill', 'none'); + path.setAttribute('stroke', 'currentColor'); + path.setAttribute('stroke-width', '1.5'); + path.setAttribute('stroke-linecap', 'round'); + path.setAttribute('stroke-linejoin', 'round'); + } + svg.append(path); + } + return svg; +} + +/** A small triangular "play" icon used on run buttons. */ +function playIcon(): SVGSVGElement { + return svgIcon(['M4 2.5v11l9-5.5z'], true); +} + +/** A spinning ring, shown wherever something is in progress. */ +function spinner(): HTMLElement { + return el('span', { className: 'spinner', ariaLabel: 'In progress' }); +} + +/** A circular-arrow "reload" icon. */ +function reloadIcon(): SVGSVGElement { + return svgIcon(['M13 8a5 5 0 1 1-1.46-3.54', 'M13 2.5v3h-3'], false); +} + +function render() { + root.replaceChildren( + renderHeader(), + renderToolbar(), + renderList(), + renderCoverage(), + ); +} + +function renderHeader(): HTMLElement { + const passed = [...results.values()].filter( + (r) => r.status === 'pass', + ).length; + const failed = [...results.values()].filter( + (r) => r.status === 'fail', + ).length; + const summary = el('p', { className: 'summary' }, [ + `${tests.length} tests · ${passed} passed · ${failed} failed`, + ]); + if (running) { + summary.append( + ' · ', + el('span', { className: 'running-badge' }, [spinner(), 'Running…']), + ); + } + + return el('header', { className: 'header' }, [ + el('h1', { className: 'title', textContent: 'Plugin API Test Suite' }), + summary, + ]); +} + +function renderToolbar(): HTMLElement { + const runAll = el('button', { + textContent: 'Run all', + disabled: running || tests.length === 0, + }); + runAll.dataset.appearance = 'primary'; + runAll.addEventListener('click', () => run('all')); + + const runSelected = el('button', { + textContent: 'Run selected', + disabled: running || selected.size === 0, + }); + runSelected.dataset.appearance = 'secondary'; + runSelected.addEventListener('click', () => run([...selected])); + + const reload = el('button', { + className: `icon-button reload${reloading ? ' is-loading' : ''}`, + title: reloading + ? 'Reloading tests…' + : 'Reload: fetch and apply edited tests without reopening the plugin', + ariaLabel: 'Reload tests', + disabled: running || reloading, + }); + reload.dataset.appearance = 'secondary'; + reload.append(reloadIcon()); + reload.addEventListener('click', () => reloadTests()); + + const toolbar = el('div', { className: 'toolbar' }, [ + runAll, + runSelected, + reload, + ]); + + if (statusText) { + toolbar.append( + el('span', { className: 'toolbar-status', textContent: statusText }), + ); + } + + return toolbar; +} + +function renderRow(test: TestMeta): HTMLElement { + const result = results.get(test.id); + const status = result?.status ?? 'pending'; + + const checkbox = el('input', { + type: 'checkbox', + className: 'checkbox-input', + checked: selected.has(test.id), + disabled: running, + }); + checkbox.addEventListener('change', () => { + if (checkbox.checked) selected.add(test.id); + else selected.delete(test.id); + render(); + }); + + const runButton = el('button', { + className: 'icon-button run-single', + title: `Run "${test.name}"`, + ariaLabel: `Run "${test.name}"`, + disabled: running, + }); + runButton.dataset.appearance = 'secondary'; + runButton.append(playIcon()); + runButton.addEventListener('click', () => run([test.id])); + + const durationCell = + status === 'running' + ? el('span', { className: 'test-duration running' }, [ + spinner(), + 'Running…', + ]) + : el('span', { + className: 'test-duration', + textContent: result ? `${result.durationMs}ms` : '', + }); + + const row = el('li', { className: `test-row status-${status}` }, [ + el('label', { className: 'test-main' }, [ + checkbox, + el('span', { + className: `status-dot dot-${status}`, + title: statusLabel(status), + }), + el('span', { className: 'test-name', textContent: test.name }), + ]), + durationCell, + runButton, + ]); + + if (result?.status === 'fail' && result.error) { + row.append( + el('pre', { className: 'test-error', textContent: result.error }), + ); + } + + return row; +} + +function renderGroupSummary( + name: string, + groupTestList: TestMeta[], +): HTMLElement { + const statuses = groupTestList.map( + (t) => results.get(t.id)?.status ?? 'pending', + ); + const passed = statuses.filter((s) => s === 'pass').length; + const failed = statuses.filter((s) => s === 'fail').length; + const total = groupTestList.length; + const aggregate = aggregateStatus(statuses); + + // Select-all checkbox for the group (indeterminate when partially selected). + const ids = groupTestList.map((t) => t.id); + const selectedCount = ids.filter((id) => selected.has(id)).length; + const groupCheckbox = el('input', { + type: 'checkbox', + className: 'checkbox-input', + checked: selectedCount === total && total > 0, + disabled: running, + }); + groupCheckbox.indeterminate = selectedCount > 0 && selectedCount < total; + // Keep the checkbox from toggling the
when clicked. + groupCheckbox.addEventListener('click', (e) => e.stopPropagation()); + groupCheckbox.addEventListener('change', () => { + if (groupCheckbox.checked) ids.forEach((id) => selected.add(id)); + else ids.forEach((id) => selected.delete(id)); + render(); + }); + + const runButton = el('button', { + className: 'icon-button run-group', + title: `Run "${name}"`, + ariaLabel: `Run "${name}"`, + disabled: running, + }); + runButton.dataset.appearance = 'secondary'; + runButton.append(playIcon()); + runButton.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + run(ids); + }); + + const counts = el('span', { className: 'group-counts' }, [ + el('span', { className: 'count-pass', textContent: `${passed}` }), + el('span', { className: 'count-sep', textContent: ' / ' }), + el('span', { className: 'count-fail', textContent: `${failed}` }), + el('span', { + className: 'count-total', + textContent: ` · ${total} test${total === 1 ? '' : 's'}`, + }), + ]); + + return el('summary', { className: 'group-summary' }, [ + groupCheckbox, + el('span', { + className: `status-dot dot-${aggregate}`, + title: statusLabel(aggregate), + }), + el('span', { className: 'group-name', textContent: name }), + counts, + runButton, + ]); +} + +function renderList(): HTMLElement { + const container = el('div', { className: 'groups' }); + + for (const group of groupTests()) { + const details = el('details', { className: 'group' }); + // Groups are collapsed by default; remember the ones the user expands. + details.open = expandedGroups.has(group.name); + details.addEventListener('toggle', () => { + if (details.open) expandedGroups.add(group.name); + else expandedGroups.delete(group.name); + }); + + details.append(renderGroupSummary(group.name, group.tests)); + + const list = el('ul', { className: 'test-list' }); + for (const test of group.tests) { + list.append(renderRow(test)); + } + details.append(list); + + container.append(details); + } + + return container; +} + +let lastCoverage: CoverageReport | null = null; + +function renderProgressBar( + percent: number, + effectivePercent: number, +): HTMLElement { + const track = el('div', { + className: 'progress-track', + role: 'progressbar', + title: `${percent}% recorded, ${effectivePercent}% effective`, + }); + track.setAttribute('aria-valuenow', String(percent)); + track.setAttribute('aria-valuemin', '0'); + track.setAttribute('aria-valuemax', '100'); + // Layered: the static segment (lighter) spans the effective coverage, the + // recorded fill (green) sits on top spanning the recorder-credited coverage. + const staticFill = el('div', { className: 'progress-fill static' }); + staticFill.style.width = `${effectivePercent}%`; + const fill = el('div', { className: 'progress-fill' }); + fill.style.width = `${percent}%`; + track.append(staticFill, fill); + return track; +} + +function renderCoverage(): HTMLElement { + const section = el('div', { className: 'coverage' }); + + if (!lastCoverage) { + section.append( + el('p', { + className: 'coverage-empty', + textContent: 'API coverage — run tests to measure', + }), + ); + return section; + } + + const { + covered, + staticallyCovered, + total, + percent, + effectivePercent, + byInterface, + } = lastCoverage; + + const valueText = + staticallyCovered > 0 + ? `${percent}% · ${effectivePercent}% eff. (${covered}+${staticallyCovered}/${total})` + : `${percent}% (${covered}/${total})`; + + section.append( + el('div', { className: 'coverage-header' }, [ + el('span', { + className: 'coverage-title', + textContent: 'API coverage', + }), + el('span', { + className: 'coverage-value', + textContent: valueText, + }), + ]), + renderProgressBar(percent, effectivePercent), + ); + + const details = el('details', { className: 'coverage-details' }); + details.append(el('summary', { textContent: 'Coverage by interface' })); + + const list = el('div', { className: 'coverage-body' }); + const interfaces = Object.entries(byInterface) + .filter(([, info]) => info.members.length > 0) + .sort(([a], [b]) => a.localeCompare(b)); + + for (const [iface, info] of interfaces) { + const members = el('div', { className: 'coverage-members' }); + // Covered (green) first, then statically covered (blue), then uncovered. + for (const m of info.covered) { + members.append( + el('span', { className: 'coverage-member covered', textContent: m }), + ); + } + for (const m of info.staticallyCovered) { + members.append( + el('span', { + className: 'coverage-member static', + textContent: m, + title: 'Exercised behaviourally; not creditable via the proxy', + }), + ); + } + for (const m of info.uncovered) { + members.append( + el('span', { className: 'coverage-member uncovered', textContent: m }), + ); + } + + const ifaceLabel = + info.staticallyCovered.length > 0 + ? `${iface} (${info.covered.length}+${info.staticallyCovered.length}/${info.members.length})` + : `${iface} (${info.covered.length}/${info.members.length})`; + + list.append( + el('div', { className: 'coverage-iface' }, [ + el('strong', { + textContent: ifaceLabel, + }), + members, + ]), + ); + } + + details.append(list); + section.append(details); + return section; +} + +function run(ids: string[] | 'all') { + if (running) return; + running = true; + + const targetIds = ids === 'all' ? tests.map((t) => t.id) : ids; + for (const id of targetIds) { + const test = tests.find((t) => t.id === id); + if (test) { + results.set(id, { + id, + name: test.name, + status: 'running', + durationMs: 0, + }); + } + } + + render(); + sendToPlugin({ type: 'run', ids }); +} + +async function reloadTests() { + if (running || reloading) return; + reloading = true; + statusText = ''; + render(); + + try { + // Fetch the freshly built tests bundle from the dev server (same origin as + // this iframe). `vite build --watch` rebuilds it on every save, so this + // picks up edited tests. The cache-busting query avoids any stale copy. + const response = await fetch(`./tests-bundle.js?t=${Date.now()}`); + if (!response.ok) { + throw new Error(`Failed to fetch tests bundle (${response.status})`); + } + const code = await response.text(); + // The sandbox evaluates the bundle and replies with `reloaded` + `tests`. + sendToPlugin({ type: 'reloadTests', code }); + } catch (err) { + reloading = false; + statusText = `Reload failed: ${err instanceof Error ? err.message : String(err)}`; + render(); + } +} + +window.addEventListener('message', (event: MessageEvent) => { + const message = event.data; + if (!message || typeof message !== 'object') return; + + switch (message.type) { + case 'tests': { + tests = message.tests; + // Drop results/selection for tests that no longer exist after a reload. + const ids = new Set(tests.map((t) => t.id)); + for (const id of [...results.keys()]) { + if (!ids.has(id)) results.delete(id); + } + for (const id of [...selected]) { + if (!ids.has(id)) selected.delete(id); + } + render(); + break; + } + case 'result': + results.set(message.result.id, message.result); + render(); + break; + case 'runComplete': + running = false; + lastCoverage = message.coverage; + render(); + break; + case 'reloaded': + reloading = false; + statusText = message.ok + ? `Reloaded ${tests.length} tests` + : `Reload failed: ${message.error ?? 'unknown error'}`; + render(); + break; + case 'theme': + applyTheme(message.theme); + break; + } +}); + +applyTheme(new URLSearchParams(window.location.search).get('theme')); +render(); +sendToPlugin({ type: 'ready' }); diff --git a/plugins/apps/plugin-api-test-suite/tools/gen-api-surface.ts b/plugins/apps/plugin-api-test-suite/tools/gen-api-surface.ts new file mode 100644 index 0000000000..bafb5f7f34 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/tools/gen-api-surface.ts @@ -0,0 +1,339 @@ +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import ts from 'typescript'; + +/** + * Generates `src/generated/api-surface.json` from `libs/plugin-types/index.d.ts` + * using the TypeScript compiler API. The output drives type-aware coverage: + * + * - `interfaces`: own (syntactically declared) members per interface — the + * coverage denominator. + * - `graph`: for every interface, all reachable members (including inherited), + * each annotated with the interface that declares it and the type it yields. + * This lets the recorder attribute an access to the interface the value really + * is, instead of matching member names across unrelated interfaces. + * - `unions`: union aliases (e.g. `Shape`) with the discriminant needed to pick + * the concrete variant of a runtime value. + * + * Re-run with `pnpm run gen:api` whenever the public Plugin API types change. + */ + +const here = dirname(fileURLToPath(import.meta.url)); +const typesPath = resolve(here, '../../../libs/plugin-types/index.d.ts'); +const outPath = resolve(here, '../src/generated/api-surface.json'); + +const program = ts.createProgram([typesPath], { skipLibCheck: true }); +const checker = program.getTypeChecker(); +const source = program.getSourceFile(typesPath); + +if (!source) { + throw new Error(`Could not load Plugin API types at ${typesPath}`); +} + +const interfaceDecls = new Map(); +const unionAliases = new Map(); +// Object-literal type aliases (e.g. `type LibraryContext = { local: Library; … }`) +// are treated like interfaces so the recorder can wrap them and follow the chain +// into the types they expose (e.g. Context.library -> LibraryContext.local -> Library). +const objectAliases = new Map< + string, + { decl: ts.TypeAliasDeclaration; literal: ts.TypeLiteralNode } +>(); + +source.forEachChild((node) => { + if (ts.isInterfaceDeclaration(node)) { + interfaceDecls.set(node.name.text, node); + } else if (ts.isTypeAliasDeclaration(node) && ts.isUnionTypeNode(node.type)) { + unionAliases.set(node.name.text, node); + } else if ( + ts.isTypeAliasDeclaration(node) && + ts.isTypeLiteralNode(node.type) + ) { + objectAliases.set(node.name.text, { decl: node, literal: node.type }); + } +}); + +const knownInterfaces = new Set([ + ...interfaceDecls.keys(), + ...objectAliases.keys(), +]); +const knownUnions = new Set(unionAliases.keys()); + +function memberName(member: ts.TypeElement): string | undefined { + if ( + (ts.isPropertySignature(member) || ts.isMethodSignature(member)) && + member.name && + (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) + ) { + return member.name.text; + } + return undefined; +} + +/** True when a declaration carries an `@deprecated` JSDoc tag. */ +function isDeprecated(node: ts.Node): boolean { + return ts.getJSDocTags(node).some((t) => t.tagName.text === 'deprecated'); +} + +// Own (declared) members per interface — the coverage denominator. Deprecated +// interfaces and members are skipped so deprecated API never counts towards +// coverage (e.g. the legacy `Image` shape, `Color.refId/refFile`). +const interfaces: Record = {}; +for (const [name, decl] of interfaceDecls) { + if (isDeprecated(decl)) continue; + const names = new Set(); + for (const member of decl.members) { + if (isDeprecated(member)) continue; + const m = memberName(member); + if (m) names.add(m); + } + if (names.size > 0) interfaces[name] = [...names].sort(); +} +for (const [name, { decl, literal }] of objectAliases) { + if (isDeprecated(decl)) continue; + const names = new Set(); + for (const member of literal.members) { + if (isDeprecated(member)) continue; + const m = memberName(member); + if (m) names.add(m); + } + if (names.size > 0) interfaces[name] = [...names].sort(); +} + +// Honor `Omit` in heritage clauses: a member the *public* interface +// removes from an internal base is not part of the reachable surface, so it must +// not count towards coverage. `Penpot extends Omit` is the motivating case — `Context` is the internal interface +// and `Penpot` is the public one — but this applies to any such omission. +function stringLiterals(node: ts.TypeNode): string[] { + const collect = (n: ts.TypeNode): string[] => { + if (ts.isLiteralTypeNode(n) && ts.isStringLiteral(n.literal)) { + return [n.literal.text]; + } + if (ts.isUnionTypeNode(n)) return n.types.flatMap(collect); + return []; + }; + return collect(node); +} + +for (const decl of interfaceDecls.values()) { + for (const clause of decl.heritageClauses ?? []) { + for (const t of clause.types) { + if ( + ts.isIdentifier(t.expression) && + t.expression.text === 'Omit' && + t.typeArguments?.length === 2 + ) { + const [baseRef, keysArg] = t.typeArguments; + if ( + ts.isTypeReferenceNode(baseRef) && + ts.isIdentifier(baseRef.typeName) + ) { + const base = baseRef.typeName.text; + const omitted = new Set(stringLiterals(keysArg)); + if (interfaces[base] && omitted.size > 0) { + interfaces[base] = interfaces[base].filter((m) => !omitted.has(m)); + } + } + } + } + } +} + +/** + * Resolves a type to a tracked interface/union name (+ array flag) by parsing + * its textual form. Using `typeToString` keeps this resilient across compiler + * versions, where the structural type-flag APIs differ. + */ +function resolveType(type: ts.Type): { name: string | null; array: boolean } { + let text = checker.typeToString(type).replace(/^readonly\s+/, ''); + + // Unwrap Promise<...> + const promiseMatch = text.match(/^Promise<(.+)>$/s); + if (promiseMatch) text = promiseMatch[1].trim(); + + // Drop nullish, string-literal and bare-primitive union parts before array + // detection, so a single tracked type can still be resolved out of unions like + // `Group | null`, `Fill[] | 'mixed'` or `string | TokenShadowValueString[]`. + // Dropping primitives is safe: the recorder never wraps primitive values, so a + // primitive run-time value is returned as-is regardless of the resolved type. + const primitives = new Set([ + 'null', + 'undefined', + 'string', + 'number', + 'boolean', + 'unknown', + 'any', + 'void', + ]); + text = text + .split('|') + .map((p) => p.trim()) + .filter((p) => !primitives.has(p) && !/^["'].*["']$/.test(p)) + .join(' | '); + + let array = false; + const arrayMatch = text.match(/^(.+)\[\]$/s) ?? text.match(/^Array<(.+)>$/s); + if (arrayMatch) { + array = true; + text = arrayMatch[1].trim(); + } + + if (knownInterfaces.has(text) || knownUnions.has(text)) { + return { name: text, array }; + } + return { name: null, array }; +} + +// Full member graph per interface (including inherited members). +const graph: Record> = {}; + +type MemberKind = 'method' | 'get' | 'getset'; + +interface ApiMemberInfoOut { + decl: string; + kind: MemberKind; + type: string | null; + array: boolean; +} + +/** Classifies a member declaration as a method, read-only, or writable property. */ +function memberKind(decl: ts.Declaration): MemberKind { + if (ts.isMethodSignature(decl)) return 'method'; + if (ts.isPropertySignature(decl)) { + if (decl.type && ts.isFunctionTypeNode(decl.type)) return 'method'; + const readonly = decl.modifiers?.some( + (m) => m.kind === ts.SyntaxKind.ReadonlyKeyword, + ); + return readonly ? 'get' : 'getset'; + } + return 'getset'; +} + +for (const [name, decl] of interfaceDecls) { + const type = checker.getTypeAtLocation(decl); + const entries: Record = {}; + + for (const prop of checker.getPropertiesOfType(type)) { + const declaration = prop.declarations?.[0]; + if (!declaration) continue; + const parent = declaration.parent; + if (!parent || !ts.isInterfaceDeclaration(parent)) continue; + const declName = parent.name.text; + if (!knownInterfaces.has(declName)) continue; + + const propType = checker.getTypeOfSymbolAtLocation(prop, decl); + const signatures = propType.getCallSignatures(); + const resolved = resolveType( + signatures.length > 0 ? signatures[0].getReturnType() : propType, + ); + + entries[prop.name] = { + decl: declName, + kind: memberKind(declaration), + type: resolved.name, + array: resolved.array, + }; + } + + graph[name] = entries; +} + +// Object-literal aliases: all members are own (no inheritance), so the declaring +// interface is always the alias itself. +for (const [name, { decl, literal }] of objectAliases) { + const entries: Record = {}; + for (const member of literal.members) { + const m = memberName(member); + if (!m) continue; + const propType = checker.getTypeAtLocation(member); + const signatures = propType.getCallSignatures(); + const resolved = resolveType( + signatures.length > 0 ? signatures[0].getReturnType() : propType, + ); + entries[m] = { + decl: name, + kind: memberKind(member), + type: resolved.name, + array: resolved.array, + }; + } + graph[name] = entries; + void decl; +} + +// Union aliases + discriminants (literal `type` field -> variant interface). +const unions: Record = {}; + +interface UnionInfoOut { + variants: string[]; + discriminant: { field: string; map: Record } | null; +} + +function literalDiscriminant( + iface: ts.InterfaceDeclaration, + field: string, +): string | null { + for (const member of iface.members) { + if (memberName(member) !== field) continue; + if (ts.isPropertySignature(member) && member.type) { + if ( + ts.isLiteralTypeNode(member.type) && + ts.isStringLiteral(member.type.literal) + ) { + return member.type.literal.text; + } + } + } + return null; +} + +for (const [name, decl] of unionAliases) { + if (!ts.isUnionTypeNode(decl.type)) continue; + const variants: string[] = []; + for (const member of decl.type.types) { + if (ts.isTypeReferenceNode(member) && ts.isIdentifier(member.typeName)) { + const variantName = member.typeName.text; + if (knownInterfaces.has(variantName)) variants.push(variantName); + } + } + if (variants.length === 0) continue; + + // Build a discriminant map using the `type` literal of each variant. + const map: Record = {}; + for (const variant of variants) { + const lit = literalDiscriminant(interfaceDecls.get(variant)!, 'type'); + if (lit) map[lit] = variant; + } + + unions[name] = { + variants, + discriminant: Object.keys(map).length > 0 ? { field: 'type', map } : null, + }; +} + +const surface = { + interfaces: Object.fromEntries( + Object.entries(interfaces).sort(([a], [b]) => a.localeCompare(b)), + ), + graph: Object.fromEntries( + Object.entries(graph).sort(([a], [b]) => a.localeCompare(b)), + ), + unions: Object.fromEntries( + Object.entries(unions).sort(([a], [b]) => a.localeCompare(b)), + ), +}; + +mkdirSync(dirname(outPath), { recursive: true }); +writeFileSync(outPath, JSON.stringify(surface, null, 2) + '\n'); + +const memberCount = Object.values(surface.interfaces).reduce( + (sum, members) => sum + members.length, + 0, +); +console.log( + `Wrote ${memberCount} members across ${Object.keys(surface.interfaces).length} ` + + `interfaces and ${Object.keys(surface.unions).length} unions to ${outPath}`, +); diff --git a/plugins/apps/plugin-api-test-suite/tsconfig.app.json b/plugins/apps/plugin-api-test-suite/tsconfig.app.json new file mode 100644 index 0000000000..951462f9d2 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "../../libs/plugin-types/index.d.ts"] +} diff --git a/plugins/apps/plugin-api-test-suite/tsconfig.json b/plugins/apps/plugin-api-test-suite/tsconfig.json new file mode 100644 index 0000000000..8262ab74ac --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "types": ["vite/client"] + }, + "include": ["src"], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/plugins/apps/plugin-api-test-suite/tsconfig.node.json b/plugins/apps/plugin-api-test-suite/tsconfig.node.json new file mode 100644 index 0000000000..15543271a6 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"], + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": [ + "tools/**/*.ts", + "ci/**/*.ts", + "vite.config.ts", + "vite.config.headless.ts", + "vite.config.tests.ts", + "vite.config.iife.ts" + ] +} diff --git a/plugins/apps/plugin-api-test-suite/vite.config.headless.ts b/plugins/apps/plugin-api-test-suite/vite.config.headless.ts new file mode 100644 index 0000000000..c2fad314f5 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/vite.config.headless.ts @@ -0,0 +1,6 @@ +import { iifeConfig } from './vite.config.iife'; + +// Builds the CI test entry as a single self-executing (IIFE) bundle, evaluated +// inside the Penpot plugin sandbox via `globalThis.ɵloadPlugin({ code })` by the +// CI runner. See vite.config.iife.ts for the shared bundle config. +export default iifeConfig('headless', 'src/ci/headless.ts'); diff --git a/plugins/apps/plugin-api-test-suite/vite.config.iife.ts b/plugins/apps/plugin-api-test-suite/vite.config.iife.ts new file mode 100644 index 0000000000..b2ee8a2985 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/vite.config.iife.ts @@ -0,0 +1,34 @@ +import { defineConfig, type UserConfig } from 'vite'; + +/** + * Shared config for the two single-file IIFE bundles (`headless.js`, + * `tests-bundle.js`). Both are self-executing chunks with no `import`/`export` + * statements so they can be evaluated directly inside the Penpot plugin sandbox + * (via `globalThis.ɵloadPlugin({ code })` for headless, or the UI "Reload" + * button's `eval` for the tests bundle). They differ only by their entry module. + * + * `emptyOutDir` stays false so a `watch` rebuild of one bundle never wipes the + * sibling outputs in the shared `dist` directory. + */ +export function iifeConfig(name: string, entry: string): UserConfig { + return defineConfig({ + root: __dirname, + resolve: { + tsconfigPaths: true, + }, + build: { + outDir: '../../dist/apps/plugin-api-test-suite', + emptyOutDir: false, + reportCompressedSize: false, + rollupOptions: { + input: { + [name]: entry, + }, + output: { + format: 'iife', + entryFileNames: '[name].js', + }, + }, + }, + }); +} diff --git a/plugins/apps/plugin-api-test-suite/vite.config.tests.ts b/plugins/apps/plugin-api-test-suite/vite.config.tests.ts new file mode 100644 index 0000000000..39468d5209 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/vite.config.tests.ts @@ -0,0 +1,8 @@ +import { iifeConfig } from './vite.config.iife'; + +// Builds the test cases as a single self-executing (IIFE) bundle that publishes +// the discovered tests on `globalThis.__penpotReloadedTests`. The UI "Reload" +// button fetches this file and the plugin sandbox `eval`s it to pick up edited +// tests without reopening the plugin. Rebuilt on save by the `watch` script. +// See vite.config.iife.ts for the shared bundle config. +export default iifeConfig('tests-bundle', 'src/tests-bundle.ts'); diff --git a/plugins/apps/plugin-api-test-suite/vite.config.ts b/plugins/apps/plugin-api-test-suite/vite.config.ts new file mode 100644 index 0000000000..cc031bca49 --- /dev/null +++ b/plugins/apps/plugin-api-test-suite/vite.config.ts @@ -0,0 +1,39 @@ +/// +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: __dirname, + server: { + port: 4202, + host: '0.0.0.0', + cors: true, + }, + preview: { + port: 4202, + host: '0.0.0.0', + cors: true, + }, + resolve: { + tsconfigPaths: true, + }, + build: { + outDir: '../../dist/apps/plugin-api-test-suite', + // Keep false so `watch` rebuilds don't wipe the sibling tests-bundle.js / + // headless.js outputs. The `build` script passes --emptyOutDir for a clean + // one-shot build. + emptyOutDir: false, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + rollupOptions: { + input: { + plugin: 'src/plugin.ts', + index: './index.html', + }, + output: { + entryFileNames: '[name].js', + }, + }, + }, +}); diff --git a/plugins/apps/poc-state-plugin/src/app/app.component.ts b/plugins/apps/poc-state-plugin/src/app/app.component.ts index 0eae67b448..7a293a1210 100644 --- a/plugins/apps/poc-state-plugin/src/app/app.component.ts +++ b/plugins/apps/poc-state-plugin/src/app/app.component.ts @@ -45,6 +45,13 @@ import type { Shape } from '@penpot/plugin-types'; +