From 97d234a56653a8d7b1f1d48eae39f1e044d7feb0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 9 Apr 2026 09:38:18 +0000 Subject: [PATCH 01/26] :sparkles: Add 2h min-age threshold to storage/gc_touched task Skip storage objects touched less than 2 hours ago, matching the pattern used by upload-session-gc. Update all affected tests to advance the clock past the threshold using ct/*clock* bindings. --- backend/src/app/storage/gc_touched.clj | 13 ++-- backend/test/backend_tests/rpc_file_test.clj | 24 ++++--- .../rpc_file_thumbnails_test.clj | 7 +- backend/test/backend_tests/rpc_font_test.clj | 71 ++++++++----------- backend/test/backend_tests/storage_test.clj | 17 +++-- 5 files changed, 71 insertions(+), 61 deletions(-) diff --git a/backend/src/app/storage/gc_touched.clj b/backend/src/app/storage/gc_touched.clj index a3ea21e44d..f00140d04e 100644 --- a/backend/src/app/storage/gc_touched.clj +++ b/backend/src/app/storage/gc_touched.clj @@ -213,8 +213,13 @@ [_ params] (assert (db/pool? (::db/pool params)) "expect valid storage")) -(defmethod ig/init-key ::handler - [_ cfg] - (fn [_] - (process-touched! (assoc cfg ::timestamp (ct/now))))) +(defmethod ig/expand-key ::handler + [k v] + {k (merge {::min-age (ct/duration {:hours 2})} v)}) + +(defmethod ig/init-key ::handler + [_ {:keys [::min-age] :as cfg}] + (fn [_] + (let [threshold (ct/minus (ct/now) min-age)] + (process-touched! (assoc cfg ::timestamp threshold))))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 921477d1b3..281c834256 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -312,7 +312,8 @@ ;; freeze because of the deduplication (we have uploaded 2 times ;; the same files). - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -386,7 +387,8 @@ ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of ;; them are marked to be deleted - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -571,7 +573,8 @@ ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of ;; them are marked to be deleted. - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -664,7 +667,8 @@ ;; because of the deduplication (we have uploaded 2 times the ;; same files). - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -714,7 +718,8 @@ ;; Now that objects-gc have deleted the object thumbnail lets ;; execute the touched-gc task - (let [res (th/run-task! "storage-gc-touched" {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! "storage-gc-touched" {}))] (t/is (= 1 (:freeze res)))) ;; check file media objects @@ -749,7 +754,8 @@ ;; Now that file-gc have deleted the object thumbnail lets ;; execute the touched-gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete res)))) ;; check file media objects @@ -1319,7 +1325,8 @@ ;; The FileGC task will schedule an inner taskq (th/run-pending-tasks!) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -1413,7 +1420,8 @@ ;; we ensure that once object-gc is passed and marked two storage ;; objects to delete - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 9a856f3210..28134da5ff 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -85,7 +85,7 @@ (t/is (map? (:result out)))) ;; run the task again - (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] (th/run-task! "storage-gc-touched" {}))] (t/is (= 2 (:freeze res)))) @@ -136,7 +136,7 @@ (t/is (some? (sto/get-object storage (:media-id row2)))) ;; run the task again - (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete res))) (t/is (= 0 (:freeze res)))) @@ -235,7 +235,8 @@ (t/is (= (:object-id data1) (:object-id row))) (t/is (uuid? (:media-id row1)))) - (let [result (th/run-task! :storage-gc-touched {})] + (let [result (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete result)))) ;; Check if storage objects still exists after file-gc diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index 1316b237c9..d68f657c59 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -130,7 +130,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) (let [params {::th/type :delete-font @@ -142,14 +143,16 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 2 (:processed res)))) + (t/is (= 2 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 6 (:delete res))))))) @@ -191,7 +194,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) (let [params {::th/type :delete-font @@ -203,14 +207,16 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 1 (:processed res)))) + (t/is (= 1 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 3 (:delete res))))))) @@ -220,57 +226,42 @@ team-id (:default-team-id prof) proj-id (:default-project-id prof) font-id (uuid/custom 10 1) - - data1 (-> (io/resource "backend_tests/test_files/font-1.woff") - (io/read*)) - - data2 (-> (io/resource "backend_tests/test_files/font-2.woff") - (io/read*)) - params1 {::th/type :create-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :font-id font-id - :font-family "somefont" - :font-weight 400 - :font-style "normal" - :data {"font/woff" data1}} - - params2 {::th/type :create-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :font-id font-id - :font-family "somefont" - :font-weight 500 - :font-style "normal" - :data {"font/woff" data2}} - + data1 (-> (io/resource "backend_tests/test_files/font-1.woff") (io/read*)) + data2 (-> (io/resource "backend_tests/test_files/font-2.woff") (io/read*)) + params1 {::th/type :create-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :font-id font-id :font-family "somefont" + :font-weight 400 :font-style "normal" :data {"font/woff" data1}} + params2 {::th/type :create-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :font-id font-id :font-family "somefont" + :font-weight 500 :font-style "normal" :data {"font/woff" data2}} out1 (th/command! params1) out2 (th/command! params2)] - - ;; (th/print-result! out1) (t/is (nil? (:error out1))) (t/is (nil? (:error out2))) - (let [res (th/run-task! :storage-gc-touched {})] + ;; freeze with hours 3 clock + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) - (let [params {::th/type :delete-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :id (-> out1 :result :id)} + (let [params {::th/type :delete-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :id (-> out1 :result :id)} out (th/command! params)] - ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + ;; no-op with hours 3 clock (nothing touched yet) + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) + ;; objects-gc at days 8, then storage-gc-touched at days 8 + 3h (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 1 (:processed res)))) + (t/is (= 1 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 3 (:delete res))))))) diff --git a/backend/test/backend_tests/storage_test.clj b/backend/test/backend_tests/storage_test.clj index cd058af250..027d54ce70 100644 --- a/backend/test/backend_tests/storage_test.clj +++ b/backend/test/backend_tests/storage_test.clj @@ -169,7 +169,8 @@ (t/is (= 2 (:count res)))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -229,7 +230,8 @@ (t/is (nil? (:error out2))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 5 (:freeze res))) (t/is (= 0 (:delete res))) @@ -249,7 +251,8 @@ (th/db-exec-one! ["update storage_object set touched_at=?" (ct/now)]) ;; Run the task again - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 3 (:delete res)))) @@ -295,7 +298,8 @@ (th/db-exec! ["update storage_object set touched_at=?" (ct/now)]) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -310,7 +314,8 @@ (t/is (= 2 (:processed res)))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -336,7 +341,7 @@ (t/is (= 0 (:delete res))))) - (binding [ct/*clock* (ct/fixed-clock (ct/plus now {:minutes 1}))] + (binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 1 (:delete res))))) From 6ea7a64e011bdc2005dfe767c0e2c8dd7712177d Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 09:33:58 +0200 Subject: [PATCH 02/26] :sparkles: Add nginx configuration for mcp server --- docker/images/files/nginx-entrypoint.sh | 3 ++- docker/images/files/nginx.conf.template | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 4512d06495..772879099f 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -30,8 +30,9 @@ update_flags /var/www/app/js/config.js export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060} export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061} export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000} +export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp} export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB -envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ +envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ < /tmp/nginx.conf.template > /etc/nginx/nginx.conf PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)" diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index d0b7bc3b1f..beec6ea913 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -135,6 +135,23 @@ http { proxy_http_version 1.1; } + location /mcp/ws { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://$PENPOT_MCP_URI:4402; + proxy_http_version 1.1; + } + + location /mcp/stream { + proxy_pass http://$PENPOT_MCP_URI:4401/mcp; + proxy_http_version 1.1; + } + + location /mcp/sse { + proxy_pass http://$PENPOT_MCP_URI:4401/sse; + proxy_http_version 1.1; + } + location /readyz { access_log off; proxy_pass $PENPOT_BACKEND_URI$request_uri; From 98e816087503d6df9f33cb48243dceea8d8dfeb2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 31 Mar 2026 21:28:09 +0000 Subject: [PATCH 03/26] :recycle: Remove worker URI from global templates and compute from public URI - Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates - Remove worker_main entry from the build manifest - Construct worker URI in config.cljs by joining public-uri with worker path - Fix global variable casing for plugins-list-uri and templates-uri - Fix alignment in worker.cljs let bindings --- frontend/resources/templates/index.mustache | 1 - frontend/resources/templates/rasterizer.mustache | 1 - frontend/scripts/_helpers.js | 1 - frontend/src/app/config.cljs | 9 ++++++--- frontend/src/app/util/worker.cljs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index f80b7e7759..60c6119fd0 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -31,7 +31,6 @@ globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersionTag = "{{& version_tag}}"; globalThis.penpotBuildDate = "{{& build_date}}"; - globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; {{# manifest}} diff --git a/frontend/resources/templates/rasterizer.mustache b/frontend/resources/templates/rasterizer.mustache index 90a7f1dfdc..6a3d815e29 100644 --- a/frontend/resources/templates/rasterizer.mustache +++ b/frontend/resources/templates/rasterizer.mustache @@ -9,7 +9,6 @@ globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersionTag = "{{& version_tag}}"; globalThis.penpotBuildDate = "{{& build_date}}"; - globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; {{# manifest}} diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index dd2e23c348..c3c9a9d575 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -209,7 +209,6 @@ async function generateManifest() { config: "./js/config.js?version=" + VERSION_TAG, polyfills: "./js/polyfills.js?version=" + VERSION_TAG, libs: "./js/libs.js?version=" + VERSION_TAG, - worker_main: "./js/worker/main.js?version=" + VERSION_TAG, default_translations: "./js/translation.en.js?version=" + VERSION_TAG, importmap: JSON.stringify({ diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 75f5010280..4be493ef52 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -153,9 +153,9 @@ (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) -(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot.app/penpothub/plugins")) +(def plugins-list-uri (obj/get global "penpotPluginsListURI" "https://penpot.app/penpothub/plugins")) (def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" []))) -(def templates-uri (obj/get global "penpotTemplatesUri" "https://penpot.github.io/penpot-files/")) +(def templates-uri (obj/get global "penpotTemplatesURI" "https://penpot.github.io/penpot-files/")) ;; We set the current parsed flags under common for make ;; it available for common code without the need to pass @@ -177,7 +177,10 @@ public-uri)) (def worker-uri - (obj/get global "penpotWorkerURI" "/js/worker/main.js")) + (-> public-uri + (u/join "js/worker/main.js") + (get :path) + (str "?version=" version-tag))) (defn external-feature-flag [flag value] diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index b23bbbee92..8d87a76795 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -90,8 +90,8 @@ "Return a initialized webworker instance." [path on-error] (let [instance (js/Worker. path) - bus (rx/subject) - worker (Worker. instance (rx/to-observable bus)) + bus (rx/subject) + worker (Worker. instance (rx/to-observable bus)) handle-message (fn [event] From 47b366724858616774ec0ca9b28071bbab8a855e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 31 Mar 2026 21:41:46 +0000 Subject: [PATCH 04/26] :bug: Fix exporter renderer URI path construction Apply consistent path construction across bitmap, PDF, and SVG renderers in the exporter. Use path join utilities instead of hardcoding the render.html path, ensuring the path is properly appended to the public URI base path. - bitmap.cljs: Use u/ensure-path-slash and u/join for path - pdf.cljs: Use u/join and ensure-path-slash on base-uri - svg.cljs: Use u/ensure-path-slash and u/join for path --- exporter/src/app/renderer/bitmap.cljs | 3 ++- exporter/src/app/renderer/pdf.cljs | 5 +++-- exporter/src/app/renderer/svg.cljs | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index b1df4a7447..5d81eebb9f 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -60,6 +60,7 @@ :route "objects" :skip-children skip-children} uri (-> (cf/get :public-uri) - (assoc :path "/render.html") + (u/ensure-path-slash) + (u/join "render.html") (assoc :query (u/map->query-string params)))] (bw/exec! (prepare-options uri) (partial render uri))))) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 25bcfc036b..edfdcda1b1 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -35,7 +35,7 @@ :object-id object-id :route "objects"}] (-> base-uri - (assoc :path "/render.html") + (u/join "render.html") (assoc :query (u/map->query-string params))))) (sync-page-size! [dom] @@ -76,6 +76,7 @@ (on-object (assoc object :path path)) (p/recur (rest objects))))))] - (let [base-uri (cf/get :public-uri)] + (let [base-uri (-> (cf/get :public-uri) + (u/ensure-path-slash))] (bw/exec! (prepare-options base-uri) (partial render base-uri))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 73558dbe5f..71da424fb3 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -349,7 +349,8 @@ :object-id (mapv :id objects) :route "objects"} uri (-> (cf/get :public-uri) - (assoc :path "/render.html") + (u/ensure-path-slash) + (u/join "render.html") (assoc :query (u/map->query-string params)))] (bw/exec! (prepare-options uri) (partial render uri))))) From 6de5370a0bdd4f22f3c62bd2442dff9ec1c0d41a Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 10:29:52 +0200 Subject: [PATCH 05/26] :bug: Fix nginx configuration for mcp --- docker/images/files/nginx.conf.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index beec6ea913..0daab4b9d4 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -138,17 +138,17 @@ http { location /mcp/ws { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; - proxy_pass http://$PENPOT_MCP_URI:4402; + proxy_pass $PENPOT_MCP_URI:4402; proxy_http_version 1.1; } location /mcp/stream { - proxy_pass http://$PENPOT_MCP_URI:4401/mcp; + proxy_pass $PENPOT_MCP_URI:4401/mcp; proxy_http_version 1.1; } location /mcp/sse { - proxy_pass http://$PENPOT_MCP_URI:4401/sse; + proxy_pass $PENPOT_MCP_URI:4401/sse; proxy_http_version 1.1; } From 3225319e0c45bf94e115e89fd65137b097e649a9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:54:07 +0200 Subject: [PATCH 06/26] :bug: Fix frontend tests --- .../logic/components_and_tokens.cljs | 8 ++++---- .../tokens/logic/token_actions_test.cljs | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 689ac76b63..9e830c9a76 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -141,7 +141,7 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -249,11 +249,11 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :c-frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -293,7 +293,7 @@ (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index cb6f2e39d8..e468e420bb 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -64,7 +64,7 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -89,11 +89,11 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -117,14 +117,14 @@ (dwta/apply-token {:attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) ;; Apply single `:r1` attribute to same shape ;; while removing other attributes from the border-radius set ;; but keep `:r4` for testing purposes (dwta/apply-token {:attributes #{:r1 :r2 :r3} :token (toht/get-token file "borderRadius.md") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -153,7 +153,7 @@ (dwta/apply-token {:shape-ids [(:id rect-2)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -762,7 +762,7 @@ rect-2 (cths/get-shape file :rect-2) events [(dwta/toggle-token {:shape-ids [(:id rect-1) (:id rect-2)] :token-type-props {:attributes #{:r1 :r2 :r3 :r4} - :on-update-shape dwta/update-shape-radius-all} + :on-update-shape dwta/update-shape-radius} :token (toht/get-token file "borderRadius.md")})]] (tohs/run-store-async store done events From 09637f9794d671bc08b95669a9e3c97c4b9d2955 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 10:01:38 +0200 Subject: [PATCH 07/26] :sparkles: Allow render entrypoint load alternative config The render entrypoint is used by exporter --- frontend/resources/templates/render.mustache | 2 +- frontend/scripts/_helpers.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index 4de213f9ad..67629b075e 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -14,7 +14,7 @@ {{# manifest}} - + {{/manifest}} diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index c3c9a9d575..240c6b0e45 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -207,6 +207,7 @@ async function generateManifest() { rasterizer_main: "./js/rasterizer.js", config: "./js/config.js?version=" + VERSION_TAG, + config_render: "./js/config-render.js?version=" + VERSION_TAG, polyfills: "./js/polyfills.js?version=" + VERSION_TAG, libs: "./js/libs.js?version=" + VERSION_TAG, default_translations: "./js/translation.en.js?version=" + VERSION_TAG, From 75d99a07256e62432d39bab2a51dea2d973fd2cc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:32:47 +0200 Subject: [PATCH 08/26] :wrench: Add missing public uri handling on nginx entrypoint --- docker/images/files/nginx-entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 772879099f..9ce2b9261d 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -19,6 +19,10 @@ update_flags() { -e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \ "$1")" > "$1" fi + + if [ -n "$PENPOT_PUBLIC_URI" ]; then + echo "var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";" >> "$1"; + fi } update_flags /var/www/app/js/config.js From 88008ce16c9ec7fdd9c46b64a8709c6009d1fd15 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:34:32 +0200 Subject: [PATCH 09/26] :paperclip: Update mcp types yaml file --- mcp/packages/server/data/api_types.yml | 49 +++++++++++++++++++------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 901037c249..54b4100e4a 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -26,6 +26,7 @@ Penpot: props?: { [key: string]: unknown }, ): symbol; off(listenerId: symbol): void; + version: string; root: Shape | null; currentFile: File | null; currentPage: Page | null; @@ -72,7 +73,7 @@ Penpot: generateFontFaces(shapes: Shape[]): Promise; openViewer(): void; createPage(): Page; - openPage(page: Page, newWindow?: boolean): void; + openPage(page: string | Page, newWindow?: boolean): void; alignHorizontal( shapes: Shape[], direction: "center" | "left" | "right", @@ -162,6 +163,12 @@ Penpot: ``` penpot.closePlugin(); ``` + version: |- + ``` + readonly version: string + ``` + + Returns the current penpot version. root: |- ``` readonly root: Shape | null @@ -725,19 +732,19 @@ Penpot: Returns Page openPage: |- ``` - openPage(page: Page, newWindow?: boolean): void + openPage(page: string | Page, newWindow?: boolean): void ``` Changes the current open page to given page. Requires `content:read` permission. Parameters - * page: Page + * page: string | Page - the page to open + the page to open (a Page object or a page UUID string) * newWindow: boolean - if true opens the page in a new window + if true opens the page in a new window, defaults to false Returns void @@ -4785,6 +4792,7 @@ Context: ``` interface Context { + version: string; root: Shape | null; currentFile: File | null; currentPage: Page | null; @@ -4837,7 +4845,7 @@ Context: removeListener(listenerId: symbol): void; openViewer(): void; createPage(): Page; - openPage(page: Page, newWindow?: boolean): void; + openPage(page: string | Page, newWindow?: boolean): void; alignHorizontal( shapes: Shape[], direction: "center" | "left" | "right", @@ -4854,6 +4862,12 @@ Context: ``` members: Properties: + version: |- + ``` + readonly version: string + ``` + + Returns the current penpot version. root: |- ``` readonly root: Shape | null @@ -5392,19 +5406,19 @@ Context: Returns Page openPage: |- ``` - openPage(page: Page, newWindow?: boolean): void + openPage(page: string | Page, newWindow?: boolean): void ``` Changes the current open page to given page. Requires `content:read` permission. Parameters - * page: Page + * page: string | Page - the page to open + the page to open (a Page object or a page UUID string) * newWindow: boolean - if true opens the page in a new window + if true opens the page in a new window, defaults to false Returns void @@ -6845,7 +6859,7 @@ Export: ``` interface Export { - type: "svg" | "png" | "jpeg" | "pdf"; + type: "svg" | "png" | "jpeg" | "webp" | "pdf"; scale?: number; suffix?: string; skipChildren?: boolean; @@ -6857,10 +6871,10 @@ Export: Properties: type: |- ``` - type: "svg" | "png" | "jpeg" | "pdf" + type: "svg" | "png" | "jpeg" | "webp" | "pdf" ``` - Type of the file to export. Can be one of the following values: png, jpeg, svg, pdf + Type of the file to export. Can be one of the following values: png, jpeg, webp, svg, pdf scale: |- ``` scale?: number @@ -7249,6 +7263,7 @@ Flags: ``` interface Flags { naturalChildOrdering: boolean; + throwValidationErrors: boolean; } ``` @@ -7264,6 +7279,14 @@ Flags: Also, appendChild method will be append the children in the top-most position. The insertchild method is changed acordingly to respect this ordering. Defaults to false + throwValidationErrors: |- + ``` + throwValidationErrors: boolean + ``` + + If `true` the validation errors will throw an exception instead of displaying an + error in the debugger console. + Defaults to false FlexLayout: overview: |- Interface FlexLayout From b0b2c0d26480eab35bf279e02caddc293b43f53c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 13:18:24 +0200 Subject: [PATCH 10/26] :paperclip: Update version on mcp/ module --- mcp/package.json | 2 +- mcp/scripts/set-version | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 mcp/scripts/set-version diff --git a/mcp/package.json b/mcp/package.json index 6324fe3898..c407617a50 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@penpot/mcp", - "version": "2.15.0-rc.1.153", + "version": "2.15.0-rc.1", "description": "MCP server for Penpot integration", "bin": { "penpot-mcp": "./bin/mcp-local.js" diff --git a/mcp/scripts/set-version b/mcp/scripts/set-version old mode 100644 new mode 100755 From f673b32567ff1d7b442a8bfe610ca81292e795ca Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 21 Apr 2026 08:57:24 +0200 Subject: [PATCH 11/26] :bug: Fix image loading callback --- frontend/src/app/render_wasm/api.cljs | 20 ++++++++++++-------- render-wasm/src/wasm/fills/image.rs | 27 +++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 897ea4aa55..96c3253c64 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -627,6 +627,7 @@ (if (empty? fills) (h/call wasm/internal-module "_clear_shape_fills") (let [fills (types.fills/coerce fills) + image-ids (types.fills/get-image-ids fills) offset (mem/alloc->offset-32 (types.fills/get-byte-size fills)) heap (mem/get-heap-u32)] @@ -648,7 +649,7 @@ (when (zero? cached-image?) (fetch-image shape-id id thumbnail?)))) - (types.fills/get-image-ids fills))))) + image-ids)))) (defn set-shape-strokes [shape-id strokes thumbnail?] @@ -675,7 +676,8 @@ (some? gradient) (do (types.fills.impl/write-gradient-fill offset dview opacity gradient) - (h/call wasm/internal-module "_add_shape_stroke_fill")) + (h/call wasm/internal-module "_add_shape_stroke_fill") + nil) (some? image) (let [image-id (get image :id) @@ -692,7 +694,8 @@ (some? color) (do (types.fills.impl/write-solid-fill offset dview opacity color) - (h/call wasm/internal-module "_add_shape_stroke_fill"))))) + (h/call wasm/internal-module "_add_shape_stroke_fill") + nil)))) strokes)) (defn set-shape-svg-attrs @@ -1302,16 +1305,17 @@ (when (or (seq pending-thumbnails) (seq pending-full)) (->> (rx/concat (->> (rx/from (vals pending-thumbnails)) - (rx/merge-map (fn [callback] (callback))) + (rx/merge-map + (fn [callback] + (if (fn? callback) (callback) (rx/empty)))) (rx/reduce conj [])) (->> (rx/from (vals pending-full)) - (rx/mapcat (fn [callback] (callback))) + (rx/mapcat + (fn [callback] + (if (fn? callback) (callback) (rx/empty)))) (rx/reduce conj []))) (rx/subs! (fn [_] - ;; Fonts are now loaded — recompute text - ;; layouts so Skia uses the real metrics - ;; instead of fallback-font estimates. (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] (when (seq text-ids) (update-text-layouts text-ids))) diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index b122de4cc8..9200861f8a 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -1,11 +1,34 @@ use crate::error::{Error, Result}; use crate::mem; +use crate::shapes::Fill; +use crate::state::State; use crate::uuid::Uuid; use crate::with_state_mut; use crate::STATE; use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; use macros::wasm_error; +fn touch_shapes_with_image(state: &mut State, image_id: Uuid) { + let ids: Vec = state + .shapes + .iter() + .filter(|shape| { + shape + .fills() + .any(|f| matches!(f, Fill::Image(i) if i.id() == image_id)) + || shape + .strokes + .iter() + .any(|s| matches!(&s.fill, Fill::Image(i) if i.id() == image_id)) + }) + .map(|shape| shape.id) + .collect(); + + for id in ids { + state.touch_shape(id); + } +} + const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0; const IMAGE_IDS_SIZE: usize = 32; const IMAGE_HEADER_SIZE: usize = 36; // 32 bytes for IDs + 4 bytes for is_thumbnail flag @@ -90,7 +113,7 @@ pub extern "C" fn store_image() -> Result<()> { { eprintln!("{}", msg); } - state.touch_shape(ids.shape_id); + touch_shapes_with_image(state, ids.image_id); }); mem::free_bytes()?; @@ -167,7 +190,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> { // FIXME: Review if we should return a RecoverableError eprintln!("store_image_from_texture error: {}", msg); } - state.touch_shape(ids.shape_id); + touch_shapes_with_image(state, ids.image_id); }); mem::free_bytes()?; From 3c542a1abc1c4409c0a46b985474692606801e0b Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 15:59:28 +0200 Subject: [PATCH 12/26] :bug: Fix email validation (#9037) --- CHANGES.md | 6 ++ common/src/app/common/spec.cljc | 20 ++++-- common/test/common_tests/runner.cljc | 2 + common/test/common_tests/spec_test.cljc | 89 +++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 common/test/common_tests/spec_test.cljc diff --git a/CHANGES.md b/CHANGES.md index 0d431c0d2b..774d17f3de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.14.4 (Unreleased) + +### :bug: Bugs fixed + +- Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006) + ## 2.14.3 ### :sparkles: New features & Enhancements diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index 38af563499..d6f0d6cacc 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -113,12 +113,19 @@ (tgen/fmap keyword))))) ;; --- SPEC: email +;; +;; Regex rules enforced: +;; local part - valid RFC chars, no leading/trailing dot, no consecutive dots +;; domain - labels can't start/end with hyphen, no empty labels +;; TLD - at least 2 alphabetic chars -(def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") +(def email-re + #"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,63}$") (defn parse-email [s] - (some->> s (re-seq email-re) first)) + (when (and (string? s) (re-matches email-re s)) + s)) (letfn [(conformer [v] (or (parse-email v) ::s/invalid)) @@ -126,11 +133,10 @@ (dm/str v))] (s/def ::email (s/with-gen (s/conformer conformer unformer) - #(as-> (tgen/let [p1 (s/gen ::not-empty-string) - p2 (s/gen ::not-empty-string) - p3 (tgen/elements ["com" "net"])] - (str p1 "@" p2 "." p3)) $ - (tgen/such-that (partial re-matches email-re) $ 50))))) + #(tgen/let [local (tgen/string-alphanumeric 1 20) + label (tgen/string-alphanumeric 2 10) + tld (tgen/elements ["com" "net" "org" "io" "co" "dev"])] + (str local "@" label "." tld))))) ;; -- SPEC: uri diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 2d9a216cbc..b3c2ad4f0e 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -49,6 +49,7 @@ [common-tests.path-names-test] [common-tests.record-test] [common-tests.schema-test] + [common-tests.spec-test] [common-tests.svg-path-test] [common-tests.svg-test] [common-tests.text-test] @@ -122,6 +123,7 @@ 'common-tests.path-names-test 'common-tests.record-test 'common-tests.schema-test + 'common-tests.spec-test 'common-tests.svg-path-test 'common-tests.svg-test 'common-tests.text-test diff --git a/common/test/common_tests/spec_test.cljc b/common/test/common_tests/spec_test.cljc new file mode 100644 index 0000000000..425f7f8066 --- /dev/null +++ b/common/test/common_tests/spec_test.cljc @@ -0,0 +1,89 @@ +;; 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 common-tests.spec-test + (:require + [app.common.spec :as spec] + [clojure.test :as t])) + +(t/deftest valid-emails + (t/testing "accepts well-formed email addresses" + (doseq [email ["user@domain.com" + "user.name@domain.com" + "user+tag@domain.com" + "user-name@domain.com" + "user_name@domain.com" + "user123@domain.com" + "USER@DOMAIN.COM" + "u@domain.io" + "user@sub.domain.com" + "user@domain.co.uk" + "user@domain.dev" + "a@bc.co"]] + (t/is (some? (spec/parse-email email)) (str "should accept: " email))))) + +(t/deftest rejects-invalid-local-part + (t/testing "rejects local part starting with a dot" + (t/is (nil? (spec/parse-email ".user@domain.com")))) + + (t/testing "rejects local part with consecutive dots" + (t/is (nil? (spec/parse-email "user..name@domain.com")))) + + (t/testing "rejects local part with spaces" + (t/is (nil? (spec/parse-email "us er@domain.com")))) + + (t/testing "rejects local part with comma" + (t/is (nil? (spec/parse-email "user,name@domain.com"))) + (t/is (nil? (spec/parse-email ",user@domain.com")))) + + (t/testing "rejects empty local part" + (t/is (nil? (spec/parse-email "@domain.com"))))) + +(t/deftest rejects-invalid-domain + (t/testing "rejects domain starting with a dot" + (t/is (nil? (spec/parse-email "user@.domain.com")))) + + (t/testing "rejects domain part with comma" + (t/is (nil? (spec/parse-email "user@domain,com"))) + (t/is (nil? (spec/parse-email "user@,domain.com")))) + + (t/testing "rejects domain with consecutive dots" + (t/is (nil? (spec/parse-email "user@sub..domain.com")))) + + (t/testing "rejects label starting with hyphen" + (t/is (nil? (spec/parse-email "user@-domain.com")))) + + (t/testing "rejects label ending with hyphen" + (t/is (nil? (spec/parse-email "user@domain-.com")))) + + (t/testing "rejects TLD shorter than 2 chars" + (t/is (nil? (spec/parse-email "user@domain.c")))) + + (t/testing "rejects domain without a dot" + (t/is (nil? (spec/parse-email "user@domain")))) + + (t/testing "rejects domain with spaces" + (t/is (nil? (spec/parse-email "user@do main.com")))) + + (t/testing "rejects domain ending with a dot" + (t/is (nil? (spec/parse-email "user@domain."))))) + +(t/deftest rejects-invalid-structure + (t/testing "rejects nil" + (t/is (nil? (spec/parse-email nil)))) + + (t/testing "rejects empty string" + (t/is (nil? (spec/parse-email "")))) + + (t/testing "rejects string without @" + (t/is (nil? (spec/parse-email "userdomain.com")))) + + (t/testing "rejects string with multiple @" + (t/is (nil? (spec/parse-email "user@@domain.com"))) + (t/is (nil? (spec/parse-email "us@er@domain.com")))) + + (t/testing "rejects empty domain" + (t/is (nil? (spec/parse-email "user@"))))) From b60695f54a9c3a6a19e94feccb6dd59f4b762614 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 22 Apr 2026 15:17:46 +0200 Subject: [PATCH 13/26] :bug: Fix indicate that the mcp is disabled if the mcp key has expired If the mcp key has expired, the switch that indicates the status in the dashboard will appear as disabled, and will show a modal for regenerate the key. It will also appear as disabled in the workspace, not allowing the plugin to connect --- frontend/src/app/main/refs.cljs | 6 +++++ .../app/main/ui/settings/integrations.cljs | 26 +++++++++---------- .../src/app/main/ui/workspace/main_menu.cljs | 26 +++++++++++-------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 30c431ac86..9e1c13cd58 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -648,3 +648,9 @@ (def progress (l/derived :progress st/state)) + +(def access-tokens + (l/derived :access-tokens st/state)) + +(def access-token-created + (l/derived :access-token-created st/state)) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 780f9918e3..ff4b39049e 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -34,15 +34,8 @@ [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr]] - [okulary.core :as l] [rumext.v2 :as mf])) -(def tokens-ref - (l/derived :access-tokens st/state)) - -(def token-created-ref - (l/derived :access-token-created st/state)) - (def notification-timeout 7000) (def ^:private schema:form-access-token @@ -78,7 +71,7 @@ (mf/defc token-created* {::mf/private true} [{:keys [title mcp-key?]}] - (let [token-created (mf/deref token-created-ref) + (let [token-created (mf/deref refs/access-token-created) on-copy-to-clipboard (mf/use-fn @@ -310,7 +303,7 @@ [] (let [created? (mf/use-state false) - tokens (mf/deref tokens-ref) + tokens (mf/deref refs/access-tokens) mcp-key (some #(when (= (:type %) "mcp") %) tokens) mcp-key-id (:id mcp-key) @@ -413,7 +406,7 @@ (mf/defc mcp-server-section* {::mf/private true} [] - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) profile (mf/deref refs/profile) mcp-key (some #(when (= (:type %) "mcp") %) tokens) @@ -422,6 +415,8 @@ expires-at (:expires-at mcp-key) expired? (and (some? expires-at) (> (ct/now) expires-at)) + show-enabled? (and mcp-enabled? (false? expired?)) + tooltip-id (mf/use-id) @@ -511,14 +506,17 @@ (tr "integrations.mcp-server.status.expired.1")]]]) [:div {:class (stl/css :mcp-server-switch)} - [:> switch* {:label (if mcp-enabled? + [:> switch* {:label (if show-enabled? (tr "integrations.mcp-server.status.enabled") (tr "integrations.mcp-server.status.disabled")) - :default-checked mcp-enabled? + :default-checked show-enabled? :on-change handle-mcp-change}] (when (and (false? mcp-enabled?) (nil? mcp-key)) [:div {:class (stl/css :mcp-server-switch-cover) - :on-click handle-generate-mcp-key}])]]] + :on-click handle-generate-mcp-key}]) + (when (true? expired?) + [:div {:class (stl/css :mcp-server-switch-cover) + :on-click handle-regenerate-mcp-key}])]]] (when (some? mcp-key) [:div {:class (stl/css :mcp-server-key)} @@ -567,7 +565,7 @@ (mf/defc access-tokens-section* {::mf/private true} [] - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) handle-click (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 4d16c79646..674a01549d 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -43,13 +43,9 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [beicon.v2.core :as rx] - [okulary.core :as l] [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(def tokens-ref - (l/derived :access-tokens st/state)) - (mf/defc shortcuts* {::mf/private true} [{:keys [id]}] @@ -749,14 +745,22 @@ (mf/defc mcp-menu* {::mf/private true} [{:keys [on-close]}] - (let [plugins? (features/active-feature? @st/state "plugins/runtime") - - profile (mf/deref refs/profile) - mcp (mf/deref refs/mcp) + (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) + + expired? (some->> tokens + (some #(when (= (:type %) "mcp") %)) + :expires-at + (> (ct/now))) mcp-enabled? (true? (-> profile :props :mcp-enabled)) mcp-connected? (= "connected" (get mcp :connection-status)) + show-enabled? (and mcp-enabled? (false? expired?)) + on-nav-to-integrations (mf/use-fn (fn [] @@ -794,7 +798,7 @@ :pos-6 plugins?) :on-close on-close} - (when mcp-enabled? + (when show-enabled? [:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin" :class (stl/css :base-menu-item :submenu-item) :on-click on-toggle-mcp-plugin @@ -809,7 +813,7 @@ :on-click on-nav-to-integrations :on-key-down on-nav-to-integrations-key-down} [:span {:class (stl/css :item-name)} - (if mcp-enabled? + (if show-enabled? (tr "workspace.header.menu.mcp.server.status.enabled") (tr "workspace.header.menu.mcp.server.status.disabled"))]]])) @@ -983,7 +987,7 @@ :class (stl/css :item-arrow)}]]) (when (contains? cf/flags :mcp) - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) expired? (some->> tokens (some #(when (= (:type %) "mcp") %)) :expires-at From ba42cc04b725104010a2d80d04b07ee89d882c28 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 14:17:15 +0000 Subject: [PATCH 14/26] :recycle: Derive v-sizing from values instead of passing as prop Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/layout_item.cljs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index ca3e53ee1a..acc1e8db02 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -539,16 +539,18 @@ [:map [:values schema:layout-item-props-schema] [:applied-tokens [:maybe [:map-of :keyword :string]]] - [:ids [::sm/vec ::sm/uuid]] - [:v-sizing {:optional true} [:maybe [:= :fill]]]]) + [:ids [::sm/vec ::sm/uuid]]]) (mf/defc layout-size-constraints* {::mf/private true ::mf/schema (sm/schema schema:layout-size-constraints)} - [{:keys [values v-sizing ids applied-tokens] :as props}] + [{:keys [values ids applied-tokens] :as props}] (let [token-numeric-inputs (features/use-feature "tokens/numeric-input") + v-sizing + (:layout-item-v-sizing values) + min-w (get values :layout-item-min-w) max-w (get values :layout-item-max-w) @@ -914,5 +916,4 @@ (= v-sizing :fill)) [:> layout-size-constraints* {:ids ids :values values - :applied-tokens applied-tokens - :v-sizing v-sizing}])])])) + :applied-tokens applied-tokens}])])])) From 9e990a975a0653093e1c5fcb0b7c11ea0d3ea951 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 22 Apr 2026 11:18:30 +0200 Subject: [PATCH 15/26] :tada: Improve atlas growth --- render-wasm/src/render.rs | 63 ++++++++++++++----- render-wasm/src/render/surfaces.rs | 99 +++++++++++++++++++++++++++--- 2 files changed, 136 insertions(+), 26 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 6272d8d9a3..2920496de4 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -720,26 +720,24 @@ impl RenderState { self.surfaces.clear_cache(self.background_color); self.cache_cleared_this_render = true; } - let tile_rect = self.get_current_aligned_tile_bounds()?; // In fast mode the viewport is moving (pan/zoom) so Cache surface // positions would be wrong — only save to the tile HashMap. + let tile_rect = self.get_current_aligned_tile_bounds()?; + let current_tile = *self + .current_tile + .as_ref() + .ok_or(Error::CriticalError("Current tile not found".to_string()))?; self.surfaces.cache_current_tile_texture( &mut self.gpu_state, &self.tile_viewbox, - &self - .current_tile - .ok_or(Error::CriticalError("Current tile not found".to_string()))?, + ¤t_tile, &tile_rect, fast_mode, self.render_area, ); - self.surfaces.draw_cached_tile_surface( - self.current_tile - .ok_or(Error::CriticalError("Current tile not found".to_string()))?, - rect, - self.background_color, - ); + self.surfaces + .draw_cached_tile_surface(current_tile, rect, self.background_color); Ok(()) } @@ -1674,6 +1672,12 @@ impl RenderState { self.cache_cleared_this_render = false; self.reset_canvas(); + // Compute and set document-space bounds (1 unit == 1 doc px @ 100% zoom) + // to clamp atlas updates. This prevents zoom-out tiles from forcing atlas + // growth far beyond real content. + let doc_bounds = self.compute_document_bounds(base_object, tree); + self.surfaces.set_atlas_doc_bounds(doc_bounds); + // During an interactive shape transform (drag/resize/rotate) the // Target is repainted tile-by-tile. If only a subset of the // invalidated tiles finishes in this rAF the remaining area @@ -1767,6 +1771,37 @@ impl RenderState { Ok(()) } + fn compute_document_bounds( + &mut self, + base_object: Option<&Uuid>, + tree: ShapesPoolRef, + ) -> Option { + let ids: Vec = if let Some(id) = base_object { + vec![*id] + } else { + let root = tree.get(&Uuid::nil())?; + root.children_ids(false) + }; + + let mut acc: Option = None; + for id in ids.iter() { + let Some(shape) = tree.get(id) else { + continue; + }; + let r = self.get_cached_extrect(shape, tree, 1.0); + if r.is_empty() { + continue; + } + acc = Some(if let Some(mut a) = acc { + a.join(r); + a + } else { + r + }); + } + acc + } + pub fn process_animation_frame( &mut self, base_object: Option<&Uuid>, @@ -2927,11 +2962,6 @@ impl RenderState { s.canvas().draw_rect(aligned_rect, &paint); }); } - - // Clear atlas region to transparent so background shows through. - let _ = self - .surfaces - .clear_doc_rect_in_atlas(&mut self.gpu_state, self.render_area); } } } @@ -3156,7 +3186,8 @@ impl RenderState { } pub fn remove_cached_tile(&mut self, tile: tiles::Tile) { - self.surfaces.remove_cached_tile_surface(tile); + self.surfaces + .remove_cached_tile_surface(&mut self.gpu_state, tile); } /// Rebuild the tile index (shape→tile mapping) for all top-level shapes. diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index ca7f2a3ef2..8d769eea0f 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -78,10 +78,16 @@ pub struct Surfaces { /// When the atlas would exceed `max_atlas_texture_size`, this value is /// reduced so the atlas stays within the fixed texture cap. atlas_scale: f32, + /// Optional document-space bounds (1 unit == 1 doc px @ 100% zoom) used to + /// clamp atlas writes/clears so the atlas doesn't grow due to outlier tile rects. + atlas_doc_bounds: Option, /// Max width/height in pixels for the atlas surface (typically browser /// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation. max_atlas_texture_size: i32, sampling_options: skia::SamplingOptions, + /// Tracks the last document-space rect written to the atlas per tile. + /// Used to clear old content without clearing the whole (potentially huge) tile rect. + atlas_tile_doc_rects: HashMap, pub margins: skia::ISize, // Tracks which surfaces have content (dirty flag bitmask) dirty_surfaces: u32, @@ -147,8 +153,10 @@ impl Surfaces { atlas_origin: skia::Point::new(0.0, 0.0), atlas_size: skia::ISize::new(0, 0), atlas_scale: 1.0, + atlas_doc_bounds: None, max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE, sampling_options, + atlas_tile_doc_rects: HashMap::default(), margins, dirty_surfaces: 0, extra_tile_dims, @@ -162,6 +170,28 @@ impl Surfaces { self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE); } + /// Sets the document-space bounds used to clamp atlas updates. + /// Pass `None` to disable clamping. + pub fn set_atlas_doc_bounds(&mut self, bounds: Option) { + self.atlas_doc_bounds = bounds.filter(|b| !b.is_empty()); + } + + fn clamp_doc_rect_to_bounds(&self, doc_rect: skia::Rect) -> skia::Rect { + if doc_rect.is_empty() { + return doc_rect; + } + if let Some(bounds) = self.atlas_doc_bounds { + let mut r = doc_rect; + if r.intersect(bounds) { + r + } else { + skia::Rect::new_empty() + } + } else { + doc_rect + } + } + fn ensure_atlas_contains( &mut self, gpu_state: &mut GpuState, @@ -271,21 +301,51 @@ impl Surfaces { &mut self, gpu_state: &mut GpuState, tile_image: &skia::Image, - doc_rect: skia::Rect, + tile_doc_rect: skia::Rect, ) -> Result<()> { - self.ensure_atlas_contains(gpu_state, doc_rect)?; + if tile_doc_rect.is_empty() { + return Ok(()); + } + + // Clamp to document bounds (if any) and compute a matching source-rect in tile pixels. + let mut clipped_doc_rect = tile_doc_rect; + if let Some(bounds) = self.atlas_doc_bounds { + if !clipped_doc_rect.intersect(bounds) { + return Ok(()); + } + } + if clipped_doc_rect.is_empty() { + return Ok(()); + } + + self.ensure_atlas_contains(gpu_state, clipped_doc_rect)?; // Destination is document-space rect mapped into atlas pixel coords. let dst = skia::Rect::from_xywh( - (doc_rect.left - self.atlas_origin.x) * self.atlas_scale, - (doc_rect.top - self.atlas_origin.y) * self.atlas_scale, - doc_rect.width() * self.atlas_scale, - doc_rect.height() * self.atlas_scale, + (clipped_doc_rect.left - self.atlas_origin.x) * self.atlas_scale, + (clipped_doc_rect.top - self.atlas_origin.y) * self.atlas_scale, + clipped_doc_rect.width() * self.atlas_scale, + clipped_doc_rect.height() * self.atlas_scale, ); - self.atlas - .canvas() - .draw_image_rect(tile_image, None, dst, &skia::Paint::default()); + // Compute source rect in tile_image pixel coordinates. + let img_w = tile_image.width() as f32; + let img_h = tile_image.height() as f32; + let tw = tile_doc_rect.width().max(1.0); + let th = tile_doc_rect.height().max(1.0); + + let sx = ((clipped_doc_rect.left - tile_doc_rect.left) / tw) * img_w; + let sy = ((clipped_doc_rect.top - tile_doc_rect.top) / th) * img_h; + let sw = (clipped_doc_rect.width() / tw) * img_w; + let sh = (clipped_doc_rect.height() / th) * img_h; + let src = skia::Rect::from_xywh(sx, sy, sw, sh); + + self.atlas.canvas().draw_image_rect( + tile_image, + Some((&src, skia::canvas::SrcRectConstraint::Fast)), + dst, + &skia::Paint::default(), + ); Ok(()) } @@ -294,6 +354,7 @@ impl Surfaces { gpu_state: &mut GpuState, doc_rect: skia::Rect, ) -> Result<()> { + let doc_rect = self.clamp_doc_rect_to_bounds(doc_rect); if doc_rect.is_empty() { return Ok(()); } @@ -316,6 +377,18 @@ impl Surfaces { Ok(()) } + /// Clears the last atlas region written by `tile` (if any). + /// + /// This avoids clearing the entire logical tile rect which, at very low + /// zoom levels, can be enormous in document space and would unnecessarily + /// grow / rescale the atlas. + pub fn clear_tile_in_atlas(&mut self, gpu_state: &mut GpuState, tile: Tile) -> Result<()> { + if let Some(doc_rect) = self.atlas_tile_doc_rects.remove(&tile) { + self.clear_doc_rect_in_atlas(gpu_state, doc_rect)?; + } + Ok(()) + } + pub fn clear_tiles(&mut self) { self.tiles.clear(); } @@ -817,6 +890,7 @@ impl Surfaces { // Incrementally update persistent 1:1 atlas in document space. // `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%). let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect); + self.atlas_tile_doc_rects.insert(*tile, tile_doc_rect); self.tiles.add(tile_viewbox, tile, tile_image); } } @@ -825,11 +899,14 @@ impl Surfaces { self.tiles.has(tile) } - pub fn remove_cached_tile_surface(&mut self, tile: Tile) { + pub fn remove_cached_tile_surface(&mut self, gpu_state: &mut GpuState, tile: Tile) { // Mark tile as invalid // Old content stays visible until new tile overwrites it atomically, // preventing flickering during tile re-renders. self.tiles.remove(tile); + // Also clear the corresponding region in the persistent atlas to avoid + // leaving stale pixels when shapes move/delete. + let _ = self.clear_tile_in_atlas(gpu_state, tile); } pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) { @@ -914,6 +991,7 @@ impl Surfaces { /// the cache canvas for scaled previews, use `invalidate_tile_cache` instead. pub fn remove_cached_tiles(&mut self, color: skia::Color) { self.tiles.clear(); + self.atlas_tile_doc_rects.clear(); self.cache.canvas().clear(color); } @@ -923,6 +1001,7 @@ impl Surfaces { /// content while new tiles are being rendered. pub fn invalidate_tile_cache(&mut self) { self.tiles.clear(); + self.atlas_tile_doc_rects.clear(); } pub fn gc(&mut self) { From dc8073f92470188e8ee0d6d100f51ad28f7ff66e Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 23 Apr 2026 09:06:10 +0200 Subject: [PATCH 16/26] :whale: Add PENPOT_PUBLIC_URI to penpot-frontend --- docker/images/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 5d3b84d09c..b4ecc2b41d 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -105,7 +105,7 @@ services: # - "traefik.http.routers.penpot-https.tls=true" environment: - << : [*penpot-flags, *penpot-http-body-size] + << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri] penpot-backend: image: "penpotapp/backend:${PENPOT_VERSION:-latest}" From 5f7de04efe0fe5fc1caa7ec42324d2ed014ba30e Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 23 Apr 2026 09:42:40 +0200 Subject: [PATCH 17/26] :ambulance: Fix email blacklisting (#9122) --- backend/src/app/email/blacklist.clj | 14 ++++++-- .../backend_tests/email_blacklist_test.clj | 34 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 backend/test/backend_tests/email_blacklist_test.clj diff --git a/backend/src/app/email/blacklist.clj b/backend/src/app/email/blacklist.clj index ca80afb6c9..a07dfccf91 100644 --- a/backend/src/app/email/blacklist.clj +++ b/backend/src/app/email/blacklist.clj @@ -36,10 +36,18 @@ :cause cause))))) (defn contains? - "Check if email is in the blacklist." + "Check if email is in the blacklist. Also matches subdomains: if + 'somedomain.com' is blacklisted, 'xxx@foo.somedomain.com' will also + be rejected." [{:keys [::email/blacklist]} email] - (let [[_ domain] (str/split email "@" 2)] - (c/contains? blacklist (str/lower domain)))) + (let [[_ domain] (str/split email "@" 2) + parts (str/split (str/lower domain) #"\.")] + (loop [parts parts] + (if (empty? parts) + false + (if (c/contains? blacklist (str/join "." parts)) + true + (recur (rest parts))))))) (defn enabled? "Check if the blacklist is enabled" diff --git a/backend/test/backend_tests/email_blacklist_test.clj b/backend/test/backend_tests/email_blacklist_test.clj new file mode 100644 index 0000000000..5cc043fe32 --- /dev/null +++ b/backend/test/backend_tests/email_blacklist_test.clj @@ -0,0 +1,34 @@ +;; 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 backend-tests.email-blacklist-test + (:require + [app.email :as-alias email] + [app.email.blacklist :as blacklist] + [clojure.test :as t])) + +(def ^:private cfg + {::email/blacklist #{"somedomain.com" "spam.net"}}) + +(t/deftest test-exact-domain-match + (t/is (true? (blacklist/contains? cfg "user@somedomain.com"))) + (t/is (true? (blacklist/contains? cfg "user@spam.net"))) + (t/is (false? (blacklist/contains? cfg "user@legit.com")))) + +(t/deftest test-subdomain-match + (t/is (true? (blacklist/contains? cfg "user@sub.somedomain.com"))) + (t/is (true? (blacklist/contains? cfg "user@a.b.somedomain.com"))) + ;; A domain that merely contains the blacklisted string but is not a + ;; subdomain must NOT be rejected. + (t/is (false? (blacklist/contains? cfg "user@notsomedomain.com")))) + +(t/deftest test-case-insensitive + (t/is (true? (blacklist/contains? cfg "user@SOMEDOMAIN.COM"))) + (t/is (true? (blacklist/contains? cfg "user@Sub.SomeDomain.Com")))) + +(t/deftest test-non-blacklisted-domain + (t/is (false? (blacklist/contains? cfg "user@example.com"))) + (t/is (false? (blacklist/contains? cfg "user@sub.legit.com")))) From c6b6b9ce00eea60e738e101ad17abba29f81f1e6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 23 Apr 2026 09:59:11 +0200 Subject: [PATCH 18/26] :paperclip: Update changelog --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 774d17f3de..f9fb9fe1a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,13 @@ # CHANGELOG -## 2.14.4 (Unreleased) +## 2.14.4 ### :bug: Bugs fixed - Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006) +- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122) +- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927) + ## 2.14.3 From 28b33b9acc15dac4e2c335fec8fc5b22f4093844 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 23 Apr 2026 10:49:48 +0200 Subject: [PATCH 19/26] :bug: Fix props on text components (#9099) --- .../workspace/sidebar/options/menus/text.cljs | 29 ++++++++++--------- .../sidebar/options/menus/typography.cljs | 9 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index f4f1bd7ee3..f27ef298f0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -27,7 +27,7 @@ [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options + [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -297,18 +297,19 @@ multiple? (->> values vals (d/seek #(= % :multiple))) - opts #js {:ids ids - :values values - :on-change on-change - :show-recent true - :on-blur - (fn [] - (ts/schedule - 100 - (fn [] - (when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name))) - (let [node (txu/get-text-editor-content)] - (dom/focus! node))))))}] + opts (mf/props + {:ids ids + :values values + :on-change on-change + :show-recent true + :on-blur + (fn [] + (ts/schedule + 100 + (fn [] + (when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name))) + (let [node (txu/get-text-editor-content)] + (dom/focus! node))))))})] (hooks/use-stream expand-stream @@ -346,7 +347,7 @@ deprecated-icon/detach]] :else - [:> text-options opts]) + [:> text-options* opts]) [:div {:class (stl/css :text-align-options)} [:> text-align-options* opts] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 940682be89..3a27f8aefa 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -444,8 +444,7 @@ :value "lowercase" :id "text-transform-lowercase"}]]])) -(mf/defc text-options - {::mf/wrap-props false} +(mf/defc text-options* [{:keys [ids editor values on-change on-blur show-recent]}] (let [full-size-selector? (and show-recent (= (mf/use-ctx ctx/sidebar) :right)) opts #js {:editor editor @@ -502,9 +501,9 @@ :on-click on-close} deprecated-icon/tick]] - [:& text-options {:values typography - :on-change on-change - :show-recent false}]] + [:> text-options* {:values typography + :on-change on-change + :show-recent false}]] [:div {:class (stl/css :typography-info-wrapper)} [:div {:class (stl/css :typography-name-wrapper)} From 5c9696e20cf2195c565e018868d2b71f4a5bdfdb Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 23 Apr 2026 10:51:20 +0200 Subject: [PATCH 20/26] :bug: Fix color dropdown option update (#9100) --- CHANGES.md | 1 + .../sidebar/options/rows/color_row.cljs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 16dea801eb..b59234b7d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) - Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962) - Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961) +- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 120da5dced..b7d7bed534 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -243,7 +243,16 @@ open-modal (mf/use-fn - (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens index applied-token) + (mf/deps disable-gradient + disable-opacity + disable-image + disable-picker + on-change + on-close + on-open + tokens + index + applied-token) (fn [color pos tab] (let [color (cond ^boolean has-multiple-colors @@ -348,6 +357,11 @@ (mf/with-effect [color prev-color disable-picker] (when (and (not disable-picker) (not= prev-color color)) (modal/update-props! :colorpicker {:data (parse-color color)}))) + + (mf/with-effect [applied-token disable-picker] + (when (not disable-picker) + (modal/update-props! :colorpicker {:applied-token applied-token}))) + [:div {:class [class row-class]} ;; Drag handler (when (some? on-reorder) From d6b341c05374887ba4e12e10d2038db0d3d5267f Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 23 Apr 2026 10:51:30 +0200 Subject: [PATCH 21/26] :bug: Fix color token (#9095) --- .../sidebar/options/menus/color_selection.cljs | 12 +++++------- .../ui/workspace/sidebar/options/rows/color_row.cljs | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index eac46af116..0ecd41be07 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -69,12 +69,11 @@ ;; Unique color attribute maps all-colors (distinct (mapv :attrs data)) - ;; Split into: library colors, token colors, and plain colors - library-colors (filterv :ref-id all-colors) + ;; Split into mutually exclusive groups: + ;; token-colors take priority; library-colors and plain colors exclude tokens token-colors (filterv :token-name all-colors) - colors (filterv #(and (nil? (:ref-id %)) - (not (:token-name %))) - all-colors)] + library-colors (filterv (fn [c] (and (some? (:ref-id c)) (nil? (:token-name c)))) all-colors) + colors (filterv (fn [c] (and (nil? (:ref-id c)) (nil? (:token-name c)))) all-colors)] {:groups groups :all-colors all-colors :colors colors @@ -242,8 +241,7 @@ [:div {:class (stl/css :selected-color-group)} (let [token-color-extract (cond->> token-colors (not @expand-token-color) (take 3))] (for [[index token-color] (d/enumerate token-color-extract)] - (let [color {:color (:color token-color) - :opacity (:opacity token-color)}] + (let [color (dissoc token-color :token-name :has-token-applied)] [:> color-row* {:key index :color color diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index b7d7bed534..cc8dbb866c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -453,4 +453,5 @@ [:> icon-button* {:variant "ghost" :aria-label (tr "settings.select-this-color") :on-click handle-select + :tooltip-position "top-left" :icon i/move}])])) From 4a549d090756e9d1ca0e62bf145eb65782bc16f0 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Thu, 23 Apr 2026 09:57:48 +0200 Subject: [PATCH 22/26] :zap: Drain GPU queue during pan/zoom to avoid render_from_cache hitch --- render-wasm/src/render.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2920496de4..0d249f06e7 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1815,16 +1815,20 @@ impl RenderState { } // In a pure viewport interaction (pan/zoom), render_from_cache - // owns the Target surface — skip flush so we don't present - // stale tile positions. The rAF still populates the Cache - // surface and tile HashMap so render_from_cache progressively - // shows more complete content. + // owns the Target surface — don't flush Target so we don't + // present stale tile positions. We still drain the GPU command + // queue with a non-Target `flush_and_submit` so the backlog + // of tile-render commands executes incrementally instead of + // piling up for hundreds of milliseconds and blowing up the + // next `render_from_cache` call into a multi-frame hitch. // // During interactive shape transforms (drag/resize/rotate) we // still need to flush every rAF so the user sees the updated // shape position — render_from_cache is not in the loop here. if !self.options.is_viewport_interaction() { self.flush_and_submit(); + } else { + self.gpu_state.context.flush_and_submit(); } if self.render_in_progress { From 96722fde4bfe99eda210d06c29b5bc492f3468d2 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 21 Apr 2026 16:10:18 +0200 Subject: [PATCH 23/26] :bug: Support EvenOdd SVG attribute across all path operations --- .../render-wasm/get-file-paths-evenodd.json | 822 ++++++++++++++++++ .../ui/render-wasm-specs/shapes.spec.js | 16 + .../Renders-svg-paths-with-evenodd-1.png | Bin 0 -> 23531 bytes render-wasm/src/render/fills.rs | 4 +- render-wasm/src/render/strokes.rs | 12 +- render-wasm/src/shapes.rs | 7 +- render-wasm/src/shapes/paths.rs | 11 +- render-wasm/src/shapes/stroke_paths.rs | 2 +- 8 files changed, 860 insertions(+), 14 deletions(-) create mode 100644 frontend/playwright/data/render-wasm/get-file-paths-evenodd.json create mode 100644 frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-svg-paths-with-evenodd-1.png diff --git a/frontend/playwright/data/render-wasm/get-file-paths-evenodd.json b/frontend/playwright/data/render-wasm/get-file-paths-evenodd.json new file mode 100644 index 0000000000..fb2a5f3f40 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-paths-evenodd.json @@ -0,0 +1,822 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/objects-map", + "render-wasm/v1", + "text-editor-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "svg_path_evenodd", + "~:revn": 18, + "~:modified-at": "~m1776843383797", + "~:vern": 0, + "~:id": "~u3e84615b-5628-818c-8007-e7563bb081fb", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node", + "0017-fix-layout-flex-dir" + ] + }, + "~:version": 67, + "~:project-id": "~u6bd7c17d-4f59-815e-8006-5c1f68846e43", + "~:created-at": "~m1776779037378", + "~:backend": "legacy-db", + "~:data": { + "~:pages": ["~u3e84615b-5628-818c-8007-e7563bb081fc"], + "~:pages-index": { + "~u3e84615b-5628-818c-8007-e7563bb081fc": { + "~:objects": { + "~#penpot/objects-map/v2": { + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b7399e693\",\"~ud0a635f7-639e-80f3-8007-e84b73ad74f4\",\"~ud0a635f7-639e-80f3-8007-e84b73bdd7da\",\"~ud0a635f7-639e-80f3-8007-e84b73ce5ead\",\"~ud0a635f7-639e-80f3-8007-e84b73d4a494\"]]]", + "~ud0a635f7-639e-80f3-8007-e84b73ce5ead": "[\"~#shape\",[\"^ \",\"~:y\",-840.999998986721,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"04-venn-circles\",\"~:width\",99.99999761581421,\"~:type\",\"~:group\",\"~:svg-attrs\",[\"^ \",\"^5\",\"120\",\"~:height\",\"120\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-840.999998986721]],[\"^;\",[\"^ \",\"~:x\",188.9999984701475,\"~:y\",-840.999998986721]],[\"^;\",[\"^ \",\"~:x\",188.9999984701475,\"~:y\",-741.0000013709068]],[\"^;\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-741.0000013709068]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73ce5ead\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",89.00000085433328,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-840.999998986721,\"^5\",99.99999761581421,\"^9\",99.99999761581421,\"~:x1\",89.00000085433328,\"~:y1\",-840.999998986721,\"~:x2\",188.9999984701475,\"~:y2\",-741.0000013709068]],\"~:fills\",[],\"~:flip-x\",null,\"^9\",99.99999761581421,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b73ce9306\",\"~ud0a635f7-639e-80f3-8007-e84b73cf1ec2\"]]]", + "~ud0a635f7-639e-80f3-8007-e84b739a1146": "[\"~#shape\",[\"^ \",\"~:y\",-725.9999912977219,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"base-background\",\"~:width\",100,\"~:type\",\"~:rect\",\"~:svg-attrs\",[\"^ \",\"~:fill\",\"none\",\"~:id\",\"base-background\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-725.9999912977219]],[\"^<\",[\"^ \",\"~:x\",357.9999885559082,\"~:y\",-725.9999912977219]],[\"^<\",[\"^ \",\"~:x\",357.9999885559082,\"~:y\",-625.9999912977219]],[\"^<\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-625.9999912977219]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:hidden\",true,\"^:\",\"~ud0a635f7-639e-80f3-8007-e84b739a1146\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b7399e693\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^5\",100,\"~:height\",100,\"~:x1\",0,\"~:y1\",0,\"~:x2\",100,\"~:y2\",100]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",257.9999885559082,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"^E\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-725.9999912977219,\"^5\",100,\"^F\",100,\"^G\",257.9999885559082,\"^H\",-725.9999912977219,\"^I\",357.9999885559082,\"^J\",-625.9999912977219]],\"~:fills\",[],\"~:flip-x\",null,\"^F\",100,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73ce9306": "[\"~#shape\",[\"^ \",\"~:y\",-840.999998986721,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"base-background\",\"~:width\",99.99999761581421,\"~:type\",\"~:rect\",\"~:svg-attrs\",[\"^ \",\"~:fill\",\"none\",\"~:id\",\"base-background\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-840.999998986721]],[\"^<\",[\"^ \",\"~:x\",188.9999984701475,\"~:y\",-840.999998986721]],[\"^<\",[\"^ \",\"~:x\",188.9999984701475,\"~:y\",-741.0000013709068]],[\"^<\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-741.0000013709068]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:hidden\",true,\"^:\",\"~ud0a635f7-639e-80f3-8007-e84b73ce9306\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73ce5ead\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^5\",120,\"~:height\",120,\"~:x1\",0,\"~:y1\",0,\"~:x2\",120,\"~:y2\",120]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",89.00000085433328,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"^E\",[\"^ \",\"~:x\",89.00000085433328,\"~:y\",-840.999998986721,\"^5\",99.99999761581421,\"^F\",99.99999761581421,\"^G\",89.00000085433328,\"^H\",-840.999998986721,\"^I\",188.9999984701475,\"^J\",-741.0000013709068]],\"~:fills\",[],\"~:flip-x\",null,\"^F\",99.99999761581421,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73be6505": "[\"~#shape\",[\"^ \",\"~:y\",-841,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"base-background\",\"~:width\",99.99998211860657,\"~:type\",\"~:rect\",\"~:svg-attrs\",[\"^ \",\"~:fill\",\"none\",\"~:id\",\"base-background\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-841]],[\"^<\",[\"^ \",\"~:x\",288.99998104572296,\"~:y\",-841]],[\"^<\",[\"^ \",\"~:x\",288.99998104572296,\"~:y\",-741]],[\"^<\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-741]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:hidden\",true,\"^:\",\"~ud0a635f7-639e-80f3-8007-e84b73be6505\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73bdd7da\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^5\",100,\"~:height\",100,\"~:x1\",0,\"~:y1\",0,\"~:x2\",100,\"~:y2\",100]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",188.9999989271164,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"^E\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-841,\"^5\",99.99998211860657,\"^F\",100,\"^G\",188.9999989271164,\"^H\",-841,\"^I\",288.99998104572296,\"^J\",-741]],\"~:fills\",[],\"~:flip-x\",null,\"^F\",100,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73ae0f65": "[\"~#shape\",[\"^ \",\"~:y\",null,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:content\",[\"~#penpot/path-data\",\"~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAgJpDAMBPxAIAAAAAAAAAAAAAAAAAAAAAAAAAAIDCQwDAT8QCAAAAAAAAAAAAAAAAAAAAAAAAAACAwkMAwDvEAgAAAAAAAAAAAAAAAAAAAAAAAAAAgJpDAMA7xAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAokMAAEzEAgAAAAAAAAAAAAAAAAAAAAAAAAAAALtDAABMxAIAAAAAAAAAAAAAAAAAAAAAAAAAAAC7QwCAP8QCAAAAAAAAAAAAAAAAAAAAAAAAAAAAokMAgD/EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAICpQwBASMQCAAAAAAAAAAAAAAAAAAAAAAAAAACAs0MAQEjEAgAAAAAAAAAAAAAAAAAAAAAAAAAAgLNDAEBDxAIAAAAAAAAAAAAAAAAAAAAAAAAAAICpQwBAQ8QEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"],\"~:name\",\"svg-path\",\"~:width\",null,\"~:type\",\"~:path\",\"~:svg-attrs\",[\"^ \",\"~:fillRule\",\"evenodd\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",309,\"~:y\",-831]],[\"^=\",[\"^ \",\"~:x\",389,\"~:y\",-831]],[\"^=\",[\"^ \",\"~:x\",389,\"~:y\",-751]],[\"^=\",[\"^ \",\"~:x\",309,\"~:y\",-751]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:svg-transform\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73ae0f65\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73ad74f4\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",10,\"~:y\",10,\"^7\",80,\"~:height\",80,\"~:x1\",10,\"~:y1\",10,\"~:x2\",90,\"~:y2\",90]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-color\",\"#1a6a3a\",\"~:stroke-opacity\",1,\"~:stroke-width\",3]],\"~:x\",null,\"~:proportion\",1,\"~:selrect\",[\"^D\",[\"^ \",\"~:x\",309,\"~:y\",-831,\"^7\",80,\"^E\",80,\"^F\",309,\"^G\",-831,\"^H\",389,\"^I\",-751]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#4ae290\"]],\"~:flip-x\",null,\"^E\",null,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73d53705": "[\"~#shape\",[\"^ \",\"~:y\",null,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:content\",[\"~#penpot/path-data\",\"~bAQAAAAAAAAAAAAAAAAAAAAAAAAABACpDq2ozxAMAAAD2/BJDq2ozxFVVAEPDwC7EVVUAQwAAKcQDAAAAVVUAQz4/I8T2/BJDVpUexAEAKkNWlR7EAwAAAAoDQUNWlR7EqqpTQz4/I8SqqlNDAAApxAMAAACqqlNDw8AuxAoDQUOrajPEAQAqQ6tqM8QCAAAAAAAAAAAAAAAAAAAAAAAAAAEAKkOrajPEAQAAAAAAAAAAAAAAAAAAAAAAAAABACpDAEAvxAMAAAAmeiRDAEAvxAEAIEN3IS7EAQAgQwDALMQDAAAAAQAgQ4leK8QmeiRDAEAqxAEAKkMAQCrEAwAAANuFL0MAQCrEAQA0Q4leK8QBADRDAMAsxAMAAAABADRDdyEuxNuFL0MAQC/EAQAqQwBAL8QCAAAAAAAAAAAAAAAAAAAAAAAAAAEAKkMAQC/EAQAAAAAAAAAAAAAAAAAAAAAAAABWVRRDrKogxAMAAABWVRRDrGokxKuqHEOr6ibEAQAqQ6vqJsQDAAAAVVU3Q6vqJsSqqj9DrGokxKqqP0OsqiDEAwAAAKqqP0MBQCDE//89Q1bVH8RVVTxDVtUfxAIAAAAAAAAAAAAAAAAAAAAAAAAAq6oXQ1bVH8QDAAAAAQAWQ1bVH8RWVRRDAUAgxFZVFEOsqiDEAgAAAAAAAAAAAAAAAAAAAAAAAABWVRRDrKogxA==\"],\"~:name\",\"svg-path\",\"~:width\",null,\"~:type\",\"~:path\",\"~:svg-attrs\",[\"^ \",\"~:fillRule\",\"evenodd\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",128.33333949247913,\"~:y\",-717.6666545867935]],[\"^=\",[\"^ \",\"~:x\",211.6666658719356,\"~:y\",-717.6666545867935]],[\"^=\",[\"^ \",\"~:x\",211.6666658719356,\"~:y\",-634.3333282073353]],[\"^=\",[\"^ \",\"~:x\",128.33333949247913,\"~:y\",-634.3333282073353]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:svg-transform\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73d53705\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73d4a494\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",5,\"~:y\",5,\"^7\",50,\"~:height\",50,\"~:x1\",5,\"~:y1\",5,\"~:x2\",55,\"~:y2\",55]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-color\",\"#6a4a1a\",\"~:stroke-opacity\",1,\"~:stroke-width\",2]],\"~:x\",null,\"~:proportion\",1,\"~:selrect\",[\"^D\",[\"^ \",\"~:x\",128.33333949247913,\"~:y\",-717.6666545867935,\"^7\",83.33332637945648,\"^E\",83.33332637945819,\"^F\",128.33333949247913,\"^G\",-717.6666545867935,\"^H\",211.6666658719356,\"^I\",-634.3333282073353]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e2a04a\"]],\"~:flip-x\",null,\"^E\",null,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73cf1ec2": "[\"~#shape\",[\"^ \",\"~:y\",null,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:content\",[\"~#penpot/path-data\",\"~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAAMtCAOBIxAMAAAAAAMtCnv9FxKKn3UKqqkPEqqr0QqqqQ8QDAAAA29YFQ6qqQ8SrKg9Dnv9FxKsqD0MA4EjEAwAAAKsqD0NhwEvE29YFQ1UVTsSqqvRCVRVOxAMAAACip91CVRVOxAAAy0JhwEvEAADLQgDgSMQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAABV1QZDAOBIxAMAAABV1QZDnv9FxCUpEEOqqkPEqqobQ6qqQ8QDAAAAMCwnQ6qqQ8QAgDBDnv9FxACAMEMA4EjEAwAAAACAMENhwEvEMCwnQ1UVTsSqqhtDVRVOxAMAAAAlKRBDVRVOxFXVBkNhwEvEVdUGQwDgSMQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAABWVexCVZVBxAMAAABWVexC9LQ+xPb8/kIAYDzEAAALQwBgPMQDAAAAhYEWQwBgPMRV1R9D9LQ+xFXVH0NVlUHEAwAAAFXVH0O2dUTEhYEWQ6rKRsQAAAtDqspGxAMAAAD2/P5CqspGxFZV7EK2dUTEVlXsQlWVQcQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"],\"~:name\",\"svg-path\",\"~:width\",null,\"~:type\",\"~:path\",\"~:svg-attrs\",[\"^ \",\"~:fillRule\",\"evenodd\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",101.50000055631006,\"~:y\",-824.3333327174187]],[\"^=\",[\"^ \",\"~:x\",176.4999987681707,\"~:y\",-824.3333327174187]],[\"^=\",[\"^ \",\"~:x\",176.4999987681707,\"~:y\",-753.5000010728836]],[\"^=\",[\"^ \",\"~:x\",101.50000055631006,\"~:y\",-753.5000010728836]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:svg-transform\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73cf1ec2\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73ce5ead\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",15,\"~:y\",20,\"^7\",90,\"~:height\",85,\"~:x1\",15,\"~:y1\",20,\"~:x2\",105,\"~:y2\",105]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-color\",\"#3a1a6a\",\"~:stroke-opacity\",1,\"~:stroke-width\",3]],\"~:x\",null,\"~:proportion\",1,\"~:selrect\",[\"^D\",[\"^ \",\"~:x\",101.50000055631006,\"~:y\",-824.3333327174187,\"^7\",74.99999821186066,\"^E\",70.83333164453506,\"^F\",101.50000055631006,\"^G\",-824.3333327174187,\"^H\",176.4999987681707,\"^I\",-753.5000010728836]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#9a4ae2\"]],\"~:flip-x\",null,\"^E\",null,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73bea73d": "[\"~#shape\",[\"^ \",\"~:y\",null,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:content\",[\"~#penpot/path-data\",\"~bAQAAAAAAAAAAAAAAAAAAAAAAAAD//25DAABRxAIAAAAAAAAAAAAAAAAAAAAAAAAAAABVQwDAPMQCAAAAAAAAAAAAAAAAAAAAAAAAAP//jEMAQEnEAgAAAAAAAAAAAAAAAAAAAAAAAAAAAERDAEBJxAIAAAAAAAAAAAAAAAAAAAAAAAAAAICEQwDAPMQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"],\"~:name\",\"svg-path\",\"~:width\",null,\"~:type\",\"~:path\",\"~:svg-attrs\",[\"^ \",\"~:fillRule\",\"evenodd\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",195.99999767541885,\"~:y\",-836]],[\"^=\",[\"^ \",\"~:x\",281.9999822974205,\"~:y\",-836]],[\"^=\",[\"^ \",\"~:x\",281.9999822974205,\"~:y\",-755]],[\"^=\",[\"^ \",\"~:x\",195.99999767541885,\"~:y\",-755]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:svg-transform\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73bea73d\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73bdd7da\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",7,\"~:y\",5,\"^7\",86,\"~:height\",81,\"~:x1\",7,\"~:y1\",5,\"~:x2\",93,\"~:y2\",86]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-color\",\"#6a1a1a\",\"~:stroke-opacity\",1,\"~:stroke-width\",3]],\"~:x\",null,\"~:proportion\",1,\"~:selrect\",[\"^D\",[\"^ \",\"~:x\",195.99999767541885,\"~:y\",-836,\"^7\",85.99998462200165,\"^E\",81,\"^F\",195.99999767541885,\"^G\",-836,\"^H\",281.9999822974205,\"^I\",-755]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e24a4a\"]],\"~:flip-x\",null,\"^E\",null,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73d4e3fc": "[\"~#shape\",[\"^ \",\"~:y\",-725.9999872247394,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"base-background\",\"~:width\",99.99999165534774,\"~:type\",\"~:rect\",\"~:svg-attrs\",[\"^ \",\"~:fill\",\"none\",\"~:id\",\"base-background\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-725.9999872247394]],[\"^<\",[\"^ \",\"~:x\",219.99999850988124,\"~:y\",-725.9999872247394]],[\"^<\",[\"^ \",\"~:x\",219.99999850988124,\"~:y\",-625.9999955693894]],[\"^<\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-625.9999955693894]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:hidden\",true,\"^:\",\"~ud0a635f7-639e-80f3-8007-e84b73d4e3fc\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73d4a494\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^5\",60,\"~:height\",60,\"~:x1\",0,\"~:y1\",0,\"~:x2\",60,\"~:y2\",60]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",120.0000068545335,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"^E\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-725.9999872247394,\"^5\",99.99999165534774,\"^F\",99.99999165534996,\"^G\",120.0000068545335,\"^H\",-725.9999872247394,\"^I\",219.99999850988124,\"^J\",-625.9999955693894]],\"~:fills\",[],\"~:flip-x\",null,\"^F\",99.99999165534996,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73ada09b": "[\"~#shape\",[\"^ \",\"~:y\",-841,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"base-background\",\"~:width\",100,\"~:type\",\"~:rect\",\"~:svg-attrs\",[\"^ \",\"~:fill\",\"none\",\"~:id\",\"base-background\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",299,\"~:y\",-841]],[\"^<\",[\"^ \",\"~:x\",399,\"~:y\",-841]],[\"^<\",[\"^ \",\"~:x\",399,\"~:y\",-741]],[\"^<\",[\"^ \",\"~:x\",299,\"~:y\",-741]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:hidden\",true,\"^:\",\"~ud0a635f7-639e-80f3-8007-e84b73ada09b\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b73ad74f4\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^5\",100,\"~:height\",100,\"~:x1\",0,\"~:y1\",0,\"~:x2\",100,\"~:y2\",100]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",299,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"^E\",[\"^ \",\"~:x\",299,\"~:y\",-841,\"^5\",100,\"^F\",100,\"^G\",299,\"^H\",-841,\"^I\",399,\"^J\",-741]],\"~:fills\",[],\"~:flip-x\",null,\"^F\",100,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73bdd7da": "[\"~#shape\",[\"^ \",\"~:y\",-841,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"03-pentagram\",\"~:width\",99.99998211860657,\"~:type\",\"~:group\",\"~:svg-attrs\",[\"^ \",\"^5\",\"100\",\"~:height\",\"100\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-841]],[\"^;\",[\"^ \",\"~:x\",288.99998104572296,\"~:y\",-841]],[\"^;\",[\"^ \",\"~:x\",288.99998104572296,\"~:y\",-741]],[\"^;\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-741]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73bdd7da\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",188.9999989271164,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",188.9999989271164,\"~:y\",-841,\"^5\",99.99998211860657,\"^9\",100,\"~:x1\",188.9999989271164,\"~:y1\",-841,\"~:x2\",288.99998104572296,\"~:y2\",-741]],\"~:fills\",[],\"~:flip-x\",null,\"^9\",100,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b73be6505\",\"~ud0a635f7-639e-80f3-8007-e84b73bea73d\"]]]", + "~ud0a635f7-639e-80f3-8007-e84b739aa576": "[\"~#shape\",[\"^ \",\"~:y\",null,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:content\",[\"~#penpot/path-data\",\"~bAQAAAAAAAAAAAAAAAAAAAAAAAAAAAJpDAAAzxAMAAABN9I5DAAAzxAAAhkPZhS7EAACGQwAAKcQDAAAAAACGQyd6I8RN9I5DAAAfxAAAmkMAAB/EAwAAALMLpUMAAB/EAACuQyd6I8QAAK5DAAApxAMAAAAAAK5D2YUuxLMLpUMAADPEAACaQwAAM8QEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAJpDAAAuxAMAAAAnepRDAAAuxAAAkEPtwivEAACQQwAAKcQDAAAAAACQQxM9JsQnepRDAAAkxAAAmkMAACTEAwAAANmFn0MAACTEAACkQxM9JsQAAKRDAAApxAMAAAAAAKRD7cIrxNmFn0MAAC7EAACaQwAALsQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"],\"~:name\",\"svg-path\",\"~:width\",null,\"~:type\",\"~:path\",\"~:svg-attrs\",[\"^ \",\"~:fillRule\",\"evenodd\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",267.9999885559082,\"~:y\",-715.9999912977219]],[\"^=\",[\"^ \",\"~:x\",347.9999885559082,\"~:y\",-715.9999912977219]],[\"^=\",[\"^ \",\"~:x\",347.9999885559082,\"~:y\",-635.9999912977219]],[\"^=\",[\"^ \",\"~:x\",267.9999885559082,\"~:y\",-635.9999912977219]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:svg-transform\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b739aa576\",\"~:parent-id\",\"~ud0a635f7-639e-80f3-8007-e84b7399e693\",\"~:svg-viewbox\",[\"~#rect\",[\"^ \",\"~:x\",10,\"~:y\",10,\"^7\",80,\"~:height\",80,\"~:x1\",10,\"~:y1\",10,\"~:x2\",90,\"~:y2\",90]],\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-color\",\"#1a3a6a\",\"~:stroke-opacity\",1,\"~:stroke-width\",3]],\"~:x\",null,\"~:proportion\",1,\"~:selrect\",[\"^D\",[\"^ \",\"~:x\",267.9999885559082,\"~:y\",-715.9999912977219,\"^7\",80,\"^E\",80,\"^F\",267.9999885559082,\"^G\",-715.9999912977219,\"^H\",347.9999885559082,\"^I\",-635.9999912977219]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#4a90e2\"]],\"~:flip-x\",null,\"^E\",null,\"~:flip-y\",null]]", + "~ud0a635f7-639e-80f3-8007-e84b73ad74f4": "[\"~#shape\",[\"^ \",\"~:y\",-841,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"02-nested-squares\",\"~:width\",100,\"~:type\",\"~:group\",\"~:svg-attrs\",[\"^ \",\"^5\",\"100\",\"~:height\",\"100\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",299,\"~:y\",-841]],[\"^;\",[\"^ \",\"~:x\",399,\"~:y\",-841]],[\"^;\",[\"^ \",\"~:x\",399,\"~:y\",-741]],[\"^;\",[\"^ \",\"~:x\",299,\"~:y\",-741]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73ad74f4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",299,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",299,\"~:y\",-841,\"^5\",100,\"^9\",100,\"~:x1\",299,\"~:y1\",-841,\"~:x2\",399,\"~:y2\",-741]],\"~:fills\",[],\"~:flip-x\",null,\"^9\",100,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b73ada09b\",\"~ud0a635f7-639e-80f3-8007-e84b73ae0f65\"]]]", + "~ud0a635f7-639e-80f3-8007-e84b73d4a494": "[\"~#shape\",[\"^ \",\"~:y\",-725.9999872247394,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"05-person-icon\",\"~:width\",99.99999165534774,\"~:type\",\"~:group\",\"~:svg-attrs\",[\"^ \",\"^5\",\"60\",\"~:height\",\"60\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-725.9999872247394]],[\"^;\",[\"^ \",\"~:x\",219.99999850988124,\"~:y\",-725.9999872247394]],[\"^;\",[\"^ \",\"~:x\",219.99999850988124,\"~:y\",-625.9999955693894]],[\"^;\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-625.9999955693894]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b73d4a494\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",120.0000068545335,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",120.0000068545335,\"~:y\",-725.9999872247394,\"^5\",99.99999165534774,\"^9\",99.99999165534996,\"~:x1\",120.0000068545335,\"~:y1\",-725.9999872247394,\"~:x2\",219.99999850988124,\"~:y2\",-625.9999955693894]],\"~:fills\",[],\"~:flip-x\",null,\"^9\",99.99999165534996,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b73d4e3fc\",\"~ud0a635f7-639e-80f3-8007-e84b73d53705\"]]]", + "~ud0a635f7-639e-80f3-8007-e84b7399e693": "[\"~#shape\",[\"^ \",\"~:y\",-725.9999912977219,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"01-ring\",\"~:width\",100,\"~:type\",\"~:group\",\"~:svg-attrs\",[\"^ \",\"^5\",\"100\",\"~:height\",\"100\"],\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-725.9999912977219]],[\"^;\",[\"^ \",\"~:x\",357.9999885559082,\"~:y\",-725.9999912977219]],[\"^;\",[\"^ \",\"~:x\",357.9999885559082,\"~:y\",-625.9999912977219]],[\"^;\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-625.9999912977219]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ud0a635f7-639e-80f3-8007-e84b7399e693\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:svg-defs\",[\"^ \"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",257.9999885559082,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",257.9999885559082,\"~:y\",-725.9999912977219,\"^5\",100,\"^9\",100,\"~:x1\",257.9999885559082,\"~:y1\",-725.9999912977219,\"~:x2\",357.9999885559082,\"~:y2\",-625.9999912977219]],\"~:fills\",[],\"~:flip-x\",null,\"^9\",100,\"~:flip-y\",null,\"~:shapes\",[\"~ud0a635f7-639e-80f3-8007-e84b739a1146\",\"~ud0a635f7-639e-80f3-8007-e84b739aa576\"]]]" + } + }, + "~:id": "~u3e84615b-5628-818c-8007-e7563bb081fc", + "~:name": "Page 1" + } + }, + "~:id": "~u3e84615b-5628-818c-8007-e7563bb081fb", + "~:options": { "~:components-v2": true, "~:base-font-size": "16px" }, + "~:components": { + "~u7c8614ca-087a-80b1-8007-e75c161f105c": { + "~:path": "Icons / 16", + "~:deleted": true, + "~:main-instance-id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:objects": { + "~u7c8614ca-087a-80b1-8007-e75c161ef12f": { + "~#shape": { + "~:y": -622.2015816167936, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Icons / 16 / profile", + "~:width": 16, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 185.68057966317429, + "~:y": -622.2015816167936 + } + }, + { + "~#point": { + "~:x": 201.68057966317429, + "~:y": -622.2015816167936 + } + }, + { + "~#point": { + "~:x": 201.68057966317429, + "~:y": -606.2015816167934 + } + }, + { + "~#point": { + "~:x": 185.68057966317429, + "~:y": -606.2015816167934 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": true, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:applied-tokens": { + "~:width": "xx.alias.icon.size.s", + "~:height": "xx.alias.icon.size.s" + }, + "~:component-id": "~u7c8614ca-087a-80b1-8007-e75c161f105c", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 185.68057966317429, + "~:main-instance": true, + "~:proportion": 0.999999999999981, + "~:selrect": { + "~#rect": { + "~:x": 185.68057966317429, + "~:y": -622.2015816167936, + "~:width": 16, + "~:height": 16.000000000000227, + "~:x1": 185.68057966317429, + "~:y1": -622.2015816167936, + "~:x2": 201.68057966317429, + "~:y2": -606.2015816167934 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 16.000000000000227, + "~:component-file": "~u3e84615b-5628-818c-8007-e7563bb081fb", + "~:flip-y": null, + "~:shapes": [ + "~u7c8614ca-087a-80b1-8007-e75c161ef130", + "~u7c8614ca-087a-80b1-8007-e75c161ef131" + ] + } + }, + "~u7c8614ca-087a-80b1-8007-e75c161ef130": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAADmWERD5wwaxAMAAADmWERDpa4ZxD4nQ0M9YhnEOq5BQz1iGcQDAAAAMjVAQz1iGcSOAz9Dpa4ZxI4DP0PnDBrEAwAAAI4DP0MpaxrEMjVAQ5G3GsQ6rkFDkbcaxAMAAAA+J0NDkbcaxOZYREMpaxrE5lhEQ+cMGsQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAACOA0ND5wwaxAMAAACOA0NDxd0ZxL5qQkORtxnEOq5BQ5G3GcQDAAAAtvFAQ5G3GcTmWEBDxd0ZxOZYQEPnDBrEAwAAAOZYQEMHPBrEtvFAQz1iGsQ6rkFDPWIaxAMAAAC+akJDPWIaxI4DQ0MHPBrEjgNDQ+cMGsQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill-rule": "evenodd", + "~:clip-rule": "evenodd" + }, + "~:points": [ + { + "~#point": { + "~:x": 191.01391299650777, + "~:y": -618.8682482834602 + } + }, + { + "~#point": { + "~:x": 196.34724632984125, + "~:y": -618.8682482834602 + } + }, + { + "~#point": { + "~:x": 196.34724632984125, + "~:y": -613.5349149501269 + } + }, + { + "~#point": { + "~:x": 191.01391299650777, + "~:y": -613.5349149501269 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:constraints-v": "~:scale", + "~:svg-transform": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + }, + "~:constraints-h": "~:scale", + "~:id": "~u7c8614ca-087a-80b1-8007-e75c161ef130", + "~:parent-id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:svg-viewbox": { + "~:y": 5, + "~:y1": 5, + "~:width": 8, + "~:x": 8, + "~:x1": 8, + "~:y2": 13, + "~:x2": 16, + "~:height": 8 + }, + "~:applied-tokens": {}, + "~:svg-defs": {}, + "~:frame-id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 1, + "~:stroke-color": "#121270", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 191.01391299650777, + "~:y": -618.8682482834604, + "~:width": 5.333333333333485, + "~:height": 5.3333333333332575, + "~:x1": 191.01391299650777, + "~:y1": -618.8682482834604, + "~:x2": 196.34724632984125, + "~:y2": -613.5349149501271 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~u7c8614ca-087a-80b1-8007-e75c161ef131": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAA6rkFDPWIbxAMAAABmoT1DPWIbxOZYOkMbkBrE5lg6Q+eMGcQDAAAA5lg6Q7OJGMRmoT1DkbcXxDquQUORtxfEAwAAAAq7RUORtxfEjgNJQ7OJGMSOA0lD54wZxAMAAACOA0lDG5AaxAq7RUM9YhvEOq5BQz1iG8QCAAAAAAAAAAAAAAAAAAAAAAAAADquQUM9YhvEAQAAAAAAAAAAAAAAAAAAAAAAAAA6rjtD54wZxAMAAAA6rjtDuTMZxNYnPEOj4RjE2vM8Q3WgGMQDAAAAggw+Q0/8GMQyxz9DkTcZxD65QUORNxnEAwAAAM6kQ0ORNxnEdlpFQ9f9GMSac0ZDA6QYxAMAAAD6OEdDneQYxDquR0NVNRnEOq5HQ+eMGcQDAAAAOq5HQ/tgGsSK/kRD5wwbxDquQUPnDBvEAwAAAOpdPkPnDBvEOq47Q/tgGsQ6rjtD54wZxAIAAAAAAAAAAAAAAAAAAAAAAAAAOq47Q+eMGcQBAAAAAAAAAAAAAAAAAAAAAAAAADquQUPnDBjEAwAAAEI/QEPnDBjEVu4+QxMtGMQm5j1DwWIYxAMAAAAuvj5D168YxCokQEM94hjEPrlBQz3iGMQDAAAAQklDQz3iGMRiq0RDFbEYxDaERUOdZRjEAwAAAP55REM5LhjEqiNDQ+cMGMQ6rkFD5wwYxAIAAAAAAAAAAAAAAAAAAAAAAAAAOq5BQ+cMGMQ=" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill-rule": "evenodd", + "~:clip-rule": "evenodd" + }, + "~:points": [ + { + "~#point": { + "~:x": 186.34724632984103, + "~:y": -621.5349149501271 + } + }, + { + "~#point": { + "~:x": 201.01391299650777, + "~:y": -621.5349149501271 + } + }, + { + "~#point": { + "~:x": 201.01391299650777, + "~:y": -606.8682482834602 + } + }, + { + "~#point": { + "~:x": 186.34724632984103, + "~:y": -606.8682482834602 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:constraints-v": "~:scale", + "~:svg-transform": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + }, + "~:constraints-h": "~:scale", + "~:id": "~u7c8614ca-087a-80b1-8007-e75c161ef131", + "~:parent-id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:svg-viewbox": { + "~:y": 1, + "~:y1": 1, + "~:width": 22, + "~:x": 1, + "~:x1": 1, + "~:y2": 23, + "~:x2": 23, + "~:height": 22 + }, + "~:applied-tokens": {}, + "~:svg-defs": {}, + "~:frame-id": "~u7c8614ca-087a-80b1-8007-e75c161ef12f", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 1, + "~:stroke-color": "#121270", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 186.3472463298408, + "~:y": -621.5349149501271, + "~:width": 14.666666666666742, + "~:height": 14.66666666666697, + "~:x1": 186.3472463298408, + "~:y1": -621.5349149501271, + "~:x2": 201.01391299650754, + "~:y2": -606.8682482834602 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + } + }, + "~:name": "profile", + "~:modified-at": "~m1776843284702", + "~:main-instance-page": "~u3e84615b-5628-818c-8007-e7563bb081fc", + "~:id": "~u7c8614ca-087a-80b1-8007-e75c161f105c" + }, + "~u2094e2d4-1854-804d-8007-e761fd29d15c": { + "~:path": "Icons / 16", + "~:deleted": true, + "~:main-instance-id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:objects": { + "~u2094e2d4-1854-804d-8007-e761fd24f93e": { + "~#shape": { + "~:y": -623.0000079548252, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Icons / 16 / profile", + "~:width": 16, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 214.0000008841555, + "~:y": -623.0000079548252 + } + }, + { + "~#point": { + "~:x": 230.0000008841555, + "~:y": -623.0000079548252 + } + }, + { + "~#point": { + "~:x": 230.0000008841555, + "~:y": -607.0000079548249 + } + }, + { + "~#point": { + "~:x": 214.0000008841555, + "~:y": -607.0000079548249 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": true, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:applied-tokens": { + "~:width": "xx.alias.icon.size.s", + "~:height": "xx.alias.icon.size.s" + }, + "~:component-id": "~u2094e2d4-1854-804d-8007-e761fd29d15c", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 214.0000008841555, + "~:main-instance": true, + "~:proportion": 0.999999999999981, + "~:selrect": { + "~#rect": { + "~:x": 214.0000008841555, + "~:y": -623.0000079548252, + "~:width": 16, + "~:height": 16.000000000000227, + "~:x1": 214.0000008841555, + "~:y1": -623.0000079548252, + "~:x2": 230.0000008841555, + "~:y2": -607.0000079548249 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 16.000000000000227, + "~:component-file": "~u3e84615b-5628-818c-8007-e7563bb081fb", + "~:flip-y": null, + "~:shapes": [ + "~u2094e2d4-1854-804d-8007-e761fd24f93f", + "~u2094e2d4-1854-804d-8007-e761fd24f940" + ] + } + }, + "~u2094e2d4-1854-804d-8007-e761fd24f93f": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAACrqmBDAEAaxAMAAACrqmBDvuEZxAN5X0NWlRnE//9dQ1aVGcQDAAAA94ZcQ1aVGcRTVVtDvuEZxFNVW0MAQBrEAwAAAFNVW0NCnhrE94ZcQ6rqGsT//11DquoaxAMAAAADeV9DquoaxKuqYENCnhrEq6pgQwBAGsQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAABTVV9DAEAaxAMAAABTVV9D3hAaxIO8XkOq6hnE//9dQ6rqGcQDAAAAe0NdQ6rqGcSrqlxD3hAaxKuqXEMAQBrEAwAAAKuqXEMgbxrEe0NdQ1aVGsT//11DVpUaxAMAAACDvF5DVpUaxFNVX0MgbxrEU1VfQwBAGsQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill-rule": "evenodd", + "~:clip-rule": "evenodd" + }, + "~:points": [ + { + "~#point": { + "~:x": 219.33333421748898, + "~:y": -619.6666746214917 + } + }, + { + "~#point": { + "~:x": 224.66666755082247, + "~:y": -619.6666746214917 + } + }, + { + "~#point": { + "~:x": 224.66666755082247, + "~:y": -614.3333412881584 + } + }, + { + "~#point": { + "~:x": 219.33333421748898, + "~:y": -614.3333412881584 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:constraints-v": "~:scale", + "~:svg-transform": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + }, + "~:constraints-h": "~:scale", + "~:id": "~u2094e2d4-1854-804d-8007-e761fd24f93f", + "~:parent-id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:svg-viewbox": { + "~:y": 5, + "~:y1": 5, + "~:width": 8, + "~:x": 8, + "~:x1": 8, + "~:y2": 13, + "~:x2": 16, + "~:height": 8 + }, + "~:applied-tokens": {}, + "~:svg-defs": {}, + "~:frame-id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 1, + "~:stroke-color": "#121270", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 219.33333421748898, + "~:y": -619.6666746214919, + "~:width": 5.333333333333485, + "~:height": 5.3333333333332575, + "~:x1": 219.33333421748898, + "~:y1": -619.6666746214919, + "~:x2": 224.66666755082247, + "~:y2": -614.3333412881586 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~u2094e2d4-1854-804d-8007-e761fd24f940": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD//11DVpUbxAMAAAAr81lDVpUbxKuqVkM0wxrEq6pWQwDAGcQDAAAAq6pWQ8y8GMQr81lDquoXxP//XUOq6hfEAwAAAM8MYkOq6hfEU1VlQ8y8GMRTVWVDAMAZxAMAAABTVWVDNMMaxM8MYkNWlRvE//9dQ1aVG8QCAAAAAAAAAAAAAAAAAAAAAAAAAP//XUNWlRvEAQAAAAAAAAAAAAAAAAAAAAAAAAD//1dDAMAZxAMAAAD//1dD0mYZxJt5WEO8FBnEn0VZQ47TGMQDAAAAR15aQ2gvGcT3GFxDqmoZxAMLXkOqahnEAwAAAJP2X0OqahnEO6xhQ/AwGcRfxWJDHNcYxAMAAAC/imNDthcZxP//Y0NuaBnE//9jQwDAGcQDAAAA//9jQxSUGsRPUGFDAEAbxP//XUMAQBvEAwAAAK+vWkMAQBvE//9XQxSUGsT//1dDAMAZxAIAAAAAAAAAAAAAAAAAAAAAAAAA//9XQwDAGcQBAAAAAAAAAAAAAAAAAAAAAAAAAP//XUMAQBjEAwAAAAeRXEMAQBjEG0BbQyxgGMTrN1pD2pUYxAMAAADzD1tD8OIYxO91XENWFRnEAwteQ1YVGcQDAAAAB5tfQ1YVGcQn/WBDLuQYxPvVYUO2mBjEAwAAAMPLYENSYRjEb3VfQwBAGMT//11DAEAYxAIAAAAAAAAAAAAAAAAAAAAAAAAA//9dQwBAGMQ=" + }, + "~:name": "svg-path", + "~:width": null, + "~:type": "~:path", + "~:svg-attrs": { + "~:fill-rule": "evenodd", + "~:clip-rule": "evenodd" + }, + "~:points": [ + { + "~#point": { + "~:x": 214.66666755082224, + "~:y": -622.3333412881586 + } + }, + { + "~#point": { + "~:x": 229.33333421748898, + "~:y": -622.3333412881586 + } + }, + { + "~#point": { + "~:x": 229.33333421748898, + "~:y": -607.6666746214917 + } + }, + { + "~#point": { + "~:x": 214.66666755082224, + "~:y": -607.6666746214917 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:page-id": "~u34c33767-b561-80aa-8007-e7024082d3b1", + "~:constraints-v": "~:scale", + "~:svg-transform": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + }, + "~:constraints-h": "~:scale", + "~:id": "~u2094e2d4-1854-804d-8007-e761fd24f940", + "~:parent-id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:svg-viewbox": { + "~:y": 1, + "~:y1": 1, + "~:width": 22, + "~:x": 1, + "~:x1": 1, + "~:y2": 23, + "~:x2": 23, + "~:height": 22 + }, + "~:applied-tokens": {}, + "~:svg-defs": {}, + "~:frame-id": "~u2094e2d4-1854-804d-8007-e761fd24f93e", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 1, + "~:stroke-color": "#121270", + "~:stroke-opacity": 1 + } + ], + "~:x": null, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 214.666667550822, + "~:y": -622.3333412881586, + "~:width": 14.666666666666742, + "~:height": 14.66666666666697, + "~:x1": 214.666667550822, + "~:y1": -622.3333412881586, + "~:x2": 229.33333421748875, + "~:y2": -607.6666746214917 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + } + }, + "~:name": "profile", + "~:modified-at": "~m1776782297854", + "~:main-instance-page": "~u3e84615b-5628-818c-8007-e7563bb081fc", + "~:id": "~u2094e2d4-1854-804d-8007-e761fd29d15c" + } + } + } +} diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 21fb267806..55158b9564 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -243,6 +243,22 @@ test("Renders a file with a closed path shape with multiple segments using strok await expect(workspace.canvas).toHaveScreenshot(); }); +test("Renders svg paths with evenodd", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-paths-evenodd.json"); + + await workspace.goToWorkspace({ + id: "3e84615b-5628-818c-8007-e7563bb081fb", + pageId: "u3e84615b-5628-818c-8007-e7563bb081fc", + }); + await workspace.waitForFirstRenderWithoutUI(); + + await expect(workspace.canvas).toHaveScreenshot(); +}); + test("Renders solid shadows after select all and zoom to selected", async ({ page, }) => { diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-svg-paths-with-evenodd-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-svg-paths-with-evenodd-1.png new file mode 100644 index 0000000000000000000000000000000000000000..64617666a1e60cfd3531326a1c64486f705f193d GIT binary patch literal 23531 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, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, + svg_attrs: Option<&SvgAttrs>, antialias: bool, ) { let is_open = path.is_open(); @@ -229,7 +231,7 @@ fn draw_stroke_on_path( if let Some(pt) = path_transform { canvas.concat(pt); } - let skia_path = path.to_skia_path(); + let skia_path = path.to_skia_path(svg_attrs); match stroke.render_kind(is_open) { StrokeKind::Inner => { @@ -510,7 +512,7 @@ fn draw_image_stroke_in_container( if let Some(p) = shape_type.path() { canvas.save(); - let path = p.to_skia_path().make_transform( + let path = p.to_skia_path(svg_attrs).make_transform( &path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?, ); let stroke_kind = stroke.render_kind(p.is_open()); @@ -574,7 +576,7 @@ fn draw_image_stroke_in_container( // Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area. if let Type::Path(p) = &shape.shape_type { if stroke.render_kind(p.is_open()) == StrokeKind::Outer { - let path = p.to_skia_path().make_transform( + let path = p.to_skia_path(svg_attrs).make_transform( &path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?, ); let mut clear_paint = skia::Paint::default(); @@ -846,6 +848,7 @@ fn render_merged( path_transform.as_ref(), None, blur_filter.as_ref(), + svg_attrs, antialias, ); } @@ -1016,6 +1019,7 @@ fn render_single_internal( path_transform.as_ref(), shadow, blur.as_ref(), + svg_attrs, antialias, ); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 9e17de41ee..98c2d13c9c 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1349,15 +1349,10 @@ impl Shape { pub fn get_skia_path(&self) -> Option { if let Some(path) = self.shape_type.path() { - let mut skia_path = path.to_skia_path(); + let mut skia_path = path.to_skia_path(self.svg_attrs.as_ref()); if let Some(path_transform) = self.to_path_transform() { skia_path = skia_path.make_transform(&path_transform); } - if let Some(svg_attrs) = &self.svg_attrs { - if svg_attrs.fill_rule == FillRule::Evenodd { - skia_path.set_fill_type(skia::PathFillType::EvenOdd); - } - } Some(skia_path) } else { None diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index 2debf44958..40d0d80067 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -1,6 +1,7 @@ use skia_safe::{self as skia, Matrix}; use crate::math; +use crate::shapes::svg_attrs::{FillRule, SvgAttrs}; mod subpaths; @@ -217,8 +218,14 @@ impl Path { Path::new(segments) } - pub fn to_skia_path(&self) -> skia::Path { - self.skia_path.snapshot() + pub fn to_skia_path(&self, svg_attrs: Option<&SvgAttrs>) -> skia::Path { + let mut path = self.skia_path.snapshot(); + if let Some(attrs) = svg_attrs { + if attrs.fill_rule == FillRule::Evenodd { + path.set_fill_type(skia::PathFillType::EvenOdd); + } + } + path } pub fn contains(&self, p: skia::Point) -> bool { diff --git a/render-wasm/src/shapes/stroke_paths.rs b/render-wasm/src/shapes/stroke_paths.rs index 14f9c09229..f98c01ca0f 100644 --- a/render-wasm/src/shapes/stroke_paths.rs +++ b/render-wasm/src/shapes/stroke_paths.rs @@ -17,7 +17,7 @@ pub fn stroke_to_path( selrect: &Rect, svg_attrs: Option<&SvgAttrs>, ) -> Option { - let skia_shape_path = shape_path.to_skia_path(); + let skia_shape_path = shape_path.to_skia_path(svg_attrs); let transformed_shape_path = if let Some(pt) = path_transform { skia_shape_path.make_transform(pt) From 0c60db56a2748caf9128fc026c78fcdf1bd071f5 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 23 Apr 2026 16:08:56 +0200 Subject: [PATCH 24/26] :bug: Fix multiselection error with typography texts (#9071) * :bug: Ensure typography-ref attrs are always present and fix nil encoding Add :typography-ref-file and :typography-ref-id (both defaulting to nil) to default-text-attrs so these keys are always present in text node maps, whether or not a typography is attached. Skip nil values in attrs-to-styles (Draft.js style encoder) and in attrs->styles (v2 CSS custom-property mapper) so nil typography-ref entries are never serialised to CSS. Replace when with if/acc in get-styles-from-style-declaration to prevent the accumulator from being clobbered to nil when a mixed-value entry is skipped during style decoding. * :tada: Add test --------- Co-authored-by: Andrey Antukh --- common/src/app/common/text.cljc | 4 +- common/src/app/common/types/text.cljc | 4 +- common/test/common_tests/attrs_test.cljc | 151 ++ common/test/common_tests/runner.cljc | 2 + .../workspace/multiselection-typography.json | 1655 +++++++++++++++++ .../ui/specs/multiseleccion.spec.js | 258 +++ .../playwright/ui/specs/tokens/apply.spec.js | 2 +- .../app/main/ui/workspace/sidebar/layers.cljs | 3 +- .../workspace/sidebar/options/menus/fill.cljs | 3 +- .../sidebar/options/menus/stroke.cljs | 2 +- .../workspace/sidebar/options/menus/text.cljs | 3 +- .../src/app/util/text/content/styles.cljs | 14 +- 12 files changed, 2089 insertions(+), 12 deletions(-) create mode 100644 common/test/common_tests/attrs_test.cljc create mode 100644 frontend/playwright/data/workspace/multiselection-typography.json create mode 100644 frontend/playwright/ui/specs/multiseleccion.spec.js diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index e0ed3515e6..cc997f62d6 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -37,7 +37,9 @@ (defn attrs-to-styles [attrs] (reduce-kv (fn [res k v] - (conj res (encode-style k v))) + (if (some? v) + (conj res (encode-style k v)) + res)) #{} attrs)) diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 053a963f84..0a629a8379 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -95,7 +95,9 @@ :text-direction "ltr"}) (def default-text-attrs - {:font-id "sourcesanspro" + {:typography-ref-file nil + :typography-ref-id nil + :font-id "sourcesanspro" :font-family "sourcesanspro" :font-variant-id "regular" :font-size "14" diff --git a/common/test/common_tests/attrs_test.cljc b/common/test/common_tests/attrs_test.cljc new file mode 100644 index 0000000000..bab8b9fbaf --- /dev/null +++ b/common/test/common_tests/attrs_test.cljc @@ -0,0 +1,151 @@ +;; 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 common-tests.attrs-test + (:require + [app.common.attrs :as attrs] + [clojure.test :as t])) + +(t/deftest get-attrs-multi-same-value + (t/testing "returns value when all objects have the same attribute value" + (let [objs [{:attr "red"} + {:attr "red"} + {:attr "red"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr "red"} result)))) + + (t/testing "returns nil when all objects have nil value" + (let [objs [{:attr nil} + {:attr nil}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr nil} result))))) + +(t/deftest get-attrs-multi-different-values + (t/testing "returns :multiple when objects have different concrete values" + (let [objs [{:attr "red"} + {:attr "blue"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr :multiple} result))))) + +(t/deftest get-attrs-multi-missing-key + (t/testing "returns value when one object has the attribute and another doesn't" + (let [objs [{:attr "red"} + {:other "value"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr "red"} result)))) + + (t/testing "returns value when one object has UUID and another is missing" + (let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000" + objs [{:attr uuid} + {:other "value"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr uuid} result)))) + + (t/testing "returns :multiple when some objects have the key and some don't" + (let [objs [{:attr "red"} + {:other "value"} + {:attr "blue"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr :multiple} result)))) + + (t/testing "returns nil when one object has nil and another is missing" + (let [objs [{:attr nil} + {:other "value"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {:attr nil} result))))) + +(t/deftest get-attrs-multi-all-missing + (t/testing "all missing → attribute NOT included in result" + (let [objs [{:other "value"} + {:different "data"}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {} result) + "Attribute should not be in result when all objects are missing"))) + + (t/testing "all missing with empty maps → attribute NOT included" + (let [objs [{} {}] + result (attrs/get-attrs-multi objs [:attr])] + (t/is (= {} result) + "Attribute should not be in result")))) + +(t/deftest get-attrs-multi-multiple-attributes + (t/testing "handles multiple attributes with different merge results" + (let [objs [{:attr1 "red" :attr2 "blue"} + {:attr1 "red" :attr2 "green"} + {:attr1 "red"}] ; :attr2 missing + result (attrs/get-attrs-multi objs [:attr1 :attr2])] + (t/is (= {:attr1 "red" :attr2 :multiple} result)))) + + (t/testing "handles mixed scenarios: same, different, and missing" + (let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000" + uuid2 #uuid "550e8400-e29b-41d4-a716-446655440001" + objs [{:id :a :ref uuid} + {:id :b :ref uuid2} + {:id :c}] ; :ref missing + result (attrs/get-attrs-multi objs [:id :ref])] + (t/is (= {:id :multiple :ref :multiple} result))))) + +(t/deftest get-attrs-multi-typography-ref-id-scenario + (t/testing "the specific bug scenario: typography-ref-id with UUID vs missing" + (let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000" + ;; Shape 1 has typography-ref-id with a UUID + shape1 {:id :shape1 :typography-ref-id uuid} + ;; Shape 2 does NOT have typography-ref-id at all + shape2 {:id :shape2} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {:typography-ref-id uuid} result)))) + + (t/testing "both shapes missing → attribute NOT included in result" + (let [shape1 {:id :shape1} + shape2 {:id :shape2} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {} result) + "Expected empty map when all shapes are missing the attribute")))) + +(t/deftest get-attrs-multi-bug-missing-vs-present + (t/testing "BUG FIXED: one shape has :typography-ref-id, other does NOT → returns uuid" + (let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000" + shape1 {:id :shape1 :typography-ref-id uuid} + shape2 {:id :shape2} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {:typography-ref-id uuid} result)))) + + (t/testing "both missing → empty map (attribute not in result)" + (let [shape1 {:id :shape1} + shape2 {:id :shape2} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {} result) + "Expected empty map when all shapes are missing the attribute"))) + + (t/testing "both equal values → return the value" + (let [uuid #uuid "550e8400-e29b-41d4-a716-446655440000" + shape1 {:id :shape1 :typography-ref-id uuid} + shape2 {:id :shape2 :typography-ref-id uuid} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {:typography-ref-id uuid} result)))) + + (t/testing "different values → return :multiple" + (let [uuid1 #uuid "550e8400-e29b-41d4-a716-446655440000" + uuid2 #uuid "550e8400-e29b-41d4-a716-446655440001" + shape1 {:id :shape1 :typography-ref-id uuid1} + shape2 {:id :shape2 :typography-ref-id uuid2} + result (attrs/get-attrs-multi [shape1 shape2] [:typography-ref-id])] + (t/is (= {:typography-ref-id :multiple} result))))) + +(t/deftest get-attrs-multi-default-equal + (t/testing "numbers use close? for equality" + (let [objs [{:value 1.0} + {:value 1.0000001}] + result (attrs/get-attrs-multi objs [:value])] + (t/is (= {:value 1.0} result) + "Numbers within tolerance should be considered equal"))) + + (t/testing "different floating point positions beyond tolerance are :multiple" + (let [objs [{:x -26} + {:x -153}] + result (attrs/get-attrs-multi objs [:x])] + (t/is (= {:x :multiple} result) + "Different positions should be :multiple")))) \ No newline at end of file diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 1ba7242d58..e8fd6ac9a9 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -8,6 +8,7 @@ (:require #?(:clj [common-tests.fressian-test]) [clojure.test :as t] + [common-tests.attrs-test] [common-tests.buffer-test] [common-tests.colors-test] [common-tests.data-test] @@ -85,6 +86,7 @@ (defn -main [& args] (t/run-tests + 'common-tests.attrs-test 'common-tests.buffer-test 'common-tests.colors-test 'common-tests.data-test diff --git a/frontend/playwright/data/workspace/multiselection-typography.json b/frontend/playwright/data/workspace/multiselection-typography.json new file mode 100644 index 0000000000..8fbb18f600 --- /dev/null +++ b/frontend/playwright/data/workspace/multiselection-typography.json @@ -0,0 +1,1655 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/objects-map", + "tokens/numeric-input", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~u647da7ef-3079-81fb-8007-8bb0246a083c", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "Nuevo Archivo 1", + "~:revn": 36, + "~:modified-at": "~m1776760054954", + "~:vern": 0, + "~:id": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node", + "0017-fix-layout-flex-dir", + "0018-remove-unneeded-objects-from-components", + "0019-fix-missing-swap-slots", + "0020-sync-component-id-with-near-main" + ] + }, + "~:version": 67, + "~:project-id": "~u647da7ef-3079-81fb-8007-8bb0246cef4d", + "~:created-at": "~m1776759390799", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u1062e0a0-8fe0-80ae-8007-e70b4993f5f0" + ], + "~:pages-index": { + "~u1062e0a0-8fe0-80ae-8007-e70b4993f5f0": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u12f7a4ff-ddae-80ff-8007-e70be436041b", + "~u12f7a4ff-ddae-80ff-8007-e70c000c9c84", + "~u12f7a4ff-ddae-80ff-8007-e70c1654b4af", + "~u12f7a4ff-ddae-80ff-8007-e70bf473916f", + "~u12f7a4ff-ddae-80ff-8007-e70c000c9c85", + "~u12f7a4ff-ddae-80ff-8007-e70c1654b4b0", + "~u12f7a4ff-ddae-80ff-8007-e70c3496ef94", + "~u12f7a4ff-ddae-80ff-8007-e70c3aa79b6a" + ] + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c1654b4af": { + "~#shape": { + "~:y": 289, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "capitalize", + "~:font-id": "gfont-rufina", + "~:key": "wgjr6b27pa", + "~:font-size": "14", + "~:font-weight": "700", + "~:typography-ref-file": null, + "~:font-variant-id": "700", + "~:text-decoration": "line-through", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Rufina", + "~:text": "Text with no typography" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "capitalize", + "~:text-align": "left", + "~:font-id": "gfont-rufina", + "~:key": "1dpjnycmsmq", + "~:font-size": "0", + "~:font-weight": "700", + "~:typography-ref-file": null, + "~:text-direction": "rtl", + "~:type": "paragraph", + "~:font-variant-id": "700", + "~:text-decoration": "line-through", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Rufina" + } + ] + } + ], + "~:vertical-align": "top" + }, + "~:hide-in-viewer": false, + "~:name": "Text with no typography", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 854, + "~:y": 289 + } + }, + { + "~#point": { + "~:x": 1122, + "~:y": 289 + } + }, + { + "~#point": { + "~:x": 1122, + "~:y": 348 + } + }, + { + "~#point": { + "~:x": 854, + "~:y": 348 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c1654b4af", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:position-data": [ + { + "~:y": 306.239990234375, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "capitalize", + "~:text-align": "left", + "~:font-id": "gfont-rufina", + "~:font-size": "14px", + "~:font-weight": "700", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 168.320007324219, + "~:font-variant-id": "regular", + "~:text-decoration": "line-through", + "~:letter-spacing": "0px", + "~:x": 854, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Rufina", + "~:height": 17.2899780273438, + "~:text": "Text with no typography" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:x": 854, + "~:selrect": { + "~#rect": { + "~:x": 854, + "~:y": 289, + "~:width": 268, + "~:height": 59, + "~:x1": 854, + "~:y1": 289, + "~:x2": 1122, + "~:y2": 348 + } + }, + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70bf473916f": { + "~#shape": { + "~:y": 380, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.4", + "~:path": "", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.4", + "~:path": "", + "~:font-style": "normal", + "~:typography-ref-id": "~u12f7a4ff-ddae-80ff-8007-e70b6a1d57ba", + "~:text-transform": "uppercase", + "~:font-id": "gfont-im-fell-french-canon-sc", + "~:key": "wgjr6b27pa", + "~:font-size": "16", + "~:font-weight": "400", + "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:modified-at": "~m1776759448186", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "IM Fell French Canon SC", + "~:text": "Text with typography asset two" + } + ], + "~:typography-ref-id": "~u12f7a4ff-ddae-80ff-8007-e70b6a1d57ba", + "~:text-transform": "uppercase", + "~:text-align": "right", + "~:font-id": "gfont-im-fell-french-canon-sc", + "~:key": "1dpjnycmsmq", + "~:font-size": "16", + "~:font-weight": "400", + "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:modified-at": "~m1776759448186", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "IM Fell French Canon SC" + } + ] + } + ], + "~:vertical-align": "center" + }, + "~:hide-in-viewer": false, + "~:name": "Text with typography asset two", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 206, + "~:y": 380 + } + }, + { + "~#point": { + "~:x": 474, + "~:y": 380 + } + }, + { + "~#point": { + "~:x": 474, + "~:y": 439 + } + }, + { + "~#point": { + "~:x": 206, + "~:y": 439 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70bf473916f", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:position-data": [ + { + "~:y": 407.670013427734, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "uppercase", + "~:text-align": "left", + "~:font-id": "gfont-im-fell-french-canon-sc", + "~:font-size": "16px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 261.320007324219, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 216.110000610352, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "IM Fell French Canon SC", + "~:height": 18.7400207519531, + "~:text": "Text with typography asset " + }, + { + "~:y": 429.669982910156, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "uppercase", + "~:text-align": "left", + "~:font-id": "gfont-im-fell-french-canon-sc", + "~:font-size": "16px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 38.9199829101563, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 435.080017089844, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "IM Fell French Canon SC", + "~:height": 18.739990234375, + "~:text": "two" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:x": 206, + "~:selrect": { + "~#rect": { + "~:x": 206, + "~:y": 380, + "~:width": 268, + "~:height": 59, + "~:x1": 206, + "~:y1": 380, + "~:x2": 474, + "~:y2": 439 + } + }, + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c3aa79b6a": { + "~#shape": { + "~:y": 371.999988555908, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 77, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 1226, + "~:y": 371.999988555908 + } + }, + { + "~#point": { + "~:x": 1303, + "~:y": 371.999988555908 + } + }, + { + "~#point": { + "~:x": 1303, + "~:y": 435.999988555908 + } + }, + { + "~#point": { + "~:x": 1226, + "~:y": 435.999988555908 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c3aa79b6a", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 1226, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70db6016174", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#000000", + "~:opacity": 0.2 + }, + "~:offset-x": 4, + "~:offset-y": 4, + "~:blur": 4, + "~:spread": 0, + "~:hidden": false + } + ], + "~:selrect": { + "~#rect": { + "~:x": 1226, + "~:y": 371.999988555908, + "~:width": 77, + "~:height": 64, + "~:x1": 1226, + "~:y1": 371.999988555908, + "~:x2": 1303, + "~:y2": 435.999988555908 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 64, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c000c9c84": { + "~#shape": { + "~:y": 289, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:text-transform": "none", + "~:font-id": "gfont-metrophobic", + "~:key": "wgjr6b27pa", + "~:font-size": "20", + "~:font-weight": "400", + "~:font-variant-id": "regular", + "~:text-decoration": "underline", + "~:letter-spacing": "2", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Metrophobic", + "~:text": "Text with typography token one" + } + ], + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "gfont-metrophobic", + "~:key": "1dpjnycmsmq", + "~:font-size": "20", + "~:font-weight": "400", + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "underline", + "~:letter-spacing": "2", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Metrophobic" + } + ] + } + ], + "~:vertical-align": "top" + }, + "~:hide-in-viewer": false, + "~:name": "Text with typography token one", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 540, + "~:y": 289 + } + }, + { + "~#point": { + "~:x": 808, + "~:y": 289 + } + }, + { + "~#point": { + "~:x": 808, + "~:y": 348 + } + }, + { + "~#point": { + "~:x": 540, + "~:y": 348 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c000c9c84", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:applied-tokens": { + "~:typography": "token-typo-one" + }, + "~:position-data": [ + { + "~:y": 313.329986572266, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "gfont-metrophobic", + "~:font-size": "20px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 242.419982910156, + "~:font-variant-id": "regular", + "~:text-decoration": "underline", + "~:letter-spacing": "2px", + "~:x": 541, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Metrophobic", + "~:height": 24.6599731445313, + "~:text": "Text with typography " + }, + { + "~:y": 337.330017089844, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "gfont-metrophobic", + "~:font-size": "20px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 109.25, + "~:font-variant-id": "regular", + "~:text-decoration": "underline", + "~:letter-spacing": "2px", + "~:x": 541, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Metrophobic", + "~:height": 24.6600036621094, + "~:text": "token one" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:x": 540, + "~:selrect": { + "~#rect": { + "~:x": 540, + "~:y": 289, + "~:width": 268, + "~:height": 59, + "~:x1": 540, + "~:y1": 289, + "~:x2": 808, + "~:y2": 348 + } + }, + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c000c9c85": { + "~#shape": { + "~:y": 378, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:text-transform": "capitalize", + "~:font-id": "gfont-alumni-sans-sc", + "~:key": "wgjr6b27pa", + "~:font-size": "18", + "~:font-weight": "400", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Alumni Sans SC", + "~:text": "Text with typography token two" + } + ], + "~:text-transform": "capitalize", + "~:text-align": "left", + "~:font-id": "gfont-alumni-sans-sc", + "~:key": "1dpjnycmsmq", + "~:font-size": "18", + "~:font-weight": "400", + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Alumni Sans SC" + } + ] + } + ], + "~:vertical-align": "top" + }, + "~:hide-in-viewer": false, + "~:name": "Text with typography token two", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 540, + "~:y": 378 + } + }, + { + "~#point": { + "~:x": 808, + "~:y": 378 + } + }, + { + "~#point": { + "~:x": 808, + "~:y": 437 + } + }, + { + "~#point": { + "~:x": 540, + "~:y": 437 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c000c9c85", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:applied-tokens": { + "~:typography": "token-typography-two" + }, + "~:position-data": [ + { + "~:y": 400, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "capitalize", + "~:text-align": "left", + "~:font-id": "gfont-alumni-sans-sc", + "~:font-size": "18px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 179.599975585938, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 540, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Alumni Sans SC", + "~:height": 21.6000061035156, + "~:text": "Text with typography token two" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:x": 540, + "~:selrect": { + "~#rect": { + "~:x": 540, + "~:y": 378, + "~:width": 268, + "~:height": 59, + "~:x1": 540, + "~:y1": 378, + "~:x2": 808, + "~:y2": 437 + } + }, + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70be436041b": { + "~#shape": { + "~:y": 291.000000417233, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:auto-height", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:path": "", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:path": "", + "~:font-style": "normal", + "~:typography-ref-id": "~u12f7a4ff-ddae-80ff-8007-e70b4e12167a", + "~:text-transform": "lowercase", + "~:font-id": "gfont-agdasima", + "~:key": "wgjr6b27pa", + "~:font-size": "18", + "~:font-weight": "700", + "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:modified-at": "~m1776759420985", + "~:font-variant-id": "700", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Agdasima", + "~:text": "Text with typography asset one" + } + ], + "~:typography-ref-id": "~u12f7a4ff-ddae-80ff-8007-e70b4e12167a", + "~:text-transform": "lowercase", + "~:text-align": "center", + "~:font-id": "gfont-agdasima", + "~:key": "1dpjnycmsmq", + "~:font-size": "18", + "~:font-weight": "700", + "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:modified-at": "~m1776759420985", + "~:font-variant-id": "700", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Agdasima" + } + ] + } + ], + "~:vertical-align": "top" + }, + "~:hide-in-viewer": false, + "~:name": "Text with typography asset one", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 206, + "~:y": 291.000000417233 + } + }, + { + "~#point": { + "~:x": 474, + "~:y": 291.000000417233 + } + }, + { + "~#point": { + "~:x": 474, + "~:y": 313.000000238419 + } + }, + { + "~#point": { + "~:x": 206, + "~:y": 313.000000238419 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70be436041b", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:position-data": [ + { + "~:y": 312.980010986328, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "lowercase", + "~:text-align": "left", + "~:font-id": "gfont-agdasima", + "~:font-size": "18px", + "~:font-weight": "700", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 189.099990844727, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 245.449996948242, + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Agdasima", + "~:height": 21.5599975585938, + "~:text": "Text with typography asset one" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:x": 206, + "~:selrect": { + "~#rect": { + "~:x": 206, + "~:y": 291.000000417233, + "~:width": 268, + "~:height": 21.9999998211861, + "~:x1": 206, + "~:y1": 291.000000417233, + "~:x2": 474, + "~:y2": 313.000000238419 + } + }, + "~:flip-x": null, + "~:height": 21.9999998211861, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c3496ef94": { + "~#shape": { + "~:y": 270.717638632528, + "~:transform": { + "~#matrix": { + "~:a": 0.866025405744331, + "~:b": 0.499999996605365, + "~:c": -0.499999999814932, + "~:d": 0.866025403891286, + "~:e": 2.27373675443232e-13, + "~:f": -1.36424205265939e-12 + } + }, + "~:rotation": 30, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 75.999946156548, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 1243.50013273954, + "~:y": 255.000030219555 + } + }, + { + "~#point": { + "~:x": 1309.31801694632, + "~:y": 293.000003039837 + } + }, + { + "~#point": { + "~:x": 1284.81801402569, + "~:y": 335.435252904892 + } + }, + { + "~#point": { + "~:x": 1219.00012981892, + "~:y": 297.43528008461 + } + } + ], + "~:r2": 4, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 0.866025403891288, + "~:b": -0.499999996605366, + "~:c": 0.499999999814933, + "~:d": 0.866025405744333, + "~:e": 4.85209646967248e-13, + "~:f": 1.2951551141376e-12 + } + }, + "~:r3": 4, + "~:r1": 4, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c3496ef94", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 1, + "~:stroke-color": "#b01c1c", + "~:stroke-opacity": 1 + } + ], + "~:x": 1226.15910030434, + "~:proportion": 1, + "~:r4": 4, + "~:selrect": { + "~#rect": { + "~:x": 1226.15910030434, + "~:y": 270.717638632528, + "~:width": 75.999946156548, + "~:height": 49.0000058593917, + "~:x1": 1226.15910030434, + "~:y1": 270.717638632528, + "~:x2": 1302.15904646089, + "~:y2": 319.717644491919 + } + }, + "~:fills": [ + { + "~:fill-color": "#1355c0", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 49.0000058593917, + "~:flip-y": null + } + }, + "~u12f7a4ff-ddae-80ff-8007-e70c1654b4b0": { + "~#shape": { + "~:y": 378.000021457672, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:auto-height", + "~:content": { + "~:type": "root", + "~:key": "8j9me9oa49", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:font-id": "gfont-playwrite-tz", + "~:key": "wgjr6b27pa", + "~:font-size": "24", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#1355c0", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Playwrite TZ", + "~:text": "Text with no typography two" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "justify", + "~:font-id": "gfont-playwrite-tz", + "~:key": "1dpjnycmsmq", + "~:font-size": "0", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "rtl", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#1355c0", + "~:fill-opacity": 1 + } + ], + "~:font-family": "Playwrite TZ" + } + ] + } + ], + "~:vertical-align": "bottom" + }, + "~:hide-in-viewer": false, + "~:name": "Text with no typography two", + "~:width": 268, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 854, + "~:y": 378.000021457672 + } + }, + { + "~#point": { + "~:x": 1122, + "~:y": 378.000021457672 + } + }, + { + "~#point": { + "~:x": 1122, + "~:y": 436.000020027161 + } + }, + { + "~#point": { + "~:x": 854, + "~:y": 436.000020027161 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70c1654b4b0", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:position-data": [ + { + "~:y": 415.780029296875, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "gfont-playwrite-tz", + "~:font-size": "24px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 275.199951171875, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 854, + "~:fills": [ + { + "~:fill-color": "#1355c0", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Playwrite TZ", + "~:height": 46.3599853515625, + "~:text": "Text with no " + }, + { + "~:y": 444.780029296875, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "gfont-playwrite-tz", + "~:font-size": "24px", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:width": 205.989990234375, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0px", + "~:x": 916.010009765625, + "~:fills": [ + { + "~:fill-color": "#1355c0", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "Playwrite TZ", + "~:height": 46.3599853515625, + "~:text": "typography two" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:outer", + "~:stroke-width": 1, + "~:stroke-color": "#ac0f0f", + "~:stroke-opacity": 1 + } + ], + "~:x": 854, + "~:selrect": { + "~#rect": { + "~:x": 854, + "~:y": 378.000021457672, + "~:width": 268, + "~:height": 57.9999985694885, + "~:x1": 854, + "~:y1": 378.000021457672, + "~:x2": 1122, + "~:y2": 436.000020027161 + } + }, + "~:flip-x": null, + "~:height": 57.9999985694885, + "~:flip-y": null + } + } + }, + "~:id": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5f0", + "~:name": "Page 1" + } + }, + "~:id": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + }, + "~:typographies": { + "~u12f7a4ff-ddae-80ff-8007-e70b4e12167a": { + "~:line-height": "1.2", + "~:path": "", + "~:font-style": "normal", + "~:text-transform": "lowercase", + "~:font-id": "gfont-agdasima", + "~:font-size": "18", + "~:font-weight": "700", + "~:name": "typography one", + "~:modified-at": "~m1776759424008", + "~:font-variant-id": "700", + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70b4e12167a", + "~:letter-spacing": "0", + "~:font-family": "Agdasima" + }, + "~u12f7a4ff-ddae-80ff-8007-e70b6a1d57ba": { + "~:line-height": "1.4", + "~:path": "", + "~:font-style": "normal", + "~:text-transform": "uppercase", + "~:font-id": "gfont-im-fell-french-canon-sc", + "~:font-size": "16", + "~:font-weight": "400", + "~:name": "typography 2", + "~:modified-at": "~m1776759451211", + "~:font-variant-id": "regular", + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70b6a1d57ba", + "~:letter-spacing": "0", + "~:font-family": "IM Fell French Canon SC" + } + }, + "~:tokens-lib": { + "~#penpot/tokens-lib": { + "~:sets": { + "~#ordered-map": [ + [ + "S-Global", + { + "~#penpot/token-set": { + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70bba7e2db2", + "~:name": "Global", + "~:description": "", + "~:modified-at": "~m1776759536029", + "~:tokens": { + "~#ordered-map": [ + [ + "token-typo-one", + { + "~#penpot/token": { + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70bba7deb47", + "~:name": "token-typo-one", + "~:type": "~:typography", + "~:value": { + "~:font-family": [ + "Metrophobic" + ], + "~:font-size": "20", + "~:letter-spacing": "2", + "~:text-decoration": "underline" + }, + "~:description": "", + "~:modified-at": "~m1776759506423" + } + } + ], + [ + "token-typography-two", + { + "~#penpot/token": { + "~:id": "~u12f7a4ff-ddae-80ff-8007-e70bd4736112", + "~:name": "token-typography-two", + "~:type": "~:typography", + "~:value": { + "~:font-family": [ + "Alumni Sans SC" + ], + "~:font-size": "18", + "~:text-case": "capitalize" + }, + "~:description": "", + "~:modified-at": "~m1776759533005" + } + } + ] + ] + } + } + } + ] + ] + }, + "~:themes": { + "~#ordered-map": [ + [ + "", + { + "~#ordered-map": [ + [ + "__PENPOT__HIDDEN__TOKEN__THEME__", + { + "~#penpot/token-theme": { + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:name": "__PENPOT__HIDDEN__TOKEN__THEME__", + "~:group": "", + "~:description": "", + "~:is-source": false, + "~:external-id": "", + "~:modified-at": "~m1776759509463", + "~:sets": { + "~#set": [ + "Global" + ] + } + } + } + ] + ] + } + ] + ] + }, + "~:active-themes": { + "~#set": [ + "/__PENPOT__HIDDEN__TOKEN__THEME__" + ] + } + } + } + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/specs/multiseleccion.spec.js b/frontend/playwright/ui/specs/multiseleccion.spec.js new file mode 100644 index 0000000000..1b4be19e4c --- /dev/null +++ b/frontend/playwright/ui/specs/multiseleccion.spec.js @@ -0,0 +1,258 @@ +import { test, expect } from "@playwright/test"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WasmWorkspacePage.init(page); +}); + +test("Multiselection - check multiple values in measures", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-copy-paste.json", + ); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-copy-paste-fragment.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "870f9f10-87b5-8137-8005-934804124660", + pageId: "870f9f10-87b5-8137-8005-934804124661", + }); + + // Select first shape (single selection first) + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + + // === CHECK SINGLE SELECTION - ALL MEASURE FIELDS === + const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' }); + await expect(measuresSection).toBeVisible(); + + // Width + const widthInput = measuresSection.getByTitle('Width', { exact: true }).getByRole('textbox'); + await expect(widthInput).toHaveValue("360"); + + // Height + const heightInput = measuresSection.getByTitle('Height', { exact: true }).getByRole('textbox'); + await expect(heightInput).toHaveValue("53"); + + // X Position (using "X axis" title) + const xPosInput = measuresSection.getByTitle('X axis', { exact: true }).getByRole('textbox'); + await expect(xPosInput).toHaveValue("1094"); + + // Y Position (using "Y axis" title) + const yPosInput = measuresSection.getByTitle('Y axis', { exact: true }).getByRole('textbox'); + await expect(yPosInput).toHaveValue("856"); + + // === CHECK MULTI-SELECTION - MIXED VALUES === + // Shift+click to add second layer to selection + await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + + // All measure fields should show "Mixed" placeholder when values differ + await expect(widthInput).toHaveAttribute('placeholder', 'Mixed'); + await expect(heightInput).toHaveAttribute('placeholder', 'Mixed'); + await expect(xPosInput).toHaveAttribute('placeholder', 'Mixed'); + await expect(yPosInput).toHaveAttribute('placeholder', 'Mixed'); +}); + + +test("Multiselection - check fill multiple values", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-copy-paste.json", + ); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-copy-paste-fragment.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "870f9f10-87b5-8137-8005-934804124660", + pageId: "870f9f10-87b5-8137-8005-934804124661", + }); + + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + + // Fill section + const fillSection = workspacePage.rightSidebar.getByRole('region', { name: "Fill section" }); + await expect(fillSection).toBeVisible(); + + // Single selection - fill color should be visible (not "Mixed") + await expect(fillSection.getByText(/Mixed/i)).not.toBeVisible(); + + // Multi-selection with Shift+click + await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + + // Should show "Mixed" for fills when shapes have different fill colors + await expect(fillSection.getByText('Mixed')).toBeVisible(); +}); + +test("Multiselection - check stroke multiple values", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-copy-paste.json", + ); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-copy-paste-fragment.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "870f9f10-87b5-8137-8005-934804124660", + pageId: "870f9f10-87b5-8137-8005-934804124661", + }); + + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + + // Stroke section + const strokeSection = workspacePage.rightSidebar.getByRole('region', { name: "Stroke section" }); + await expect(strokeSection).toBeVisible(); + + // Single selection - stroke should be visible (not "Mixed") + await expect(strokeSection.getByText(/Mixed/i)).not.toBeVisible(); + + // Multi-selection + await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + + // Should show "Mixed" for strokes when shapes have different stroke colors + await expect(strokeSection.getByText('Mixed')).toBeVisible(); +}); + +test("Multiselection - check rotation multiple values", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-copy-paste.json", + ); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-copy-paste-fragment.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "870f9f10-87b5-8137-8005-934804124660", + pageId: "870f9f10-87b5-8137-8005-934804124661", + }); + + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + // Measures section contains rotation + const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' }); + await expect(measuresSection).toBeVisible(); + + // Rotation field exists + const rotationInput = measuresSection.getByTitle('Rotation', { exact: true }).getByRole('textbox'); + await expect(rotationInput).toBeVisible(); + + // Rotate that shape + await rotationInput.fill("45"); + await page.keyboard.press('Enter'); + await expect(rotationInput).toHaveValue("45"); // Rotation should be 45 + + // Multi-selection + await workspacePage.layers.getByTestId("layer-row").nth(0).click({ modifiers: ['Shift'] }); + + // Rotation should show "Mixed" placeholder + await expect(rotationInput).toHaveAttribute('placeholder', 'Mixed'); +}); + + +test("Multiselection of text and typographies", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/multiselection-typography.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0", + }); + + const plainTextLayer = workspacePage.layers.getByTestId("layer-row").nth(5); + const plainTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(2); + const typographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(7); + const typographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(4); + const tokenTypographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(6); + const tokenTypographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(3); + const rectangleLayer = workspacePage.layers.getByTestId("layer-row").nth(1); + const elipseLayer = workspacePage.layers.getByTestId("layer-row").nth(0); + const textSection = workspacePage.rightSidebar.getByRole('region', { name: "Text section" }); + // Select rectangle and elipse together + await rectangleLayer.click(); + await elipseLayer.click({ modifiers: ['Control'] }); + await expect(textSection).not.toBeVisible(); + + // Select plain text layer + await plainTextLayer.click(); + + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).not.toBeVisible(); + + // Select two plain text layer with different font family + await plainTextLayerTwo.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible(); + + // Select typography text layer + await typographyTextLayerOne.click(); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Typography one")).toBeVisible(); + + // Select two typography text layer with different typography + await typographyTextLayerTwo.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).toBeVisible(); + + // Select token typography text layer + // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY + await tokenTypographyTextLayerOne.click(); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText('Metrophobic')).toBeVisible(); + + // Select two token typography text layer with different token typography + // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY + await tokenTypographyTextLayerTwo.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible(); + + //Select plain text layer and typography text layer together + await plainTextLayer.click(); + await typographyTextLayerOne.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).toBeVisible(); + + //Select plain text layer and typography text layer together on reverse order + await typographyTextLayerOne.click(); + await plainTextLayer.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).toBeVisible(); + + //Selen token typography text layer and typography text layer together + await tokenTypographyTextLayerOne.click(); + await typographyTextLayerOne.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).toBeVisible(); + + //Select token typography text layer and typography text layer together on reverse order + await typographyTextLayerOne.click(); + await tokenTypographyTextLayerOne.click({ modifiers: ['Control'] }); + await expect(textSection).toBeVisible(); + await expect(textSection.getByText("Multiple typographies")).toBeVisible(); + + // Select rectangle and elipse together + await rectangleLayer.click(); + await elipseLayer.click({ modifiers: ['Control'] }); + await expect(textSection).not.toBeVisible(); +}); \ No newline at end of file diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index b52de56e16..23a25f3669 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -738,7 +738,7 @@ test.describe("Tokens: Apply token", () => { // Check if token pill is visible on right sidebar const strokeSectionSidebar = rightSidebar.getByRole("region", { - name: "stroke-section", + name: "Stroke section", }); await expect(strokeSectionSidebar).toBeVisible(); const firstStrokeRow = strokeSectionSidebar.getByLabel("stroke-row-0"); diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 49433489c6..3440a4e43f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -113,7 +113,8 @@ result)) result)))] - [:div {:class (stl/css :element-list) :data-testid "layer-item"} + [:div {:class (stl/css :element-list) + :data-testid "layer-item"} [:> hooks/sortable-container* {} (for [obj shapes] (if (cfh/frame-shape? obj) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index ba6ea893f2..67d6d1370d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -195,7 +195,8 @@ (dom/set-attribute! checkbox "indeterminate" true) (dom/remove-attribute! checkbox "indeterminate")))) - [:div {:class (stl/css :fill-section)} + [:section {:class (stl/css :fill-section) + :aria-label "Fill section"} [:div {:class (stl/css :fill-title)} [:> title-bar* {:collapsable has-fills? :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 5431343b73..03972b5367 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -177,7 +177,7 @@ :shape-ids ids}))))] [:section {:class (stl/css :stroke-section) - :aria-label "stroke-section"} + :aria-label "Stroke section"} [:div {:class (stl/css :stroke-title)} [:> title-bar* {:collapsable has-strokes? :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index f27ef298f0..38b624b2cc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -315,7 +315,8 @@ expand-stream #(swap! state* assoc-in [:more-options] true)) - [:div {:class (stl/css :element-set)} + [:section {:class (stl/css :element-set) + :aria-label "Text section"} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable true :collapsed (not main-menu-open?) diff --git a/frontend/src/app/util/text/content/styles.cljs b/frontend/src/app/util/text/content/styles.cljs index c67ca4d629..a02e4e57df 100644 --- a/frontend/src/app/util/text/content/styles.cljs +++ b/frontend/src/app/util/text/content/styles.cljs @@ -132,7 +132,9 @@ "Maps attrs to styles" [styles] (let [mapped-styles - (into {} (map attr->style styles))] + (into {} (comp (filter (fn [[_ v]] (some? v))) + (map attr->style)) + styles)] (clj->js mapped-styles))) (defn style-needs-mapping? @@ -199,12 +201,14 @@ (let [style-name (get-style-name-as-css-variable k) [_ style-decode] (get mapping k) style-value (.getPropertyValue style-declaration style-name)] - (when (or (not removed-mixed) (not (contains? mixed-values style-value))) - (assoc acc k (style-decode style-value)))) + (if (or (not removed-mixed) (not (contains? mixed-values style-value))) + (assoc acc k (style-decode style-value)) + acc)) (let [style-name (get-style-name k) style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))] - (when (or (not removed-mixed) (not (contains? mixed-values style-value))) - (assoc acc k style-value))))) {} txt/text-style-attrs)) + (if (or (not removed-mixed) (not (contains? mixed-values style-value))) + (assoc acc k style-value) + acc)))) {} txt/text-style-attrs)) (defn get-styles-from-event "Returns a ClojureScript object compatible with text nodes" From cd417443f63c63ad1bdc0879faa9477960ebab48 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Thu, 23 Apr 2026 18:00:40 +0200 Subject: [PATCH 25/26] :bug: Fix layer hierarchy to match old and new SCSS (#9126) --- .../styles/common/refactor/basic-rules.scss | 2 +- .../resources/styles/common/refactor/z-index.scss | 4 ++-- .../app/main/ui/components/context_menu_a11y.scss | 3 ++- frontend/src/app/main/ui/dashboard/deleted.scss | 12 ++++++------ frontend/src/app/main/ui/dashboard/files.scss | 3 +++ frontend/src/app/main/ui/dashboard/grid.scss | 2 +- frontend/src/app/main/ui/dashboard/projects.scss | 5 ++--- frontend/src/app/main/ui/dashboard/templates.scss | 9 ++++++--- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index c82907a5b6..91068275cc 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -800,7 +800,7 @@ position: absolute; padding: $s-4; border-radius: $br-8; - z-index: $z-index-10; + z-index: $z-index-dropdown; color: var(--title-foreground-color-hover); background-color: var(--menu-background-color); border: $s-2 solid var(--panel-border-color); diff --git a/frontend/resources/styles/common/refactor/z-index.scss b/frontend/resources/styles/common/refactor/z-index.scss index 755b2e9fad..3d36cb37f5 100644 --- a/frontend/resources/styles/common/refactor/z-index.scss +++ b/frontend/resources/styles/common/refactor/z-index.scss @@ -11,5 +11,5 @@ $z-index-4: 4; // context menu $z-index-5: 5; // modal $z-index-10: 10; $z-index-20: 20; -$z-index-modal: 30; // When refactor finish we can reduce this number, -$z-index-alert: 40; // When refactor finish we can reduce this number, +$z-index-modal: 300; +$z-index-dropdown: 400; diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index e0fc29989e..787941b595 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -5,12 +5,13 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/z-index.scss" as *; .context-menu { position: relative; visibility: hidden; opacity: deprecated.$op-0; - z-index: deprecated.$z-index-4; + z-index: var(--z-index-dropdown); &.is-open { position: relative; diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss index 8a04eda993..7187633722 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.scss +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -6,11 +6,11 @@ @use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; -@use "../ds/typography.scss" as t; -@use "../ds/_borders.scss" as *; -@use "../ds/spacing.scss" as *; -@use "../ds/_sizes.scss" as *; -@use "../ds/z-index.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/spacing.scss" as *; +@use "ds/z-index.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; .dashboard-container { flex: 1 0 0; @@ -51,7 +51,7 @@ padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl); position: sticky; top: 0; - z-index: $z-index-100; + z-index: var(--z-index-panels); } .nav-inside { diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss index 79f3563168..838f8ea78c 100644 --- a/frontend/src/app/main/ui/dashboard/files.scss +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -6,6 +6,8 @@ @use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .dashboard-container { flex: 1 0 0; @@ -13,6 +15,7 @@ overflow-y: auto; width: 100%; border-top: deprecated.$s-1 solid var(--color-background-quaternary); + padding-block-end: var(--sp-xxxl); &.dashboard-projects { user-select: none; diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 3f4189c729..e1aaef396a 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -17,7 +17,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width height: 100%; overflow-y: auto; overflow-x: hidden; - padding: 0 deprecated.$s-16; + padding: 0 var(--sp-l) deprecated.$s-16; } .grid-row { diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 7df6a0f9c9..a37575c38e 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -19,16 +19,15 @@ margin-inline-end: var(--sp-l); border-block-start: $b-1 solid var(--panel-border-color); overflow-y: auto; - padding-block-end: var(--sp-xxxl); } .dashboard-projects { user-select: none; - block-size: calc(100vh - px2rem(64)); + block-size: calc(100vh - px2rem(80)); } .with-team-hero { - block-size: calc(100vh - px2rem(280)); + block-size: calc(100vh - px2rem(360)); } .dashboard-shared { diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 8a5ab660c0..5d58ac4fea 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -4,10 +4,11 @@ // // Copyright (c) KALEIDOS INC -@use "ds/_borders.scss" as *; -@use "ds/_utils.scss" as *; -@use "ds/_sizes.scss" as *; @use "ds/typography.scss" as t; +@use "ds/z-index.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .dashboard-templates-section { background-color: var(--color-background-tertiary); @@ -26,6 +27,8 @@ transition: bottom 300ms; width: calc(100% - $sz-12); pointer-events: none; + z-index: var(--z-index-set); + &.collapsed { inset-block-end: calc(-1 * px2rem(228)); background-color: transparent; From 2d5e50f35254f38c98303716e08097e051a5f1c5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 24 Apr 2026 08:17:32 +0200 Subject: [PATCH 26/26] :arrow_up: Update root repo deps --- package.json | 6 +- pnpm-lock.yaml | 182 ++++++++++++++++++++++++------------------------- 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index fbb4c5d92f..d2d6a9f5a8 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@github/copilot": "^1.0.21", - "@types/node": "^25.5.2", + "@github/copilot": "^1.0.35", + "@types/node": "^25.6.0", "esbuild": "^0.28.0", - "opencode-ai": "^1.14.19" + "opencode-ai": "^1.14.22" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32b4fa3382..cf5a098662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,17 +9,17 @@ importers: .: devDependencies: '@github/copilot': - specifier: ^1.0.21 - version: 1.0.21 + specifier: ^1.0.35 + version: 1.0.35 '@types/node': - specifier: ^25.5.2 - version: 25.5.2 + specifier: ^25.6.0 + version: 25.6.0 esbuild: specifier: ^0.28.0 version: 0.28.0 opencode-ai: - specifier: ^1.14.19 - version: 1.14.19 + specifier: ^1.14.22 + version: 1.14.22 packages: @@ -179,120 +179,120 @@ packages: cpu: [x64] os: [win32] - '@github/copilot-darwin-arm64@1.0.21': - resolution: {integrity: sha512-aB+s9ldTwcyCOYmzjcQ4SknV6g81z92T8aUJEJZBwOXOTBeWKAJtk16ooAKangZgdwuLgO3or1JUjx1FJAm5nQ==} + '@github/copilot-darwin-arm64@1.0.35': + resolution: {integrity: sha512-NNZE0TOz0HOlv7eqlh6EcQbNkhtnIHReBLieW6pfDUUTKkgsqbUu1MOitF8m+LUQk3ml1T0MQ5MOfad1HSa/MQ==} cpu: [arm64] os: [darwin] hasBin: true - '@github/copilot-darwin-x64@1.0.21': - resolution: {integrity: sha512-aNad81DOGuGShmaiFNIxBUSZLwte0dXmDYkGfAF9WJIgY4qP4A8CPWFoNr8//gY+4CwaIf9V+f/OC6k2BdECbw==} + '@github/copilot-darwin-x64@1.0.35': + resolution: {integrity: sha512-XCv/mfdv0rnrtrNVOluio/N/kyCge0uG2hghvtlgO/+z6EjvzFygkpXXS1gVxiXhWc3lX232cTXQU3zklC/8Ng==} cpu: [x64] os: [darwin] hasBin: true - '@github/copilot-linux-arm64@1.0.21': - resolution: {integrity: sha512-FL0NsCnHax4czHVv1S8iBqPLGZDhZ28N3+6nT29xWGhmjBWTkIofxLThKUPcyyMsfPTTxIlrdwWa8qQc5z2Q+g==} + '@github/copilot-linux-arm64@1.0.35': + resolution: {integrity: sha512-mbaadATfJPzmXq2SD1TWocIG/GobcYC6OvNFhCG8UXMsiXY5cevhszl5ujuayhPJBxS77Yj5uvIFjNQ1Kf5V8Q==} cpu: [arm64] os: [linux] hasBin: true - '@github/copilot-linux-x64@1.0.21': - resolution: {integrity: sha512-S7pWVI16hesZtxYbIyfw+MHZpc5ESoGKUVr5Y+lZJNaM2340gJGPQzQwSpvKIRMLHRKI2hXLwciAnYeMFxE/Tg==} + '@github/copilot-linux-x64@1.0.35': + resolution: {integrity: sha512-NrZ0VjztdBbJ5qAmuUtuKsWkimOaqzjDV+ZGUv1FxSxoys40kiiakQ5WbnMFDzaIFaf47zDi++6ixgQzq7Jk5A==} cpu: [x64] os: [linux] hasBin: true - '@github/copilot-win32-arm64@1.0.21': - resolution: {integrity: sha512-a9qc2Ku+XbyBkXCclbIvBbIVnECACTIWnPctmXWsQeSdeapGxgfHGux7y8hAFV5j6+nhCm6cnyEMS3rkZjAhdA==} + '@github/copilot-win32-arm64@1.0.35': + resolution: {integrity: sha512-KQN7Q7+oPyglmvUEiMp6SYWjl30VSu91T0dUpNHbUs/xRM3qgnCymLPPUyBZGWHog/FueUAsRkhisMHWQVnO+g==} cpu: [arm64] os: [win32] hasBin: true - '@github/copilot-win32-x64@1.0.21': - resolution: {integrity: sha512-9klu+7NQ6tEyb8sibb0rsbimBivDrnNltZho10Bgbf1wh3o+erTjffXDjW9Zkyaw8lZA9Fz8bqhVkKntZq58Lg==} + '@github/copilot-win32-x64@1.0.35': + resolution: {integrity: sha512-J0XhXO2FmlFr8pGa970xEd4tr1rqFiZxoaPW5WvkJYZoZUHbBhFcGasp5/yEeJ71b3vI4PHm/mSZZebD3ALMKQ==} cpu: [x64] os: [win32] hasBin: true - '@github/copilot@1.0.21': - resolution: {integrity: sha512-P+nORjNKAtl92jYCG6Qr1Rsw2JoyScgeQSkIR6O2WB37WS5JVdA4ax1WVualMbfuc9V58CPHX6fwyNpkI89FkQ==} + '@github/copilot@1.0.35': + resolution: {integrity: sha512-O1nUy8DXOTE+v86b/FTkyu09EMrDy+vj+2rhmUOcmsXGe0RE5ECyESsasUTUoHK/CSgAExFTziNxbubUoiMMfg==} hasBin: true - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true - opencode-ai@1.14.19: - resolution: {integrity: sha512-67h56qYcJivd2U9VK8LJvyMBCc3ZE3HcJ/qL4YtaidSnjEumy4SxO+HHlDISsLase7TUQ0w1nULOAibdZxGzbQ==} + opencode-ai@1.14.22: + resolution: {integrity: sha512-J+q1Ehlfg7SSXw2aIY8Mb47FHhPTN8IciKNt0/D+H/brO8RWLe67WjFzxhh/z9SSad9wPcCiLRGAc/iAn8W8wA==} hasBin: true - opencode-darwin-arm64@1.14.19: - resolution: {integrity: sha512-gjJ97dTiBbCas3Y7K6KQMQm5/KNIBOM9+10KZHqNr2bQfn0N09O977ZkXoX6IVBBE2632Ahaa71d4pzLKN9ULw==} + opencode-darwin-arm64@1.14.22: + resolution: {integrity: sha512-h9FjzNoDRsuJD0EEg535P9ul5TyrWovwx591VmuG8fp9d4PoSrAN1O3Zi07GJjkrYyrB8g3c+x5whDqJCz+qog==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.14.19: - resolution: {integrity: sha512-UuwLpa511h7qQ+rTmOUmsHch/N4NBQT64dzg+iLWnR5yoR/81inENpbwxiS7hXpVdzCUGo/YnxI1u6SIBoMlTQ==} + opencode-darwin-x64-baseline@1.14.22: + resolution: {integrity: sha512-GgfP0wSm9/I+j3shOxfeA++7yZpXS6Y1Vis258nEFoRS9Xfv3YlHom7c/8BR9rYqeUE/+rrijP7PrGWGl+IHBw==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.14.19: - resolution: {integrity: sha512-uksrjOtWI7Ob5JvjZBSsrKuy3JVF9d89oYZYfWS5m8ordNyv1Nob39MXJXizv85ozsXjSb0rqjpJurnJw8K+tQ==} + opencode-darwin-x64@1.14.22: + resolution: {integrity: sha512-cyKRo22sxDwu4ITOlENwXaqVM9kMGndwSaAd95gz1Rmz5NYMShUO/8eckrD2MhS2wm+QvKw9XkRVWVHWQlZw3Q==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.14.19: - resolution: {integrity: sha512-R1BJuBGWHfBxfKvIA/Hb4nhYaJgCKl1B+mAGNydu+z0CLtGtwU8r+kQWF/G2N0y8Vx6Y6DRfJiv1X0eZEfD1BQ==} + opencode-linux-arm64-musl@1.14.22: + resolution: {integrity: sha512-DtSd5tbGk6R5+hGhqViSvbY8ICf+u4oVQhfvCAplQCb1UEwYVc0+oAF6PimFJ+o8i8L6x14O0rry0NaRzZ0CzA==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.14.19: - resolution: {integrity: sha512-Bo+aZOppLF366mgGfK0CnIcAVy1EmsrBv93eot1CmPSN1oeud07GpGdn3Bjl5f6KuBx1io6JsvjQyHno+MH5AA==} + opencode-linux-arm64@1.14.22: + resolution: {integrity: sha512-ohK4LkkGvzB4ptr0nqDOVi2JEJMLROfy1s2U2A4Qrh+1Y0QimgH2b5VgTm+BjA3bC2Hm8Yf/IfkitqlUnCp7YA==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.14.19: - resolution: {integrity: sha512-+K8MuGoHugtUec4P/nKcTwZFUipHfW7oPpwlIoPiAQou3bNFTzzP6rslbzzNwjXlQRsUw9GAtuIPDOCL6CkgDg==} + opencode-linux-x64-baseline-musl@1.14.22: + resolution: {integrity: sha512-oZffotEbGXbA38Y0Dmj7IVq0ATl3nKbP8j91Z0zR5kBEBykOqExJIyc9pZpModgfPf86k98XBsRHiVLK4u9ARw==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.14.19: - resolution: {integrity: sha512-KuvITzg4iK0hdIjpNZepwu3bLZ/dUZDI6BwCoV4w//VEP1j3UfDyeS3vWghKcQLd2T1+yybuEMM/3RXcwm/lGQ==} + opencode-linux-x64-baseline@1.14.22: + resolution: {integrity: sha512-J67YAIWr3E03o9e6wNaPEqBo+9FcPKf5CzjIUSb8yNDyobWON1HHihcuu0hCJ6wF9J9awmlp2/4mO1HOoCo3QQ==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.14.19: - resolution: {integrity: sha512-0qe2+X76UJdrCdhdlJyfubMC4tveHAVxjSmPq7g9Zm95heBeJdcQDCLeyQk/lGgeXgsZzVPfLmyTWNtBvCZYFQ==} + opencode-linux-x64-musl@1.14.22: + resolution: {integrity: sha512-r+QnqwR/OPmMm197Kb8VLD9mkZGFXz4m5QCZFxOAL34k8AhQZqn3d2mx2bfrMBVfoSiSVxa3jEjZEbNNFGlICQ==} cpu: [x64] os: [linux] - opencode-linux-x64@1.14.19: - resolution: {integrity: sha512-2GljfL7BeG4xALBJVRwaAGCM/dzYF5aQf6bfLTKsQIl6QpLUguYSF+fkStBHLeehyqbDP5MtiEEuXjC0+mecjA==} + opencode-linux-x64@1.14.22: + resolution: {integrity: sha512-MSUaO/Cvfb8DFRYETVrVeCnKtoIfgLflyB+O8xQOkVtjMKJ41M+1dFSMyZ3LQa2Vfp5tDskyMhj7eUxvT/owgQ==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.14.19: - resolution: {integrity: sha512-8/vRHe5tHexikfPceLmpjsQiEhuDTOSCSlEmP4s0Yq3UAkVaDAxpiWq7Bx4g8hjr1gzfXD9vbjV+WHq/BtMC/w==} + opencode-windows-arm64@1.14.22: + resolution: {integrity: sha512-8grcxLSf9BD9Bt38MIxXfkI6aOFophVgM0US5r8nAUdVU78/8TS9Flnn6D39GM5RmxzqGWMl1u10vMFrBtMwPA==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.14.19: - resolution: {integrity: sha512-Z8imEJK/srE/r1fr7oNLvpLTeRJQyuL7vsbXvCt3T7j2Ew9BOZ7RuYa8EE0R6bNqQ+MLhBGPiAG7NWc++MgK8Q==} + opencode-windows-x64-baseline@1.14.22: + resolution: {integrity: sha512-R/o36LpmQmbv/tL2pkcmApn6030z/1oJIYmjDkW5a4K5MXmV7aq+jWrH5p6iYKp9fo9L8oCtOp/rELMBqDS3UA==} cpu: [x64] os: [win32] - opencode-windows-x64@1.14.19: - resolution: {integrity: sha512-/TqGN91WiUzx7IPMPwmpMIzRixi5TMjaBcC9FH1TgD7DCqKnP6pokvu+ak0C9xwA4wKorE9PoZzeRt/+c3rDCQ==} + opencode-windows-x64@1.14.22: + resolution: {integrity: sha512-jVbZ4VA5b5MF2QhWQOE1VYBKdBE0v/ZebFjwzs6Vieazfgr6OFnGSHVP5WJbU/r6zDssbTBzzpnFxo0IY1SQWw==} cpu: [x64] os: [win32] - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} snapshots: @@ -374,36 +374,36 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@github/copilot-darwin-arm64@1.0.21': + '@github/copilot-darwin-arm64@1.0.35': optional: true - '@github/copilot-darwin-x64@1.0.21': + '@github/copilot-darwin-x64@1.0.35': optional: true - '@github/copilot-linux-arm64@1.0.21': + '@github/copilot-linux-arm64@1.0.35': optional: true - '@github/copilot-linux-x64@1.0.21': + '@github/copilot-linux-x64@1.0.35': optional: true - '@github/copilot-win32-arm64@1.0.21': + '@github/copilot-win32-arm64@1.0.35': optional: true - '@github/copilot-win32-x64@1.0.21': + '@github/copilot-win32-x64@1.0.35': optional: true - '@github/copilot@1.0.21': + '@github/copilot@1.0.35': optionalDependencies: - '@github/copilot-darwin-arm64': 1.0.21 - '@github/copilot-darwin-x64': 1.0.21 - '@github/copilot-linux-arm64': 1.0.21 - '@github/copilot-linux-x64': 1.0.21 - '@github/copilot-win32-arm64': 1.0.21 - '@github/copilot-win32-x64': 1.0.21 + '@github/copilot-darwin-arm64': 1.0.35 + '@github/copilot-darwin-x64': 1.0.35 + '@github/copilot-linux-arm64': 1.0.35 + '@github/copilot-linux-x64': 1.0.35 + '@github/copilot-win32-arm64': 1.0.35 + '@github/copilot-win32-x64': 1.0.35 - '@types/node@25.5.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 esbuild@0.28.0: optionalDependencies: @@ -434,55 +434,55 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 - opencode-ai@1.14.19: + opencode-ai@1.14.22: optionalDependencies: - opencode-darwin-arm64: 1.14.19 - opencode-darwin-x64: 1.14.19 - opencode-darwin-x64-baseline: 1.14.19 - opencode-linux-arm64: 1.14.19 - opencode-linux-arm64-musl: 1.14.19 - opencode-linux-x64: 1.14.19 - opencode-linux-x64-baseline: 1.14.19 - opencode-linux-x64-baseline-musl: 1.14.19 - opencode-linux-x64-musl: 1.14.19 - opencode-windows-arm64: 1.14.19 - opencode-windows-x64: 1.14.19 - opencode-windows-x64-baseline: 1.14.19 + opencode-darwin-arm64: 1.14.22 + opencode-darwin-x64: 1.14.22 + opencode-darwin-x64-baseline: 1.14.22 + opencode-linux-arm64: 1.14.22 + opencode-linux-arm64-musl: 1.14.22 + opencode-linux-x64: 1.14.22 + opencode-linux-x64-baseline: 1.14.22 + opencode-linux-x64-baseline-musl: 1.14.22 + opencode-linux-x64-musl: 1.14.22 + opencode-windows-arm64: 1.14.22 + opencode-windows-x64: 1.14.22 + opencode-windows-x64-baseline: 1.14.22 - opencode-darwin-arm64@1.14.19: + opencode-darwin-arm64@1.14.22: optional: true - opencode-darwin-x64-baseline@1.14.19: + opencode-darwin-x64-baseline@1.14.22: optional: true - opencode-darwin-x64@1.14.19: + opencode-darwin-x64@1.14.22: optional: true - opencode-linux-arm64-musl@1.14.19: + opencode-linux-arm64-musl@1.14.22: optional: true - opencode-linux-arm64@1.14.19: + opencode-linux-arm64@1.14.22: optional: true - opencode-linux-x64-baseline-musl@1.14.19: + opencode-linux-x64-baseline-musl@1.14.22: optional: true - opencode-linux-x64-baseline@1.14.19: + opencode-linux-x64-baseline@1.14.22: optional: true - opencode-linux-x64-musl@1.14.19: + opencode-linux-x64-musl@1.14.22: optional: true - opencode-linux-x64@1.14.19: + opencode-linux-x64@1.14.22: optional: true - opencode-windows-arm64@1.14.19: + opencode-windows-arm64@1.14.22: optional: true - opencode-windows-x64-baseline@1.14.19: + opencode-windows-x64-baseline@1.14.22: optional: true - opencode-windows-x64@1.14.19: + opencode-windows-x64@1.14.22: optional: true - undici-types@7.18.2: {} + undici-types@7.19.2: {}