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()?;