From 8b16326334917fdc1f53cd5fbabbc66c3693cc36 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Jun 2026 14:49:09 +0200 Subject: [PATCH 01/17] :paperclip: Update changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2076738b75..6d4c63d06f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # CHANGELOG -## 2.16.0 (Unreleased) +## 2.16.0 ### :boom: Breaking changes & Deprecations From 6827c4554bca814068ef540515765d25b0119010 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Jun 2026 14:49:48 +0200 Subject: [PATCH 02/17] :paperclip: Update version on mcp package.json --- mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/package.json b/mcp/package.json index 7c1a12d5e8..9fbb029950 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@penpot/mcp", - "version": "2.16.0-rc.9.11", + "version": "2.16.0", "description": "MCP server for Penpot integration", "bin": { "penpot-mcp": "./bin/mcp-local.js" From 715bd1c09e8a8cf3f9911cb94b24c5803c5b7e2f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Jun 2026 14:51:02 +0200 Subject: [PATCH 03/17] :paperclip: Update mcp server api_types.yml file --- mcp/packages/server/data/api_types.yml | 82 +++++++++++++------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 54b4100e4a..9ba21fee07 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -4592,23 +4592,23 @@ CommonLayout: ``` interface CommonLayout { - alignItems?: "center" | "start" | "end" | "stretch"; + alignItems?: "end" | "start" | "center" | "stretch"; alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" | "space-evenly"; - justifyItems?: "center" + justifyItems?: "end" | "start" - | "end" + | "center" | "stretch"; justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -4638,7 +4638,7 @@ CommonLayout: Properties: alignItems: |- ``` - alignItems?: "center" | "start" | "end" | "stretch" + alignItems?: "end" | "start" | "center" | "stretch" ``` The `alignItems` property specifies the default alignment for items inside the container. @@ -4651,9 +4651,9 @@ CommonLayout: alignContent: |- ``` alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -4672,7 +4672,7 @@ CommonLayout: * 'stretch': Content is stretched to fill the container. justifyItems: |- ``` - justifyItems?: "center" | "start" | "end" | "stretch" + justifyItems?: "end" | "start" | "center" | "stretch" ``` The `justifyItems` property specifies the default justification for items inside the container. @@ -4685,9 +4685,9 @@ CommonLayout: justifyContent: |- ``` justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7298,23 +7298,23 @@ FlexLayout: ``` interface FlexLayout { - alignItems?: "center" | "start" | "end" | "stretch"; + alignItems?: "end" | "start" | "center" | "stretch"; alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" | "space-evenly"; - justifyItems?: "center" + justifyItems?: "end" | "start" - | "end" + | "center" | "stretch"; justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7348,7 +7348,7 @@ FlexLayout: Properties: alignItems: |- ``` - alignItems?: "center" | "start" | "end" | "stretch" + alignItems?: "end" | "start" | "center" | "stretch" ``` The `alignItems` property specifies the default alignment for items inside the container. @@ -7361,9 +7361,9 @@ FlexLayout: alignContent: |- ``` alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7382,7 +7382,7 @@ FlexLayout: * 'stretch': Content is stretched to fill the container. justifyItems: |- ``` - justifyItems?: "center" | "start" | "end" | "stretch" + justifyItems?: "end" | "start" | "center" | "stretch" ``` The `justifyItems` property specifies the default justification for items inside the container. @@ -7395,9 +7395,9 @@ FlexLayout: justifyContent: |- ``` justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7856,23 +7856,23 @@ GridLayout: ``` interface GridLayout { - alignItems?: "center" | "start" | "end" | "stretch"; + alignItems?: "end" | "start" | "center" | "stretch"; alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" | "space-evenly"; - justifyItems?: "center" + justifyItems?: "end" | "start" - | "end" + | "center" | "stretch"; justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7915,7 +7915,7 @@ GridLayout: Properties: alignItems: |- ``` - alignItems?: "center" | "start" | "end" | "stretch" + alignItems?: "end" | "start" | "center" | "stretch" ``` The `alignItems` property specifies the default alignment for items inside the container. @@ -7928,9 +7928,9 @@ GridLayout: alignContent: |- ``` alignContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -7949,7 +7949,7 @@ GridLayout: * 'stretch': Content is stretched to fill the container. justifyItems: |- ``` - justifyItems?: "center" | "start" | "end" | "stretch" + justifyItems?: "end" | "start" | "center" | "stretch" ``` The `justifyItems` property specifies the default justification for items inside the container. @@ -7962,9 +7962,9 @@ GridLayout: justifyContent: |- ``` justifyContent?: - | "center" - | "start" | "end" + | "start" + | "center" | "stretch" | "space-between" | "space-around" @@ -10592,7 +10592,7 @@ LayoutChildProperties: zIndex: number; horizontalSizing: "fill" | "auto" | "fix"; verticalSizing: "fill" | "auto" | "fix"; - alignSelf: "center" | "auto" | "start" | "end" | "stretch"; + alignSelf: "end" | "start" | "center" | "auto" | "stretch"; horizontalMargin: number; verticalMargin: number; topMargin: number; @@ -10645,7 +10645,7 @@ LayoutChildProperties: * 'fix': The height is fixed. alignSelf: |- ``` - alignSelf: "center" | "auto" | "start" | "end" | "stretch" + alignSelf: "end" | "start" | "center" | "auto" | "stretch" ``` Aligns the child element within its container. From b1ec03a5d288bcc6af5c09ef5a58797a900abd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Jun 2026 11:02:35 +0200 Subject: [PATCH 04/17] :wrench: Remove the confirmation step for publishing docker images --- .github/workflows/build-docker-devenv.yml | 1 - .github/workflows/release.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index 3ba45267a5..53b5cf67ef 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -6,7 +6,6 @@ on: jobs: build-and-push: name: Build and push DevEnv Docker image - environment: release-admins runs-on: penpot-runner-02 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0890324c8..76e3d91964 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,6 @@ permissions: jobs: release: - environment: release-admins runs-on: ubuntu-24.04 outputs: version: ${{ steps.vars.outputs.gh_ref }} From b2c7854f5ec59a6f7e77b36161782b0164e0259d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 11 Jun 2026 16:47:35 +0200 Subject: [PATCH 05/17] :whale: Pin docker images to 2.16 --- docker/images/docker-compose.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index f6979e5e43..94193a977c 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -78,7 +78,7 @@ services: # - "443:443" penpot-frontend: - image: "penpotapp/frontend:${PENPOT_VERSION:-2.15}" + image: "penpotapp/frontend:${PENPOT_VERSION:-2.16}" restart: always ports: - 9001:8080 @@ -111,7 +111,7 @@ services: # PENPOT_DISABLE_IPV6_LISTEN: "true" penpot-backend: - image: "penpotapp/backend:${PENPOT_VERSION:-2.15}" + image: "penpotapp/backend:${PENPOT_VERSION:-2.16}" restart: always volumes: @@ -180,13 +180,13 @@ services: PENPOT_SMTP_SSL: "false" penpot-mcp: - image: "penpotapp/mcp:${PENPOT_VERSION:-2.15}" + image: "penpotapp/mcp:${PENPOT_VERSION:-2.16}" restart: always networks: - penpot penpot-exporter: - image: "penpotapp/exporter:${PENPOT_VERSION:-2.15}" + image: "penpotapp/exporter:${PENPOT_VERSION:-2.16}" restart: always depends_on: From 5f21ebd08d360d061c994e2b5f6b06c40233838a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Jun 2026 16:59:00 +0200 Subject: [PATCH 06/17] :bug: Filter ignorable React removeChild errors at error boundary and fix HTML anti-pattern (#10145) * :bug: Filter ignorable exceptions in error-boundary onError callback The global uncaught-error-handler already skips NotFoundError/removeChild and other harmless errors, but react-error-boundary's onError callback fires independently of the window.onerror pipeline. This means the error boundary was still logging these errors and setting last-exception, causing them to continue appearing in error reports despite being non-actionable. Add the is-ignorable-exception? check to the error-boundary* onError so harmless errors are silently ignored, matching the behavior of the global handler. * :bug: Fix dangerouslySetInnerHTML anti-pattern in context-notification The previous code used dangerouslySetInnerHTML on the same element that could also contain React children. This is a React anti-pattern that can cause reconciliation mismatches and lead to removeChild DOMExceptions. Refactor to use two separate element branches: one for raw HTML injection and one for normal React children with links. --- frontend/src/app/main/ui/error_boundary.cljs | 14 ++++++++++- .../notifications/context_notification.cljs | 24 +++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/main/ui/error_boundary.cljs b/frontend/src/app/main/ui/error_boundary.cljs index 60b42b45c0..226b87369b 100644 --- a/frontend/src/app/main/ui/error_boundary.cljs +++ b/frontend/src/app/main/ui/error_boundary.cljs @@ -37,8 +37,20 @@ ;; If the error is a stale-asset error (cross-build ;; module mismatch), force a hard page reload instead ;; of showing the error page to the user. - (if (errors/stale-asset-error? error) + (cond + (errors/stale-asset-error? error) (cf/throttled-reload :reason (ex-message error)) + + ;; If the error is known to be harmless (browser + ;; extensions, React DOM conflicts, etc.), ignore it + ;; silently — the global uncaught-error-handler + ;; already does this, but react-error-boundary's + ;; onError fires independently of the window.onerror + ;; pipeline, so we must also filter here. + (errors/is-ignorable-exception? error) + nil + + :else (do (set! errors/last-exception error) (ex/print-throwable error) diff --git a/frontend/src/app/main/ui/notifications/context_notification.cljs b/frontend/src/app/main/ui/notifications/context_notification.cljs index 6777dddf64..99259ddb99 100644 --- a/frontend/src/app/main/ui/notifications/context_notification.cljs +++ b/frontend/src/app/main/ui/notifications/context_notification.cljs @@ -54,16 +54,16 @@ ;; The content can arrive in markdown format, in these cases ;; we will use the prop is-html to true to indicate it and ;; that the html injection is performed and the necessary css classes are applied. - [:div {:class (stl/css :context-text) - :dangerouslySetInnerHTML (when is-html #js {:__html content})} - (when-not is-html - [:* - content - (when (some? links) - (for [[index link] (d/enumerate links)] - ;; TODO Review this component - [:& lb/link-button {:class (stl/css :link) - :on-click (:callback link) - :value (:label link) - :key (dm/str "link-" index)}]))])]]) + (if is-html + [:div {:class (stl/css :context-text) + :dangerouslySetInnerHTML #js {:__html content}}] + [:div {:class (stl/css :context-text)} + content + (when (some? links) + (for [[index link] (d/enumerate links)] + ;; TODO Review this component + [:& lb/link-button {:class (stl/css :link) + :on-click (:callback link) + :value (:label link) + :key (dm/str "link-" index)}]))])]) From 2e1839f89837eb64955aee16646381b3be7a3333 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Jun 2026 13:23:50 +0000 Subject: [PATCH 07/17] :bug: Fix NotReadableError in rasterizer during thumbnail generation The rasterizer's create-image function was clearing image.src in its Rx teardown cleanup. This caused the decoded pixel data to be discarded before downstream operators (drawImage / createImageBitmap) could read it, resulting in a browser NotReadableError. Changes: - Remove image.src = "" from cleanup; the image element will be garbage collected naturally. Event handler nulling is kept to break circular references. - Add dimension validation in svg-get-adjusted-size to return nil for zero/NaN dimensions instead of producing invalid sizes. - Add fallback in svg-set-intrinsic-size! to use [max max] when SVG dimensions can't be determined. Error occurred in production (2.16.0-RC10) during thumbnail generation in the workspace. Signed-off-by: Andrey Antukh --- frontend/src/app/rasterizer.cljs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/rasterizer.cljs b/frontend/src/app/rasterizer.cljs index 66f906686f..ed516fc85f 100644 --- a/frontend/src/app/rasterizer.cljs +++ b/frontend/src/app/rasterizer.cljs @@ -49,7 +49,14 @@ :hint "operation aborted"))) (obj/set! image "src" uri) (fn [] - (obj/set! image "src" "") + ;; NOTE: We intentionally do NOT set `image.src = ""` here. + ;; Doing so discards the decoded pixel data immediately when this + ;; observable completes, but downstream operators (e.g. drawImage / + ;; createImageBitmap in `render-image-bitmap`) still need to read + ;; the pixel data after the image is emitted. Clearing `src` before + ;; the downstream pipeline finishes causes a NotReadableError + ;; ("The requested file could not be read..."). The image element + ;; will be garbage collected naturally when no references remain. (obj/set! image "onload" nil) (obj/set! image "onerror" nil) (obj/set! image "onabort" nil)))))) @@ -57,10 +64,13 @@ (defn- svg-get-adjusted-size "Returns the adjusted size of an SVG." [width height max] - (let [ratio (/ width height)] - (if (< width height) - [max (* max (/ 1 ratio))] - [(* max ratio) max]))) + ;; Guard against zero/NaN dimensions that would cause division by zero + ;; or produce invalid sizes, leading to createImageBitmap failures. + (when (and (pos? width) (pos? height)) + (let [ratio (/ width height)] + (if (< width height) + [max (* max (/ 1 ratio))] + [(* max ratio) max])))) (defn- svg-get-size-from-viewbox "Returns the size of an SVG from its viewbox." @@ -101,7 +111,10 @@ "Sets the intrinsic size of an SVG to the given max size." [^js svg max] (let [doc (get-document-element svg) - [w h] (svg-get-size svg max)] + [w h] (or (svg-get-size svg max) + ;; Fallback: if we can't determine size from viewBox or + ;; intrinsic attributes, use the max size as a square. + [max max])] (dom/set-attribute! doc "width" (dm/str w)) (dom/set-attribute! doc "height" (dm/str h))) svg) From 9c7af3aeb5bfccb818e049d9c0df157221c7dc91 Mon Sep 17 00:00:00 2001 From: Justin Lin <30039756+lancatlin@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:55:23 +1000 Subject: [PATCH 08/17] :books: Add WebSocket proxy configuration for MCP in Nginx example (#10152) The current Nginx example doesn't include the required Websocket settings to correctly proxy /mcp/ws, and leads to WebSocket connection error when trying to connect in a design. Adding this lines fixed the issue. Signed-off-by: Justin Lin <30039756+lancatlin@users.noreply.github.com> --- docs/technical-guide/getting-started/docker.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/technical-guide/getting-started/docker.md b/docs/technical-guide/getting-started/docker.md index 53560235d3..a8c7b55ffa 100644 --- a/docs/technical-guide/getting-started/docker.md +++ b/docs/technical-guide/getting-started/docker.md @@ -206,6 +206,12 @@ server { proxy_pass http://localhost:9001/ws/notifications; } + location /mcp/ws { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://localhost:9001/mcp/ws; + } + # Proxy pass location / { proxy_set_header Host $http_host; From 7cb7f7adb28be4117cdcc75f9b04b604be2a3828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 11 Jun 2026 09:39:27 +0200 Subject: [PATCH 09/17] :whale: Add command to upgrade packages during the creation --- docker/images/Dockerfile.exporter | 1 + docker/images/Dockerfile.mcp | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 0049c6dd76..27ab66af71 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -13,6 +13,7 @@ RUN set -ex; \ mkdir -p /etc/resolvconf/resolv.conf.d; \ echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \ apt-get -qq update; \ + apt-get -qq upgrade; \ apt-get -qqy --no-install-recommends install \ curl \ tzdata \ diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index 14b1172035..5b9952a34d 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -13,6 +13,7 @@ RUN set -ex; \ mkdir -p /etc/resolvconf/resolv.conf.d; \ echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \ apt-get -qq update; \ + apt-get -qq upgrade; \ apt-get -qqy --no-install-recommends install \ curl \ tzdata \ From a8d0c18c1b4eb28f527461ce8cff25546f7860d1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 12 Jun 2026 11:40:02 +0200 Subject: [PATCH 10/17] :bug: Fix race condition between MCP initialization and plugin runtime (#10137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix race condition between MCP init and plugin runtime Add promise-based synchronization to ensure MCP initialization waits for plugin runtime to be ready before calling global.ɵloadPlugin. - Add runtime-ready-promise in app.plugins that resolves when init-plugins-runtime completes - Add wait-for-runtime function for other modules to await readiness - MCP init now waits for runtime via rx/from before starting plugin - Add defensive guards in start-plugin!, load-plugin!, close-plugin! to check if plugin APIs exist before calling - Rename init-plugins-runtime! to init-plugins-runtime Fixes: global.ɵloadPlugin is not a function error when MCP plugin starts before async plugin runtime initialization completes. * :paperclip: Add 'create-pr' opencode skill --- .opencode/skills/create-pr/SKILL.md | 81 +++++++++++++++++ frontend/src/app/main/data/plugins.cljs | 88 +++++++++++-------- frontend/src/app/main/data/workspace/mcp.cljs | 72 ++++++++------- frontend/src/app/plugins.cljs | 9 +- frontend/src/app/plugins/register.cljs | 18 +++- frontend/src/features.cljs | 2 +- 6 files changed, 196 insertions(+), 74 deletions(-) create mode 100644 .opencode/skills/create-pr/SKILL.md diff --git a/.opencode/skills/create-pr/SKILL.md b/.opencode/skills/create-pr/SKILL.md new file mode 100644 index 0000000000..88b5464481 --- /dev/null +++ b/.opencode/skills/create-pr/SKILL.md @@ -0,0 +1,81 @@ +--- +name: create-pr +description: Create a GitHub PR following Penpot conventions, with a concise engineer-focused description +--- + +# Create Pull Request + +Create a GitHub PR with proper title format and a concise description that explains reasoning, not implementation details. + +## When to Use + +- Opening a new pull request +- The user asks to create a PR +- Code changes are ready and committed + +## Workflow + +### 1. Verify Prerequisites + +```bash +git branch --show-current +git log --oneline main..HEAD +``` + +### 2. Check if Branch is Pushed + +```bash +BRANCH=$(git branch --show-current) +if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + echo "Branch is pushed, proceeding with PR creation" +else + echo "ERROR: Branch '$BRANCH' is not pushed to remote. Please push the branch first." + exit 1 +fi +``` + +**If the branch is not pushed, STOP here and ask the user to push it. The LLM does not have push permissions.** + +### 3. Create PR Body + +Write to `/tmp/pr-body.md` to avoid shell quoting issues: + +```bash +cat > /tmp/pr-body.md << 'EOF' +**Note:** This PR was created with AI assistance. + +## What + + + +## Why + + + +## How + + +EOF +``` + +### 4. Create the PR + +Follow title and description format from `mem:workflow/creating-prs` and `mem:workflow/creating-commits`. + +```bash +gh pr create --base main --project "Main" --title "" --body-file /tmp/pr-body.md +``` + +### 5. What NOT to Include + +- ❌ List of files changed (visible in diff) +- ❌ Testing steps (CI handles this) +- ❌ Screenshots unless UI-visible +- ❌ Migration notes unless breaking changes +- ❌ Regression fixes introduced during the PR (they're part of the development process, not the feature) + +## Key Principles + +- **Write for humans.** The diff shows what changed. The description explains why. +- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it? +- **Skip the obvious.** Don't explain what `git diff` already shows. diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs index 2ed86cf968..77f5d55013 100644 --- a/frontend/src/app/main/data/plugins.cljs +++ b/frontend/src/app/main/data/plugins.cljs @@ -9,6 +9,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.changes-builder :as pcb] + [app.common.logging :as log] [app.common.time :as ct] [app.main.data.changes :as dch] [app.main.data.event :as ev] @@ -57,45 +58,57 @@ (defn start-plugin! [{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions] - (-> (.ɵloadPlugin - ^js ug/global - #js {:pluginId plugin-id - :name name - :version version - :description description - :host host - :code code - :allowBackground (boolean allow-background) - :permissions (apply array permissions)} - nil - extensions) + (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] + (if (fn? load-plugin) + (-> (load-plugin + #js {:pluginId plugin-id + :name name + :version version + :description description + :host host + :code code + :allowBackground (boolean allow-background) + :permissions (apply array permissions)} + nil + extensions) - (p/catch (fn [cause] - (ex/print-throwable cause :prefix "Plugin Error") - (errors/flash :cause cause :type :handled))))) + (p/catch (fn [cause] + (ex/print-throwable cause :prefix "Plugin Error") + (errors/flash :cause cause :type :handled)))) + + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "start-plugin!")))) (defn- load-plugin! [{:keys [plugin-id name version description host code icon permissions]}] (st/emit! (pflag/clear plugin-id) (save-current-plugin plugin-id)) - (-> (.ɵloadPlugin - ^js ug/global - #js {:pluginId plugin-id - :name name - :description description - :version version - :host host - :code code - :icon icon - :permissions (apply array permissions)} - (fn [] - (st/emit! (remove-current-plugin plugin-id)))) + (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] + (if (fn? load-plugin) + (-> (load-plugin + #js {:pluginId plugin-id + :name name + :description description + :version version + :host host + :code code + :icon icon + :permissions (apply array permissions)} + (fn [] + (st/emit! (remove-current-plugin plugin-id)))) - (p/catch (fn [cause] - (st/emit! (remove-current-plugin plugin-id)) - (ex/print-throwable cause :prefix "Plugin Error") - (errors/flash :cause cause :type :handled))))) + (p/catch (fn [cause] + (st/emit! (remove-current-plugin plugin-id)) + (ex/print-throwable cause :prefix "Plugin Error") + (errors/flash :cause cause :type :handled)))) + + (do + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "load-plugin!") + (st/emit! (remove-current-plugin plugin-id)))))) (defn open-plugin! [{:keys [url] :as manifest} user-can-edit?] @@ -135,10 +148,15 @@ (defn close-plugin! [{:keys [plugin-id]}] - (try - (.ɵunloadPlugin ^js ug/global plugin-id) - (catch :default e - (.error js/console "Error" e)))) + (let [unload-plugin (unchecked-get ug/global "ɵunloadPlugin")] + (if (fn? unload-plugin) + (try + (unload-plugin plugin-id) + (catch :default e + (.error js/console "Error" e))) + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "close-plugin!")))) (defn close-current-plugin [& {:keys [close-only-edition-plugins?]}] diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 7f0b0e3363..61e1116b49 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -15,7 +15,7 @@ [app.main.data.plugins :as dp] [app.main.repo :as rp] [app.main.store :as st] - [app.plugins.register :refer [mcp-plugin-id]] + [app.plugins.register :as preg] [app.util.i18n :refer [tr]] [app.util.timers :as ts] [beicon.v2.core :as rx] @@ -29,7 +29,7 @@ {:code "plugin.js" :name "Penpot MCP Plugin" :version 2 - :plugin-id mcp-plugin-id + :plugin-id preg/mcp-plugin-id :description "This plugin enables interaction with the Penpot MCP server" :allow-background true :permissions @@ -195,41 +195,45 @@ (defn init-mcp [stream] - (->> (rp/cmd! :get-current-mcp-token) - (rx/tap - (fn [{:keys [token]}] - (when token - (dp/start-plugin! - (assoc default-manifest - :url (str (u/join cf/public-uri "plugins/mcp/manifest.json")) - :host (str (u/join cf/public-uri "plugins/mcp/"))) + ;; Wait for plugins runtime to be initialized before starting the MCP plugin. + ;; This ensures global.ɵloadPlugin is available when start-plugin! is called. + (->> (rx/from (preg/wait-for-runtime)) + (rx/mapcat + (fn [_] + (->> (rp/cmd! :get-current-mcp-token) + (rx/tap + (fn [{:keys [token]}] + (when token + (dp/start-plugin! + (assoc default-manifest + :url (str (u/join cf/public-uri "plugins/mcp/manifest.json")) + :host (str (u/join cf/public-uri "plugins/mcp/"))) - ;; API extension for MCP server - #js {:mcp - #js - {:getToken (constantly token) - :getServerUrl #(str cf/mcp-ws-uri) - :setMcpStatus - (fn [status] - (when (= status "connected") - (start-reconnect-watcher!)) - (st/emit! (update-mcp-connection-status status)) - (log/info :hint "MCP STATUS" :status status)) + ;; API extension for MCP server + #js {:mcp + #js + {:getToken (constantly token) + :getServerUrl #(str cf/mcp-ws-uri) + :setMcpStatus + (fn [status] + (when (= status "connected") + (start-reconnect-watcher!)) + (st/emit! (update-mcp-connection-status status)) + (log/info :hint "MCP STATUS" :status status)) - :on - (fn [event cb] - (when-let [event - (case event - "disconnect" ::disconnect - "connect" ::connect - nil)] + :on + (fn [event cb] + (when-let [event + (case event + "disconnect" ::disconnect + "connect" ::connect + nil)] - (let [stopper (rx/filter finalize-workspace? stream)] - (->> stream - (rx/filter (ptk/type? event)) - (rx/take-until stopper) - (rx/subs! #(cb))))))}})))) - (rx/ignore))) + (let [stopper (rx/filter finalize-workspace? stream)] + (->> stream + (rx/filter (ptk/type? event)) + (rx/take-until stopper) + (rx/subs! #(cb))))))}}))))))))) (defn init [] diff --git a/frontend/src/app/plugins.cljs b/frontend/src/app/plugins.cljs index 779c09b9fd..8937b4dde6 100644 --- a/frontend/src/app/plugins.cljs +++ b/frontend/src/app/plugins.cljs @@ -17,14 +17,17 @@ [app.plugins.grid :as grid] [app.plugins.library :as library] [app.plugins.public-utils] + [app.plugins.register :as preg] [app.plugins.ruler-guides :as rg] [app.plugins.shape :as shape] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(defn init-plugins-runtime! +(defn init-plugins-runtime [] - (runtime/initPluginsRuntime (fn [plugin-id] (api/create-context plugin-id)))) + (runtime/initPluginsRuntime (fn [plugin-id] (api/create-context plugin-id))) + ;; Signal that runtime is ready + (preg/signal-runtime-ready)) (defn initialize [] @@ -38,7 +41,7 @@ (rx/observe-on :async) (rx/filter #(features/active-feature? @st/state "plugins/runtime")) (rx/take 1) - (rx/tap init-plugins-runtime!) + (rx/tap init-plugins-runtime) (rx/ignore))))) ;; Prevent circular dependency diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index d05b08d547..e4837ce75b 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -15,12 +15,28 @@ [app.main.repo :as rp] [app.main.store :as st] [app.util.object :as obj] - [beicon.v2.core :as rx])) + [beicon.v2.core :as rx] + [promesa.core :as p])) ;; Needs to be here because moving it to `app.main.data.workspace.mcp` will ;; cause a circular dependency (def mcp-plugin-id "96dfa740-005d-8020-8007-55ede24a2bae") +;; Promise that resolves when plugins runtime is initialized. +;; Lives here to avoid circular dependency: workspace.mcp -> app.plugins -> app.plugins.api -> workspace +(defonce ^:private runtime-ready-promise (p/deferred)) + +(defn wait-for-runtime + "Returns a promise that resolves when plugins runtime is initialized." + [] + runtime-ready-promise) + +(defn signal-runtime-ready + "Signals that plugins runtime has been initialized. Called by app.plugins/init-plugins-runtime." + [] + (when (p/pending? runtime-ready-promise) + (p/resolve! runtime-ready-promise true))) + ;; Stores the installed plugins information (defonce ^:private registry (atom {})) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs index 77f15664ba..be61548a1b 100644 --- a/frontend/src/features.cljs +++ b/frontend/src/features.cljs @@ -24,5 +24,5 @@ (defn ^:export plugins [] (st/emit! (features/enable-feature "plugins/runtime")) - (plugins/init-plugins-runtime!) + (plugins/init-plugins-runtime) nil) From 739a2d49580e68c7b1d30aa4ff0c4a1e1e1d676d Mon Sep 17 00:00:00 2001 From: Luis de Dios <luis.dedios@kaleidos.net> Date: Mon, 15 Jun 2026 10:57:08 +0200 Subject: [PATCH 11/17] :lipstick: Update translations for company size options in in-app onboarding (#10164) --- frontend/translations/en.po | 8 ++++---- frontend/translations/es.po | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 2a89b87596..3ad9c6e30c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4329,16 +4329,16 @@ msgid "onboarding.questions.team-size.just-me" msgstr "Just me" msgid "onboarding.questions.team-size.2-100" -msgstr "2-100" +msgstr "2 - 100" msgid "onboarding.questions.team-size.101-500" -msgstr "101-500" +msgstr "101 - 500" msgid "onboarding.questions.team-size.501-1000" -msgstr "501-1,000" +msgstr "501 - 1,000" msgid "onboarding.questions.team-size.1001-5000" -msgstr "1,001-5,000" +msgstr "1,001 - 5,000" msgid "onboarding.questions.team-size.more-than-5001" msgstr "5,001+" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 24291c4d57..f72aff6a1c 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -4237,16 +4237,16 @@ msgid "onboarding.questions.team-size.just-me" msgstr "Sólo yo" msgid "onboarding.questions.team-size.2-100" -msgstr "2-100" +msgstr "2 - 100" msgid "onboarding.questions.team-size.101-500" -msgstr "101-500" +msgstr "101 - 500" msgid "onboarding.questions.team-size.501-1000" -msgstr "501-1000" +msgstr "501 - 1000" msgid "onboarding.questions.team-size.1001-5000" -msgstr "1001-5000" +msgstr "1001 - 5000" msgid "onboarding.questions.team-size.more-than-5001" msgstr "5001+" From 572b094babea5b5174611b47ab1c5cee25979630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= <david.barragan@kaleidos.net> Date: Fri, 12 Jun 2026 12:14:36 +0200 Subject: [PATCH 12/17] :whale: Migrate frontend image to alpine --- docker/images/Dockerfile.frontend | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/images/Dockerfile.frontend b/docker/images/Dockerfile.frontend index f77a6d187a..fe1ba7833c 100644 --- a/docker/images/Dockerfile.frontend +++ b/docker/images/Dockerfile.frontend @@ -1,10 +1,15 @@ -FROM nginxinc/nginx-unprivileged:1.30.0 +FROM nginxinc/nginx-unprivileged:1.30.2-alpine LABEL maintainer="Penpot <docker@penpot.app>" USER root RUN set -ex; \ - useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ + apk update; \ + apk upgrade; \ + apk add --no-cache bash gettext; \ + rm -rf /var/cache/apk/*; \ + addgroup -g 1001 penpot; \ + adduser -D -H -u 1001 -s /bin/false -h /opt/penpot -G penpot penpot; \ mkdir -p /opt/data/assets; \ chown -R penpot:penpot /opt/data; \ mkdir -p /etc/nginx/overrides/main.d/; \ From fe86359dc897764ce36f84afd74867edc32e69a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= <david.barragan@kaleidos.net> Date: Fri, 12 Jun 2026 12:22:47 +0200 Subject: [PATCH 13/17] :whale: Migrate storybook image to alpine --- docker/images/Dockerfile.storybook | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/images/Dockerfile.storybook b/docker/images/Dockerfile.storybook index 9cccbe799b..9eb5a55663 100644 --- a/docker/images/Dockerfile.storybook +++ b/docker/images/Dockerfile.storybook @@ -1,10 +1,14 @@ -FROM nginxinc/nginx-unprivileged:1.30.0 +FROM nginxinc/nginx-unprivileged:1.30.2-alpine LABEL maintainer="Penpot <docker@penpot.app>" USER root RUN set -ex; \ - useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; + apk update; \ + apk upgrade; \ + rm -rf /var/cache/apk/*; \ + addgroup -g 1001 penpot; \ + adduser -D -H -u 1001 -s /bin/false -h /opt/penpot -G penpot penpot; ARG BUNDLE_PATH="./bundle-storybook/" COPY $BUNDLE_PATH /var/www/ From 6a1c3348f3e15d1c73dd39be824ed32909cb3882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= <david.barragan@kaleidos.net> Date: Fri, 12 Jun 2026 13:10:39 +0200 Subject: [PATCH 14/17] :whale: Clean apt package cache in all Dockerfiles --- docker/images/Dockerfile.backend | 5 +++-- docker/images/Dockerfile.exporter | 2 ++ docker/images/Dockerfile.mcp | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index c27d2c4363..b94ab1ca93 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -16,6 +16,7 @@ RUN set -ex; \ ca-certificates \ curl \ ; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/* RUN set -eux; \ @@ -115,13 +116,13 @@ RUN set -ex; \ woff2 \ ; \ find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \ + apt-get clean; \ rm -rf /var/lib /var/cache; \ rm -rf /usr/include; \ mkdir -p /opt/data/assets; \ mkdir -p /opt/penpot; \ chown -R penpot:penpot /opt/penpot; \ - chown -R penpot:penpot /opt/data; \ - rm -rf /var/lib/apt/lists/*; + chown -R penpot:penpot /opt/data; COPY --from=build /opt/jre /opt/jre COPY --from=build /opt/node /opt/node diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 27ab66af71..53a081744b 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -20,6 +20,7 @@ RUN set -ex; \ locales \ ca-certificates \ ; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ locale-gen; \ @@ -81,6 +82,7 @@ RUN set -ex; \ libzip4t64 \ libzstd1 \ ; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/*; RUN set -eux; \ diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index 5b9952a34d..f13fbd5e99 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -20,6 +20,7 @@ RUN set -ex; \ locales \ ca-certificates \ ; \ + apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ locale-gen; \ From ddeaf3ce2a5c80a47f1e0beac4ccd142257e2b6c Mon Sep 17 00:00:00 2001 From: Juan de la Cruz <delacruzgarciajuan@gmail.com> Date: Mon, 15 Jun 2026 11:29:25 +0200 Subject: [PATCH 15/17] :sparkles: Add MCP status button to workspace toolbar with single-tab connection control (#9930) * :sparkles: Add MCP connection badge to the workspace toolbar * :sparkles: Add MCP status button with single-tab connection control * :recycle: Extract component for MCP indicator in the toolbar * :recycle: Some improvements --------- Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net> --- frontend/src/app/main/data/workspace.cljs | 3 +- frontend/src/app/main/data/workspace/mcp.cljs | 133 ++++-------------- frontend/src/app/main/refs.cljs | 9 ++ .../app/main/ui/settings/integrations.cljs | 14 +- .../src/app/main/ui/workspace/main_menu.cljs | 26 ++-- .../app/main/ui/workspace/top_toolbar.cljs | 65 ++++++++- .../app/main/ui/workspace/top_toolbar.scss | 99 +++++++++++++ .../data/workspace_mcp_test.cljs | 50 +++++++ frontend/test/frontend_tests/runner.cljs | 2 + frontend/translations/en.po | 12 +- frontend/translations/es.po | 12 +- 11 files changed, 279 insertions(+), 146 deletions(-) create mode 100644 frontend/test/frontend_tests/data/workspace_mcp_test.cljs diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 046f23310d..429ccf95ef 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -504,8 +504,7 @@ (rx/of (dwu/append-undo entry stack-undo?))) (rx/empty)))))) - (rx/take-until stoper-s)) - (rx/of (mcp/notify-other-tabs-disconnect))))) + (rx/take-until stoper-s))))) ptk/EffectEvent (effect [_ _ _] diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 61e1116b49..f2c3c069db 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -10,13 +10,10 @@ [app.common.uri :as u] [app.config :as cf] [app.main.broadcast :as mbc] - [app.main.data.event :as ev] - [app.main.data.notifications :as ntf] [app.main.data.plugins :as dp] [app.main.repo :as rp] [app.main.store :as st] [app.plugins.register :as preg] - [app.util.i18n :refer [tr]] [app.util.timers :as ts] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -39,6 +36,14 @@ (defonce interval-sub (atom nil)) +(defn connect-mcp + [] + (ptk/reify ::connect-mcp + ptk/WatchEvent + (watch [_ _ _] + (rx/of (mbc/event :mcp/force-disconnect {}) + (ptk/data-event ::connect))))) + (defn finalize-workspace? [event] (= (ptk/type event) :app.main.data.workspace/finalize-workspace)) @@ -72,45 +77,6 @@ (rx/dispose! @interval-sub) (reset! interval-sub nil))) -(declare manage-mcp-notification) - -(defn handle-pong - [{:keys [id data]}] - (ptk/reify ::handle-pong - ptk/UpdateEvent - (update [_ state] - (let [mcp-state (get state :mcp)] - (cond - (= "connected" (:connection-status data)) - (update state :mcp assoc :connected-tab id) - - (and (= "disconnected" (:connection-status data)) - (= id (:connected-tab mcp-state))) - (update state :mcp dissoc :connected-tab) - - :else - state))) - - ptk/WatchEvent - (watch [_ _ _] - (rx/of (manage-mcp-notification))))) - -;; This event will arrive when a new workspace is open in another tab -(defn handle-ping - [] - (ptk/reify ::handle-ping - ptk/WatchEvent - (watch [_ state _] - (let [conn-status (get-in state [:mcp :connection-status])] - (rx/of (mbc/event :mcp/pong {:connection-status conn-status})))))) - -(defn notify-other-tabs-disconnect - [] - (ptk/reify ::notify-other-tabs-disconnect - ptk/WatchEvent - (watch [_ _ _] - (rx/of (mbc/event :mcp/pong {:connection-status "disconnected"}))))) - ;; This event will arrive when the mcp is enabled in the dashboard (defn update-mcp-status [value] @@ -121,12 +87,10 @@ ptk/WatchEvent (watch [_ _ _] - (rx/merge - (rx/of (manage-mcp-notification)) - (case value - true (rx/of (ptk/data-event ::connect)) - false (rx/of (ptk/data-event ::disconnect)) - nil))))) + (case value + true (rx/of (connect-mcp)) + false (rx/of (ptk/data-event ::disconnect)) + nil)))) (defn update-mcp-connection-status [value] @@ -137,20 +101,13 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (manage-mcp-notification) - (mbc/event :mcp/pong {:connection-status value}))))) - -(defn connect-mcp - [] - (ptk/reify ::connect-mcp - ptk/UpdateEvent - (update [_ state] - (update state :mcp assoc :connected-tab (:session-id state))) - - ptk/WatchEvent - (watch [_ _ _] - (rx/of (mbc/event :mcp/force-disconect {}) - (ptk/data-event ::connect))))) + ;; Only one MCP plugin instance may be active across browser tabs. + ;; When this tab becomes connected, tell every other tab to + ;; disconnect (which also stops their reconnect watcher). Otherwise + ;; several tabs stay connected at once and the MCP server reports + ;; "multiple instances connected" and the agent fails. + (when (= "connected" value) + (rx/of (mbc/event :mcp/force-disconnect {})))))) ;; This event will arrive when the user selects disconnect on the menu ;; or there is a broadcast message for disconnection @@ -166,33 +123,6 @@ (effect [_ _ _] (stop-reconnect-watcher!)))) -(defn- manage-mcp-notification - [] - (ptk/reify ::manage-mcp-notification - ptk/WatchEvent - (watch [_ state _] - (let [mcp-state (get state :mcp) - - mcp-enabled? (-> state :profile :props :mcp-enabled) - - current-tab-id (get state :session-id) - connected-tab-id (get mcp-state :connected-tab)] - - (if mcp-enabled? - (if (= connected-tab-id current-tab-id) - (rx/of (ntf/hide)) - (rx/of (ntf/dialog - {:content (tr "notifications.mcp.active-in-another-tab") - :cancel {:label (tr "labels.dismiss") - :callback #(st/emit! (ntf/hide) - (ev/event {::ev/name "dismiss-mcp-tab-switch" - ::ev/origin "workspace-notification"}))} - :accept {:label (tr "labels.switch") - :callback #(st/emit! (connect-mcp) - (ev/event {::ev/name "confirm-mcp-tab-switch" - ::ev/origin "workspace-notification"}))}}))) - (rx/of (ntf/hide))))))) - (defn init-mcp [stream] ;; Wait for plugins runtime to be initialized before starting the MCP plugin. @@ -240,7 +170,7 @@ (ptk/reify ::init ptk/UpdateEvent (update [_ state] - (update state :mcp assoc :connected-tab (:session-id state) :active true)) + (update state :mcp assoc :active true)) ptk/WatchEvent (watch [_ state stream] @@ -255,22 +185,8 @@ (rx/merge (init-mcp stream) - (rx/of (mbc/event :mcp/ping {})) - (->> mbc/stream - (rx/filter (mbc/type? :mcp/ping)) - (rx/filter (fn [{:keys [id]}] - (not= session-id id))) - (rx/map handle-ping)) - - (->> mbc/stream - (rx/filter (mbc/type? :mcp/pong)) - (rx/filter (fn [{:keys [id]}] - (not= session-id id))) - (rx/map handle-pong)) - - (->> mbc/stream - (rx/filter (mbc/type? :mcp/force-disconect)) + (rx/filter (mbc/type? :mcp/force-disconnect)) (rx/filter (fn [{:keys [id]}] (not= session-id id))) (rx/map deref) @@ -280,9 +196,9 @@ (->> mbc/stream (rx/filter (mbc/type? :mcp/enable)) (rx/mapcat (fn [_] - ;; NOTE: we don't need an explicit - ;; connect because the plugin has - ;; auto-connect + ;; Re-init so the force-disconnect + ;; listener is set up now that MCP + ;; is enabled. (rx/of (update-mcp-status true) (init))))) @@ -290,7 +206,6 @@ (rx/filter (mbc/type? :mcp/disable)) (rx/mapcat (fn [_] (rx/of (update-mcp-status false) - (init) (user-disconnect-mcp)))))) (rx/take-until stoper-s)))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index cc77c0a14d..b87b0eff43 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cph] + [app.common.time :as ct] [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.common.types.tokens-lib :as ctob] @@ -155,6 +156,14 @@ (def mcp (l/derived :mcp st/state)) +(def mcp-key-expired? + (l/derived (fn [state] + (when-let [expires-at (some->> (:access-tokens state) + (some #(when (= (:type %) "mcp") %)) + :expires-at)] + (> (ct/now) expires-at))) + st/state)) + (def workspace-drawing (l/derived :workspace-drawing st/state)) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 50b5bd57bd..e7d51438bc 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -406,18 +406,16 @@ (mf/defc mcp-server-section* {::mf/private true} [] - (let [tokens (mf/deref refs/access-tokens) - profile (mf/deref refs/profile) + (let [tokens (mf/deref refs/access-tokens) + profile (mf/deref refs/profile) + mcp-key-expired? (mf/deref refs/mcp-key-expired?) mcp-key (some #(when (= (:type %) "mcp") %) tokens) mcp-token (:token mcp-key "") mcp-url (dm/str cf/mcp-server-url "?userToken=" mcp-token) mcp-enabled? (true? (-> profile :props :mcp-enabled)) - expires-at (:expires-at mcp-key) - expired? (and (some? expires-at) (> (ct/now) expires-at)) - - show-enabled? (and mcp-enabled? (false? expired?)) + show-enabled? (and mcp-enabled? (false? mcp-key-expired?)) tooltip-id (mf/use-id) @@ -494,7 +492,7 @@ (tr "integrations.mcp-server.status")] [:div {:class (stl/css :mcp-server-block)} - (when expired? + (when mcp-key-expired? [:> notification-pill* {:level :error :type :context} [:div {:class (stl/css :mcp-server-notification)} @@ -517,7 +515,7 @@ (when (and (false? mcp-enabled?) (nil? mcp-key)) [:div {:class (stl/css :mcp-server-switch-cover) :on-click handle-generate-mcp-key}]) - (when (true? expired?) + (when (true? mcp-key-expired?) [:div {:class (stl/css :mcp-server-switch-cover) :on-click handle-regenerate-mcp-key}])]]] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 20a50c729a..53d51a7970 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -10,7 +10,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] - [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.common :as dcm] @@ -791,20 +790,15 @@ [{:keys [on-close]}] (let [plugins? (features/active-feature? @st/state "plugins/runtime") - profile (mf/deref refs/profile) - mcp (mf/deref refs/mcp) - tokens (mf/deref refs/access-tokens) - - expires-at (some->> tokens - (some #(when (= (:type %) "mcp") %)) - :expires-at) - expired? (and (some? expires-at) (> (ct/now) expires-at)) + profile (mf/deref refs/profile) + mcp (mf/deref refs/mcp) + mcp-key-expired? (mf/deref refs/mcp-key-expired?) mcp-enabled? (true? (-> profile :props :mcp-enabled)) mcp-connection (get mcp :connection-status) mcp-connected? (= mcp-connection "connected") - show-enabled? (and mcp-enabled? (false? expired?)) + show-enabled? (and mcp-enabled? (false? mcp-key-expired?)) on-nav-to-integrations (mf/use-fn @@ -843,7 +837,7 @@ :pos-6 plugins?) :on-close on-close} - (when (and show-enabled? (not expired?)) + (when (and show-enabled? (not mcp-key-expired?)) [:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin" :class (stl/css :base-menu-item :submenu-item) :on-click on-toggle-mcp-plugin @@ -865,7 +859,6 @@ (mf/defc menu* [{:keys [layout file]}] (let [profile (mf/deref refs/profile) - mcp (mf/deref refs/mcp) show-menu* (mf/use-state false) show-menu? (deref show-menu*) @@ -1062,11 +1055,8 @@ :class (stl/css :item-arrow)}]]) (when (contains? cf/flags :mcp) - (let [tokens (mf/deref refs/access-tokens) - expires-at (some->> tokens - (some #(when (= (:type %) "mcp") %)) - :expires-at) - expired? (and (some? expires-at) (> (ct/now) expires-at)) + (let [mcp (mf/deref refs/mcp) + mcp-key-expired? (mf/deref refs/mcp-key-expired?) mcp-enabled? (true? (-> profile :props :mcp-enabled)) mcp-connection (get mcp :connection-status) @@ -1075,7 +1065,7 @@ active? (and mcp-enabled? mcp-connected?) failed? (or (and mcp-enabled? mcp-error?) - (true? expired?))] + (true? mcp-key-expired?))] [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 67ae4d8956..6b36ae2664 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -8,15 +8,18 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.geom.point :as gpt] + [app.config :as cf] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.shortcuts :as sc] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.context :as ctx] [app.main.ui.icons :as deprecated-icon] @@ -26,6 +29,61 @@ [okulary.core :as l] [rumext.v2 :as mf])) +(mf/defc mcp-indicator* + [] + (let [profile (mf/deref refs/profile) + mcp (mf/deref refs/mcp) + mcp-key-expired? (mf/deref refs/mcp-key-expired?) + + mcp-enabled? (true? (-> profile :props :mcp-enabled)) + mcp-connected? (= "connected" (:connection-status mcp)) + show-indicator? (and mcp-enabled? (false? mcp-key-expired?)) + + mcp-menu-open* (mf/use-state false) + mcp-menu-open? (deref mcp-menu-open*) + + toggle-mcp-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! mcp-menu-open* not))) + + close-mcp-menu + (mf/use-fn + #(reset! mcp-menu-open* false)) + + connect-mcp + (mf/use-fn + #(st/emit! (mcp/connect-mcp) + (ev/event {::ev/name "connect-mcp-plugin" + ::ev/origin "workspace:toolbar"})))] + (when show-indicator? + [:li + [:button + {:title (tr "workspace.toolbar.mcp") + :aria-label (tr "workspace.toolbar.mcp") + :class (stl/css-case :main-toolbar-options-button true + :mcp-button true + :selected mcp-menu-open?) + :on-click toggle-mcp-menu + :data-tool "mcp" + :data-testid "mcp-btn"} + [:span {:class (stl/css-case :mcp-status-dot true + :connected mcp-connected?)}] + [:span {:class (stl/css-case :mcp-button-label true + :connected mcp-connected?)} + (tr "workspace.toolbar.mcp")]] + [:> dropdown-menu* {:show mcp-menu-open? + :on-close close-mcp-menu + :class (stl/css :mcp-menu)} + (if mcp-connected? + [:li {:class (stl/css :mcp-menu-info) + :role "presentation"} + (tr "workspace.toolbar.mcp-connected")] + [:> dropdown-menu-item* {:class (stl/css :mcp-menu-item) + :on-click connect-mcp} + (tr "workspace.toolbar.mcp-connect-here")])]]))) + (mf/defc image-upload* {::mf/wrap [mf/memo]} [] @@ -221,12 +279,13 @@ {:title "Debugging tool" :class (stl/css-case :main-toolbar-options-button true :selected (contains? layout :debug-panel)) :on-click toggle-debug-panel} - deprecated-icon/bug]])]] + deprecated-icon/bug]]) + + (when (contains? cf/flags :mcp) + [:> mcp-indicator*])]] [:button {:title (tr "workspace.toolbar.toggle-toolbar") :aria-label (tr "workspace.toolbar.toggle-toolbar") :class (stl/css :toolbar-handler) :on-click toggle-toolbar} [:div {:class (stl/css :toolbar-handler-btn)}]]]))) - - diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index e543964db0..7a185eb692 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -5,6 +5,9 @@ // Copyright (c) KALEIDOS INC Sucursal en España SL @use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/typography.scss" as t; .main-toolbar { cursor: initial; @@ -83,6 +86,102 @@ } } +.mcp-button { + display: flex; + align-items: center; + gap: var(--sp-xs); + width: fit-content; + margin-inline-start: var(--sp-xs); + padding-inline: var(--sp-s); +} + +.mcp-button-label { + @include t.use-typography("body-small"); + + &.connected { + color: var(--color-accent-primary); + } +} + +.mcp-status-dot { + // Connection indicator placed before the label, vertically centered: + // a muted gray when disconnected, the primary accent when connected. + position: relative; + flex-shrink: 0; + inline-size: px2rem(6); + block-size: px2rem(6); + border-radius: 50%; + background-color: var(--color-background-disabled); + + &.connected { + background-color: var(--color-accent-primary); + } + + // One-shot "blob" ripple that confirms the moment the tab becomes + // connected (e.g. after using "Switch here"). Triggered purely by the + // `.connected` class appearing, in sync with the color change. + &.connected::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 50%; + background-color: var(--color-accent-primary); + opacity: 0; + animation: mcp-status-blob 0.5s ease-out; + } +} + +@keyframes mcp-status-blob { + from { + opacity: 0.5; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(2.5); + } +} + +.mcp-menu { + @include deprecated.menu-shadow; + + position: absolute; + inset-block-start: calc(100% + var(--sp-xs)); + inset-inline-start: var(--sp-xs); + z-index: var(--z-index-dropdown); + margin: 0; + padding: var(--sp-xs); + list-style: none; + border: $b-1 solid var(--panel-border-color); + border-radius: $br-8; + background-color: var(--menu-background-color); +} + +// Non-interactive informational text inside the menu (no hover, not focusable). +.mcp-menu-info { + @include t.use-typography("body-small"); + + padding: var(--sp-s) var(--sp-m); + color: var(--color-foreground-secondary); + white-space: nowrap; +} + +.mcp-menu-item { + @include t.use-typography("body-small"); + + display: flex; + align-items: center; + padding: var(--sp-s) var(--sp-m); + border-radius: $br-8; + color: var(--menu-foreground-color); + white-space: nowrap; + + &:hover { + background-color: var(--menu-background-color-hover); + } +} + .toolbar-handler { @include deprecated.flex-center; @include deprecated.button-style; diff --git a/frontend/test/frontend_tests/data/workspace_mcp_test.cljs b/frontend/test/frontend_tests/data/workspace_mcp_test.cljs new file mode 100644 index 0000000000..e81dab6780 --- /dev/null +++ b/frontend/test/frontend_tests/data/workspace_mcp_test.cljs @@ -0,0 +1,50 @@ +;; 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.data.workspace-mcp-test + (:require + [app.main.data.workspace.mcp :as mcp] + [cljs.test :as t :include-macros true] + [potok.v2.core :as ptk])) + +(t/deftest test-set-mcp-active + (t/testing "sets :active to true" + (let [state {:mcp {:active false}} + result (ptk/update (mcp/set-mcp-active true) state)] + (t/is (true? (get-in result [:mcp :active]))))) + + (t/testing "sets :active to false" + (let [state {:mcp {:active true}} + result (ptk/update (mcp/set-mcp-active false) state)] + (t/is (false? (get-in result [:mcp :active])))))) + +(t/deftest test-update-mcp-status + (t/testing "enables MCP in profile props" + (let [state {:profile {:props {:mcp-enabled false}}} + result (ptk/update (mcp/update-mcp-status true) state)] + (t/is (true? (get-in result [:profile :props :mcp-enabled]))))) + + (t/testing "disables MCP in profile props" + (let [state {:profile {:props {:mcp-enabled true}}} + result (ptk/update (mcp/update-mcp-status false) state)] + (t/is (false? (get-in result [:profile :props :mcp-enabled])))))) + +(t/deftest test-update-mcp-connection-status + (t/testing "sets connection status to connected" + (let [state {:mcp {:connection-status "disconnected"}} + result (ptk/update (mcp/update-mcp-connection-status "connected") state)] + (t/is (= "connected" (get-in result [:mcp :connection-status]))))) + + (t/testing "sets connection status to disconnected" + (let [state {:mcp {:connection-status "connected"}} + result (ptk/update (mcp/update-mcp-connection-status "disconnected") state)] + (t/is (= "disconnected" (get-in result [:mcp :connection-status])))))) + +(t/deftest test-init-sets-active + (t/testing "init sets :mcp :active to true" + (let [state {:mcp {:active false}} + result (ptk/update (mcp/init) state)] + (t/is (true? (get-in result [:mcp :active])))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 5f9078f910..c1a9025f90 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -7,6 +7,7 @@ [frontend-tests.data.uploads-test] [frontend-tests.data.viewer-test] [frontend-tests.data.workspace-colors-test] + [frontend-tests.data.workspace-mcp-test] [frontend-tests.data.workspace-media-test] [frontend-tests.data.workspace-texts-test] [frontend-tests.data.workspace-thumbnails-test] @@ -55,6 +56,7 @@ 'frontend-tests.data.uploads-test 'frontend-tests.data.viewer-test 'frontend-tests.data.workspace-colors-test + 'frontend-tests.data.workspace-mcp-test 'frontend-tests.data.workspace-media-test 'frontend-tests.data.workspace-texts-test 'frontend-tests.data.workspace-thumbnails-test diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3ad9c6e30c..6ee5b9c4fa 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4043,9 +4043,6 @@ msgstr "Invitation sent successfully" msgid "notifications.invitation-link-copied" msgstr "Invitation link copied" -msgid "notifications.mcp.active-in-another-tab" -msgstr "MCP is active in another tab. Switch here?" - msgid "notifications.mcp.active-in-this-tab" msgstr "MCP is now active in this tab." @@ -8986,6 +8983,15 @@ msgstr "Create board. Click and drag to define its size. (%s)" msgid "workspace.toolbar.image" msgstr "Image (%s)" +msgid "workspace.toolbar.mcp" +msgstr "MCP" + +msgid "workspace.toolbar.mcp-connected" +msgstr "MCP connected" + +msgid "workspace.toolbar.mcp-connect-here" +msgstr "Connect here" + #: src/app/main/ui/workspace/top_toolbar.cljs:143 msgid "workspace.toolbar.move" msgstr "Move (%s)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index f72aff6a1c..d0a915e4a8 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3952,9 +3952,6 @@ msgstr "Invitación enviada con éxito" msgid "notifications.invitation-link-copied" msgstr "Enlace de invitacion copiado" -msgid "notifications.mcp.active-in-another-tab" -msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?" - msgid "notifications.mcp.active-in-this-tab" msgstr "MCP está ahora activo en esta pestaña." @@ -8708,6 +8705,15 @@ msgstr "Crear tablero. Click y arrastrar para definir el tamaño. (%s)" msgid "workspace.toolbar.image" msgstr "Imagen (%s)" +msgid "workspace.toolbar.mcp" +msgstr "MCP" + +msgid "workspace.toolbar.mcp-connected" +msgstr "MCP conectado" + +msgid "workspace.toolbar.mcp-connect-here" +msgstr "Conectar aquí" + #: src/app/main/ui/workspace/top_toolbar.cljs:143 msgid "workspace.toolbar.move" msgstr "Mover (%s)" From 79227c4de84bf2622beb62c815a1c82724209d63 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Mon, 15 Jun 2026 12:01:32 +0200 Subject: [PATCH 16/17] :sparkles: Batch multiple thumbnail deletions into a single RPC call (#9943) * :sparkles: Batch multiple thumbnail deletions into a single RPC call Replace the old per-object immediate thumbnail deletion with a debounced batched approach. The frontend queues object-ids in state and waits 200ms before sending a single RPC request with up to 200 object-ids. The backend deletes all matching thumbnails in one SQL statement with a single RETURNING clause, then touches the affected media objects. This reduces RPC overhead when rapidly clearing thumbnails (e.g. navigating pages) and makes deletions more efficient. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * :paperclip: Fix missing issues --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> --- .../src/app/rpc/commands/files_thumbnails.clj | 43 ++ .../rpc_file_thumbnails_test.clj | 535 ++++++++++++++++++ .../app/main/data/workspace/thumbnails.cljs | 97 +++- .../data/workspace_thumbnails_test.cljs | 266 ++++++++- .../test/frontend_tests/helpers/mock.cljs | 103 ++++ 5 files changed, 1013 insertions(+), 31 deletions(-) create mode 100644 frontend/test/frontend_tests/helpers/mock.cljs diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index dd9c031232..4596dbd15f 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -311,11 +311,30 @@ :object-id object-id :tag tag}))) +(defn- delete-file-object-thumbnails! + "Soft-deletes multiple object thumbnails in a single UPDATE statement + with RETURNING, then touches all returned media objects." + [{:keys [::db/conn ::sto/storage]} object-ids] + (let [ids (db/create-array conn "text" (seq object-ids)) + sql (str/concat + "UPDATE file_tagged_object_thumbnail" + " SET deleted_at = now()" + " WHERE object_id = ANY(?)" + " AND deleted_at IS NULL" + " RETURNING media_id") + rows (db/exec! conn [sql ids])] + (doseq [{:keys [media-id]} rows] + (sto/touch-object! storage media-id)))) + (def ^:private schema:delete-file-object-thumbnail [:map {:title "delete-file-object-thumbnail"} [:file-id ::sm/uuid] [:object-id [:string {:max 250}]]]) +(def ^:private schema:delete-file-object-thumbnails + [:map {:title "delete-file-object-thumbnails"} + [:object-ids [:vector {:max 200} [:string {:max 250}]]]]) + (sv/defmethod ::delete-file-object-thumbnail {::doc/added "1.19" ::doc/module :files @@ -329,6 +348,30 @@ (delete-file-object-thumbnail! file-id object-id)) nil))) +(sv/defmethod ::delete-file-object-thumbnails + {::doc/added "1.19" + ::doc/module :files + ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] + [:file-thumbnail-ops/global]] + ::sm/params schema:delete-file-object-thumbnails + ::audit/skip true} + [cfg {:keys [::rpc/profile-id object-ids]}] + (when (seq object-ids) + ;; Extract unique file-ids from object-ids for permission checks + (let [file-ids (->> object-ids + (map thc/get-file-id) + (into #{}))] + ;; Check permissions for each unique file using a single connection + (db/run! cfg (fn [{:keys [::db/conn]}] + (doseq [file-id file-ids] + (files/check-edition-permissions! conn profile-id file-id)))) + ;; Delete all matching thumbnails in one transaction + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (-> cfg + (update ::sto/storage sto/configure conn) + (delete-file-object-thumbnails! object-ids)) + nil))))) + ;; --- MUTATION COMMAND: create-file-thumbnail (defn- create-file-thumbnail diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 8f24ccb580..000735df0c 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -354,3 +354,538 @@ (t/is (nil? (:error out))) (t/is (map? (:result out)))))) +;; --- delete-file-object-thumbnails (batch) + +(t/deftest delete-file-object-thumbnails-basic + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid1 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid3 (thc/fmt-object-id (:id file) page-id (uuid/random) "component")] + + ;; Create three thumbnails + (doseq [oid [oid1 oid2 oid3]] + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out))))) + + ;; Verify all three exist and are not soft-deleted + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false + :order-by [[:created-at :asc]]})] + (t/is (= 3 (count rows))) + (doseq [row rows] + (t/is (nil? (:deleted-at row))))) + + ;; Batch delete all three + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1 oid2 oid3]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify all three are now soft-deleted + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false + :order-by [[:created-at :asc]]})] + (t/is (= 3 (count rows))) + (doseq [row rows] + (t/is (some? (:deleted-at row))))))) + +(t/deftest delete-file-object-thumbnails-empty + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids []} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + +(t/deftest delete-file-object-thumbnails-non-existent + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid1 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Batch delete non-existent object-ids (no thumbnails were created) + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1 oid2]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))))) + +(t/deftest delete-file-object-thumbnails-mixed-exists + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid1 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid3 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Create only one thumbnail + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid1 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; Batch delete mix of existing and non-existing + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1 oid2 oid3]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify oid1 is soft-deleted, others don't exist + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows))) + (t/is (= oid1 (:object-id (first rows)))) + (t/is (some? (:deleted-at (first rows))))))) + +(t/deftest delete-file-object-thumbnails-already-deleted + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Create a thumbnail + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; First batch delete + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Second batch delete (idempotent — no rows match deleted_at IS NULL) + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify still 1 row, still soft-deleted, not duplicated + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows))) + (t/is (= oid (:object-id (first rows)))) + (t/is (some? (:deleted-at (first rows))))))) + +(t/deftest delete-file-object-thumbnails-unauthorized + (let [profile1 (th/create-profile* 1) + profile2 (th/create-profile* 2) + file (th/create-file* 1 {:profile-id (:id profile1) + :project-id (:default-project-id profile1) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; profile1 creates a thumbnail on their file + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile1) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; profile2 tries to batch delete thumbnails from profile1's file + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile2) + :object-ids [oid]} + out (th/command! data)] + (t/is (some? (:error out))) + (t/is (th/ex-info? (:error out))) + (t/is (= :not-found (th/ex-type (:error out))))) + + ;; Verify the thumbnail is NOT deleted + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows))) + (t/is (nil? (:deleted-at (first rows))))))) + +(t/deftest delete-file-object-thumbnails-cross-file + (let [profile (th/create-profile* 1) + file1 (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + file2 (th/create-file* 2 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page1-id (first (get-in file1 [:data :pages])) + page2-id (first (get-in file2 [:data :pages])) + oid1 (thc/fmt-object-id (:id file1) page1-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file2) page2-id (uuid/random) "frame")] + + ;; Create thumbnails on both files + (doseq [[oid fid] [[oid1 (:id file1)] [oid2 (:id file2)]]] + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id fid + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out))))) + + ;; Batch delete from both files in one call + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1 oid2]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify both are soft-deleted + (let [rows1 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file1)} + {::db/remove-deleted false}) + rows2 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file2)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows1))) + (t/is (some? (:deleted-at (first rows1)))) + (t/is (= 1 (count rows2))) + (t/is (some? (:deleted-at (first rows2))))))) + +(t/deftest delete-file-object-thumbnails-cross-file-unauthorized + (let [profile1 (th/create-profile* 1) + profile2 (th/create-profile* 2) + file1 (th/create-file* 1 {:profile-id (:id profile1) + :project-id (:default-project-id profile1) + :is-shared false}) + file2 (th/create-file* 2 {:profile-id (:id profile2) + :project-id (:default-project-id profile2) + :is-shared false}) + page1-id (first (get-in file1 [:data :pages])) + page2-id (first (get-in file2 [:data :pages])) + oid1 (thc/fmt-object-id (:id file1) page1-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file2) page2-id (uuid/random) "frame")] + + ;; Create thumbnails on both files (by their respective owners) + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile1) + :file-id (:id file1) + :object-id oid1 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile2) + :file-id (:id file2) + :object-id oid2 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; profile1 tries to batch delete thumbnails from both files + ;; (profile1 does NOT have access to file2) + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile1) + :object-ids [oid1 oid2]} + out (th/command! data)] + (t/is (some? (:error out))) + (t/is (th/ex-info? (:error out))) + (t/is (= :not-found (th/ex-type (:error out))))) + + ;; Verify NEITHER thumbnail was deleted (all-or-nothing) + (let [rows1 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file1)} + {::db/remove-deleted false}) + rows2 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file2)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows1))) + (t/is (nil? (:deleted-at (first rows1)))) + (t/is (= 1 (count rows2))) + (t/is (nil? (:deleted-at (first rows2))))))) + +(t/deftest delete-file-object-thumbnails-media-touch + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid1 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Create two thumbnails + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid1 + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid2 + :media {:filename "sample.jpg" + :size 312043 + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; Get media-ids for both thumbnails + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {:order-by [[:created-at :asc]]}) + mid1 (:media-id (first rows)) + mid2 (:media-id (second rows))] + + ;; Verify storage objects exist (they are created with touched-at already set) + (t/is (some? (th/db-get :storage-object {:id mid1}))) + (t/is (some? (th/db-get :storage-object {:id mid2}))) + + ;; Batch delete both thumbnails + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1 oid2]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; After soft-delete, storage objects should STILL exist + ;; (they are only garbage-collected later by storage-gc-touched task) + (t/is (some? (th/db-get :storage-object {:id mid1}))) + (t/is (some? (th/db-get :storage-object {:id mid2})))))) + +(t/deftest delete-file-object-thumbnails-max-batch + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + cnt 200 + oids (vec (repeatedly cnt + #(thc/fmt-object-id (:id file) page-id + (uuid/random) "frame")))] + + ;; Create 200 thumbnails + (doseq [oid oids] + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out))))) + + ;; Verify all 200 exist + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= cnt (count rows)))) + + ;; Batch delete all 200 in one call + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids oids} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify all 200 are now soft-deleted + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= cnt (count rows))) + (doseq [row rows] + (t/is (some? (:deleted-at row))))))) + +(t/deftest delete-file-object-thumbnails-single + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Create a single thumbnail + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; Batch delete just one + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify it's soft-deleted + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows))) + (t/is (some? (:deleted-at (first rows))))))) + +(t/deftest delete-file-object-thumbnails-same-object-twice-in-batch + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (first (get-in file [:data :pages])) + oid (thc/fmt-object-id (:id file) page-id (uuid/random) "frame")] + + ;; Create one thumbnail + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + ;; Batch delete with the same object-id listed twice + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid oid]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify it's soft-deleted (only one row) + (let [rows (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows))) + (t/is (some? (:deleted-at (first rows))))))) + +(t/deftest delete-file-object-thumbnails-keeps-other-files-intact + (let [profile (th/create-profile* 1) + file1 (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + file2 (th/create-file* 2 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page1-id (first (get-in file1 [:data :pages])) + page2-id (first (get-in file2 [:data :pages])) + oid1 (thc/fmt-object-id (:id file1) page1-id (uuid/random) "frame") + oid2 (thc/fmt-object-id (:id file2) page2-id (uuid/random) "frame")] + + ;; Create thumbnails on both files + (doseq [[oid fid] [[oid1 (:id file1)] [oid2 (:id file2)]]] + (let [data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id fid + :object-id oid + :media {:filename "sample.jpg" + :size 7923 + :path (th/tempfile "backend_tests/test_files/sample2.jpg") + :mtype "image/jpeg"}} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out))))) + + ;; Delete only thumbnail from file1 + (let [data {::th/type :delete-file-object-thumbnails + ::rpc/profile-id (:id profile) + :object-ids [oid1]} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + ;; Verify file1's thumbnail is deleted, file2's is not + (let [rows1 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file1)} + {::db/remove-deleted false}) + rows2 (th/db-query :file-tagged-object-thumbnail + {:file-id (:id file2)} + {::db/remove-deleted false})] + (t/is (= 1 (count rows1))) + (t/is (some? (:deleted-at (first rows1)))) + (t/is (= 1 (count rows2))) + (t/is (nil? (:deleted-at (first rows2))))))) + diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 80c83f6153..3770a24a4a 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -85,40 +85,70 @@ (let [request (create-request file-id page-id shape-id tag)] (q/enqueue-unique queue request (partial render-thumbnail state file-id page-id shape-id tag)))) +(defn- clear-thumbnail-batch + [] + (let [pending (volatile! nil)] + (ptk/reify ::clear-thumbnail-batch + ptk/UpdateEvent + (update [_ state] + (let [items (get state ::thumbnails-deletion-queue)] + (when (seq items) + (vreset! pending items)) + (dissoc state ::thumbnails-deletion-queue))) + + ptk/WatchEvent + (watch [_ _ _] + (let [items (reduce-kv (fn [acc object-id uri] + (when (str/starts-with? uri "blob:") + (tm/schedule-on-idle (partial wapi/revoke-uri uri))) + (conj acc object-id)) + [] + @pending)] + (l/dbg :hint "clear-thumbnail-batch" :total (count items)) + (->> (rx/from (partition-all 200 items)) + (rx/mapcat + (fn [batch] + (l/dbg :hint "clear-thumbnail-batch" :batch-size (count batch)) + (->> (rp/cmd! :delete-file-object-thumbnails + {:object-ids (vec batch)}) + (rx/catch rx/empty) + (rx/ignore)))))))))) + +(defn remove-from-deletion-queue + "Removes an object-id from the pending deletion queue in state. + Used by update-thumbnail to cancel a pending batched delete before + creating a new thumbnail for the same object." + [object-id] + (ptk/reify ::remove-from-deletion-queue + ptk/UpdateEvent + (update [_ state] + (update state ::thumbnails-deletion-queue dissoc object-id)))) + (defn clear-thumbnail ([file-id page-id frame-id tag] (clear-thumbnail file-id (thc/fmt-object-id file-id page-id frame-id tag))) - ([file-id object-id] - (let [pending (volatile! false)] - (ptk/reify ::clear-thumbnail - cljs.core/IDeref - (-deref [_] object-id) + ([_file-id object-id] + (ptk/reify ::clear-thumbnail + cljs.core/IDeref + (-deref [_] object-id) - ptk/UpdateEvent - (update [_ state] + ptk/UpdateEvent + (update [_ state] + (let [uri (dm/get-in state [:thumbnails object-id])] + (l/dbg :hint "clear-thumbnail" :object-id object-id :uri uri) (-> state - (update :thumbnails - (fn [thumbs] - (if-let [uri (get thumbs object-id)] - (do (vreset! pending uri) - (dissoc thumbs object-id)) - thumbs))) - (update :thumbnails-meta dissoc object-id))) + (update ::thumbnails-deletion-queue assoc object-id uri) + (update :thumbnails dissoc object-id) + (update :thumbnails-meta dissoc object-id)))) - ptk/WatchEvent - (watch [_ _ _] - (if-let [uri @pending] - (do - (l/trc :hint "clear-thumbnail" :uri uri) - (when (str/starts-with? uri "blob:") - (tm/schedule-on-idle (partial wapi/revoke-uri uri))) - - (let [params {:file-id file-id - :object-id object-id}] - (->> (rp/cmd! :delete-file-object-thumbnail params) - (rx/catch rx/empty) - (rx/ignore)))) - (rx/empty))))))) + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper-s (->> stream + (rx/filter (ptk/type? ::clear-thumbnail)))] + (->> (rx/timer 200) + (rx/take 1) + (rx/map (fn [_] (clear-thumbnail-batch))) + (rx/take-until stopper-s))))))) (defn assoc-thumbnail [object-id uri] @@ -173,7 +203,8 @@ :tag (or tag "frame")}] (rx/merge - (rx/of (assoc-thumbnail object-id uri)) + (rx/of (assoc-thumbnail object-id uri) + (remove-from-deletion-queue object-id)) (->> (rp/cmd! :create-file-object-thumbnail params) (rx/catch rx/empty) (rx/ignore)))))) @@ -305,6 +336,14 @@ ;; and interrupt any ongoing update-thumbnail process ;; related to current frame-id (->> all-commits-s + ;; Ensure each clear-thumbnail event is dispatched in its + ;; own macrotask tick. Without this, multiple changes + ;; arriving on the same synchronous tick would emit + ;; several clear-thumbnail events back-to-back, causing + ;; their debounce timers (rx/take-until stopper-s) to + ;; race and potentially leave multiple clear-thumbnail-batch + ;; timers alive simultaneously. + (rx/observe-on :async) (rx/mapcat (fn [[tag frame-id]] (rx/of (clear-thumbnail file-id page-id frame-id tag))))) diff --git a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs index f9c21e9be5..2ccf1c51fc 100644 --- a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs @@ -6,9 +6,18 @@ (ns frontend-tests.data.workspace-thumbnails-test (:require + [app.common.thumbnails :as thc] [app.common.uuid :as uuid] [app.main.data.workspace.thumbnails :as thumbnails] - [cljs.test :as t :include-macros true])) + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock] + [potok.v2.core :as ptk])) + +;; The qualified keyword used internally by app.main.data.workspace.thumbnails +;; for tracking the pending deletion queue in application state. +(def ^:private deletion-queue-key + :app.main.data.workspace.thumbnails/thumbnails-deletion-queue) (t/deftest extract-frame-changes-handles-cyclic-frame-links (let [page-id (uuid/next) @@ -33,4 +42,257 @@ :component-root true}}}}}] (t/is (= #{["frame" root-id] ["component" shape-b-id]} - (#'thumbnails/extract-frame-changes page-id [event [old-data new-data]]))))) + (#'thumbnails/extract-frame-changes page-id [event [old-data new-data]])))) + + ;; --- Batch deletion queue state management --- + + (t/deftest clear-thumbnail-adds-to-deletion-queue + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/test-thumb" + event (thumbnails/clear-thumbnail file-id object-id) + state {:thumbnails {object-id uri}} + result (ptk/update event state)] + ;; Thumbnail removed from the map + (t/is (nil? (get-in result [:thumbnails object-id]))) + ;; Object-id added to the deletion queue with its URI + (t/is (= uri (get-in result [deletion-queue-key object-id]))))) + + (t/deftest clear-thumbnail-keeps-other-thumbnails + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event (thumbnails/clear-thumbnail file-id object-id1) + state {:thumbnails {object-id1 uri1 object-id2 uri2}} + result (ptk/update event state)] + ;; Only the cleared thumbnail is removed + (t/is (nil? (get-in result [:thumbnails object-id1]))) + (t/is (= uri2 (get-in result [:thumbnails object-id2]))) + ;; Only the cleared thumbnail is queued + (t/is (= uri1 (get-in result [deletion-queue-key object-id1]))) + (t/is (nil? (get-in result [deletion-queue-key object-id2]))))) + + (t/deftest clear-thumbnail-accumulates-in-queue + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event1 (thumbnails/clear-thumbnail file-id object-id1) + event2 (thumbnails/clear-thumbnail file-id object-id2) + state {:thumbnails {object-id1 uri1 object-id2 uri2}} + state1 (ptk/update event1 state) + state2 (ptk/update event2 state1)] + ;; Both removed from thumbnails + (t/is (nil? (get-in state2 [:thumbnails object-id1]))) + (t/is (nil? (get-in state2 [:thumbnails object-id2]))) + ;; Both accumulated in the queue + (t/is (= uri1 (get-in state2 [deletion-queue-key object-id1]))) + (t/is (= uri2 (get-in state2 [deletion-queue-key object-id2]))) + (t/is (= 2 (count (get state2 deletion-queue-key)))))) + + (t/deftest remove-from-deletion-queue-removes-entry + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + event (thumbnails/remove-from-deletion-queue object-id) + state {deletion-queue-key {object-id "blob:http://localhost/thumb"}} + result (ptk/update event state)] + (t/is (nil? (get-in result [deletion-queue-key object-id]))) + (t/is (empty? (get result deletion-queue-key))))) + + (t/deftest remove-from-deletion-queue-keeps-other-entries + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event (thumbnails/remove-from-deletion-queue object-id1) + state {deletion-queue-key {object-id1 uri1 + object-id2 uri2}} + result (ptk/update event state)] + ;; Only the specified entry is removed + (t/is (nil? (get-in result [deletion-queue-key object-id1]))) + (t/is (= uri2 (get-in result [deletion-queue-key object-id2]))) + (t/is (= 1 (count (get result deletion-queue-key)))))) + + (t/deftest remove-before-clear-cancels-pending-delete + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/thumb" + ;; Step 1: clear-thumbnail queues the delete + state1 (ptk/update (thumbnails/clear-thumbnail file-id object-id) + {:thumbnails {object-id uri}}) + ;; Step 2: remove-from-deletion-queue cancels the pending delete + state2 (ptk/update (thumbnails/remove-from-deletion-queue object-id) + state1)] + ;; Thumbnail was removed from :thumbnails map by clear-thumbnail + (t/is (nil? (get-in state2 [:thumbnails object-id]))) + ;; But the deletion queue entry was cancelled by remove-from-deletion-queue + (t/is (nil? (get-in state2 [deletion-queue-key object-id]))) + (t/is (empty? (get state2 deletion-queue-key))))) + + (t/deftest clear-thumbnail-batch-drains-queue + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + ;; Build up the queue state manually (simulating accumulated clear-thumbnails) + state {deletion-queue-key {object-id1 uri1 object-id2 uri2}} + event (#'thumbnails/clear-thumbnail-batch) + result (ptk/update event state)] + ;; The queue is drained from application state + (t/is (empty? (get result deletion-queue-key))))) + + (t/deftest clear-thumbnail-batch-empty-queue-noop + (let [state {deletion-queue-key {}} + event (#'thumbnails/clear-thumbnail-batch) + result (ptk/update event state)] + ;; State unchanged when queue is already empty + (t/is (empty? (get result deletion-queue-key))) + (t/is (= state (dissoc result deletion-queue-key))))) + + (t/deftest assoc-thumbnail-adds-to-map + (let [object-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/new-thumb" + event (#'thumbnails/assoc-thumbnail object-id uri) + state {:thumbnails {}} + result (ptk/update event state)] + (t/is (= uri (get-in result [:thumbnails object-id]))))) + + (t/deftest duplicate-thumbnail-copies-entry + (let [old-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + new-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/dup-thumb" + event (thumbnails/duplicate-thumbnail old-id new-id) + state {:thumbnails {old-id uri}} + result (ptk/update event state)] + (t/is (= uri (get-in result [:thumbnails old-id]))) + (t/is (= uri (get-in result [:thumbnails new-id]))))) + + ;; --- Async WatchEvent tests --- + + (defn- make-obj-ids + "Helper to create n properly-formatted object-ids for a single file." + [file-id n] + (vec (repeatedly n #(thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame")))) + + (t/deftest clear-thumbnail-batch-watch-calls-rpc-with-object-ids + (t/async done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 3) + state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {#'app.main.repo/cmd! mock/rpc-cmd!-mock + #'app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= 1 (count @mock/rpc-calls))) + (let [[{:keys [cmd params]}] @mock/rpc-calls] + (t/is (= :delete-file-object-thumbnails cmd)) + (t/is (= (vec oids) (:object-ids params)))) + (done'))))) + done)))) + + (t/deftest clear-thumbnail-batch-watch-partitions-large-batch + (t/async done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 250) + state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {#'app.main.repo/cmd! mock/rpc-cmd!-mock + #'app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= 2 (count @mock/rpc-calls))) + (let [[c1 c2] @mock/rpc-calls] + (t/is (= :delete-file-object-thumbnails (:cmd c1))) + (t/is (= :delete-file-object-thumbnails (:cmd c2))) + (t/is (= 200 (count (:object-ids (:params c1))))) + (t/is (= 50 (count (:object-ids (:params c2))))) + (t/is (= (set oids) + (set (concat (:object-ids (:params c1)) + (:object-ids (:params c2))))))) + (done'))))) + done)))) + + (t/deftest clear-thumbnail-batch-watch-revokes-blob-uris + (t/async done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 2) + uris ["blob:http://localhost/thumb-1" + "blob:http://localhost/thumb-2"] + state {deletion-queue-key (zipmap oids uris)} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {#'app.main.repo/cmd! (fn [_ _] (rx/of nil)) + #'app.util.webapi/revoke-uri mock/revoke-uri-mock + #'app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= (set uris) (set @mock/revoked-uris))) + (done'))))) + done)))) + + (t/deftest clear-thumbnail-batch-watch-empty-queue-no-rpc + (t/async done + (let [event (#'thumbnails/clear-thumbnail-batch) + state {}] + (ptk/update event state) + (mock/with-mocks + {#'app.main.repo/cmd! mock/rpc-cmd!-mock + #'app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (empty? @mock/rpc-calls)) + (done'))))) + done)))) + + (t/deftest clear-thumbnail-watch-emits-batch-after-debounce + (t/async done + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/thumb" + state {:thumbnails {object-id uri}} + event (thumbnails/clear-thumbnail file-id object-id)] + (ptk/update event state) + (mock/with-mocks + {#'beicon.v2.core/timer mock/timer-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [events] + (t/is (= 1 (count events))) + (t/is (ptk/event? (first events))) + (done'))))) + done))))) diff --git a/frontend/test/frontend_tests/helpers/mock.cljs b/frontend/test/frontend_tests/helpers/mock.cljs new file mode 100644 index 0000000000..bce234f4cf --- /dev/null +++ b/frontend/test/frontend_tests/helpers/mock.cljs @@ -0,0 +1,103 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.helpers.mock + "Async-first mocking primitives for ClojureScript tests. + + Uses `with-redefs` — the standard CLJS mechanism for rebinding Vars + within a dynamic scope. Recording atoms (`rpc-calls`, `revoked-uris`) + persist across the entire test lifecycle, making captured data + inspectable regardless of whether callbacks fire synchronously or + asynchronously. + + The `with-mocks` helper wraps the lifecycle: + 1. Reset recording atoms + 2. Install mocks via `with-redefs` + 3. Execute `(test-fn inner-done)` + 4. `inner-done` calls `outer-done` (typically `cljs.test/async`'s done) + + Since all mock functions return synchronous `rx/of` observables, + callbacks always fire within the `with-redefs` body." + (:require + [beicon.v2.core :as rx])) + +;; ═══════════════════════════════════════════════════════════════ +;; Recording atoms +;; ═══════════════════════════════════════════════════════════════ + +(def rpc-calls + "Atom accumulating mocked `rp/cmd!` calls as `{:cmd kw :params map}`." + (atom [])) + +(def revoked-uris + "Atom accumulating URIs passed to `wapi/revoke-uri`." + (atom [])) + +;; ═══════════════════════════════════════════════════════════════ +;; Mock implementations +;; ═══════════════════════════════════════════════════════════════ + +(defn rpc-cmd!-mock + "Records [cmd params] in [[rpc-calls]], returns `(rx/of nil)`." + [cmd params] + (swap! rpc-calls conj {:cmd cmd :params params}) + (rx/of nil)) + +(defn revoke-uri-mock + "Records `uri` in [[revoked-uris]]." + [uri] + (swap! revoked-uris conj uri)) + +(defn schedule-on-idle-mock + "Calls `f` immediately instead of deferring to the idle queue." + [f] + (f)) + +(defn timer-mock + "Returns `(rx/of :immediate)` so debounce timers fire instantly + during tests." + [_ms] + (rx/of :immediate)) + +;; ═══════════════════════════════════════════════════════════════ +;; Lifecycle +;; ═══════════════════════════════════════════════════════════════ + +(defn reset! + "Clear all recording atoms. Called automatically by [[with-mocks]]." + [] + (reset! rpc-calls []) + (reset! revoked-uris [])) + +;; ═══════════════════════════════════════════════════════════════ +;; Public API +;; ═══════════════════════════════════════════════════════════════ + +(defn with-mocks + "Resets recording atoms, installs `mocks` via `with-redefs`, then + calls `(test-fn inner-done)`. + + `mocks` is a map of `Var → mock-fn` (e.g. `{#'rp/cmd! mock-fn}`). + `inner-done` tears down the `with-redefs` (by returning) and calls + `outer-done` (the `cljs.test/async` `done` callback). + + Example: + + (t/deftest my-async-test + (t/async done + (mock/with-mocks + {#'rp/cmd! mock/rpc-cmd!-mock} + (fn [done'] + (->> (some-async-flow) + (rx/subs! + (fn [v] ...) + (fn [err] (done')) + (fn [] (done'))))))))" + [mocks test-fn outer-done] + (reset!) + (apply with-redefs (mapcat identity mocks) + (test-fn (fn inner-done [] + (outer-done))))) From 61c52a665d2ef71f3796a969ae07fb8af313b087 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Mon, 15 Jun 2026 12:03:59 +0200 Subject: [PATCH 17/17] :arrow_up: Update dependencies (#10166) * :arrow_up: Update exporter dependencies * :arrow_up: Update frontend dependencies * :arrow_up: Update nodejs version devenv and docker images --- docker/devenv/Dockerfile | 2 +- docker/images/Dockerfile.backend | 2 +- docker/images/Dockerfile.exporter | 2 +- exporter/package.json | 2 +- exporter/pnpm-lock.yaml | 184 ++++---- exporter/pnpm-workspace.yaml | 7 + exporter/src/app/renderer/svg.cljs | 2 +- frontend/package.json | 16 +- frontend/pnpm-lock.yaml | 715 ++++++++++++++++++++++------- package.json | 2 +- pnpm-workspace.yaml | 1 + 11 files changed, 658 insertions(+), 277 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 24ec6dfd54..c4587b798e 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.15.0 \ +ENV NODE_VERSION=v24.16.0 \ PATH=/opt/node/bin:$PATH RUN set -eux; \ diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index b94ab1ca93..46dc7277aa 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \ LC_ALL='C.UTF-8' \ JAVA_HOME="/opt/jdk" \ DEBIAN_FRONTEND=noninteractive \ - NODE_VERSION=v24.15.0 \ + NODE_VERSION=v24.16.0 \ TZ=Etc/UTC RUN set -ex; \ diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 53a081744b..2d028b5ff6 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>" ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v24.15.0 \ + NODE_VERSION=v24.16.0 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/node/bin:/opt/imagick/bin:$PATH \ PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers diff --git a/exporter/package.json b/exporter/package.json index a5f9dc2267..9ae777ceaf 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -20,7 +20,7 @@ "playwright": "^1.60.0", "raw-body": "^3.0.2", "source-map-support": "^0.5.21", - "svgo": "penpot/svgo#v3.1", + "@penpot/svgo": "penpot/svgo#3.3.0", "undici": "^8.4.1", "xml-js": "^1.6.11", "xregexp": "^5.1.2" diff --git a/exporter/pnpm-lock.yaml b/exporter/pnpm-lock.yaml index 2375106e10..0bac244214 100644 --- a/exporter/pnpm-lock.yaml +++ b/exporter/pnpm-lock.yaml @@ -4,10 +4,18 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + lodash@<=4.17.23: ^4.17.24 + lodash@>=4.0.0 <=4.17.22: ^4.17.23 + lodash@>=4.0.0 <=4.17.23: ^4.17.24 + importers: .: dependencies: + '@penpot/svgo': + specifier: penpot/svgo#3.3.0 + version: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021 archiver: specifier: 8.0.0 version: 8.0.0 @@ -35,9 +43,6 @@ importers: source-map-support: specifier: ^0.5.21 version: 0.5.21 - svgo: - specifier: penpot/svgo#v3.1 - version: https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180 undici: specifier: ^8.4.1 version: 8.4.1 @@ -54,16 +59,16 @@ importers: packages: - '@babel/runtime-corejs3@7.28.4': - resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} + '@babel/runtime-corejs3@7.29.7': + resolution: {integrity: sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==} engines: {node: '>=6.9.0'} '@ioredis/commands@1.10.0': resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} + '@penpot/svgo@https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021': + resolution: {gitHosted: true, integrity: sha512-hG/pgVEWhmHEFMU+evGZkB5kHauff5Zo6ZO+Ro7HY0efsQTJft6svM4isH5jDISeSVrZ1CDGnhWBXuqkztsTWw==, tarball: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021} + version: 3.3.0 abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -132,8 +137,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} @@ -165,8 +171,8 @@ packages: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} - core-js-pure@3.47.0: - resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==} + core-js-pure@3.49.0: + resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -180,20 +186,21 @@ packages: resolution: {integrity: sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==} engines: {node: '>=18'} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -219,22 +226,25 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} @@ -263,8 +273,8 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.7.1: - resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -291,20 +301,19 @@ packages: keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} @@ -317,8 +326,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} playwright-core@1.60.0: resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} @@ -369,8 +379,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - 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'} setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -402,11 +413,6 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - svgo@https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180: - resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180} - version: 4.0.0 - engines: {node: '>=16.0.0'} - tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} @@ -460,13 +466,19 @@ packages: snapshots: - '@babel/runtime-corejs3@7.28.4': + '@babel/runtime-corejs3@7.29.7': dependencies: - core-js-pure: 3.47.0 + core-js-pure: 3.49.0 '@ioredis/commands@1.10.0': {} - '@trysound/sax@0.2.0': {} + '@penpot/svgo@https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021': + dependencies: + css-select: 7.0.0 + css-tree: 3.2.1 + csso: 5.0.5 + lodash: 4.18.1 + sax: 1.6.0 abort-controller@3.0.0: dependencies: @@ -528,7 +540,7 @@ snapshots: base64-js@1.5.1: {} - boolbase@1.0.0: {} + boolbase@2.0.0: {} brace-expansion@5.0.6: dependencies: @@ -560,7 +572,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - core-js-pure@3.47.0: {} + core-js-pure@3.49.0: {} core-util-is@1.0.3: {} @@ -571,25 +583,25 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - css-select@5.2.2: + css-select@7.0.0: dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 css-tree@2.2.1: dependencies: mdn-data: 2.0.28 source-map-js: 1.2.1 - css-tree@3.1.0: + css-tree@3.2.1: dependencies: - mdn-data: 2.12.2 + mdn-data: 2.27.1 source-map-js: 1.2.1 - css-what@6.2.2: {} + css-what@8.0.0: {} csso@5.0.5: dependencies: @@ -605,25 +617,25 @@ snapshots: depd@2.0.0: {} - dom-serializer@2.0.0: + dom-serializer@3.1.1: dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.0.0 - domelementtype@2.3.0: {} + domelementtype@3.0.0: {} - domhandler@5.0.3: + domhandler@6.0.1: dependencies: - domelementtype: 2.3.0 + domelementtype: 3.0.0 - domutils@3.2.2: + domutils@4.0.2: dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 - entities@4.5.0: {} + entities@8.0.0: {} event-target-shim@5.0.1: {} @@ -650,7 +662,7 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - iconv-lite@0.7.1: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -684,11 +696,11 @@ snapshots: dependencies: readable-stream: 2.3.8 - lodash@4.17.21: {} + lodash@4.18.1: {} mdn-data@2.0.28: {} - mdn-data@2.12.2: {} + mdn-data@2.27.1: {} minimatch@10.2.5: dependencies: @@ -698,9 +710,9 @@ snapshots: normalize-path@3.0.0: {} - nth-check@2.1.1: + nth-check@3.0.1: dependencies: - boolbase: 1.0.0 + boolbase: 2.0.0 playwright-core@1.60.0: {} @@ -718,7 +730,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 readable-stream@2.3.8: @@ -755,7 +767,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.3: {} + sax@1.6.0: {} setprototypeof@1.2.0: {} @@ -789,14 +801,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - svgo@https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180: - dependencies: - '@trysound/sax': 0.2.0 - css-select: 5.2.2 - css-tree: 3.1.0 - csso: 5.0.5 - lodash: 4.17.21 - tar-stream@3.2.0: dependencies: b4a: 1.8.1 @@ -835,11 +839,11 @@ snapshots: xml-js@1.6.11: dependencies: - sax: 1.4.3 + sax: 1.6.0 xregexp@5.1.2: dependencies: - '@babel/runtime-corejs3': 7.28.4 + '@babel/runtime-corejs3': 7.29.7 zip-stream@7.0.5: dependencies: diff --git a/exporter/pnpm-workspace.yaml b/exporter/pnpm-workspace.yaml index e18f8f5482..d71aa3ebaf 100644 --- a/exporter/pnpm-workspace.yaml +++ b/exporter/pnpm-workspace.yaml @@ -1,2 +1,9 @@ allowBuilds: core-js-pure: false +minimumReleaseAgeExclude: + - lodash@4.17.24 + - lodash@4.17.23 +overrides: + lodash@<=4.17.23: ^4.17.24 + lodash@>=4.0.0 <=4.17.22: ^4.17.23 + lodash@>=4.0.0 <=4.17.23: ^4.17.24 diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index ebac22349c..7a47e46fe0 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -6,7 +6,7 @@ (ns app.renderer.svg (:require - ["svgo" :as svgo] + ["@penpot/svgo" :as svgo] ["xml-js" :as xml] [app.browser :as bw] [app.common.data :as d] diff --git a/frontend/package.json b/frontend/package.json index 8a70a82cb4..f1008e3e4d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,17 +52,17 @@ "@penpot/draft-js": "link:packages/draft-js", "@penpot/mousetrap": "link:packages/mousetrap", "@penpot/plugins-runtime": "link:../plugins/libs/plugins-runtime", - "@penpot/svgo": "github:penpot/svgo#v3.2", + "@penpot/svgo": "penpot/svgo#3.3.0", "@penpot/text-editor": "link:text-editor", "@penpot/tokenscript": "link:packages/tokenscript", "@penpot/ui": "link:packages/ui", "@playwright/test": "1.60.0", - "@storybook/addon-docs": "10.4.3", - "@storybook/addon-themes": "10.4.3", - "@storybook/addon-vitest": "10.4.3", - "@storybook/react-vite": "10.4.3", + "@storybook/addon-docs": "10.4.4", + "@storybook/addon-themes": "10.4.4", + "@storybook/addon-vitest": "10.4.4", + "@storybook/react-vite": "10.4.4", "@tokens-studio/sd-transforms": "2.0.3", - "@types/node": "^25.9.2", + "@types/node": "^25.9.3", "@vitest/browser": "4.1.8", "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "4.1.8", @@ -71,7 +71,7 @@ "compression": "^1.8.1", "concurrently": "^10.0.3", "date-fns": "^4.4.0", - "esbuild": "^0.28.0", + "esbuild": "^0.28.1", "eventsource-parser": "^3.1.0", "express": "^5.1.0", "fancy-log": "^2.0.0", @@ -110,7 +110,7 @@ "sax": "^1.6.0", "scheduler": "^0.27.0", "source-map-support": "^0.5.21", - "storybook": "10.4.3", + "storybook": "10.4.4", "style-dictionary": "5.4.4", "stylelint": "^17.13.0", "stylelint-config-standard-scss": "^17.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8e216b93fe..9fdf5719fd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: link:../plugins/libs/plugins-runtime version: link:../plugins/libs/plugins-runtime '@penpot/svgo': - specifier: github:penpot/svgo#v3.2 - version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b + specifier: penpot/svgo#3.3.0 + version: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021 '@penpot/text-editor': specifier: link:text-editor version: link:text-editor @@ -36,29 +36,29 @@ importers: specifier: 1.60.0 version: 1.60.0 '@storybook/addon-docs': - specifier: 10.4.3 - version: 10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + specifier: 10.4.4 + version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2)) '@storybook/addon-themes': - specifier: 10.4.3 - version: 10.4.3(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + specifier: 10.4.4 + version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) '@storybook/addon-vitest': - specifier: 10.4.3 - version: 10.4.3(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8) + specifier: 10.4.4 + version: 10.4.4(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8) '@storybook/react-vite': - specifier: 10.4.3 - version: 10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + specifier: 10.4.4 + version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(supports-color@5.5.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.100.0)(yaml@2.8.2)) '@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.2 - version: 25.9.2 + specifier: ^25.9.3 + version: 25.9.3 '@vitest/browser': specifier: 4.1.8 - version: 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + version: 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) '@vitest/browser-playwright': specifier: ^4.1.8 - version: 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + version: 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) '@vitest/coverage-v8': specifier: 4.1.8 version: 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8) @@ -78,8 +78,8 @@ importers: specifier: ^4.4.0 version: 4.4.0 esbuild: - specifier: ^0.28.0 - version: 0.28.0 + specifier: ^0.28.1 + version: 0.28.1 eventsource-parser: specifier: ^3.1.0 version: 3.1.0 @@ -195,8 +195,8 @@ importers: specifier: ^0.5.21 version: 0.5.21 storybook: - specifier: 10.4.3 - version: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 10.4.4 + version: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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) @@ -229,10 +229,10 @@ importers: version: 2.0.10 vite: specifier: ^8.0.16 - version: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + version: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + version: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) wait-on: specifier: ^9.0.4 version: 9.0.10(supports-color@5.5.0) @@ -293,7 +293,7 @@ importers: version: 10.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3) '@storybook/react-vite': specifier: 10.3.5 - version: 10.3.5(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + version: 10.3.5(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -308,7 +308,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + version: 6.0.1(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.100.0)(yaml@2.8.2)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -332,7 +332,7 @@ importers: version: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@25.9.2)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + version: 4.5.4(@types/node@25.9.2)(rollup@4.61.1)(supports-color@5.5.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.100.0)(yaml@2.8.2)) text-editor: devDependencies: @@ -375,8 +375,8 @@ importers: packages: - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} '@ark/schema@0.56.0': resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} @@ -712,6 +712,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} @@ -730,6 +736,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} @@ -748,6 +760,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} @@ -766,6 +784,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} @@ -784,6 +808,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} @@ -802,6 +832,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} @@ -820,6 +856,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} @@ -838,6 +880,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} @@ -856,6 +904,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} @@ -874,6 +928,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} @@ -892,6 +952,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} @@ -910,6 +976,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} @@ -928,6 +1000,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} @@ -946,6 +1024,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} @@ -964,6 +1048,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} @@ -982,6 +1072,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -1000,6 +1096,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} @@ -1018,6 +1120,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -1036,6 +1144,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} @@ -1054,6 +1168,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -1072,6 +1192,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} @@ -1090,6 +1216,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} @@ -1108,6 +1240,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} @@ -1126,6 +1264,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} @@ -1144,6 +1288,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -1162,6 +1312,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1788,6 +1944,10 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} + '@penpot/svgo@https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021': + resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021} + version: 3.3.0 + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2180,27 +2340,27 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.4.3': - resolution: {integrity: sha512-CJGEXSo0zpIy7gvEeeUi09ZbjQUSNDi4YipAeb+lZGGEn8ShZUr2Pk330yd2ZO+ngNWJXD4ZxOb0e3/aIlxb3Q==} + '@storybook/addon-docs@10.4.4': + resolution: {integrity: sha512-yPshCvtmQTq52T2sXuXgjy7B/QbhA/WIZxLYggptNjBL8BJMvbOfp9bAfCKh7+KpRWGqDZ6Y6tWL1Q48Wj3vtw==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.3 + storybook: ^10.4.4 peerDependenciesMeta: '@types/react': optional: true - '@storybook/addon-themes@10.4.3': - resolution: {integrity: sha512-pL73C5h6uyIpeLXjptefjo52Kjhq6ZQh+XgTs8b93ytYy5fkupvQeoGjUnMknTjoIabNPSOhvb0Mcj5OUA+XCw==} + '@storybook/addon-themes@10.4.4': + resolution: {integrity: sha512-VH443z7o/JO5K9QFVuB9IzwaMu0jEiq4ybpzTlAmt0ZUEqNBuM+ESBvkVMkZ5QeNghKrs/J9yvum2g2t94YR4Q==} peerDependencies: - storybook: ^10.4.3 + storybook: ^10.4.4 - '@storybook/addon-vitest@10.4.3': - resolution: {integrity: sha512-np5qbyc/A7bZTvRlap9eaNmp9ix9yBBhMc3ClF4u2NkyI9MNLRH2xh66mI9lsShTyUZ6NAD8Uj72YJXkcigP6w==} + '@storybook/addon-vitest@10.4.4': + resolution: {integrity: sha512-VPpBwf1Elr+0g33am8ZE6aHhLB+r1TPxUsnDuCVNhxGjRxMFyQkAE8+jPJFPvS/YIUGMbVXarzaV7PcI/sJuVQ==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.4.3 + storybook: ^10.4.4 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@vitest/browser': @@ -2218,10 +2378,10 @@ packages: storybook: ^10.3.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/builder-vite@10.4.3': - resolution: {integrity: sha512-gvXVJvcsqFuSO5PLhS5+BdoYbQDxbkVVrW2cHrf4g089bXVTImZrOSzTO7SEOTs4I9acJqv4nK22WEeJpY+VZw==} + '@storybook/builder-vite@10.4.4': + resolution: {integrity: sha512-VyuZ4mEvhhVXjJa1qXMWKH8ohnas0rgEuJDf6u4aJ54XeENFebPUEAHde1Qo2PflJ4rUdVdXieOZzKbYwP5RAQ==} peerDependencies: - storybook: ^10.4.3 + storybook: ^10.4.4 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@storybook/csf-plugin@10.3.5': @@ -2242,12 +2402,12 @@ packages: webpack: optional: true - '@storybook/csf-plugin@10.4.3': - resolution: {integrity: sha512-D+XF5CVhZmIOI0uhfTKxlQr+gR1z8X9djPy9phiA1USLPAOHagBAucp/PhLwlFVUxrKzEIf8yImrvkCv50IcDg==} + '@storybook/csf-plugin@10.4.4': + resolution: {integrity: sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.4.3 + storybook: ^10.4.4 vite: '*' webpack: '*' peerDependenciesMeta: @@ -2282,14 +2442,14 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 storybook: ^10.3.5 - '@storybook/react-dom-shim@10.4.3': - resolution: {integrity: sha512-aPZ+Afd+zdoSygISOVCJIYdiqWJM6uRZFw0zhsONwNcn3kU1TNce6iAoBCY8cpEhDAu61M1QjeIHM3LPy/ieog==} + '@storybook/react-dom-shim@10.4.4': + resolution: {integrity: sha512-y6SObmoW78AydE6VfKQSUmCkuqiaMPy9LgMpMdMEyWfJ/pSxBDMIKycr9dlRMJP1cvNgByaJgrusWtA46ndSQw==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.3 + storybook: ^10.4.4 peerDependenciesMeta: '@types/react': optional: true @@ -2304,12 +2464,12 @@ packages: storybook: ^10.3.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react-vite@10.4.3': - resolution: {integrity: sha512-Pk/hi10JFuwJ5sj/HAapWrgaa9Z83oT4MJPbHqeKzMt3A3jkIru4L9ibnt82bzV3crOaiErprvOlAFDsjxhrrQ==} + '@storybook/react-vite@10.4.4': + resolution: {integrity: sha512-hXw1c9Jq2eFzwmJ3u9phmszbHoPjwPLYjcR1Grd6Xbe2g3bReGH35urm/fTZ0HNdjXAgQlUaXp2bWw6vz0BHQw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.3 + storybook: ^10.4.4 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@storybook/react@10.3.5': @@ -2323,14 +2483,14 @@ packages: typescript: optional: true - '@storybook/react@10.4.3': - resolution: {integrity: sha512-Td+Zoi8ylJTPC1jg5vHw8OK7U2kJgqc5kuAn92UvD4IbAkcpMTBRPHDziK1piv6q7r8yNLVah+ku6IKHpTLeXA==} + '@storybook/react@10.4.4': + resolution: {integrity: sha512-6K5/uHrvjswrueyVpUt6IWGuSgYCMtMOYyVs86XJZYqKBV3Pv7nGsGNH7YSMLAVQBZW4CQqm2etd5Op0GHY9Kg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.3 + storybook: ^10.4.4 typescript: '>= 4.9.x' peerDependenciesMeta: '@types/react': @@ -2383,10 +2543,6 @@ packages: '@tokens-studio/types@0.5.2': resolution: {integrity: sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==} - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} - '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -2435,6 +2591,9 @@ packages: '@types/node@25.9.2': resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2591,6 +2750,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2803,6 +2967,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} + brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} @@ -3088,8 +3256,9 @@ packages: css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} css-selector-parser@1.4.1: resolution: {integrity: sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==} @@ -3102,10 +3271,6 @@ packages: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -3114,6 +3279,10 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -3274,25 +3443,31 @@ packages: dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} + domhandler@4.3.1: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0: resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0} @@ -3355,10 +3530,6 @@ packages: entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -3424,6 +3595,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4464,9 +4640,6 @@ packages: mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} @@ -4650,6 +4823,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5528,8 +5705,8 @@ packages: prettier: optional: true - storybook@10.4.3: - resolution: {integrity: sha512-oTfNXtS/K4PmASbcD+RyW7kxWGt3tknJL3RZodCMh+nnp3X4ng8vYg8yvIhTG/q0dqBxw7Ba8dHsfsEy4631vg==} + storybook@10.4.4: + resolution: {integrity: sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5728,11 +5905,6 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b: - resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b} - version: 4.0.0 - engines: {node: '>=16.0.0'} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -6272,7 +6444,7 @@ packages: snapshots: - '@adobe/css-tools@4.4.4': {} + '@adobe/css-tools@4.5.0': {} '@ark/schema@0.56.0': dependencies: @@ -6691,6 +6863,9 @@ snapshots: '@esbuild/aix-ppc64@0.28.0': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true @@ -6700,6 +6875,9 @@ snapshots: '@esbuild/android-arm64@0.28.0': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.27.3': optional: true @@ -6709,6 +6887,9 @@ snapshots: '@esbuild/android-arm@0.28.0': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.27.3': optional: true @@ -6718,6 +6899,9 @@ snapshots: '@esbuild/android-x64@0.28.0': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true @@ -6727,6 +6911,9 @@ snapshots: '@esbuild/darwin-arm64@0.28.0': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true @@ -6736,6 +6923,9 @@ snapshots: '@esbuild/darwin-x64@0.28.0': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true @@ -6745,6 +6935,9 @@ snapshots: '@esbuild/freebsd-arm64@0.28.0': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true @@ -6754,6 +6947,9 @@ snapshots: '@esbuild/freebsd-x64@0.28.0': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true @@ -6763,6 +6959,9 @@ snapshots: '@esbuild/linux-arm64@0.28.0': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true @@ -6772,6 +6971,9 @@ snapshots: '@esbuild/linux-arm@0.28.0': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true @@ -6781,6 +6983,9 @@ snapshots: '@esbuild/linux-ia32@0.28.0': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true @@ -6790,6 +6995,9 @@ snapshots: '@esbuild/linux-loong64@0.28.0': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true @@ -6799,6 +7007,9 @@ snapshots: '@esbuild/linux-mips64el@0.28.0': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true @@ -6808,6 +7019,9 @@ snapshots: '@esbuild/linux-ppc64@0.28.0': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true @@ -6817,6 +7031,9 @@ snapshots: '@esbuild/linux-riscv64@0.28.0': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true @@ -6826,6 +7043,9 @@ snapshots: '@esbuild/linux-s390x@0.28.0': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true @@ -6835,6 +7055,9 @@ snapshots: '@esbuild/linux-x64@0.28.0': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.27.3': optional: true @@ -6844,6 +7067,9 @@ snapshots: '@esbuild/netbsd-arm64@0.28.0': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true @@ -6853,6 +7079,9 @@ snapshots: '@esbuild/netbsd-x64@0.28.0': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.27.3': optional: true @@ -6862,6 +7091,9 @@ snapshots: '@esbuild/openbsd-arm64@0.28.0': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true @@ -6871,6 +7103,9 @@ snapshots: '@esbuild/openbsd-x64@0.28.0': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.27.3': optional: true @@ -6880,6 +7115,9 @@ snapshots: '@esbuild/openharmony-arm64@0.28.0': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true @@ -6889,6 +7127,9 @@ snapshots: '@esbuild/sunos-x64@0.28.0': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true @@ -6898,6 +7139,9 @@ snapshots: '@esbuild/win32-arm64@0.28.0': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true @@ -6907,6 +7151,9 @@ snapshots: '@esbuild/win32-ia32@0.28.0': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true @@ -6916,6 +7163,9 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': dependencies: eslint: 9.39.2 @@ -7013,11 +7263,11 @@ 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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@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.100.0)(yaml@2.8.2))': dependencies: glob: 13.0.1 react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) optionalDependencies: typescript: 6.0.3 @@ -7435,6 +7685,14 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.6 optional: true + '@penpot/svgo@https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021': + dependencies: + css-select: 7.0.0 + css-tree: 3.2.1 + csso: 5.0.5 + lodash: 4.18.1 + sax: 1.6.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -7689,15 +7947,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/addon-docs@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.7) - '@storybook/csf-plugin': 10.4.3(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.4.4(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2)) '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@storybook/react-dom-shim': 10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@storybook/react-dom-shim': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-dedent: 2.3.0 optionalDependencies: '@types/react': 19.2.14 @@ -7708,64 +7966,64 @@ snapshots: - vite - webpack - '@storybook/addon-themes@10.4.3(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + '@storybook/addon-themes@10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: - storybook: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-dedent: 2.3.0 - '@storybook/addon-vitest@10.4.3(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8)': + '@storybook/addon-vitest@10.4.4(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8)': 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.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: - '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) - '@vitest/browser-playwright': 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + '@vitest/browser-playwright': 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) '@vitest/runner': 4.1.8 - vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + vitest: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': dependencies: - '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ts-dedent: 2.2.0 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/builder-vite@10.4.3(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.4.4(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2))': dependencies: - '@storybook/csf-plugin': 10.4.3(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) - storybook: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@storybook/csf-plugin': 10.4.4(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2)) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/csf-plugin@10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': dependencies: storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.0 rollup: 4.61.1 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) - '@storybook/csf-plugin@10.4.3(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/csf-plugin@10.4.4(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2))': dependencies: - storybook: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) unplugin: 2.3.11 optionalDependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 rollup: 4.61.1 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) '@storybook/global@5.0.0': {} @@ -7785,20 +8043,20 @@ snapshots: react-dom: 19.2.3(react@19.2.3) storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim@10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + '@storybook/react-dom-shim@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@storybook/react-vite@10.3.5(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/react-vite@10.3.5(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@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.100.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0(rollup@4.61.1) - '@storybook/builder-vite': 10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@storybook/builder-vite': 10.3.5(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) '@storybook/react': 10.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@6.0.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -7808,7 +8066,7 @@ snapshots: resolve: 1.22.11 storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tsconfig-paths: 4.2.0 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) transitivePeerDependencies: - esbuild - rollup @@ -7816,21 +8074,21 @@ snapshots: - typescript - webpack - '@storybook/react-vite@10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@storybook/react-vite@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(supports-color@5.5.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.100.0)(yaml@2.8.2))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@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.100.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.4.0(rollup@4.61.1) - '@storybook/builder-vite': 10.4.3(esbuild@0.28.0)(rollup@4.61.1)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) - '@storybook/react': 10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.4(esbuild@0.28.1)(rollup@4.61.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(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.100.0)(yaml@2.8.2)) + '@storybook/react': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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-docgen: 8.0.3(supports-color@5.5.0) react-dom: 19.2.7(react@19.2.7) resolve: 1.22.12 - storybook: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -7854,15 +8112,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react@10.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)': + '@storybook/react@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@storybook/react-dom-shim': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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: 8.0.3(supports-color@5.5.0) react-docgen-typescript: 2.4.0(typescript@6.0.3) react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -7883,7 +8141,7 @@ snapshots: '@testing-library/jest-dom@6.9.1': dependencies: - '@adobe/css-tools': 4.4.4 + '@adobe/css-tools': 4.5.0 aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 @@ -7923,8 +8181,6 @@ snapshots: '@tokens-studio/types@0.5.2': {} - '@trysound/sax@0.2.0': {} - '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -7978,6 +8234,10 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/node@25.9.3': + dependencies: + undici-types: 7.24.6 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -7990,10 +8250,10 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@vitejs/plugin-react@6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + '@vitejs/plugin-react@6.0.1(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.100.0)(yaml@2.8.2))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) optionalDependencies: babel-plugin-react-compiler: 1.0.0 @@ -8009,6 +8269,20 @@ snapshots: - msw - utf-8-validate - vite + optional: true + + '@vitest/browser-playwright@4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8)': + dependencies: + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + playwright: 1.60.0 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite '@vitest/browser@4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8)': dependencies: @@ -8027,6 +8301,23 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -8039,9 +8330,9 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + vitest: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) optionalDependencies: - '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) '@vitest/expect@3.2.4': dependencies: @@ -8068,6 +8359,14 @@ snapshots: optionalDependencies: vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.8 + 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.100.0)(yaml@2.8.2) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -8103,7 +8402,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + vitest: 4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)) '@vitest/utils@3.2.4': dependencies: @@ -8175,14 +8474,16 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.16.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 acorn@8.15.0: {} acorn@8.16.0: {} + acorn@8.17.0: {} + agent-base@6.0.2(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) @@ -8440,6 +8741,8 @@ snapshots: boolbase@1.0.0: {} + boolbase@2.0.0: {} + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 @@ -8747,13 +9050,13 @@ snapshots: domutils: 2.8.0 nth-check: 2.1.1 - css-select@5.2.2: + css-select@7.0.0: dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 css-selector-parser@1.4.1: {} @@ -8767,11 +9070,6 @@ snapshots: mdn-data: 2.0.28 source-map-js: 1.2.1 - css-tree@3.1.0: - dependencies: - mdn-data: 2.12.2 - source-map-js: 1.2.1 - css-tree@3.2.1: dependencies: mdn-data: 2.27.1 @@ -8779,6 +9077,8 @@ snapshots: css-what@6.2.2: {} + css-what@8.0.0: {} + css.escape@1.5.1: {} cssesc@3.0.0: {} @@ -8912,21 +9212,23 @@ snapshots: domhandler: 4.3.1 entities: 2.2.0 - dom-serializer@2.0.0: + dom-serializer@3.1.1: dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.0.0 domelementtype@2.3.0: {} + domelementtype@3.0.0: {} + domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - domhandler@5.0.3: + domhandler@6.0.1: dependencies: - domelementtype: 2.3.0 + domelementtype: 3.0.0 domutils@2.8.0: dependencies: @@ -8934,11 +9236,11 @@ snapshots: domelementtype: 2.3.0 domhandler: 4.3.1 - domutils@3.2.2: + domutils@4.0.2: dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: @@ -8995,8 +9297,6 @@ snapshots: entities@2.2.0: {} - entities@4.5.0: {} - entities@7.0.1: {} entities@8.0.0: {} @@ -9197,6 +9497,35 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 + esbuild@0.28.1: + optionalDependencies: + '@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: {} escape-html@1.0.3: {} @@ -9351,8 +9680,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -10274,8 +10603,6 @@ snapshots: mdn-data@2.0.28: {} - mdn-data@2.12.2: {} - mdn-data@2.27.1: {} media-typer@1.1.0: {} @@ -10443,6 +10770,10 @@ snapshots: dependencies: boolbase: 1.0.0 + nth-check@3.0.1: + dependencies: + boolbase: 2.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -10892,7 +11223,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-docgen@8.0.3: + react-docgen@8.0.3(supports-color@5.5.0): dependencies: '@babel/core': 7.29.7(supports-color@5.5.0) '@babel/traverse': 7.29.7 @@ -11445,7 +11776,7 @@ snapshots: - react-dom - utf-8-validate - storybook@10.4.3(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.14)(prettier@3.8.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) @@ -11751,14 +12082,6 @@ snapshots: sax: 1.6.0 stable: 0.1.8 - svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b: - dependencies: - '@trysound/sax': 0.2.0 - css-select: 5.2.2 - css-tree: 3.1.0 - csso: 5.0.5 - lodash: 4.18.1 - symbol-tree@3.2.4: {} sync-child-process@1.0.2: @@ -12031,7 +12354,7 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite-plugin-dts@4.5.4(@types/node@25.9.2)(rollup@4.61.1)(supports-color@5.5.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)): + vite-plugin-dts@4.5.4(@types/node@25.9.2)(rollup@4.61.1)(supports-color@5.5.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.100.0)(yaml@2.8.2)): dependencies: '@microsoft/api-extractor': 7.56.2(@types/node@25.9.2) '@rollup/pluginutils': 5.3.0(rollup@4.61.1) @@ -12044,7 +12367,7 @@ snapshots: magic-string: 0.30.21 typescript: 6.0.3 optionalDependencies: - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - rollup @@ -12065,6 +12388,21 @@ snapshots: sass-embedded: 1.100.0 yaml: 2.8.2 + vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.3 + esbuild: 0.28.1 + fsevents: 2.3.3 + sass: 1.100.0 + sass-embedded: 1.100.0 + yaml: 2.8.2 + vitest@4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1(canvas@3.2.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)): dependencies: '@vitest/expect': 4.1.8 @@ -12096,6 +12434,37 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.8(@types/node@25.9.3)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(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.100.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.3 + '@vitest/browser-playwright': 4.1.8(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(yaml@2.8.2))(vitest@4.1.8) + '@vitest/coverage-v8': 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8) + '@vitest/ui': 4.1.8(vitest@4.1.8) + jsdom: 29.1.1(canvas@3.2.3) + transitivePeerDependencies: + - msw + vscode-uri@3.1.0: {} w3c-xmlserializer@5.0.0: diff --git a/package.json b/package.json index f9e05bb2cc..1aed4629d2 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@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", + "packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ca93fb1e8d..381d72d613 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ allowBuilds: + esbuild: true opencode-ai: true