diff --git a/CHANGES.md b/CHANGES.md index 4e63461ac5..35dd115bb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -101,6 +101,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) @@ -113,6 +114,14 @@ ### :bug: Bugs fixed - Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041) + + +## 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) 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")))) 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/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 d9cd5488dc..cde27c19c4 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..06f7926c47 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] @@ -54,6 +55,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] @@ -85,6 +87,7 @@ (defn -main [& args] (t/run-tests + 'common-tests.attrs-test 'common-tests.buffer-test 'common-tests.colors-test 'common-tests.data-test @@ -132,6 +135,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@"))))) 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}" diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index adf5844dea..01e918ec5c 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_oidc_name() { @@ -39,8 +43,9 @@ update_oidc_name /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..0daab4b9d4 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 $PENPOT_MCP_URI:4402; + proxy_http_version 1.1; + } + + location /mcp/stream { + proxy_pass $PENPOT_MCP_URI:4401/mcp; + proxy_http_version 1.1; + } + + location /mcp/sse { + proxy_pass $PENPOT_MCP_URI:4401/sse; + proxy_http_version 1.1; + } + location /readyz { access_log off; proxy_pass $PENPOT_BACKEND_URI$request_uri; diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index bc8b06abf3..63276ee48a 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -63,6 +63,7 @@ :wasm (when is-wasm "true") :scale scale} 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 27402db513..bdfd8c6dc5 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] @@ -77,6 +77,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 66e6dd61b2..135edee8d0 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -350,7 +350,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))))) 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/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/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 0000000000..64617666a1 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-svg-paths-with-evenodd-1.png differ 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 5b20d089d0..64f49733ce 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -735,7 +735,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/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 5cb82f6c55..efac327f36 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -915,7 +915,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/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/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 d837b93e9d..c6396f99bc 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -207,9 +207,9 @@ 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, - 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 ddcd2c08b5..79487fbfc6 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -161,9 +161,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/")) (def upload-chunk-size (obj/get global "penpotUploadChunkSize" (* 1024 1024 25))) ;; 25 MiB ;; We set the current parsed flags under common for make @@ -190,7 +190,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/main/refs.cljs b/frontend/src/app/main/refs.cljs index 8b65b32fb6..870e659746 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -651,3 +651,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/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index b87754a356..5297f75422 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 0ebba0c81b..69eeb9585d 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; @@ -52,7 +52,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 0a5fe94dbd..39cfe4958b 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 f0bf82dac2..394979b7e7 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -15,7 +15,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width font-size: deprecated.$fs-14; height: 100%; overflow: hidden auto; - 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 d910ffb0d2..2616edcd49 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 4ca5d92e47..e48fc26de3 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); @@ -25,6 +26,7 @@ transition: inset-block-end 300ms; width: calc(100% - $sz-12); pointer-events: none; + z-index: var(--z-index-set); &.collapsed { inset-block-end: calc(-1 * px2rem(228)); 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 ab4c7896ab..9215f49d82 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]}] @@ -780,14 +776,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 [] @@ -825,7 +829,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 @@ -840,7 +844,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"))]]])) @@ -1014,7 +1018,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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 7d41d982c1..df02e1d0d9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -115,7 +115,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/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index cbc9e6c31a..f00762b6f2 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 @@ -243,8 +242,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/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 529eee6b36..c420053c43 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 @@ -194,7 +194,8 @@ (dom/set-attribute! checkbox "indeterminate" true) (dom/remove-attribute! checkbox "indeterminate")))) - [:section {:class (stl/css :fill-section) :aria-label (tr "workspace.options.fill.section")} + [:section {:class (stl/css :fill-section) + :aria-label (tr "workspace.options.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 154be987f7..be2a4a1fee 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 @@ -186,7 +186,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 0c388627e0..974a943b55 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 @@ -30,7 +30,7 @@ [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options typography-entry]] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry]] [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -429,10 +429,12 @@ (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name)) (dom/focus! (txu/get-text-editor-content))))))) - common-props - (mf/props {:values values - :on-change on-change - :on-blur on-text-blur})] + opts (mf/props + {:ids ids + :values values + :on-change on-change + :show-recent true + :on-blur on-text-blur})] (hooks/use-stream expand-stream @@ -496,11 +498,7 @@ :icon i/detach}]] :else - [:> text-options #js {:ids ids - :values values - :on-change on-change - :show-recent true - :on-blur on-text-blur}]) + [:> text-options* opts]) [:div {:class (stl/css :text-align-options)} [:> text-align-options* common-props] 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 ba2380bfba..4266716f7b 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 @@ -451,8 +451,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 @@ -541,9 +540,9 @@ :on-click on-close :icon i/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)} 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 31e98bf832..3a8153c746 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 @@ -240,7 +240,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 @@ -345,6 +354,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) @@ -436,4 +450,5 @@ [:> icon-button* {:variant "ghost" :aria-label (tr "settings.select-this-color") :on-click handle-select + :tooltip-position "top-left" :icon i/move}])])) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 1f16c29779..fc79e7f584 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?] @@ -676,7 +677,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) @@ -689,11 +691,13 @@ (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) (fetch-image shape-id image-id thumbnail?))) - + (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 @@ -1303,16 +1307,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/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" 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] 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 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: {} diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 6272d8d9a3..0d249f06e7 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>, @@ -1780,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 { @@ -2927,11 +2966,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 +3190,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/fills.rs b/render-wasm/src/render/fills.rs index 615d8b9532..45f8fc3048 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -62,7 +62,9 @@ fn draw_image_fill( if let Some(path) = shape_type.path() { if let Some(path_transform) = path_transform { canvas.clip_path( - &path.to_skia_path().make_transform(&path_transform), + &path + .to_skia_path(shape.svg_attrs.as_ref()) + .make_transform(&path_transform), skia::ClipOp::Intersect, antialias, ); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index c5a3a26bf1..affd2a168c 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -1,7 +1,8 @@ use crate::math::{Matrix, Point, Rect}; use crate::shapes::{ - merge_fills, Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type, + merge_fills, Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, SvgAttrs, + Type, }; use skia_safe::{self as skia, ImageFilter, RRect}; @@ -210,6 +211,7 @@ fn draw_stroke_on_path( path_transform: Option<&Matrix>, 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/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) { 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) 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()?;