mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
984d292ab2
@ -101,6 +101,7 @@
|
|||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
- 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 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 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)
|
## 2.15.0 (Unreleased)
|
||||||
@ -113,6 +114,14 @@
|
|||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041)
|
- 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)
|
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -36,10 +36,18 @@
|
|||||||
:cause cause)))))
|
:cause cause)))))
|
||||||
|
|
||||||
(defn contains?
|
(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]
|
[{:keys [::email/blacklist]} email]
|
||||||
(let [[_ domain] (str/split email "@" 2)]
|
(let [[_ domain] (str/split email "@" 2)
|
||||||
(c/contains? blacklist (str/lower domain))))
|
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?
|
(defn enabled?
|
||||||
"Check if the blacklist is enabled"
|
"Check if the blacklist is enabled"
|
||||||
|
|||||||
34
backend/test/backend_tests/email_blacklist_test.clj
Normal file
34
backend/test/backend_tests/email_blacklist_test.clj
Normal file
@ -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"))))
|
||||||
@ -113,12 +113,19 @@
|
|||||||
(tgen/fmap keyword)))))
|
(tgen/fmap keyword)))))
|
||||||
|
|
||||||
;; --- SPEC: email
|
;; --- 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
|
(defn parse-email
|
||||||
[s]
|
[s]
|
||||||
(some->> s (re-seq email-re) first))
|
(when (and (string? s) (re-matches email-re s))
|
||||||
|
s))
|
||||||
|
|
||||||
(letfn [(conformer [v]
|
(letfn [(conformer [v]
|
||||||
(or (parse-email v) ::s/invalid))
|
(or (parse-email v) ::s/invalid))
|
||||||
@ -126,11 +133,10 @@
|
|||||||
(dm/str v))]
|
(dm/str v))]
|
||||||
(s/def ::email
|
(s/def ::email
|
||||||
(s/with-gen (s/conformer conformer unformer)
|
(s/with-gen (s/conformer conformer unformer)
|
||||||
#(as-> (tgen/let [p1 (s/gen ::not-empty-string)
|
#(tgen/let [local (tgen/string-alphanumeric 1 20)
|
||||||
p2 (s/gen ::not-empty-string)
|
label (tgen/string-alphanumeric 2 10)
|
||||||
p3 (tgen/elements ["com" "net"])]
|
tld (tgen/elements ["com" "net" "org" "io" "co" "dev"])]
|
||||||
(str p1 "@" p2 "." p3)) $
|
(str local "@" label "." tld)))))
|
||||||
(tgen/such-that (partial re-matches email-re) $ 50)))))
|
|
||||||
|
|
||||||
;; -- SPEC: uri
|
;; -- SPEC: uri
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,9 @@
|
|||||||
(defn attrs-to-styles
|
(defn attrs-to-styles
|
||||||
[attrs]
|
[attrs]
|
||||||
(reduce-kv (fn [res k v]
|
(reduce-kv (fn [res k v]
|
||||||
(conj res (encode-style k v)))
|
(if (some? v)
|
||||||
|
(conj res (encode-style k v))
|
||||||
|
res))
|
||||||
#{}
|
#{}
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,9 @@
|
|||||||
:text-direction "ltr"})
|
:text-direction "ltr"})
|
||||||
|
|
||||||
(def default-text-attrs
|
(def default-text-attrs
|
||||||
{:font-id "sourcesanspro"
|
{:typography-ref-file nil
|
||||||
|
:typography-ref-id nil
|
||||||
|
:font-id "sourcesanspro"
|
||||||
:font-family "sourcesanspro"
|
:font-family "sourcesanspro"
|
||||||
:font-variant-id "regular"
|
:font-variant-id "regular"
|
||||||
:font-size "14"
|
:font-size "14"
|
||||||
|
|||||||
151
common/test/common_tests/attrs_test.cljc
Normal file
151
common/test/common_tests/attrs_test.cljc
Normal file
@ -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"))))
|
||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
#?(:clj [common-tests.fressian-test])
|
#?(:clj [common-tests.fressian-test])
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
|
[common-tests.attrs-test]
|
||||||
[common-tests.buffer-test]
|
[common-tests.buffer-test]
|
||||||
[common-tests.colors-test]
|
[common-tests.colors-test]
|
||||||
[common-tests.data-test]
|
[common-tests.data-test]
|
||||||
@ -54,6 +55,7 @@
|
|||||||
[common-tests.path-names-test]
|
[common-tests.path-names-test]
|
||||||
[common-tests.record-test]
|
[common-tests.record-test]
|
||||||
[common-tests.schema-test]
|
[common-tests.schema-test]
|
||||||
|
[common-tests.spec-test]
|
||||||
[common-tests.svg-path-test]
|
[common-tests.svg-path-test]
|
||||||
[common-tests.svg-test]
|
[common-tests.svg-test]
|
||||||
[common-tests.text-test]
|
[common-tests.text-test]
|
||||||
@ -85,6 +87,7 @@
|
|||||||
(defn -main
|
(defn -main
|
||||||
[& args]
|
[& args]
|
||||||
(t/run-tests
|
(t/run-tests
|
||||||
|
'common-tests.attrs-test
|
||||||
'common-tests.buffer-test
|
'common-tests.buffer-test
|
||||||
'common-tests.colors-test
|
'common-tests.colors-test
|
||||||
'common-tests.data-test
|
'common-tests.data-test
|
||||||
@ -132,6 +135,7 @@
|
|||||||
'common-tests.path-names-test
|
'common-tests.path-names-test
|
||||||
'common-tests.record-test
|
'common-tests.record-test
|
||||||
'common-tests.schema-test
|
'common-tests.schema-test
|
||||||
|
'common-tests.spec-test
|
||||||
'common-tests.svg-path-test
|
'common-tests.svg-path-test
|
||||||
'common-tests.svg-test
|
'common-tests.svg-test
|
||||||
'common-tests.text-test
|
'common-tests.text-test
|
||||||
|
|||||||
89
common/test/common_tests/spec_test.cljc
Normal file
89
common/test/common_tests/spec_test.cljc
Normal file
@ -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@")))))
|
||||||
@ -105,7 +105,7 @@ services:
|
|||||||
# - "traefik.http.routers.penpot-https.tls=true"
|
# - "traefik.http.routers.penpot-https.tls=true"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
<< : [*penpot-flags, *penpot-http-body-size]
|
<< : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]
|
||||||
|
|
||||||
penpot-backend:
|
penpot-backend:
|
||||||
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
|
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
|
||||||
|
|||||||
@ -19,6 +19,10 @@ update_flags() {
|
|||||||
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
||||||
"$1")" > "$1"
|
"$1")" > "$1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PENPOT_PUBLIC_URI" ]; then
|
||||||
|
echo "var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";" >> "$1";
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
update_oidc_name() {
|
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_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||||
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
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
|
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
|
< /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)"
|
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||||
|
|||||||
@ -135,6 +135,23 @@ http {
|
|||||||
proxy_http_version 1.1;
|
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 {
|
location /readyz {
|
||||||
access_log off;
|
access_log off;
|
||||||
proxy_pass $PENPOT_BACKEND_URI$request_uri;
|
proxy_pass $PENPOT_BACKEND_URI$request_uri;
|
||||||
|
|||||||
@ -63,6 +63,7 @@
|
|||||||
:wasm (when is-wasm "true")
|
:wasm (when is-wasm "true")
|
||||||
:scale scale}
|
:scale scale}
|
||||||
uri (-> (cf/get :public-uri)
|
uri (-> (cf/get :public-uri)
|
||||||
(assoc :path "/render.html")
|
(u/ensure-path-slash)
|
||||||
|
(u/join "render.html")
|
||||||
(assoc :query (u/map->query-string params)))]
|
(assoc :query (u/map->query-string params)))]
|
||||||
(bw/exec! (prepare-options uri) (partial render uri)))))
|
(bw/exec! (prepare-options uri) (partial render uri)))))
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
:object-id object-id
|
:object-id object-id
|
||||||
:route "objects"}]
|
:route "objects"}]
|
||||||
(-> base-uri
|
(-> base-uri
|
||||||
(assoc :path "/render.html")
|
(u/join "render.html")
|
||||||
(assoc :query (u/map->query-string params)))))
|
(assoc :query (u/map->query-string params)))))
|
||||||
|
|
||||||
(sync-page-size! [dom]
|
(sync-page-size! [dom]
|
||||||
@ -77,6 +77,7 @@
|
|||||||
(on-object (assoc object :path path))
|
(on-object (assoc object :path path))
|
||||||
(p/recur (rest objects))))))]
|
(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)
|
(bw/exec! (prepare-options base-uri)
|
||||||
(partial render base-uri)))))
|
(partial render base-uri)))))
|
||||||
|
|||||||
@ -350,7 +350,8 @@
|
|||||||
:object-id (mapv :id objects)
|
:object-id (mapv :id objects)
|
||||||
:route "objects"}
|
:route "objects"}
|
||||||
uri (-> (cf/get :public-uri)
|
uri (-> (cf/get :public-uri)
|
||||||
(assoc :path "/render.html")
|
(u/ensure-path-slash)
|
||||||
|
(u/join "render.html")
|
||||||
(assoc :query (u/map->query-string params)))]
|
(assoc :query (u/map->query-string params)))]
|
||||||
(bw/exec! (prepare-options uri)
|
(bw/exec! (prepare-options uri)
|
||||||
(partial render uri)))))
|
(partial render uri)))))
|
||||||
|
|||||||
822
frontend/playwright/data/render-wasm/get-file-paths-evenodd.json
Normal file
822
frontend/playwright/data/render-wasm/get-file-paths-evenodd.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1655
frontend/playwright/data/workspace/multiselection-typography.json
Normal file
1655
frontend/playwright/data/workspace/multiselection-typography.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -243,6 +243,22 @@ test("Renders a file with a closed path shape with multiple segments using strok
|
|||||||
await expect(workspace.canvas).toHaveScreenshot();
|
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 ({
|
test("Renders solid shadows after select all and zoom to selected", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
258
frontend/playwright/ui/specs/multiseleccion.spec.js
Normal file
258
frontend/playwright/ui/specs/multiseleccion.spec.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
@ -735,7 +735,7 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
|
|
||||||
// Check if token pill is visible on right sidebar
|
// Check if token pill is visible on right sidebar
|
||||||
const strokeSectionSidebar = rightSidebar.getByRole("region", {
|
const strokeSectionSidebar = rightSidebar.getByRole("region", {
|
||||||
name: "stroke-section",
|
name: "Stroke section",
|
||||||
});
|
});
|
||||||
await expect(strokeSectionSidebar).toBeVisible();
|
await expect(strokeSectionSidebar).toBeVisible();
|
||||||
const firstStrokeRow = strokeSectionSidebar.getByLabel("stroke-row-0");
|
const firstStrokeRow = strokeSectionSidebar.getByLabel("stroke-row-0");
|
||||||
|
|||||||
@ -915,7 +915,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
padding: $s-4;
|
padding: $s-4;
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
z-index: $z-index-10;
|
z-index: $z-index-dropdown;
|
||||||
color: var(--title-foreground-color-hover);
|
color: var(--title-foreground-color-hover);
|
||||||
background-color: var(--menu-background-color);
|
background-color: var(--menu-background-color);
|
||||||
border: $s-2 solid var(--panel-border-color);
|
border: $s-2 solid var(--panel-border-color);
|
||||||
|
|||||||
@ -11,5 +11,5 @@ $z-index-4: 4; // context menu
|
|||||||
$z-index-5: 5; // modal
|
$z-index-5: 5; // modal
|
||||||
$z-index-10: 10;
|
$z-index-10: 10;
|
||||||
$z-index-20: 20;
|
$z-index-20: 20;
|
||||||
$z-index-modal: 30; // When refactor finish we can reduce this number,
|
$z-index-modal: 300;
|
||||||
$z-index-alert: 40; // When refactor finish we can reduce this number,
|
$z-index-dropdown: 400;
|
||||||
|
|||||||
@ -31,7 +31,6 @@
|
|||||||
globalThis.penpotVersion = "{{& version}}";
|
globalThis.penpotVersion = "{{& version}}";
|
||||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
globalThis.penpotVersion = "{{& version}}";
|
globalThis.penpotVersion = "{{& version}}";
|
||||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
<script src="{{& config}}"></script>
|
<script src="{{& config_render}}"></script>
|
||||||
<script src="{{& polyfills}}"></script>
|
<script src="{{& polyfills}}"></script>
|
||||||
<script type="importmap">{{& importmap }}</script>
|
<script type="importmap">{{& importmap }}</script>
|
||||||
{{/manifest}}
|
{{/manifest}}
|
||||||
|
|||||||
@ -207,9 +207,9 @@ async function generateManifest() {
|
|||||||
rasterizer_main: "./js/rasterizer.js",
|
rasterizer_main: "./js/rasterizer.js",
|
||||||
|
|
||||||
config: "./js/config.js?version=" + VERSION_TAG,
|
config: "./js/config.js?version=" + VERSION_TAG,
|
||||||
|
config_render: "./js/config-render.js?version=" + VERSION_TAG,
|
||||||
polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
|
polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
|
||||||
libs: "./js/libs.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,
|
default_translations: "./js/translation.en.js?version=" + VERSION_TAG,
|
||||||
|
|
||||||
importmap: JSON.stringify({
|
importmap: JSON.stringify({
|
||||||
|
|||||||
@ -161,9 +161,9 @@
|
|||||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
(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 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 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 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
|
(def upload-chunk-size (obj/get global "penpotUploadChunkSize" (* 1024 1024 25))) ;; 25 MiB
|
||||||
|
|
||||||
;; We set the current parsed flags under common for make
|
;; We set the current parsed flags under common for make
|
||||||
@ -190,7 +190,10 @@
|
|||||||
public-uri))
|
public-uri))
|
||||||
|
|
||||||
(def worker-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
|
(defn external-feature-flag
|
||||||
[flag value]
|
[flag value]
|
||||||
|
|||||||
@ -651,3 +651,9 @@
|
|||||||
|
|
||||||
(def progress
|
(def progress
|
||||||
(l/derived :progress st/state))
|
(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))
|
||||||
|
|||||||
@ -5,12 +5,13 @@
|
|||||||
// Copyright (c) KALEIDOS INC
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
|
@use "ds/z-index.scss" as *;
|
||||||
|
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: deprecated.$op-0;
|
opacity: deprecated.$op-0;
|
||||||
z-index: deprecated.$z-index-4;
|
z-index: var(--z-index-dropdown);
|
||||||
|
|
||||||
&.is-open {
|
&.is-open {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
@use "common/refactor/common-dashboard";
|
@use "common/refactor/common-dashboard";
|
||||||
@use "../ds/typography.scss" as t;
|
@use "ds/typography.scss" as t;
|
||||||
@use "../ds/_borders.scss" as *;
|
@use "ds/spacing.scss" as *;
|
||||||
@use "../ds/spacing.scss" as *;
|
@use "ds/z-index.scss" as *;
|
||||||
@use "../ds/_sizes.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
@use "../ds/z-index.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
|
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
@ -52,7 +52,7 @@
|
|||||||
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
|
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: $z-index-100;
|
z-index: var(--z-index-panels);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-inside {
|
.nav-inside {
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
@use "common/refactor/common-dashboard";
|
@use "common/refactor/common-dashboard";
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
@ -13,6 +15,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: deprecated.$s-1 solid var(--color-background-quaternary);
|
border-top: deprecated.$s-1 solid var(--color-background-quaternary);
|
||||||
|
padding-block-end: var(--sp-xxxl);
|
||||||
|
|
||||||
&.dashboard-projects {
|
&.dashboard-projects {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
|
|||||||
font-size: deprecated.$fs-14;
|
font-size: deprecated.$fs-14;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
padding: 0 deprecated.$s-16;
|
padding: 0 var(--sp-l) deprecated.$s-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-row {
|
.grid-row {
|
||||||
|
|||||||
@ -19,16 +19,15 @@
|
|||||||
margin-inline-end: var(--sp-l);
|
margin-inline-end: var(--sp-l);
|
||||||
border-block-start: $b-1 solid var(--panel-border-color);
|
border-block-start: $b-1 solid var(--panel-border-color);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-block-end: var(--sp-xxxl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-projects {
|
.dashboard-projects {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
block-size: calc(100vh - px2rem(64));
|
block-size: calc(100vh - px2rem(80));
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-team-hero {
|
.with-team-hero {
|
||||||
block-size: calc(100vh - px2rem(280));
|
block-size: calc(100vh - px2rem(360));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-shared {
|
.dashboard-shared {
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS INC
|
// 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/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 {
|
.dashboard-templates-section {
|
||||||
background-color: var(--color-background-tertiary);
|
background-color: var(--color-background-tertiary);
|
||||||
@ -25,6 +26,7 @@
|
|||||||
transition: inset-block-end 300ms;
|
transition: inset-block-end 300ms;
|
||||||
width: calc(100% - $sz-12);
|
width: calc(100% - $sz-12);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: var(--z-index-set);
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
inset-block-end: calc(-1 * px2rem(228));
|
inset-block-end: calc(-1 * px2rem(228));
|
||||||
|
|||||||
@ -34,15 +34,8 @@
|
|||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[okulary.core :as l]
|
|
||||||
[rumext.v2 :as mf]))
|
[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 notification-timeout 7000)
|
||||||
|
|
||||||
(def ^:private schema:form-access-token
|
(def ^:private schema:form-access-token
|
||||||
@ -78,7 +71,7 @@
|
|||||||
(mf/defc token-created*
|
(mf/defc token-created*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [title mcp-key?]}]
|
[{: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
|
on-copy-to-clipboard
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -310,7 +303,7 @@
|
|||||||
[]
|
[]
|
||||||
(let [created? (mf/use-state false)
|
(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 (some #(when (= (:type %) "mcp") %) tokens)
|
||||||
mcp-key-id (:id mcp-key)
|
mcp-key-id (:id mcp-key)
|
||||||
|
|
||||||
@ -413,7 +406,7 @@
|
|||||||
(mf/defc mcp-server-section*
|
(mf/defc mcp-server-section*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[]
|
[]
|
||||||
(let [tokens (mf/deref tokens-ref)
|
(let [tokens (mf/deref refs/access-tokens)
|
||||||
profile (mf/deref refs/profile)
|
profile (mf/deref refs/profile)
|
||||||
|
|
||||||
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||||
@ -422,6 +415,8 @@
|
|||||||
expires-at (:expires-at mcp-key)
|
expires-at (:expires-at mcp-key)
|
||||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||||
|
|
||||||
|
show-enabled? (and mcp-enabled? (false? expired?))
|
||||||
|
|
||||||
tooltip-id
|
tooltip-id
|
||||||
(mf/use-id)
|
(mf/use-id)
|
||||||
|
|
||||||
@ -511,14 +506,17 @@
|
|||||||
(tr "integrations.mcp-server.status.expired.1")]]])
|
(tr "integrations.mcp-server.status.expired.1")]]])
|
||||||
|
|
||||||
[:div {:class (stl/css :mcp-server-switch)}
|
[: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.enabled")
|
||||||
(tr "integrations.mcp-server.status.disabled"))
|
(tr "integrations.mcp-server.status.disabled"))
|
||||||
:default-checked mcp-enabled?
|
:default-checked show-enabled?
|
||||||
:on-change handle-mcp-change}]
|
:on-change handle-mcp-change}]
|
||||||
(when (and (false? mcp-enabled?) (nil? mcp-key))
|
(when (and (false? mcp-enabled?) (nil? mcp-key))
|
||||||
[:div {:class (stl/css :mcp-server-switch-cover)
|
[: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)
|
(when (some? mcp-key)
|
||||||
[:div {:class (stl/css :mcp-server-key)}
|
[:div {:class (stl/css :mcp-server-key)}
|
||||||
@ -567,7 +565,7 @@
|
|||||||
(mf/defc access-tokens-section*
|
(mf/defc access-tokens-section*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[]
|
[]
|
||||||
(let [tokens (mf/deref tokens-ref)
|
(let [tokens (mf/deref refs/access-tokens)
|
||||||
|
|
||||||
handle-click
|
handle-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|||||||
@ -43,13 +43,9 @@
|
|||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[okulary.core :as l]
|
|
||||||
[potok.v2.core :as ptk]
|
[potok.v2.core :as ptk]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def tokens-ref
|
|
||||||
(l/derived :access-tokens st/state))
|
|
||||||
|
|
||||||
(mf/defc shortcuts*
|
(mf/defc shortcuts*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [id]}]
|
[{:keys [id]}]
|
||||||
@ -780,14 +776,22 @@
|
|||||||
(mf/defc mcp-menu*
|
(mf/defc mcp-menu*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [on-close]}]
|
[{:keys [on-close]}]
|
||||||
(let [plugins? (features/active-feature? @st/state "plugins/runtime")
|
(let [plugins? (features/active-feature? @st/state "plugins/runtime")
|
||||||
|
|
||||||
profile (mf/deref refs/profile)
|
profile (mf/deref refs/profile)
|
||||||
mcp (mf/deref refs/mcp)
|
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-enabled? (true? (-> profile :props :mcp-enabled))
|
||||||
mcp-connected? (= "connected" (get mcp :connection-status))
|
mcp-connected? (= "connected" (get mcp :connection-status))
|
||||||
|
|
||||||
|
show-enabled? (and mcp-enabled? (false? expired?))
|
||||||
|
|
||||||
on-nav-to-integrations
|
on-nav-to-integrations
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
@ -825,7 +829,7 @@
|
|||||||
:pos-6 plugins?)
|
:pos-6 plugins?)
|
||||||
:on-close on-close}
|
:on-close on-close}
|
||||||
|
|
||||||
(when mcp-enabled?
|
(when show-enabled?
|
||||||
[:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin"
|
[:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin"
|
||||||
:class (stl/css :base-menu-item :submenu-item)
|
:class (stl/css :base-menu-item :submenu-item)
|
||||||
:on-click on-toggle-mcp-plugin
|
:on-click on-toggle-mcp-plugin
|
||||||
@ -840,7 +844,7 @@
|
|||||||
:on-click on-nav-to-integrations
|
:on-click on-nav-to-integrations
|
||||||
:on-key-down on-nav-to-integrations-key-down}
|
:on-key-down on-nav-to-integrations-key-down}
|
||||||
[:span {:class (stl/css :item-name)}
|
[: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.enabled")
|
||||||
(tr "workspace.header.menu.mcp.server.status.disabled"))]]]))
|
(tr "workspace.header.menu.mcp.server.status.disabled"))]]]))
|
||||||
|
|
||||||
@ -1014,7 +1018,7 @@
|
|||||||
:class (stl/css :item-arrow)}]])
|
:class (stl/css :item-arrow)}]])
|
||||||
|
|
||||||
(when (contains? cf/flags :mcp)
|
(when (contains? cf/flags :mcp)
|
||||||
(let [tokens (mf/deref tokens-ref)
|
(let [tokens (mf/deref refs/access-tokens)
|
||||||
expired? (some->> tokens
|
expired? (some->> tokens
|
||||||
(some #(when (= (:type %) "mcp") %))
|
(some #(when (= (:type %) "mcp") %))
|
||||||
:expires-at
|
:expires-at
|
||||||
|
|||||||
@ -115,7 +115,8 @@
|
|||||||
result))
|
result))
|
||||||
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* {}
|
[:> hooks/sortable-container* {}
|
||||||
(for [obj shapes]
|
(for [obj shapes]
|
||||||
(if (cfh/frame-shape? obj)
|
(if (cfh/frame-shape? obj)
|
||||||
|
|||||||
@ -69,12 +69,11 @@
|
|||||||
;; Unique color attribute maps
|
;; Unique color attribute maps
|
||||||
all-colors (distinct (mapv :attrs data))
|
all-colors (distinct (mapv :attrs data))
|
||||||
|
|
||||||
;; Split into: library colors, token colors, and plain colors
|
;; Split into mutually exclusive groups:
|
||||||
library-colors (filterv :ref-id all-colors)
|
;; token-colors take priority; library-colors and plain colors exclude tokens
|
||||||
token-colors (filterv :token-name all-colors)
|
token-colors (filterv :token-name all-colors)
|
||||||
colors (filterv #(and (nil? (:ref-id %))
|
library-colors (filterv (fn [c] (and (some? (:ref-id c)) (nil? (:token-name c)))) all-colors)
|
||||||
(not (:token-name %)))
|
colors (filterv (fn [c] (and (nil? (:ref-id c)) (nil? (:token-name c)))) all-colors)]
|
||||||
all-colors)]
|
|
||||||
{:groups groups
|
{:groups groups
|
||||||
:all-colors all-colors
|
:all-colors all-colors
|
||||||
:colors colors
|
:colors colors
|
||||||
@ -243,8 +242,7 @@
|
|||||||
[:div {:class (stl/css :selected-color-group)}
|
[:div {:class (stl/css :selected-color-group)}
|
||||||
(let [token-color-extract (cond->> token-colors (not @expand-token-color) (take 3))]
|
(let [token-color-extract (cond->> token-colors (not @expand-token-color) (take 3))]
|
||||||
(for [[index token-color] (d/enumerate token-color-extract)]
|
(for [[index token-color] (d/enumerate token-color-extract)]
|
||||||
(let [color {:color (:color token-color)
|
(let [color (dissoc token-color :token-name :has-token-applied)]
|
||||||
:opacity (:opacity token-color)}]
|
|
||||||
[:> color-row*
|
[:> color-row*
|
||||||
{:key index
|
{:key index
|
||||||
:color color
|
:color color
|
||||||
|
|||||||
@ -194,7 +194,8 @@
|
|||||||
(dom/set-attribute! checkbox "indeterminate" true)
|
(dom/set-attribute! checkbox "indeterminate" true)
|
||||||
(dom/remove-attribute! checkbox "indeterminate"))))
|
(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)}
|
[:div {:class (stl/css :fill-title)}
|
||||||
[:> title-bar* {:collapsable has-fills?
|
[:> title-bar* {:collapsable has-fills?
|
||||||
:collapsed (not open?)
|
:collapsed (not open?)
|
||||||
|
|||||||
@ -186,7 +186,7 @@
|
|||||||
:shape-ids ids}))))]
|
:shape-ids ids}))))]
|
||||||
|
|
||||||
[:section {:class (stl/css :stroke-section)
|
[:section {:class (stl/css :stroke-section)
|
||||||
:aria-label "stroke-section"}
|
:aria-label "Stroke section"}
|
||||||
[:div {:class (stl/css :stroke-title)}
|
[:div {:class (stl/css :stroke-title)}
|
||||||
[:> title-bar* {:collapsable has-strokes?
|
[:> title-bar* {:collapsable has-strokes?
|
||||||
:collapsed (not open?)
|
:collapsed (not open?)
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.main.ui.hooks :as hooks]
|
[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.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.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
@ -429,10 +429,12 @@
|
|||||||
(when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name))
|
(when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name))
|
||||||
(dom/focus! (txu/get-text-editor-content)))))))
|
(dom/focus! (txu/get-text-editor-content)))))))
|
||||||
|
|
||||||
common-props
|
opts (mf/props
|
||||||
(mf/props {:values values
|
{:ids ids
|
||||||
:on-change on-change
|
:values values
|
||||||
:on-blur on-text-blur})]
|
:on-change on-change
|
||||||
|
:show-recent true
|
||||||
|
:on-blur on-text-blur})]
|
||||||
|
|
||||||
(hooks/use-stream
|
(hooks/use-stream
|
||||||
expand-stream
|
expand-stream
|
||||||
@ -496,11 +498,7 @@
|
|||||||
:icon i/detach}]]
|
:icon i/detach}]]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:> text-options #js {:ids ids
|
[:> text-options* opts])
|
||||||
:values values
|
|
||||||
:on-change on-change
|
|
||||||
:show-recent true
|
|
||||||
:on-blur on-text-blur}])
|
|
||||||
|
|
||||||
[:div {:class (stl/css :text-align-options)}
|
[:div {:class (stl/css :text-align-options)}
|
||||||
[:> text-align-options* common-props]
|
[:> text-align-options* common-props]
|
||||||
|
|||||||
@ -451,8 +451,7 @@
|
|||||||
:value "lowercase"
|
:value "lowercase"
|
||||||
:id "text-transform-lowercase"}]]]))
|
:id "text-transform-lowercase"}]]]))
|
||||||
|
|
||||||
(mf/defc text-options
|
(mf/defc text-options*
|
||||||
{::mf/wrap-props false}
|
|
||||||
[{:keys [ids editor values on-change on-blur show-recent]}]
|
[{:keys [ids editor values on-change on-blur show-recent]}]
|
||||||
(let [full-size-selector? (and show-recent (= (mf/use-ctx ctx/sidebar) :right))
|
(let [full-size-selector? (and show-recent (= (mf/use-ctx ctx/sidebar) :right))
|
||||||
opts #js {:editor editor
|
opts #js {:editor editor
|
||||||
@ -541,9 +540,9 @@
|
|||||||
:on-click on-close
|
:on-click on-close
|
||||||
:icon i/tick}]]]
|
:icon i/tick}]]]
|
||||||
|
|
||||||
[:& text-options {:values typography
|
[:> text-options* {:values typography
|
||||||
:on-change on-change
|
:on-change on-change
|
||||||
:show-recent false}]]
|
:show-recent false}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :typography-info-wrapper)}
|
[:div {:class (stl/css :typography-info-wrapper)}
|
||||||
[:div {:class (stl/css :typography-name-wrapper)}
|
[:div {:class (stl/css :typography-name-wrapper)}
|
||||||
|
|||||||
@ -240,7 +240,16 @@
|
|||||||
|
|
||||||
open-modal
|
open-modal
|
||||||
(mf/use-fn
|
(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]
|
(fn [color pos tab]
|
||||||
(let [color (cond
|
(let [color (cond
|
||||||
^boolean has-multiple-colors
|
^boolean has-multiple-colors
|
||||||
@ -345,6 +354,11 @@
|
|||||||
(mf/with-effect [color prev-color disable-picker]
|
(mf/with-effect [color prev-color disable-picker]
|
||||||
(when (and (not disable-picker) (not= prev-color color))
|
(when (and (not disable-picker) (not= prev-color color))
|
||||||
(modal/update-props! :colorpicker {:data (parse-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]}
|
[:div {:class [class row-class]}
|
||||||
;; Drag handler
|
;; Drag handler
|
||||||
(when (some? on-reorder)
|
(when (some? on-reorder)
|
||||||
@ -436,4 +450,5 @@
|
|||||||
[:> icon-button* {:variant "ghost"
|
[:> icon-button* {:variant "ghost"
|
||||||
:aria-label (tr "settings.select-this-color")
|
:aria-label (tr "settings.select-this-color")
|
||||||
:on-click handle-select
|
:on-click handle-select
|
||||||
|
:tooltip-position "top-left"
|
||||||
:icon i/move}])]))
|
:icon i/move}])]))
|
||||||
|
|||||||
@ -627,6 +627,7 @@
|
|||||||
(if (empty? fills)
|
(if (empty? fills)
|
||||||
(h/call wasm/internal-module "_clear_shape_fills")
|
(h/call wasm/internal-module "_clear_shape_fills")
|
||||||
(let [fills (types.fills/coerce 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))
|
offset (mem/alloc->offset-32 (types.fills/get-byte-size fills))
|
||||||
heap (mem/get-heap-u32)]
|
heap (mem/get-heap-u32)]
|
||||||
|
|
||||||
@ -648,7 +649,7 @@
|
|||||||
(when (zero? cached-image?)
|
(when (zero? cached-image?)
|
||||||
(fetch-image shape-id id thumbnail?))))
|
(fetch-image shape-id id thumbnail?))))
|
||||||
|
|
||||||
(types.fills/get-image-ids fills)))))
|
image-ids))))
|
||||||
|
|
||||||
(defn set-shape-strokes
|
(defn set-shape-strokes
|
||||||
[shape-id strokes thumbnail?]
|
[shape-id strokes thumbnail?]
|
||||||
@ -676,7 +677,8 @@
|
|||||||
(some? gradient)
|
(some? gradient)
|
||||||
(do
|
(do
|
||||||
(types.fills.impl/write-gradient-fill offset dview opacity gradient)
|
(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)
|
(some? image)
|
||||||
(let [image-id (get image :id)
|
(let [image-id (get image :id)
|
||||||
@ -689,11 +691,13 @@
|
|||||||
(h/call wasm/internal-module "_add_shape_stroke_fill")
|
(h/call wasm/internal-module "_add_shape_stroke_fill")
|
||||||
(when (== cached-image? 0)
|
(when (== cached-image? 0)
|
||||||
(fetch-image shape-id image-id thumbnail?)))
|
(fetch-image shape-id image-id thumbnail?)))
|
||||||
|
|
||||||
(some? color)
|
(some? color)
|
||||||
(do
|
(do
|
||||||
(types.fills.impl/write-solid-fill offset dview opacity color)
|
(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))
|
strokes))
|
||||||
|
|
||||||
(defn set-shape-svg-attrs
|
(defn set-shape-svg-attrs
|
||||||
@ -1303,16 +1307,17 @@
|
|||||||
(when (or (seq pending-thumbnails) (seq pending-full))
|
(when (or (seq pending-thumbnails) (seq pending-full))
|
||||||
(->> (rx/concat
|
(->> (rx/concat
|
||||||
(->> (rx/from (vals pending-thumbnails))
|
(->> (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/reduce conj []))
|
||||||
(->> (rx/from (vals pending-full))
|
(->> (rx/from (vals pending-full))
|
||||||
(rx/mapcat (fn [callback] (callback)))
|
(rx/mapcat
|
||||||
|
(fn [callback]
|
||||||
|
(if (fn? callback) (callback) (rx/empty))))
|
||||||
(rx/reduce conj [])))
|
(rx/reduce conj [])))
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
(fn [_]
|
(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)]
|
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||||
(when (seq text-ids)
|
(when (seq text-ids)
|
||||||
(update-text-layouts text-ids)))
|
(update-text-layouts text-ids)))
|
||||||
|
|||||||
@ -132,7 +132,9 @@
|
|||||||
"Maps attrs to styles"
|
"Maps attrs to styles"
|
||||||
[styles]
|
[styles]
|
||||||
(let [mapped-styles
|
(let [mapped-styles
|
||||||
(into {} (map attr->style styles))]
|
(into {} (comp (filter (fn [[_ v]] (some? v)))
|
||||||
|
(map attr->style))
|
||||||
|
styles)]
|
||||||
(clj->js mapped-styles)))
|
(clj->js mapped-styles)))
|
||||||
|
|
||||||
(defn style-needs-mapping?
|
(defn style-needs-mapping?
|
||||||
@ -199,12 +201,14 @@
|
|||||||
(let [style-name (get-style-name-as-css-variable k)
|
(let [style-name (get-style-name-as-css-variable k)
|
||||||
[_ style-decode] (get mapping k)
|
[_ style-decode] (get mapping k)
|
||||||
style-value (.getPropertyValue style-declaration style-name)]
|
style-value (.getPropertyValue style-declaration style-name)]
|
||||||
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
|
(if (or (not removed-mixed) (not (contains? mixed-values style-value)))
|
||||||
(assoc acc k (style-decode style-value))))
|
(assoc acc k (style-decode style-value))
|
||||||
|
acc))
|
||||||
(let [style-name (get-style-name k)
|
(let [style-name (get-style-name k)
|
||||||
style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))]
|
style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))]
|
||||||
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
|
(if (or (not removed-mixed) (not (contains? mixed-values style-value)))
|
||||||
(assoc acc k style-value))))) {} txt/text-style-attrs))
|
(assoc acc k style-value)
|
||||||
|
acc)))) {} txt/text-style-attrs))
|
||||||
|
|
||||||
(defn get-styles-from-event
|
(defn get-styles-from-event
|
||||||
"Returns a ClojureScript object compatible with text nodes"
|
"Returns a ClojureScript object compatible with text nodes"
|
||||||
|
|||||||
@ -90,8 +90,8 @@
|
|||||||
"Return a initialized webworker instance."
|
"Return a initialized webworker instance."
|
||||||
[path on-error]
|
[path on-error]
|
||||||
(let [instance (js/Worker. path)
|
(let [instance (js/Worker. path)
|
||||||
bus (rx/subject)
|
bus (rx/subject)
|
||||||
worker (Worker. instance (rx/to-observable bus))
|
worker (Worker. instance (rx/to-observable bus))
|
||||||
|
|
||||||
handle-message
|
handle-message
|
||||||
(fn [event]
|
(fn [event]
|
||||||
|
|||||||
@ -26,6 +26,7 @@ Penpot:
|
|||||||
props?: { [key: string]: unknown },
|
props?: { [key: string]: unknown },
|
||||||
): symbol;
|
): symbol;
|
||||||
off(listenerId: symbol): void;
|
off(listenerId: symbol): void;
|
||||||
|
version: string;
|
||||||
root: Shape | null;
|
root: Shape | null;
|
||||||
currentFile: File | null;
|
currentFile: File | null;
|
||||||
currentPage: Page | null;
|
currentPage: Page | null;
|
||||||
@ -72,7 +73,7 @@ Penpot:
|
|||||||
generateFontFaces(shapes: Shape[]): Promise<string>;
|
generateFontFaces(shapes: Shape[]): Promise<string>;
|
||||||
openViewer(): void;
|
openViewer(): void;
|
||||||
createPage(): Page;
|
createPage(): Page;
|
||||||
openPage(page: Page, newWindow?: boolean): void;
|
openPage(page: string | Page, newWindow?: boolean): void;
|
||||||
alignHorizontal(
|
alignHorizontal(
|
||||||
shapes: Shape[],
|
shapes: Shape[],
|
||||||
direction: "center" | "left" | "right",
|
direction: "center" | "left" | "right",
|
||||||
@ -162,6 +163,12 @@ Penpot:
|
|||||||
```
|
```
|
||||||
penpot.closePlugin();
|
penpot.closePlugin();
|
||||||
```
|
```
|
||||||
|
version: |-
|
||||||
|
```
|
||||||
|
readonly version: string
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the current penpot version.
|
||||||
root: |-
|
root: |-
|
||||||
```
|
```
|
||||||
readonly root: Shape | null
|
readonly root: Shape | null
|
||||||
@ -725,19 +732,19 @@ Penpot:
|
|||||||
Returns Page
|
Returns Page
|
||||||
openPage: |-
|
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.
|
Changes the current open page to given page. Requires `content:read` permission.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
||||||
* page: Page
|
* page: string | Page
|
||||||
|
|
||||||
the page to open
|
the page to open (a Page object or a page UUID string)
|
||||||
* newWindow: boolean
|
* 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
|
Returns void
|
||||||
|
|
||||||
@ -4785,6 +4792,7 @@ Context:
|
|||||||
|
|
||||||
```
|
```
|
||||||
interface Context {
|
interface Context {
|
||||||
|
version: string;
|
||||||
root: Shape | null;
|
root: Shape | null;
|
||||||
currentFile: File | null;
|
currentFile: File | null;
|
||||||
currentPage: Page | null;
|
currentPage: Page | null;
|
||||||
@ -4837,7 +4845,7 @@ Context:
|
|||||||
removeListener(listenerId: symbol): void;
|
removeListener(listenerId: symbol): void;
|
||||||
openViewer(): void;
|
openViewer(): void;
|
||||||
createPage(): Page;
|
createPage(): Page;
|
||||||
openPage(page: Page, newWindow?: boolean): void;
|
openPage(page: string | Page, newWindow?: boolean): void;
|
||||||
alignHorizontal(
|
alignHorizontal(
|
||||||
shapes: Shape[],
|
shapes: Shape[],
|
||||||
direction: "center" | "left" | "right",
|
direction: "center" | "left" | "right",
|
||||||
@ -4854,6 +4862,12 @@ Context:
|
|||||||
```
|
```
|
||||||
members:
|
members:
|
||||||
Properties:
|
Properties:
|
||||||
|
version: |-
|
||||||
|
```
|
||||||
|
readonly version: string
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the current penpot version.
|
||||||
root: |-
|
root: |-
|
||||||
```
|
```
|
||||||
readonly root: Shape | null
|
readonly root: Shape | null
|
||||||
@ -5392,19 +5406,19 @@ Context:
|
|||||||
Returns Page
|
Returns Page
|
||||||
openPage: |-
|
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.
|
Changes the current open page to given page. Requires `content:read` permission.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
||||||
* page: Page
|
* page: string | Page
|
||||||
|
|
||||||
the page to open
|
the page to open (a Page object or a page UUID string)
|
||||||
* newWindow: boolean
|
* 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
|
Returns void
|
||||||
|
|
||||||
@ -6845,7 +6859,7 @@ Export:
|
|||||||
|
|
||||||
```
|
```
|
||||||
interface Export {
|
interface Export {
|
||||||
type: "svg" | "png" | "jpeg" | "pdf";
|
type: "svg" | "png" | "jpeg" | "webp" | "pdf";
|
||||||
scale?: number;
|
scale?: number;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
skipChildren?: boolean;
|
skipChildren?: boolean;
|
||||||
@ -6857,10 +6871,10 @@ Export:
|
|||||||
Properties:
|
Properties:
|
||||||
type: |-
|
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: |-
|
||||||
```
|
```
|
||||||
scale?: number
|
scale?: number
|
||||||
@ -7249,6 +7263,7 @@ Flags:
|
|||||||
```
|
```
|
||||||
interface Flags {
|
interface Flags {
|
||||||
naturalChildOrdering: boolean;
|
naturalChildOrdering: boolean;
|
||||||
|
throwValidationErrors: boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -7264,6 +7279,14 @@ Flags:
|
|||||||
Also, appendChild method will be append the children in the top-most position.
|
Also, appendChild method will be append the children in the top-most position.
|
||||||
The insertchild method is changed acordingly to respect this ordering.
|
The insertchild method is changed acordingly to respect this ordering.
|
||||||
Defaults to false
|
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:
|
FlexLayout:
|
||||||
overview: |-
|
overview: |-
|
||||||
Interface FlexLayout
|
Interface FlexLayout
|
||||||
|
|||||||
@ -16,9 +16,9 @@
|
|||||||
"fmt": "./scripts/fmt"
|
"fmt": "./scripts/fmt"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@github/copilot": "^1.0.21",
|
"@github/copilot": "^1.0.35",
|
||||||
"@types/node": "^25.5.2",
|
"@types/node": "^25.6.0",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"opencode-ai": "^1.14.19"
|
"opencode-ai": "^1.14.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
pnpm-lock.yaml
generated
182
pnpm-lock.yaml
generated
@ -9,17 +9,17 @@ importers:
|
|||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@github/copilot':
|
'@github/copilot':
|
||||||
specifier: ^1.0.21
|
specifier: ^1.0.35
|
||||||
version: 1.0.21
|
version: 1.0.35
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.2
|
specifier: ^25.6.0
|
||||||
version: 25.5.2
|
version: 25.6.0
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.28.0
|
specifier: ^0.28.0
|
||||||
version: 0.28.0
|
version: 0.28.0
|
||||||
opencode-ai:
|
opencode-ai:
|
||||||
specifier: ^1.14.19
|
specifier: ^1.14.22
|
||||||
version: 1.14.19
|
version: 1.14.22
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -179,120 +179,120 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@github/copilot-darwin-arm64@1.0.21':
|
'@github/copilot-darwin-arm64@1.0.35':
|
||||||
resolution: {integrity: sha512-aB+s9ldTwcyCOYmzjcQ4SknV6g81z92T8aUJEJZBwOXOTBeWKAJtk16ooAKangZgdwuLgO3or1JUjx1FJAm5nQ==}
|
resolution: {integrity: sha512-NNZE0TOz0HOlv7eqlh6EcQbNkhtnIHReBLieW6pfDUUTKkgsqbUu1MOitF8m+LUQk3ml1T0MQ5MOfad1HSa/MQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot-darwin-x64@1.0.21':
|
'@github/copilot-darwin-x64@1.0.35':
|
||||||
resolution: {integrity: sha512-aNad81DOGuGShmaiFNIxBUSZLwte0dXmDYkGfAF9WJIgY4qP4A8CPWFoNr8//gY+4CwaIf9V+f/OC6k2BdECbw==}
|
resolution: {integrity: sha512-XCv/mfdv0rnrtrNVOluio/N/kyCge0uG2hghvtlgO/+z6EjvzFygkpXXS1gVxiXhWc3lX232cTXQU3zklC/8Ng==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot-linux-arm64@1.0.21':
|
'@github/copilot-linux-arm64@1.0.35':
|
||||||
resolution: {integrity: sha512-FL0NsCnHax4czHVv1S8iBqPLGZDhZ28N3+6nT29xWGhmjBWTkIofxLThKUPcyyMsfPTTxIlrdwWa8qQc5z2Q+g==}
|
resolution: {integrity: sha512-mbaadATfJPzmXq2SD1TWocIG/GobcYC6OvNFhCG8UXMsiXY5cevhszl5ujuayhPJBxS77Yj5uvIFjNQ1Kf5V8Q==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot-linux-x64@1.0.21':
|
'@github/copilot-linux-x64@1.0.35':
|
||||||
resolution: {integrity: sha512-S7pWVI16hesZtxYbIyfw+MHZpc5ESoGKUVr5Y+lZJNaM2340gJGPQzQwSpvKIRMLHRKI2hXLwciAnYeMFxE/Tg==}
|
resolution: {integrity: sha512-NrZ0VjztdBbJ5qAmuUtuKsWkimOaqzjDV+ZGUv1FxSxoys40kiiakQ5WbnMFDzaIFaf47zDi++6ixgQzq7Jk5A==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot-win32-arm64@1.0.21':
|
'@github/copilot-win32-arm64@1.0.35':
|
||||||
resolution: {integrity: sha512-a9qc2Ku+XbyBkXCclbIvBbIVnECACTIWnPctmXWsQeSdeapGxgfHGux7y8hAFV5j6+nhCm6cnyEMS3rkZjAhdA==}
|
resolution: {integrity: sha512-KQN7Q7+oPyglmvUEiMp6SYWjl30VSu91T0dUpNHbUs/xRM3qgnCymLPPUyBZGWHog/FueUAsRkhisMHWQVnO+g==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot-win32-x64@1.0.21':
|
'@github/copilot-win32-x64@1.0.35':
|
||||||
resolution: {integrity: sha512-9klu+7NQ6tEyb8sibb0rsbimBivDrnNltZho10Bgbf1wh3o+erTjffXDjW9Zkyaw8lZA9Fz8bqhVkKntZq58Lg==}
|
resolution: {integrity: sha512-J0XhXO2FmlFr8pGa970xEd4tr1rqFiZxoaPW5WvkJYZoZUHbBhFcGasp5/yEeJ71b3vI4PHm/mSZZebD3ALMKQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@github/copilot@1.0.21':
|
'@github/copilot@1.0.35':
|
||||||
resolution: {integrity: sha512-P+nORjNKAtl92jYCG6Qr1Rsw2JoyScgeQSkIR6O2WB37WS5JVdA4ax1WVualMbfuc9V58CPHX6fwyNpkI89FkQ==}
|
resolution: {integrity: sha512-O1nUy8DXOTE+v86b/FTkyu09EMrDy+vj+2rhmUOcmsXGe0RE5ECyESsasUTUoHK/CSgAExFTziNxbubUoiMMfg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@types/node@25.5.2':
|
'@types/node@25.6.0':
|
||||||
resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==}
|
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||||
|
|
||||||
esbuild@0.28.0:
|
esbuild@0.28.0:
|
||||||
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
opencode-ai@1.14.19:
|
opencode-ai@1.14.22:
|
||||||
resolution: {integrity: sha512-67h56qYcJivd2U9VK8LJvyMBCc3ZE3HcJ/qL4YtaidSnjEumy4SxO+HHlDISsLase7TUQ0w1nULOAibdZxGzbQ==}
|
resolution: {integrity: sha512-J+q1Ehlfg7SSXw2aIY8Mb47FHhPTN8IciKNt0/D+H/brO8RWLe67WjFzxhh/z9SSad9wPcCiLRGAc/iAn8W8wA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
opencode-darwin-arm64@1.14.19:
|
opencode-darwin-arm64@1.14.22:
|
||||||
resolution: {integrity: sha512-gjJ97dTiBbCas3Y7K6KQMQm5/KNIBOM9+10KZHqNr2bQfn0N09O977ZkXoX6IVBBE2632Ahaa71d4pzLKN9ULw==}
|
resolution: {integrity: sha512-h9FjzNoDRsuJD0EEg535P9ul5TyrWovwx591VmuG8fp9d4PoSrAN1O3Zi07GJjkrYyrB8g3c+x5whDqJCz+qog==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
opencode-darwin-x64-baseline@1.14.19:
|
opencode-darwin-x64-baseline@1.14.22:
|
||||||
resolution: {integrity: sha512-UuwLpa511h7qQ+rTmOUmsHch/N4NBQT64dzg+iLWnR5yoR/81inENpbwxiS7hXpVdzCUGo/YnxI1u6SIBoMlTQ==}
|
resolution: {integrity: sha512-GgfP0wSm9/I+j3shOxfeA++7yZpXS6Y1Vis258nEFoRS9Xfv3YlHom7c/8BR9rYqeUE/+rrijP7PrGWGl+IHBw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
opencode-darwin-x64@1.14.19:
|
opencode-darwin-x64@1.14.22:
|
||||||
resolution: {integrity: sha512-uksrjOtWI7Ob5JvjZBSsrKuy3JVF9d89oYZYfWS5m8ordNyv1Nob39MXJXizv85ozsXjSb0rqjpJurnJw8K+tQ==}
|
resolution: {integrity: sha512-cyKRo22sxDwu4ITOlENwXaqVM9kMGndwSaAd95gz1Rmz5NYMShUO/8eckrD2MhS2wm+QvKw9XkRVWVHWQlZw3Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
opencode-linux-arm64-musl@1.14.19:
|
opencode-linux-arm64-musl@1.14.22:
|
||||||
resolution: {integrity: sha512-R1BJuBGWHfBxfKvIA/Hb4nhYaJgCKl1B+mAGNydu+z0CLtGtwU8r+kQWF/G2N0y8Vx6Y6DRfJiv1X0eZEfD1BQ==}
|
resolution: {integrity: sha512-DtSd5tbGk6R5+hGhqViSvbY8ICf+u4oVQhfvCAplQCb1UEwYVc0+oAF6PimFJ+o8i8L6x14O0rry0NaRzZ0CzA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-linux-arm64@1.14.19:
|
opencode-linux-arm64@1.14.22:
|
||||||
resolution: {integrity: sha512-Bo+aZOppLF366mgGfK0CnIcAVy1EmsrBv93eot1CmPSN1oeud07GpGdn3Bjl5f6KuBx1io6JsvjQyHno+MH5AA==}
|
resolution: {integrity: sha512-ohK4LkkGvzB4ptr0nqDOVi2JEJMLROfy1s2U2A4Qrh+1Y0QimgH2b5VgTm+BjA3bC2Hm8Yf/IfkitqlUnCp7YA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-linux-x64-baseline-musl@1.14.19:
|
opencode-linux-x64-baseline-musl@1.14.22:
|
||||||
resolution: {integrity: sha512-+K8MuGoHugtUec4P/nKcTwZFUipHfW7oPpwlIoPiAQou3bNFTzzP6rslbzzNwjXlQRsUw9GAtuIPDOCL6CkgDg==}
|
resolution: {integrity: sha512-oZffotEbGXbA38Y0Dmj7IVq0ATl3nKbP8j91Z0zR5kBEBykOqExJIyc9pZpModgfPf86k98XBsRHiVLK4u9ARw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-linux-x64-baseline@1.14.19:
|
opencode-linux-x64-baseline@1.14.22:
|
||||||
resolution: {integrity: sha512-KuvITzg4iK0hdIjpNZepwu3bLZ/dUZDI6BwCoV4w//VEP1j3UfDyeS3vWghKcQLd2T1+yybuEMM/3RXcwm/lGQ==}
|
resolution: {integrity: sha512-J67YAIWr3E03o9e6wNaPEqBo+9FcPKf5CzjIUSb8yNDyobWON1HHihcuu0hCJ6wF9J9awmlp2/4mO1HOoCo3QQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-linux-x64-musl@1.14.19:
|
opencode-linux-x64-musl@1.14.22:
|
||||||
resolution: {integrity: sha512-0qe2+X76UJdrCdhdlJyfubMC4tveHAVxjSmPq7g9Zm95heBeJdcQDCLeyQk/lGgeXgsZzVPfLmyTWNtBvCZYFQ==}
|
resolution: {integrity: sha512-r+QnqwR/OPmMm197Kb8VLD9mkZGFXz4m5QCZFxOAL34k8AhQZqn3d2mx2bfrMBVfoSiSVxa3jEjZEbNNFGlICQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-linux-x64@1.14.19:
|
opencode-linux-x64@1.14.22:
|
||||||
resolution: {integrity: sha512-2GljfL7BeG4xALBJVRwaAGCM/dzYF5aQf6bfLTKsQIl6QpLUguYSF+fkStBHLeehyqbDP5MtiEEuXjC0+mecjA==}
|
resolution: {integrity: sha512-MSUaO/Cvfb8DFRYETVrVeCnKtoIfgLflyB+O8xQOkVtjMKJ41M+1dFSMyZ3LQa2Vfp5tDskyMhj7eUxvT/owgQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
opencode-windows-arm64@1.14.19:
|
opencode-windows-arm64@1.14.22:
|
||||||
resolution: {integrity: sha512-8/vRHe5tHexikfPceLmpjsQiEhuDTOSCSlEmP4s0Yq3UAkVaDAxpiWq7Bx4g8hjr1gzfXD9vbjV+WHq/BtMC/w==}
|
resolution: {integrity: sha512-8grcxLSf9BD9Bt38MIxXfkI6aOFophVgM0US5r8nAUdVU78/8TS9Flnn6D39GM5RmxzqGWMl1u10vMFrBtMwPA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
opencode-windows-x64-baseline@1.14.19:
|
opencode-windows-x64-baseline@1.14.22:
|
||||||
resolution: {integrity: sha512-Z8imEJK/srE/r1fr7oNLvpLTeRJQyuL7vsbXvCt3T7j2Ew9BOZ7RuYa8EE0R6bNqQ+MLhBGPiAG7NWc++MgK8Q==}
|
resolution: {integrity: sha512-R/o36LpmQmbv/tL2pkcmApn6030z/1oJIYmjDkW5a4K5MXmV7aq+jWrH5p6iYKp9fo9L8oCtOp/rELMBqDS3UA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
opencode-windows-x64@1.14.19:
|
opencode-windows-x64@1.14.22:
|
||||||
resolution: {integrity: sha512-/TqGN91WiUzx7IPMPwmpMIzRixi5TMjaBcC9FH1TgD7DCqKnP6pokvu+ak0C9xwA4wKorE9PoZzeRt/+c3rDCQ==}
|
resolution: {integrity: sha512-jVbZ4VA5b5MF2QhWQOE1VYBKdBE0v/ZebFjwzs6Vieazfgr6OFnGSHVP5WJbU/r6zDssbTBzzpnFxo0IY1SQWw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
undici-types@7.18.2:
|
undici-types@7.19.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@ -374,36 +374,36 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.28.0':
|
'@esbuild/win32-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-darwin-arm64@1.0.21':
|
'@github/copilot-darwin-arm64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-darwin-x64@1.0.21':
|
'@github/copilot-darwin-x64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-linux-arm64@1.0.21':
|
'@github/copilot-linux-arm64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-linux-x64@1.0.21':
|
'@github/copilot-linux-x64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-win32-arm64@1.0.21':
|
'@github/copilot-win32-arm64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot-win32-x64@1.0.21':
|
'@github/copilot-win32-x64@1.0.35':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@github/copilot@1.0.21':
|
'@github/copilot@1.0.35':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@github/copilot-darwin-arm64': 1.0.21
|
'@github/copilot-darwin-arm64': 1.0.35
|
||||||
'@github/copilot-darwin-x64': 1.0.21
|
'@github/copilot-darwin-x64': 1.0.35
|
||||||
'@github/copilot-linux-arm64': 1.0.21
|
'@github/copilot-linux-arm64': 1.0.35
|
||||||
'@github/copilot-linux-x64': 1.0.21
|
'@github/copilot-linux-x64': 1.0.35
|
||||||
'@github/copilot-win32-arm64': 1.0.21
|
'@github/copilot-win32-arm64': 1.0.35
|
||||||
'@github/copilot-win32-x64': 1.0.21
|
'@github/copilot-win32-x64': 1.0.35
|
||||||
|
|
||||||
'@types/node@25.5.2':
|
'@types/node@25.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.19.2
|
||||||
|
|
||||||
esbuild@0.28.0:
|
esbuild@0.28.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -434,55 +434,55 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.28.0
|
'@esbuild/win32-ia32': 0.28.0
|
||||||
'@esbuild/win32-x64': 0.28.0
|
'@esbuild/win32-x64': 0.28.0
|
||||||
|
|
||||||
opencode-ai@1.14.19:
|
opencode-ai@1.14.22:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
opencode-darwin-arm64: 1.14.19
|
opencode-darwin-arm64: 1.14.22
|
||||||
opencode-darwin-x64: 1.14.19
|
opencode-darwin-x64: 1.14.22
|
||||||
opencode-darwin-x64-baseline: 1.14.19
|
opencode-darwin-x64-baseline: 1.14.22
|
||||||
opencode-linux-arm64: 1.14.19
|
opencode-linux-arm64: 1.14.22
|
||||||
opencode-linux-arm64-musl: 1.14.19
|
opencode-linux-arm64-musl: 1.14.22
|
||||||
opencode-linux-x64: 1.14.19
|
opencode-linux-x64: 1.14.22
|
||||||
opencode-linux-x64-baseline: 1.14.19
|
opencode-linux-x64-baseline: 1.14.22
|
||||||
opencode-linux-x64-baseline-musl: 1.14.19
|
opencode-linux-x64-baseline-musl: 1.14.22
|
||||||
opencode-linux-x64-musl: 1.14.19
|
opencode-linux-x64-musl: 1.14.22
|
||||||
opencode-windows-arm64: 1.14.19
|
opencode-windows-arm64: 1.14.22
|
||||||
opencode-windows-x64: 1.14.19
|
opencode-windows-x64: 1.14.22
|
||||||
opencode-windows-x64-baseline: 1.14.19
|
opencode-windows-x64-baseline: 1.14.22
|
||||||
|
|
||||||
opencode-darwin-arm64@1.14.19:
|
opencode-darwin-arm64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-darwin-x64-baseline@1.14.19:
|
opencode-darwin-x64-baseline@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-darwin-x64@1.14.19:
|
opencode-darwin-x64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-arm64-musl@1.14.19:
|
opencode-linux-arm64-musl@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-arm64@1.14.19:
|
opencode-linux-arm64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-x64-baseline-musl@1.14.19:
|
opencode-linux-x64-baseline-musl@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-x64-baseline@1.14.19:
|
opencode-linux-x64-baseline@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-x64-musl@1.14.19:
|
opencode-linux-x64-musl@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-linux-x64@1.14.19:
|
opencode-linux-x64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-windows-arm64@1.14.19:
|
opencode-windows-arm64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-windows-x64-baseline@1.14.19:
|
opencode-windows-x64-baseline@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
opencode-windows-x64@1.14.19:
|
opencode-windows-x64@1.14.22:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.19.2: {}
|
||||||
|
|||||||
@ -720,26 +720,24 @@ impl RenderState {
|
|||||||
self.surfaces.clear_cache(self.background_color);
|
self.surfaces.clear_cache(self.background_color);
|
||||||
self.cache_cleared_this_render = true;
|
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
|
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||||
// positions would be wrong — only save to the tile HashMap.
|
// 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(
|
self.surfaces.cache_current_tile_texture(
|
||||||
&mut self.gpu_state,
|
&mut self.gpu_state,
|
||||||
&self.tile_viewbox,
|
&self.tile_viewbox,
|
||||||
&self
|
¤t_tile,
|
||||||
.current_tile
|
|
||||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
|
||||||
&tile_rect,
|
&tile_rect,
|
||||||
fast_mode,
|
fast_mode,
|
||||||
self.render_area,
|
self.render_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.surfaces.draw_cached_tile_surface(
|
self.surfaces
|
||||||
self.current_tile
|
.draw_cached_tile_surface(current_tile, rect, self.background_color);
|
||||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
|
||||||
rect,
|
|
||||||
self.background_color,
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1674,6 +1672,12 @@ impl RenderState {
|
|||||||
self.cache_cleared_this_render = false;
|
self.cache_cleared_this_render = false;
|
||||||
self.reset_canvas();
|
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
|
// During an interactive shape transform (drag/resize/rotate) the
|
||||||
// Target is repainted tile-by-tile. If only a subset of the
|
// Target is repainted tile-by-tile. If only a subset of the
|
||||||
// invalidated tiles finishes in this rAF the remaining area
|
// invalidated tiles finishes in this rAF the remaining area
|
||||||
@ -1767,6 +1771,37 @@ impl RenderState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compute_document_bounds(
|
||||||
|
&mut self,
|
||||||
|
base_object: Option<&Uuid>,
|
||||||
|
tree: ShapesPoolRef,
|
||||||
|
) -> Option<skia::Rect> {
|
||||||
|
let ids: Vec<Uuid> = if let Some(id) = base_object {
|
||||||
|
vec![*id]
|
||||||
|
} else {
|
||||||
|
let root = tree.get(&Uuid::nil())?;
|
||||||
|
root.children_ids(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut acc: Option<skia::Rect> = 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(
|
pub fn process_animation_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
base_object: Option<&Uuid>,
|
base_object: Option<&Uuid>,
|
||||||
@ -1780,16 +1815,20 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In a pure viewport interaction (pan/zoom), render_from_cache
|
// In a pure viewport interaction (pan/zoom), render_from_cache
|
||||||
// owns the Target surface — skip flush so we don't present
|
// owns the Target surface — don't flush Target so we don't
|
||||||
// stale tile positions. The rAF still populates the Cache
|
// present stale tile positions. We still drain the GPU command
|
||||||
// surface and tile HashMap so render_from_cache progressively
|
// queue with a non-Target `flush_and_submit` so the backlog
|
||||||
// shows more complete content.
|
// 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
|
// During interactive shape transforms (drag/resize/rotate) we
|
||||||
// still need to flush every rAF so the user sees the updated
|
// still need to flush every rAF so the user sees the updated
|
||||||
// shape position — render_from_cache is not in the loop here.
|
// shape position — render_from_cache is not in the loop here.
|
||||||
if !self.options.is_viewport_interaction() {
|
if !self.options.is_viewport_interaction() {
|
||||||
self.flush_and_submit();
|
self.flush_and_submit();
|
||||||
|
} else {
|
||||||
|
self.gpu_state.context.flush_and_submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.render_in_progress {
|
if self.render_in_progress {
|
||||||
@ -2927,11 +2966,6 @@ impl RenderState {
|
|||||||
s.canvas().draw_rect(aligned_rect, &paint);
|
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) {
|
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.
|
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
|
||||||
|
|||||||
@ -62,7 +62,9 @@ fn draw_image_fill(
|
|||||||
if let Some(path) = shape_type.path() {
|
if let Some(path) = shape_type.path() {
|
||||||
if let Some(path_transform) = path_transform {
|
if let Some(path_transform) = path_transform {
|
||||||
canvas.clip_path(
|
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,
|
skia::ClipOp::Intersect,
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use crate::math::{Matrix, Point, Rect};
|
use crate::math::{Matrix, Point, Rect};
|
||||||
|
|
||||||
use crate::shapes::{
|
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};
|
use skia_safe::{self as skia, ImageFilter, RRect};
|
||||||
|
|
||||||
@ -210,6 +211,7 @@ fn draw_stroke_on_path(
|
|||||||
path_transform: Option<&Matrix>,
|
path_transform: Option<&Matrix>,
|
||||||
shadow: Option<&ImageFilter>,
|
shadow: Option<&ImageFilter>,
|
||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let is_open = path.is_open();
|
let is_open = path.is_open();
|
||||||
@ -229,7 +231,7 @@ fn draw_stroke_on_path(
|
|||||||
if let Some(pt) = path_transform {
|
if let Some(pt) = path_transform {
|
||||||
canvas.concat(pt);
|
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) {
|
match stroke.render_kind(is_open) {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
@ -510,7 +512,7 @@ fn draw_image_stroke_in_container(
|
|||||||
if let Some(p) = shape_type.path() {
|
if let Some(p) = shape_type.path() {
|
||||||
canvas.save();
|
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()))?,
|
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
|
||||||
);
|
);
|
||||||
let stroke_kind = stroke.render_kind(p.is_open());
|
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.
|
// 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 let Type::Path(p) = &shape.shape_type {
|
||||||
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
|
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()))?,
|
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
|
||||||
);
|
);
|
||||||
let mut clear_paint = skia::Paint::default();
|
let mut clear_paint = skia::Paint::default();
|
||||||
@ -846,6 +848,7 @@ fn render_merged(
|
|||||||
path_transform.as_ref(),
|
path_transform.as_ref(),
|
||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
|
svg_attrs,
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1016,6 +1019,7 @@ fn render_single_internal(
|
|||||||
path_transform.as_ref(),
|
path_transform.as_ref(),
|
||||||
shadow,
|
shadow,
|
||||||
blur.as_ref(),
|
blur.as_ref(),
|
||||||
|
svg_attrs,
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,10 +78,16 @@ pub struct Surfaces {
|
|||||||
/// When the atlas would exceed `max_atlas_texture_size`, this value is
|
/// When the atlas would exceed `max_atlas_texture_size`, this value is
|
||||||
/// reduced so the atlas stays within the fixed texture cap.
|
/// reduced so the atlas stays within the fixed texture cap.
|
||||||
atlas_scale: f32,
|
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<skia::Rect>,
|
||||||
/// Max width/height in pixels for the atlas surface (typically browser
|
/// Max width/height in pixels for the atlas surface (typically browser
|
||||||
/// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation.
|
/// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation.
|
||||||
max_atlas_texture_size: i32,
|
max_atlas_texture_size: i32,
|
||||||
sampling_options: skia::SamplingOptions,
|
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<Tile, skia::Rect>,
|
||||||
pub margins: skia::ISize,
|
pub margins: skia::ISize,
|
||||||
// Tracks which surfaces have content (dirty flag bitmask)
|
// Tracks which surfaces have content (dirty flag bitmask)
|
||||||
dirty_surfaces: u32,
|
dirty_surfaces: u32,
|
||||||
@ -147,8 +153,10 @@ impl Surfaces {
|
|||||||
atlas_origin: skia::Point::new(0.0, 0.0),
|
atlas_origin: skia::Point::new(0.0, 0.0),
|
||||||
atlas_size: skia::ISize::new(0, 0),
|
atlas_size: skia::ISize::new(0, 0),
|
||||||
atlas_scale: 1.0,
|
atlas_scale: 1.0,
|
||||||
|
atlas_doc_bounds: None,
|
||||||
max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE,
|
max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE,
|
||||||
sampling_options,
|
sampling_options,
|
||||||
|
atlas_tile_doc_rects: HashMap::default(),
|
||||||
margins,
|
margins,
|
||||||
dirty_surfaces: 0,
|
dirty_surfaces: 0,
|
||||||
extra_tile_dims,
|
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);
|
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<skia::Rect>) {
|
||||||
|
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(
|
fn ensure_atlas_contains(
|
||||||
&mut self,
|
&mut self,
|
||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
@ -271,21 +301,51 @@ impl Surfaces {
|
|||||||
&mut self,
|
&mut self,
|
||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
tile_image: &skia::Image,
|
tile_image: &skia::Image,
|
||||||
doc_rect: skia::Rect,
|
tile_doc_rect: skia::Rect,
|
||||||
) -> Result<()> {
|
) -> 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.
|
// Destination is document-space rect mapped into atlas pixel coords.
|
||||||
let dst = skia::Rect::from_xywh(
|
let dst = skia::Rect::from_xywh(
|
||||||
(doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
(clipped_doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
||||||
(doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
(clipped_doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
||||||
doc_rect.width() * self.atlas_scale,
|
clipped_doc_rect.width() * self.atlas_scale,
|
||||||
doc_rect.height() * self.atlas_scale,
|
clipped_doc_rect.height() * self.atlas_scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.atlas
|
// Compute source rect in tile_image pixel coordinates.
|
||||||
.canvas()
|
let img_w = tile_image.width() as f32;
|
||||||
.draw_image_rect(tile_image, None, dst, &skia::Paint::default());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,6 +354,7 @@ impl Surfaces {
|
|||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
doc_rect: skia::Rect,
|
doc_rect: skia::Rect,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let doc_rect = self.clamp_doc_rect_to_bounds(doc_rect);
|
||||||
if doc_rect.is_empty() {
|
if doc_rect.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -316,6 +377,18 @@ impl Surfaces {
|
|||||||
Ok(())
|
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) {
|
pub fn clear_tiles(&mut self) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
}
|
}
|
||||||
@ -817,6 +890,7 @@ impl Surfaces {
|
|||||||
// Incrementally update persistent 1:1 atlas in document space.
|
// Incrementally update persistent 1:1 atlas in document space.
|
||||||
// `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%).
|
// `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);
|
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);
|
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -825,11 +899,14 @@ impl Surfaces {
|
|||||||
self.tiles.has(tile)
|
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
|
// Mark tile as invalid
|
||||||
// Old content stays visible until new tile overwrites it atomically,
|
// Old content stays visible until new tile overwrites it atomically,
|
||||||
// preventing flickering during tile re-renders.
|
// preventing flickering during tile re-renders.
|
||||||
self.tiles.remove(tile);
|
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) {
|
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.
|
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
|
||||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
|
self.atlas_tile_doc_rects.clear();
|
||||||
self.cache.canvas().clear(color);
|
self.cache.canvas().clear(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -923,6 +1001,7 @@ impl Surfaces {
|
|||||||
/// content while new tiles are being rendered.
|
/// content while new tiles are being rendered.
|
||||||
pub fn invalidate_tile_cache(&mut self) {
|
pub fn invalidate_tile_cache(&mut self) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
|
self.atlas_tile_doc_rects.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gc(&mut self) {
|
pub fn gc(&mut self) {
|
||||||
|
|||||||
@ -1349,15 +1349,10 @@ impl Shape {
|
|||||||
|
|
||||||
pub fn get_skia_path(&self) -> Option<skia::Path> {
|
pub fn get_skia_path(&self) -> Option<skia::Path> {
|
||||||
if let Some(path) = self.shape_type.path() {
|
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() {
|
if let Some(path_transform) = self.to_path_transform() {
|
||||||
skia_path = skia_path.make_transform(&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)
|
Some(skia_path)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use skia_safe::{self as skia, Matrix};
|
use skia_safe::{self as skia, Matrix};
|
||||||
|
|
||||||
use crate::math;
|
use crate::math;
|
||||||
|
use crate::shapes::svg_attrs::{FillRule, SvgAttrs};
|
||||||
|
|
||||||
mod subpaths;
|
mod subpaths;
|
||||||
|
|
||||||
@ -217,8 +218,14 @@ impl Path {
|
|||||||
Path::new(segments)
|
Path::new(segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_skia_path(&self) -> skia::Path {
|
pub fn to_skia_path(&self, svg_attrs: Option<&SvgAttrs>) -> skia::Path {
|
||||||
self.skia_path.snapshot()
|
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 {
|
pub fn contains(&self, p: skia::Point) -> bool {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ pub fn stroke_to_path(
|
|||||||
selrect: &Rect,
|
selrect: &Rect,
|
||||||
svg_attrs: Option<&SvgAttrs>,
|
svg_attrs: Option<&SvgAttrs>,
|
||||||
) -> Option<Path> {
|
) -> Option<Path> {
|
||||||
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 {
|
let transformed_shape_path = if let Some(pt) = path_transform {
|
||||||
skia_shape_path.make_transform(pt)
|
skia_shape_path.make_transform(pt)
|
||||||
|
|||||||
@ -1,11 +1,34 @@
|
|||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
|
use crate::shapes::Fill;
|
||||||
|
use crate::state::State;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
use crate::with_state_mut;
|
use crate::with_state_mut;
|
||||||
use crate::STATE;
|
use crate::STATE;
|
||||||
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
|
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
|
||||||
use macros::wasm_error;
|
use macros::wasm_error;
|
||||||
|
|
||||||
|
fn touch_shapes_with_image(state: &mut State, image_id: Uuid) {
|
||||||
|
let ids: Vec<Uuid> = 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 FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0;
|
||||||
const IMAGE_IDS_SIZE: usize = 32;
|
const IMAGE_IDS_SIZE: usize = 32;
|
||||||
const IMAGE_HEADER_SIZE: usize = 36; // 32 bytes for IDs + 4 bytes for is_thumbnail flag
|
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);
|
eprintln!("{}", msg);
|
||||||
}
|
}
|
||||||
state.touch_shape(ids.shape_id);
|
touch_shapes_with_image(state, ids.image_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes()?;
|
mem::free_bytes()?;
|
||||||
@ -167,7 +190,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> {
|
|||||||
// FIXME: Review if we should return a RecoverableError
|
// FIXME: Review if we should return a RecoverableError
|
||||||
eprintln!("store_image_from_texture error: {}", msg);
|
eprintln!("store_image_from_texture error: {}", msg);
|
||||||
}
|
}
|
||||||
state.touch_shape(ids.shape_id);
|
touch_shapes_with_image(state, ids.image_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes()?;
|
mem::free_bytes()?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user