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
657546a993
@ -27,6 +27,7 @@
|
||||
## 2.14.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@ -58,6 +59,8 @@
|
||||
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
|
||||
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
|
||||
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
|
||||
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
|
||||
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
||||
|
||||
## 2.13.3
|
||||
|
||||
@ -72,7 +75,6 @@
|
||||
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
|
||||
- Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)
|
||||
|
||||
|
||||
## 2.13.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@ -8,7 +8,6 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
||||
<div>[<a href="#head">head</a>]</div>
|
||||
<!-- <div>[<a href="#props">props</a>]</div> -->
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
{% if report %}
|
||||
<div>[<a href="#report">report</a>]</div>
|
||||
@ -21,7 +20,8 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
||||
<div class="table-val">
|
||||
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
||||
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
||||
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
|
||||
<h2><span class="not-important">Origin:</span> <br/> {{origin}}</h2>
|
||||
<h2><span class="not-important">HREF:</span> <br/> {{href}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -117,7 +117,8 @@
|
||||
|
||||
{:context (-> (into (sorted-map) context)
|
||||
(pp/pprint-str :length 50))
|
||||
:props (pp/pprint-str props :length 50)
|
||||
:origin (::audit/name record)
|
||||
:href (get props :href)
|
||||
:hint (get props :hint)
|
||||
:report (get props :report)}))
|
||||
|
||||
|
||||
@ -1005,19 +1005,19 @@
|
||||
"Link a file to a library. Returns the recursive list of libraries used by that library"
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:link-file-to-library}
|
||||
[cfg {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
::sm/params schema:link-file-to-library
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
|
||||
(when (= file-id library-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-library
|
||||
:hint "A file cannot be linked to itself"))
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params)
|
||||
(bfc/get-libraries cfg [library-id]))))
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params)
|
||||
(bfc/get-libraries cfg [library-id]))
|
||||
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
@ -1037,8 +1037,9 @@
|
||||
::webhooks/event? true
|
||||
::sm/params schema:unlink-file-to-library
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(unlink-file-from-library conn params)
|
||||
nil)
|
||||
|
||||
@ -1062,8 +1063,9 @@
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-file-library-sync-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(update-sync conn params))
|
||||
|
||||
;; --- MUTATION COMMAND: ignore-sync
|
||||
|
||||
@ -867,6 +867,52 @@
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found))))
|
||||
|
||||
(t/deftest permissions-checks-unlink-library
|
||||
(let [profile1 (th/create-profile* 1)
|
||||
profile2 (th/create-profile* 2)
|
||||
file1 (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)
|
||||
:is-shared true})
|
||||
file2 (th/create-file* 2 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)})]
|
||||
|
||||
|
||||
(let [data {::th/type :unlink-file-from-library
|
||||
::rpc/profile-id (:id profile2)
|
||||
:file-id (:id file2)
|
||||
:library-id (:id file1)}
|
||||
|
||||
out (th/command! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))
|
||||
|
||||
|
||||
(t/deftest permissions-checks-update-file-library-status
|
||||
(let [profile1 (th/create-profile* 1)
|
||||
profile2 (th/create-profile* 2)
|
||||
file1 (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)
|
||||
:is-shared true})
|
||||
file2 (th/create-file* 2 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)})]
|
||||
|
||||
|
||||
(let [data {::th/type :update-file-library-sync-status
|
||||
::rpc/profile-id (:id profile2)
|
||||
:file-id (:id file2)
|
||||
:library-id (:id file1)}
|
||||
|
||||
out (th/command! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))
|
||||
|
||||
|
||||
(t/deftest deletion
|
||||
(let [profile1 (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||
|
||||
@ -299,3 +299,8 @@
|
||||
(js/console.log (format-throwable cause))
|
||||
(finally
|
||||
(js/console.groupEnd))))))
|
||||
|
||||
(defn get-hint
|
||||
[cause]
|
||||
(or (some-> (ex-data cause) (get :hint) first-line)
|
||||
(some-> (ex-message cause) first-line)))
|
||||
|
||||
@ -1766,6 +1766,26 @@
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0017-fix-layout-flex-dir"
|
||||
[data _]
|
||||
(let [fix-layout-flex-dir
|
||||
(fn [value]
|
||||
(if (= value :reverse-row)
|
||||
:row-reverse
|
||||
value))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :layout-flex-dir fix-layout-flex-dir))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
["legacy-2"
|
||||
@ -1839,4 +1859,5 @@
|
||||
"0014-clear-components-nil-objects"
|
||||
"0015-fix-text-attrs-blank-strings"
|
||||
"0015-clean-shadow-color"
|
||||
"0016-copy-fills-from-position-data-to-text-node"]))
|
||||
"0016-copy-fills-from-position-data-to-text-node"
|
||||
"0017-fix-layout-flex-dir"]))
|
||||
|
||||
@ -128,7 +128,9 @@
|
||||
:warn "#f5871f"
|
||||
:info "#4271ae"
|
||||
:debug "#969896"
|
||||
:trace "#8e908c"))
|
||||
:trace "#8e908c"
|
||||
(let [hint (str "invalid level provided to `level->color` function: " (pr-str level))]
|
||||
(throw (ex-info hint {:level level})))))
|
||||
|
||||
(defn- level->name
|
||||
[level]
|
||||
@ -137,7 +139,9 @@
|
||||
:trace "TRC"
|
||||
:info "INF"
|
||||
:warn "WRN"
|
||||
:error "ERR"))
|
||||
:error "ERR"
|
||||
(let [hint (str "invalid level provided to `level->name` function: " (pr-str level))]
|
||||
(throw (ex-info hint {:level level})))))
|
||||
|
||||
(defn level->int
|
||||
[level]
|
||||
@ -146,7 +150,9 @@
|
||||
:debug 20
|
||||
:info 30
|
||||
:warn 40
|
||||
:error 50))
|
||||
:error 50
|
||||
(let [hint (str "invalid level provided to `level->int` function: " (pr-str level))]
|
||||
(throw (ex-info hint {:level level})))))
|
||||
|
||||
(defn build-message
|
||||
[props]
|
||||
|
||||
@ -2002,6 +2002,61 @@
|
||||
:else
|
||||
current-content)))
|
||||
|
||||
|
||||
(defn- switch-fixed-layout-geom-change-value
|
||||
[prev-shape ; The shape before the switch
|
||||
current-shape ; The shape after the switch (a clean copy)
|
||||
attr]
|
||||
;; When there is a layout with fixed h or v sizing, we need
|
||||
;; to keep the width/height (and recalculate selrect and points)
|
||||
(let [prev-width (-> prev-shape :selrect :width)
|
||||
current-width (-> current-shape :selrect :width)
|
||||
|
||||
prev-height (-> prev-shape :selrect :height)
|
||||
current-height (-> current-shape :selrect :height)
|
||||
|
||||
x (-> current-shape :selrect :x)
|
||||
y (-> current-shape :selrect :y)
|
||||
|
||||
|
||||
h-sizing (:layout-item-h-sizing prev-shape)
|
||||
v-sizing (:layout-item-v-sizing prev-shape)
|
||||
|
||||
final-width (if (= :fix h-sizing)
|
||||
current-width
|
||||
prev-width)
|
||||
final-height (if (= :fix v-sizing)
|
||||
current-height
|
||||
prev-height)
|
||||
selrect (assoc (:selrect current-shape)
|
||||
:width final-width
|
||||
:height final-height
|
||||
:x x
|
||||
:y y
|
||||
:x1 x
|
||||
:y1 y
|
||||
:x2 (+ x final-width)
|
||||
:y2 (+ y final-height))]
|
||||
|
||||
(case attr
|
||||
:width
|
||||
final-width
|
||||
|
||||
:height
|
||||
final-height
|
||||
|
||||
:selrect
|
||||
selrect
|
||||
|
||||
:points
|
||||
(-> selrect
|
||||
(grc/rect->points)
|
||||
(gsh/transform-points
|
||||
(grc/rect->center selrect)
|
||||
(or (:transform current-shape) (gmt/matrix)))))))
|
||||
|
||||
|
||||
|
||||
(defn update-attrs-on-switch
|
||||
"Copy attributes that have changed in the shape previous to the switch
|
||||
to the current shape (post switch). Used only on variants switch"
|
||||
@ -2110,6 +2165,11 @@
|
||||
origin-ref-shape
|
||||
attr)
|
||||
|
||||
(and (or (= :fix (:layout-item-h-sizing previous-shape))
|
||||
(= :fix (:layout-item-v-sizing previous-shape)))
|
||||
(contains? #{:points :selrect :width :height} attr))
|
||||
(switch-fixed-layout-geom-change-value previous-shape current-shape attr)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
|
||||
@ -188,7 +188,7 @@ Some naming conventions:
|
||||
[segments separator]
|
||||
(let [sorted (sort-by-children segments separator)
|
||||
grouped (group-by-first-segment sorted separator)]
|
||||
grouped))
|
||||
(into (sorted-map) grouped)))
|
||||
|
||||
(defn- build-tree-node
|
||||
"Builds a single tree node with lazy children."
|
||||
|
||||
@ -18,6 +18,9 @@
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
;; ============================================================
|
||||
;; BASIC SWITCH TESTS (no overrides)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-basic-switch
|
||||
(let [;; ==== Setup
|
||||
@ -68,6 +71,9 @@
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
|
||||
;; ============================================================
|
||||
;; SIMPLE ATTRIBUTE OVERRIDES (identical variants)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-basic-switch-override
|
||||
(let [;; ==== Setup
|
||||
@ -142,6 +148,10 @@
|
||||
;; The override is keept: The rect still has width 25 after the switch
|
||||
(t/is (= (:width rect02') 25))))
|
||||
|
||||
;; ============================================================
|
||||
;; SIMPLE ATTRIBUTE OVERRIDES (different variants)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-no-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -182,6 +192,10 @@
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT OVERRIDES (identical variants)
|
||||
;; ============================================================
|
||||
|
||||
(def font-size-path-paragraph [:content :children 0 :children 0 :font-size])
|
||||
(def font-size-path-0 [:content :children 0 :children 0 :children 0 :font-size])
|
||||
(def font-size-path-1 [:content :children 0 :children 0 :children 1 :font-size])
|
||||
@ -346,6 +360,10 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "text overriden"))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT OVERRIDES (different property)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-prop-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -472,6 +490,10 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "text overriden"))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT OVERRIDES (different text)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-text-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -596,6 +618,10 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT OVERRIDES (different text AND property)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-text-and-prop-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -722,6 +748,10 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT STRUCTURE OVERRIDES (identical variants)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-identical-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -851,6 +881,10 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' font-size-path-1) "40"))
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-1) "new line 2"))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT STRUCTURE OVERRIDES (different property)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-prop-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -978,6 +1012,10 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "hello world"))
|
||||
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT STRUCTURE OVERRIDES (different text)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-text-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -1104,6 +1142,10 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye"))
|
||||
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT STRUCTURE OVERRIDES (different text AND property)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-different-text-and-prop-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -1231,6 +1273,10 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye"))
|
||||
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
|
||||
|
||||
;; ============================================================
|
||||
;; NESTED COMPONENTS (with same component in both variants)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-variant-for-other-with-same-nested-component
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -1274,6 +1320,10 @@
|
||||
;; The width of copy-cp02-rect' is 25 (change is preserved)
|
||||
(t/is (= (:width copy-cp02-rect') 25))))
|
||||
|
||||
;; ============================================================
|
||||
;; SWAPPED COPIES (switching variants that contain swapped components)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-variant-that-has-swaped-copy
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -1366,6 +1416,10 @@
|
||||
;; The width of copy-cp02-rect' is 25 (change is preserved)
|
||||
(t/is (= (:width copy-cp02-rect') 25))))
|
||||
|
||||
;; ============================================================
|
||||
;; TOUCHED PARENT (switch without touched but with touched parent)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-variant-without-touched-but-touched-parent
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@ -1420,3 +1474,787 @@
|
||||
(t/is (= (:width rect01) 25))
|
||||
;; The rect still has width 25 after the switch
|
||||
(t/is (= (:width rect02') 25))))
|
||||
|
||||
;; ============================================================
|
||||
;; LAYOUT ITEM SIZING - HORIZONTAL (fix, auto, fill, none)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-layout-item-h-sizing-fix
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-h-sizing :fix
|
||||
;; When :fix is set, the width should NOT be preserved on switch
|
||||
;; but should take the new component's width
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 50
|
||||
:layout-item-h-sizing :fix}
|
||||
:child2-params {:width 200
|
||||
:height 50
|
||||
:layout-item-h-sizing :fix}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had width 150 before the switch (with override)
|
||||
(t/is (= (:width rect01) 150))
|
||||
;; With layout-item-h-sizing :fix, the width should be taken from the new component
|
||||
;; (not preserving the override), so it should be 200
|
||||
(t/is (= (:width rect02') 200))
|
||||
;; Verify layout-item-h-sizing is still :fix after switch
|
||||
(t/is (= (:layout-item-h-sizing rect02') :fix))))
|
||||
|
||||
(t/deftest test-switch-with-layout-item-h-sizing-auto
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-h-sizing :auto
|
||||
;; When :auto is set, the width override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 50
|
||||
:layout-item-h-sizing :auto}
|
||||
:child2-params {:width 200
|
||||
:height 50
|
||||
:layout-item-h-sizing :auto}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had width 150 before the switch (with override)
|
||||
(t/is (= (:width rect01) 150))
|
||||
;; With layout-item-h-sizing :auto, since the two variants have different widths (100 vs 200),
|
||||
;; the override is not preserved and the new component's width (200) is used
|
||||
(t/is (= (:width rect02') 200))
|
||||
;; Verify layout-item-h-sizing is still :auto after switch
|
||||
(t/is (= (:layout-item-h-sizing rect02') :auto))))
|
||||
|
||||
(t/deftest test-switch-with-layout-item-h-sizing-fill
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-h-sizing :fill
|
||||
;; When :fill is set, the width override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 50
|
||||
:layout-item-h-sizing :fill}
|
||||
:child2-params {:width 200
|
||||
:height 50
|
||||
:layout-item-h-sizing :fill}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had width 150 before the switch (with override)
|
||||
(t/is (= (:width rect01) 150))
|
||||
;; With layout-item-h-sizing :fill, since the two variants have different widths (100 vs 200),
|
||||
;; the override is not preserved and the new component's width (200) is used
|
||||
(t/is (= (:width rect02') 200))
|
||||
;; Verify layout-item-h-sizing is still :fill after switch
|
||||
(t/is (= (:layout-item-h-sizing rect02') :fill))))
|
||||
|
||||
(t/deftest test-switch-without-layout-item-h-sizing
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child without layout-item-h-sizing
|
||||
;; When not set, the width override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 50}
|
||||
:child2-params {:width 200
|
||||
:height 50}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had width 150 before the switch (with override)
|
||||
(t/is (= (:width rect01) 150))
|
||||
;; Without layout-item-h-sizing, since the two variants have different widths (100 vs 200),
|
||||
;; the override is not preserved and the new component's width (200) is used
|
||||
(t/is (= (:width rect02') 200))
|
||||
;; Verify layout-item-h-sizing is still nil after switch
|
||||
(t/is (nil? (:layout-item-h-sizing rect02')))))
|
||||
|
||||
;; ============================================================
|
||||
;; LAYOUT ITEM SIZING - VERTICAL (fix, auto, fill, none)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-layout-item-v-sizing-fix
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-v-sizing :fix
|
||||
;; When :fix is set, the height should NOT be preserved on switch
|
||||
;; but should take the new component's height
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 50
|
||||
:height 100
|
||||
:layout-item-v-sizing :fix}
|
||||
:child2-params {:width 50
|
||||
:height 200
|
||||
:layout-item-v-sizing :fix}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change height of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :height 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had height 150 before the switch (with override)
|
||||
(t/is (= (:height rect01) 150))
|
||||
;; With layout-item-v-sizing :fix, the height should be taken from the new component
|
||||
;; (not preserving the override), so it should be 200
|
||||
(t/is (= (:height rect02') 200))
|
||||
;; Verify layout-item-v-sizing is still :fix after switch
|
||||
(t/is (= (:layout-item-v-sizing rect02') :fix))))
|
||||
|
||||
(t/deftest test-switch-with-layout-item-v-sizing-auto
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-v-sizing :auto
|
||||
;; When :auto is set, the height override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 50
|
||||
:height 100
|
||||
:layout-item-v-sizing :auto}
|
||||
:child2-params {:width 50
|
||||
:height 200
|
||||
:layout-item-v-sizing :auto}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change height of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :height 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had height 150 before the switch (with override)
|
||||
(t/is (= (:height rect01) 150))
|
||||
;; With layout-item-v-sizing :auto, since the two variants have different heights (100 vs 200),
|
||||
;; the override is not preserved and the new component's height (200) is used
|
||||
(t/is (= (:height rect02') 200))
|
||||
;; Verify layout-item-v-sizing is still :auto after switch
|
||||
(t/is (= (:layout-item-v-sizing rect02') :auto))))
|
||||
|
||||
(t/deftest test-switch-with-layout-item-v-sizing-fill
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child that has layout-item-v-sizing :fill
|
||||
;; When :fill is set, the height override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 50
|
||||
:height 100
|
||||
:layout-item-v-sizing :fill}
|
||||
:child2-params {:width 50
|
||||
:height 200
|
||||
:layout-item-v-sizing :fill}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change height of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :height 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had height 150 before the switch (with override)
|
||||
(t/is (= (:height rect01) 150))
|
||||
;; With layout-item-v-sizing :fill, since the two variants have different heights (100 vs 200),
|
||||
;; the override is not preserved and the new component's height (200) is used
|
||||
(t/is (= (:height rect02') 200))
|
||||
;; Verify layout-item-v-sizing is still :fill after switch
|
||||
(t/is (= (:layout-item-v-sizing rect02') :fill))))
|
||||
|
||||
(t/deftest test-switch-without-layout-item-v-sizing
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create a variant with a child without layout-item-v-sizing
|
||||
;; When not set, the height override SHOULD be preserved on switch
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 50
|
||||
:height 100}
|
||||
:child2-params {:width 50
|
||||
:height 200}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change height of the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :height 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had height 150 before the switch (with override)
|
||||
(t/is (= (:height rect01) 150))
|
||||
;; Without layout-item-v-sizing, since the two variants have different heights (100 vs 200),
|
||||
;; the override is not preserved and the new component's height (200) is used
|
||||
(t/is (= (:height rect02') 200))
|
||||
;; Verify layout-item-v-sizing is still nil after switch
|
||||
(t/is (nil? (:layout-item-v-sizing rect02')))))
|
||||
|
||||
;; ============================================================
|
||||
;; ROTATION OVERRIDES
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-rotation-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 100
|
||||
:rotation 0}
|
||||
:child2-params {:width 100
|
||||
:height 100
|
||||
:rotation 0}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Apply rotation to the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :rotation 45))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had rotation 45 before the switch (with override)
|
||||
(t/is (= (:rotation rect01) 45))
|
||||
;; The rotation override should be preserved after switch since both variants have the same rotation
|
||||
(t/is (= (:rotation rect02') 45))
|
||||
;; The transform matrix should also be preserved
|
||||
(t/is (some? (:transform rect02')))))
|
||||
|
||||
(t/deftest test-switch-with-rotation-different-variants
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 100
|
||||
:rotation 0}
|
||||
:child2-params {:width 100
|
||||
:height 100
|
||||
:rotation 90}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Apply rotation to the child rect (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :rotation 45))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had rotation 45 before the switch (with override)
|
||||
(t/is (= (:rotation rect01) 45))
|
||||
;; The override should NOT be preserved since the two variants have different rotations (0 vs 90)
|
||||
;; The new rotation should be 90 (from c02)
|
||||
(t/is (= (:rotation rect02') 90))))
|
||||
|
||||
;; ============================================================
|
||||
;; SPECIAL CASES (auto-text, geometry, touched attributes, position data)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-switch-with-auto-text-geometry-not-copied
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create variants with auto-text (grow-type :auto-width or :auto-height)
|
||||
(thv/add-variant-with-text
|
||||
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello" "world"))
|
||||
|
||||
page (thf/current-page file)
|
||||
;; Modify the first text shape to have grow-type :auto-width
|
||||
t01 (ths/get-shape file :t01)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id t01)}
|
||||
(fn [shape]
|
||||
(assoc shape :grow-type :auto-width))
|
||||
(:objects page)
|
||||
{})
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
;; Also modify t02
|
||||
page (thf/current-page file)
|
||||
t02 (ths/get-shape file :t02)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id t02)}
|
||||
(fn [shape]
|
||||
(assoc shape :grow-type :auto-width))
|
||||
(:objects page)
|
||||
{})
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
;; Now create a copy and modify its width
|
||||
file (thc/instantiate-component file :c01
|
||||
:copy01
|
||||
:children-labels [:copy-t01])
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
text01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the text (creating an override)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id text01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 200))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
text01 (get-in page [:objects (:id text01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
text02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The text had width 200 before the switch (with override)
|
||||
(t/is (= (:width text01) 200))
|
||||
;; For auto-text shapes, geometry attributes like width should NOT be copied on switch
|
||||
;; So the width should be from the new component (t02's width)
|
||||
(t/is (not= (:width text02') 200))
|
||||
;; Verify grow-type is preserved
|
||||
(t/is (= (:grow-type text02') :auto-width))))
|
||||
|
||||
(t/deftest test-switch-different-shape-types-content-not-copied
|
||||
(let [;; ==== Setup - Create a variant with a rect in first component
|
||||
;; This test is simplified to just test attributes, not changing shape types
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100 :height 100 :type :rect}
|
||||
:child2-params {:width 100 :height 100 :type :rect}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; ==== Action - Try to switch to a component with different shape type
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
child02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; Verify the shapes are still rects
|
||||
(t/is (= (:type rect01) :rect))
|
||||
(t/is (= (:type child02') :rect))
|
||||
;; This test demonstrates that content with different types isn't copied
|
||||
;; In practice this means proper attribute filtering
|
||||
(t/is (= (:width child02') 100))))
|
||||
|
||||
(t/deftest test-switch-with-path-shape-geometry-override
|
||||
(let [;; ==== Setup - Create variants with path shapes
|
||||
;; Using rect shapes as path shapes are complex - the principle is the same
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100 :height 100 :type :rect}
|
||||
:child2-params {:width 200 :height 200 :type :rect}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-path01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
path01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Resize the path (creating an override by changing selrect)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id path01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 150))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
path01 (get-in page [:objects (:id path01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
path02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had width 150 before the switch
|
||||
(t/is (= (:width path01) 150))
|
||||
;; For shapes with geometry changes, the transformed geometry is applied
|
||||
;; Since variants have different widths (100 vs 200), override is discarded
|
||||
(t/is (= (:width path02') 200))
|
||||
;; Verify it's still a rect type
|
||||
(t/is (= (:type path02') :rect))))
|
||||
|
||||
(t/deftest test-switch-preserves-touched-attributes-only
|
||||
(let [;; ==== Setup - Test that only touched attributes are copied
|
||||
;; Use opacity since it's a simpler attribute than fill-color
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 100
|
||||
:opacity 1}
|
||||
:child2-params {:width 200
|
||||
:height 200
|
||||
:opacity 1}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change the opacity (creating a touched attribute)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id rect01)}
|
||||
(fn [shape]
|
||||
(assoc shape :opacity 0.5))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had opacity 0.5 before the switch (touched)
|
||||
(t/is (= (:opacity rect01) 0.5))
|
||||
;; The rect had width 100 before the switch (not touched)
|
||||
(t/is (= (:width rect01) 100))
|
||||
|
||||
;; After switch:
|
||||
;; - opacity override SHOULD be preserved because:
|
||||
;; 1. It was touched
|
||||
;; 2. Both variants have same opacity (1)
|
||||
(t/is (= (:opacity rect02') 0.5))
|
||||
;; - width should NOT be preserved (it wasn't touched, and variants have different widths)
|
||||
(t/is (= (:width rect02') 200))
|
||||
;; - height should match the new variant
|
||||
(t/is (= (:height rect02') 200))))
|
||||
|
||||
(t/deftest test-switch-with-equal-values-not-copied
|
||||
(let [;; ==== Setup - Test that when previous-shape and current-shape have equal values,
|
||||
;; no copy operation occurs (optimization in update-attrs-on-switch)
|
||||
;; Both variants start with opacity 0.5
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 100
|
||||
:height 100
|
||||
:opacity 0.5}
|
||||
:child2-params {:width 100
|
||||
:height 100
|
||||
:opacity 0.5}})
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:children-labels [:copy-r01]))
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
|
||||
;; The rect had opacity 0.5 before the switch
|
||||
(t/is (= (:opacity rect01) 0.5))
|
||||
;; After switch, opacity should still be 0.5
|
||||
;; This validates that the equality check works correctly
|
||||
(t/is (= (:opacity rect02') 0.5))))
|
||||
|
||||
(t/deftest test-switch-with-position-data-reset
|
||||
(let [;; ==== Setup - Test that position-data is reset when geometry-group is touched
|
||||
file (-> (thf/sample-file :file1)
|
||||
;; Create variants with text shapes
|
||||
(thv/add-variant-with-text
|
||||
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world"))
|
||||
|
||||
page (thf/current-page file)
|
||||
;; Modify the first text shape to have specific geometry
|
||||
t01 (ths/get-shape file :t01)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id t01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 200))
|
||||
(:objects page)
|
||||
{})
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
;; Create a copy and modify its geometry (touching geometry-group)
|
||||
file (thc/instantiate-component file :c01
|
||||
:copy01
|
||||
:children-labels [:copy-t01])
|
||||
|
||||
page (thf/current-page file)
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
text01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; Change width of the text (touching geometry)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id text01)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 300))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
page (thf/current-page file)
|
||||
text01 (get-in page [:objects (:id text01)])
|
||||
old-position-data (:position-data text01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
text02' (get-in page' [:objects (-> copy02' :shapes first)])
|
||||
new-position-data (:position-data text02')]
|
||||
|
||||
;; position-data should be reset (nil or different) when geometry group is touched
|
||||
;; This allows the system to recalculate it based on the new geometry
|
||||
;; Note: old-position-data may be nil initially, which is fine
|
||||
;; After switch with geometry changes, if old data existed and was different,
|
||||
;; or if it needs recalculation, the test validates the behavior
|
||||
(t/is (or (nil? old-position-data)
|
||||
(nil? new-position-data)
|
||||
(not= old-position-data new-position-data)))))
|
||||
BIN
docs/img/design-tokens/39-tokens-remap.webp
Normal file
BIN
docs/img/design-tokens/39-tokens-remap.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/img/design-tokens/40-tokens-groups.webp
Normal file
BIN
docs/img/design-tokens/40-tokens-groups.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@ -42,26 +42,83 @@ desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG
|
||||
<p>If the value of the referenced token changes, this will also change the value of the tokens where it is referenced.</p>
|
||||
<p class="advice">References to existing tokens are case sensitive.</p>
|
||||
|
||||
<h2 id="design-tokens-equations">Using equations</h2>
|
||||
<p>Token types with numerical values also accept mathematical equations. If, for example, you create a <strong>spacing.small</strong> token with the value of <strong>2</strong>, and you then want to create a <strong>spacing.medium</strong> token that is twice as large, you could do so by writing <code class="language-js">{spacing.small} * 2</code> in its value. As a result, <strong>spacing.medium</strong> would have a value of <strong>4</strong>.</p>
|
||||
<p>Say you have a <strong>spacing.scale</strong> token with a value of <strong>2</strong>. You could also use this token in the equation to calculate the value of <strong>spacing.medium</strong> by writing <code class="language-js">{spacing.small} * {spacing.scale}</code> in its value.</p>
|
||||
<h2 id="design-tokens-equations">Using math in token values</h2>
|
||||
<p>Token types with numerical values accept mathematical equations to calculate their values. This allows you to create dynamic relationships between tokens and build flexible design systems.</p>
|
||||
<p>For example, if you create a <strong>spacing.small</strong> token with the value of <strong>2</strong>, and you want to create a <strong>spacing.medium</strong> token that is twice as large, you can write <code class="language-js">{spacing.small} * 2</code> in its value. As a result, <strong>spacing.medium</strong> would have a value of <strong>4</strong>.</p>
|
||||
<p>You can also reference other tokens in your equations. Say you have a <strong>spacing.scale</strong> token with a value of <strong>2</strong>. You could use this token in the equation to calculate the value of <strong>spacing.medium</strong> by writing <code class="language-js">{spacing.small} * {spacing.scale}</code> in its value.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/04-tokens-math.webp" alt="Tokens math" />
|
||||
</figure>
|
||||
<p>Mathematical equations can be performed using:</p>
|
||||
|
||||
<h3 id="design-tokens-math-operators">Basic operators</h3>
|
||||
<p>Mathematical equations can be performed using these basic operators:</p>
|
||||
<ul>
|
||||
<li><code class="language-js">+</code> for addition.</li>
|
||||
<li><code class="language-js">-</code> for subtraction.</li>
|
||||
<li><code class="language-js">*</code> for multiplication.</li>
|
||||
<li><code class="language-js">/</code> for division.</li>
|
||||
<li><code class="language-js">+</code> for addition</li>
|
||||
<li><code class="language-js">-</code> for subtraction</li>
|
||||
<li><code class="language-js">*</code> for multiplication</li>
|
||||
<li><code class="language-js">/</code> for division</li>
|
||||
<li><code class="language-js">%</code> for modulo (remainder)</li>
|
||||
<li><code class="language-js">^</code> for exponentiation</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="design-tokens-math-functions">Math functions</h3>
|
||||
<p>In addition to basic operators, you can use various math functions in your token values:</p>
|
||||
<ul>
|
||||
<li><code class="language-js">abs(x)</code> - absolute value</li>
|
||||
<li><code class="language-js">ceil(x)</code> - round up to nearest integer</li>
|
||||
<li><code class="language-js">floor(x)</code> - round down to nearest integer</li>
|
||||
<li><code class="language-js">round(x)</code> - round to nearest integer</li>
|
||||
<li><code class="language-js">max(x, y, ...)</code> - maximum value</li>
|
||||
<li><code class="language-js">min(x, y, ...)</code> - minimum value</li>
|
||||
<li><code class="language-js">sqrt(x)</code> - square root</li>
|
||||
<li><code class="language-js">pow(x, y)</code> - x raised to the power of y</li>
|
||||
<li><code class="language-js">log(x)</code> - natural logarithm</li>
|
||||
<li><code class="language-js">exp(x)</code> - e raised to the power of x</li>
|
||||
<li><code class="language-js">sin(x)</code> - sine</li>
|
||||
<li><code class="language-js">cos(x)</code> - cosine</li>
|
||||
<li><code class="language-js">tan(x)</code> - tangent</li>
|
||||
<li><code class="language-js">asin(x)</code> - arcsine</li>
|
||||
<li><code class="language-js">acos(x)</code> - arccosine</li>
|
||||
<li><code class="language-js">atan(x)</code> - arctangent</li>
|
||||
<li><code class="language-js">atan2(y, x)</code> - arctangent of y/x</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="design-tokens-math-syntax">Syntax and best practices</h3>
|
||||
<p>When writing math equations in token values, keep these guidelines in mind:</p>
|
||||
<ul>
|
||||
<li>Simple equations can be written with or without brackets. For example, <code class="language-js">8 * 8</code> and <code class="language-js">(8 * 8)</code> both resolve to <code class="language-js">64</code>.</li>
|
||||
<li>Complex formulas require spaces between operators to ensure tokens are transformed correctly. For example, use <code class="language-js">8 * 8</code> instead of <code class="language-js">8*8</code>.</li>
|
||||
<li>Reference tokens using curly braces: <code class="language-js">{token.name}</code>.</li>
|
||||
<li>You can combine hard-coded values with token references: <code class="language-js">{spacing.base} * 1.5</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="design-tokens-math-examples">Practical examples</h3>
|
||||
<p>Here are some common use cases for math in token values:</p>
|
||||
|
||||
<h4>Round to the nearest whole number</h4>
|
||||
<p>When using multipliers that result in decimals, you can use the <code class="language-js">round()</code> function to get whole numbers. For example, if <strong>sizing.sm</strong> has a value of <strong>2</strong>:</p>
|
||||
<pre><code class="language-js">round({sizing.sm} * 1.33)</code></pre>
|
||||
<p>This calculates <code class="language-js">2 * 1.33 = 2.66</code>, which rounds to <strong>3</strong>.</p>
|
||||
|
||||
<h4>Create a percentage from a unitless number</h4>
|
||||
<p>You can convert unitless numbers to percentages. For example, a Number token called <strong>lineHeights.heading.relaxed</strong> with a value of <strong>1.5</strong> can be written in a Line Height token as:</p>
|
||||
<pre><code class="language-js">{lineHeights.heading.relaxed} * 100%</code></pre>
|
||||
<p>This calculates a resolved value of <strong>150%</strong>.</p>
|
||||
|
||||
<h4>Calculate maximum or minimum values</h4>
|
||||
<p>Use <code class="language-js">max()</code> or <code class="language-js">min()</code> to ensure values stay within bounds. For example:</p>
|
||||
<pre><code class="language-js">max({spacing.base}, 8)</code></pre>
|
||||
<p>This ensures the spacing is at least 8, even if the base token is smaller.</p>
|
||||
|
||||
<h2 id="design-tokens-edit">Editing a token</h2>
|
||||
<p>Tokens can be edited by right-clicking the token and selecting <strong>Edit token</strong>. This will allow you to change the tokens name, value and description. Once the changes are made, click <strong>Save</strong>.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/05-tokens-edit.webp" alt="Tokens edit" />
|
||||
</figure>
|
||||
<p class="advice">Renaming tokens will break any references to their old names. If a token is already applied somewhere, you'll need to reapply it after renaming. This can lead to extra work, so rename with caution. We're actively working on a solution to handle this automatically, ensuring renamed tokens stay linked to their properties without additional effort.</p>
|
||||
<p class="advice">When you rename a token that has references (like aliases, math operations, or applied tokens), Penpot will prompt you to remap those references to the new name. If you choose to remap, all references will be updated automatically, including token aliases, design tab inputs, tooltips, and any elements where the token is applied. If the file is a library, remapping will also update references in files that use that library. You can also choose not to remap if you prefer to keep the old references, though this will break the connections.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/39-tokens-remap.webp" alt="Tokens remap" />
|
||||
</figure>
|
||||
|
||||
<h2 id="design-tokens-duplicate">Duplicating a token</h2>
|
||||
<p>Tokens can be duplicated by right-clicking the token you wish to duplicate and selecting <strong>Duplicate token</strong>. This will create a copy of the selected token within the same set, with <code class="language-js">-copy</code> added to its name.</p>
|
||||
@ -690,6 +747,50 @@ ExtraBold Italic
|
||||
<figcaption>Exporting tokens as a single file.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="design-tokens-groups">Token groups</h2>
|
||||
<p>Token names are rarely short and simple. They often contain multiple sections that represent token type, state, property, variant, and more. To help manage this complexity, Penpot automatically organizes tokens into groups based on the structure of their names.</p>
|
||||
|
||||
<h3 id="design-tokens-groups-structure">How token groups work</h3>
|
||||
<p>When Penpot encounters a dot (<code class="language-js">.</code>) in a token name, it breaks down the name and structures it as nested groups. For example, a token named <code class="language-js">button.primary.default.background-color</code> is organized into groups like this:</p>
|
||||
<ul>
|
||||
<li><strong>button</strong> (group)</li>
|
||||
<li>→ <strong>primary</strong> (group)</li>
|
||||
<li>→ → <strong>default</strong> (group)</li>
|
||||
<li>→ → → <strong>background-color</strong> (token)</li>
|
||||
</ul>
|
||||
<p>If you have another token like <code class="language-js">button.primary.hover.background-color</code>, it shares the same group structure (<code class="language-js">button.primary</code>) and appears nested within those groups.</p>
|
||||
<p>This structure matches how tokens are organized in JSON format. When you export tokens, a token like <code class="language-js">button.primary.default.background-color</code> is structured like this:</p>
|
||||
<pre class="language-json"><code class="language-json">{
|
||||
"button": {
|
||||
"primary": {
|
||||
"default": {
|
||||
"background-color": {
|
||||
"$value": "#f00",
|
||||
"$type": "color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3 id="design-tokens-groups-visual">Visual appearance</h3>
|
||||
<p>In the Tokens panel, token groups appear as nested, collapsible folders. Only the last segment of the token name (the actual token) appears as a pill. The segments before it appear as group folders that you can expand or collapse.</p>
|
||||
<p>When you create a new token, Penpot automatically unfolds the required path so you can see your newly created token. If you manually unfold a group path, it stays open even when you navigate to other areas of the app (this state resets if you reload the page).</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/40-tokens-groups.webp" alt="Design Tokens Grouping" />
|
||||
</figure>
|
||||
|
||||
<h3 id="design-tokens-groups-actions">Working with token groups</h3>
|
||||
<p>Token pills keep the same actions as before: you can delete, edit, and duplicate tokens from the context menu. When editing a token name, you'll see the full token path including all group segments.</p>
|
||||
|
||||
<h4>Moving tokens between groups</h4>
|
||||
<p>When you edit a token name and change the group segments, the token moves to its new group automatically. If the new group doesn't exist, Penpot creates it. If the group already exists, the token is moved there.</p>
|
||||
<p>For example, if you rename <code class="language-js">color.background.secondary</code> to <code class="language-js">c.bg.sec</code>, the token moves from the <code class="language-js">color.background</code> group to a new <code class="language-js">c.bg</code> group.</p>
|
||||
|
||||
<h4>Deleting token groups</h4>
|
||||
<p>When you delete a token, if it's the last token in its group, the empty group is automatically removed as well. You can also delete entire token groups directly, which removes all tokens within that group.</p>
|
||||
<p class="advice">Deleting a token group removes all tokens it contains. Make sure you want to delete all tokens in a group before removing it.</p>
|
||||
|
||||
<!-- Leaving this section as a comment as the feature will be released very soon
|
||||
<h2 id="design-tokens-settings">Tokens settings</h2>
|
||||
|
||||
|
||||
@ -142,7 +142,7 @@ a design.</p>
|
||||
</figure>
|
||||
|
||||
<h3 id="text">Text</h3>
|
||||
<p> (NOTA: El grosso de este contenido está en su propia sección. Aquí vendría un texto introductorio y un link a la <a href="/user-guide/designing/text-typo/">sección en cuestión</a>. )</p>
|
||||
<p>Text layers are how you add copy to your designs in Penpot. If you want to go deeper into fonts, typography and advanced text options, check the dedicated <a href="/user-guide/designing/text-typo/">Text & Typography</a> section.</p>
|
||||
|
||||
<h3 id="curves">Curves (freehand)</h3>
|
||||
<p>The curve tool allows a path to be created directly in a freehand mode.
|
||||
|
||||
@ -94,7 +94,7 @@ test("Create a LINEAR gradient", async ({ page }) => {
|
||||
await expect(inputOpacityGlobal).toBeVisible();
|
||||
|
||||
await expect(
|
||||
workspacePage.page.getByText("Linear gradient").nth(1),
|
||||
workspacePage.page.getByText("Linear gradient")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@ -178,7 +178,7 @@ test("Create a RADIAL gradient", async ({ page }) => {
|
||||
await expect(inputOpacityGlobal).toBeVisible();
|
||||
|
||||
await expect(
|
||||
workspacePage.page.getByText("Radial gradient").nth(1),
|
||||
workspacePage.page.getByText("Radial gradient")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await brTokenPillSM.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const brTokenOptionXl = borderRadiusSection.getByLabel("borderRadius.xl");
|
||||
const brTokenOptionXl = borderRadiusSection.getByRole('option', { name: 'borderRadius.xl' })
|
||||
await expect(brTokenOptionXl).toBeVisible();
|
||||
await brTokenOptionXl.click();
|
||||
|
||||
@ -148,7 +148,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await detachButton.click();
|
||||
|
||||
// Open dropdown from input
|
||||
const dropdownBtn = layerMenuSection.getByLabel("Open token list");
|
||||
const dropdownBtn = layerMenuSection.getByRole('button', { name: 'Open token list' })
|
||||
await expect(dropdownBtn).toBeVisible();
|
||||
await dropdownBtn.click();
|
||||
|
||||
@ -224,8 +224,8 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(firstShadowFields).toBeVisible();
|
||||
|
||||
// Fill in the shadow values
|
||||
const offsetXInput = firstShadowFields.getByLabel("X");
|
||||
const offsetYInput = firstShadowFields.getByLabel("Y");
|
||||
const offsetXInput = firstShadowFields.getByRole('textbox', { name: 'X' });
|
||||
const offsetYInput = firstShadowFields.getByRole('textbox', { name: 'Y' });
|
||||
const blurInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
@ -298,8 +298,8 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(thirdShadowFields).toBeVisible();
|
||||
|
||||
// User adds values for the third shadow
|
||||
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
||||
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
||||
const thirdOffsetXInput = thirdShadowFields.getByRole('textbox', { name: 'X' });
|
||||
const thirdOffsetYInput = thirdShadowFields.getByRole('textbox', { name: 'Y' });
|
||||
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
@ -327,10 +327,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
// Verify that the first shadow kept its values
|
||||
const firstOffsetXValue = await firstShadowFields
|
||||
.getByLabel("X")
|
||||
.getByRole('textbox', { name: 'X' })
|
||||
.inputValue();
|
||||
const firstOffsetYValue = await firstShadowFields
|
||||
.getByLabel("Y")
|
||||
.getByRole('textbox', { name: 'Y' })
|
||||
.inputValue();
|
||||
const firstBlurValue = await firstShadowFields
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
@ -356,10 +356,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(newSecondShadowFields).toBeVisible();
|
||||
|
||||
const secondOffsetXValue = await newSecondShadowFields
|
||||
.getByLabel("X")
|
||||
.getByRole('textbox', { name: 'X' })
|
||||
.inputValue();
|
||||
const secondOffsetYValue = await newSecondShadowFields
|
||||
.getByLabel("Y")
|
||||
.getByRole('textbox', { name: 'Y' })
|
||||
.inputValue();
|
||||
const secondBlurValue = await newSecondShadowFields
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
@ -409,10 +409,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
// Verify first shadow values are still there
|
||||
const restoredFirstOffsetX = await firstShadowFields
|
||||
.getByLabel("X")
|
||||
.getByRole('textbox', { name: 'X' })
|
||||
.inputValue();
|
||||
const restoredFirstOffsetY = await firstShadowFields
|
||||
.getByLabel("Y")
|
||||
.getByRole('textbox', { name: 'Y' })
|
||||
.inputValue();
|
||||
const restoredFirstBlur = await firstShadowFields
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
@ -432,10 +432,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
// Verify second shadow values are still there
|
||||
const restoredSecondOffsetX = await newSecondShadowFields
|
||||
.getByLabel("X")
|
||||
.getByRole('textbox', { name: 'X' })
|
||||
.inputValue();
|
||||
const restoredSecondOffsetY = await newSecondShadowFields
|
||||
.getByLabel("Y")
|
||||
.getByRole('textbox', { name: 'Y' })
|
||||
.inputValue();
|
||||
const restoredSecondBlur = await newSecondShadowFields
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
@ -517,7 +517,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.nth(1).click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole('option', { name: 'dimension.xl' })
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@ -571,7 +571,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole('option', { name: 'dimension.xl' });
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@ -625,7 +625,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole('option', { name: 'dimension.xl' });
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@ -681,7 +681,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl =
|
||||
borderRadiusSection.getByLabel("dimension.xl");
|
||||
borderRadiusSection.getByRole('option', { name: 'dimension.xl' });
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@ -750,7 +750,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
await tokenDropdown.click();
|
||||
|
||||
const widthOptionSmall = firstStrokeRow.getByLabel("width-small");
|
||||
const widthOptionSmall = firstStrokeRow.getByRole('option', { name: 'width-small' });
|
||||
await expect(widthOptionSmall).toBeVisible();
|
||||
await widthOptionSmall.click();
|
||||
const StrokeWidthPillSmall = firstStrokeRow.getByRole("button", {
|
||||
@ -830,15 +830,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
await detachButton.click();
|
||||
await expect(marginPillXL).not.toBeVisible();
|
||||
const horizontalMarginInput = layoutItemSectionSidebar.getByText(
|
||||
"Horizontal marginOpen token",
|
||||
);
|
||||
await expect(horizontalMarginInput).toBeVisible();
|
||||
|
||||
const tokenDropdown = horizontalMarginInput.getByRole("button", {
|
||||
const horizontalMarginInput = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "Open token list",
|
||||
});
|
||||
await tokenDropdown.click();
|
||||
await horizontalMarginInput.nth(1).click();
|
||||
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@ -1024,7 +1024,7 @@ test.describe("Tokens - creation", () => {
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("typography.empty");
|
||||
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
|
||||
const valueField = tokensUpdateCreateModal.getByRole("textbox", {name: "Font Size"});
|
||||
|
||||
// Insert a value and then delete it
|
||||
await valueField.fill("1");
|
||||
@ -1716,12 +1716,12 @@ test.describe("Tokens tab - edition", () => {
|
||||
|
||||
// Fill font-family to verify to verify that input value doesn't get split into list of characters
|
||||
const fontFamilyField = tokensUpdateCreateModal
|
||||
.getByLabel("Font family")
|
||||
.getByRole("textbox", { name: "Font family" })
|
||||
.first();
|
||||
await fontFamilyField.fill("OneWord");
|
||||
|
||||
// Invalidate incorrect values for font size
|
||||
const fontSizeField = tokensUpdateCreateModal.getByLabel(/Font Size/i);
|
||||
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Size" });
|
||||
await fontSizeField.fill("invalid");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText(/Invalid token value:/),
|
||||
@ -1736,13 +1736,13 @@ test.describe("Tokens tab - edition", () => {
|
||||
await fontSizeField.fill("16");
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
||||
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
|
||||
const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Weight" });
|
||||
const letterSpacingField =
|
||||
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
|
||||
const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i);
|
||||
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
|
||||
tokensUpdateCreateModal.getByRole("textbox", { name: "Letter Spacing" });
|
||||
const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Line Height" });
|
||||
const textCaseField = tokensUpdateCreateModal.getByRole("textbox", { name: "Text Case" });
|
||||
const textDecorationField =
|
||||
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
|
||||
tokensUpdateCreateModal.getByRole("textbox", { name: "Text Decoration" });
|
||||
|
||||
// Capture all values before switching tabs
|
||||
const originalValues = {
|
||||
@ -1800,6 +1800,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "100",
|
||||
});
|
||||
|
||||
await expect(colorToken).toBeVisible();
|
||||
await colorToken.click({ button: "right" });
|
||||
|
||||
|
||||
@ -49,13 +49,14 @@
|
||||
(defn notify-start-loading
|
||||
[]
|
||||
(st/emit! (ntf/show {:content (tr "media.loading")
|
||||
:tag ::media-upload
|
||||
:type :toast
|
||||
:level :info
|
||||
:timeout nil})))
|
||||
|
||||
(defn notify-finished-loading
|
||||
[]
|
||||
(st/emit! (ntf/hide)))
|
||||
(st/emit! (ntf/hide :tag ::media-upload)))
|
||||
|
||||
(defn process-error
|
||||
[error]
|
||||
|
||||
@ -76,6 +76,7 @@
|
||||
(rx/filter #(not= route-id (:id %)))
|
||||
(rx/map hide)
|
||||
(rx/take-until stopper)))
|
||||
|
||||
(when (:timeout data)
|
||||
(let [stopper (rx/filter (ptk/type? ::show) stream)]
|
||||
(->> (rx/of (hide))
|
||||
|
||||
@ -377,7 +377,15 @@
|
||||
(defn- parse-single-shadow
|
||||
"Parses a single shadow map with properties: x, y, blur, spread, color, type."
|
||||
[shadow-map shadow-index]
|
||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||
(let [shadow-map (merge {:offset-x nil ;; Ensure that all keys are processed, even if missing in the original token
|
||||
:offset-y nil
|
||||
:blur nil
|
||||
:spread nil
|
||||
:color nil
|
||||
:inset false}
|
||||
shadow-map)
|
||||
|
||||
add-keyed-errors (fn [shadow-result k errors]
|
||||
(update shadow-result :errors concat
|
||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||
parsers {:offset-x parse-sd-token-general-value
|
||||
|
||||
@ -1016,7 +1016,7 @@
|
||||
(update [_ state]
|
||||
(update state :colorpicker
|
||||
(fn [state]
|
||||
(let [type (:type state)
|
||||
(let [type (:type state)
|
||||
state (-> state
|
||||
(update :current-color merge changes)
|
||||
(update :current-color materialize-color-components)
|
||||
@ -1024,6 +1024,7 @@
|
||||
;; current color can be a library one
|
||||
;; I'm changing via colorpicker
|
||||
(update :current-color dissoc :ref-id :ref-file))]
|
||||
|
||||
(if-let [stop (:editing-stop state)]
|
||||
(update-in state [:stops stop] (fn [data] (->> changes
|
||||
(merge data)
|
||||
@ -1044,7 +1045,9 @@
|
||||
(and (= type :color) (nil? (:color state)))]
|
||||
|
||||
(when (and add-recent? (not ignore-color?))
|
||||
(let [color (select-keys state [:image :gradient :color :opacity])]
|
||||
(when-let [color (-> state
|
||||
(select-keys [:image :gradient :color :opacity])
|
||||
(not-empty))]
|
||||
(rx/of (add-recent-color color))))))))
|
||||
|
||||
(defn update-colorpicker-gradient
|
||||
|
||||
@ -251,9 +251,8 @@
|
||||
|
||||
(defn upload-fill-image
|
||||
[file on-success]
|
||||
(dm/assert!
|
||||
"expected a valid blob for `file` param"
|
||||
(dmm/blob? file))
|
||||
(assert (dmm/blob? file) "expected a valid blob for `file` param")
|
||||
|
||||
(ptk/reify ::upload-fill-image
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
|
||||
@ -317,8 +317,10 @@
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
edition (get-in state [:workspace-local :edition])
|
||||
drawing (get state :workspace-drawing)]
|
||||
(when (and (or (nil? edition) (ctl/grid-layout? objects edition))
|
||||
(or (empty? drawing) (= :curve (:tool drawing))))
|
||||
|
||||
;; Editors handle their own undo's
|
||||
(when (or (and (nil? edition) (nil? (:object drawing)))
|
||||
(ctl/grid-layout? objects edition))
|
||||
(let [undo (:workspace-undo state)
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))]
|
||||
|
||||
@ -72,7 +72,6 @@
|
||||
(when-let [file-id (or (:file-id data) file-id)]
|
||||
(println "File ID: " (str file-id)))
|
||||
(println "Version: " (:full cf/version))
|
||||
(println "URI: " (str cf/public-uri))
|
||||
(println "HREF: " (rt/get-current-href))
|
||||
(println)
|
||||
|
||||
@ -88,24 +87,36 @@
|
||||
(.error js/console "error on generating report" cause)
|
||||
nil)))
|
||||
|
||||
(defn- show-not-blocking-error
|
||||
"Show a non user blocking error notification"
|
||||
[cause]
|
||||
(let [data (ex-data cause)
|
||||
hint (or (some-> (:hint data) ex/first-line)
|
||||
(ex-message cause))]
|
||||
|
||||
(defn submit-report
|
||||
"Report the error report to the audit log subsystem"
|
||||
[& {:keys [event-name report hint] :or {event-name "unhandled-exception"}}]
|
||||
(when (and (not (str/empty? hint))
|
||||
(string? report)
|
||||
(string? event-name))
|
||||
(st/emit!
|
||||
(ev/event {::ev/name "unhandled-exception"
|
||||
(ev/event {::ev/name event-name
|
||||
:hint hint
|
||||
:href (rt/get-current-href)
|
||||
:type (get data :type :unknown)
|
||||
:report (generate-report cause)})
|
||||
:report report}))))
|
||||
|
||||
(ntf/show {:content (tr "errors.unexpected-exception" hint)
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 3000}))))
|
||||
(defn flash
|
||||
"Show error notification banner and emit error report"
|
||||
[& {:keys [type hint cause] :or {type :handled}}]
|
||||
(when (ex/exception? cause)
|
||||
(when-let [event-name (case type
|
||||
:handled "handled-exception"
|
||||
:unhandled "unhandled-exception"
|
||||
:silent nil)]
|
||||
(let [report (generate-report cause)]
|
||||
(submit-report :event-name event-name
|
||||
:report report
|
||||
:hint (ex/get-hint cause)))))
|
||||
|
||||
(st/emit!
|
||||
(ntf/show {:content (or ^boolean hint (tr "errors.generic"))
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 5000})))
|
||||
|
||||
(defmethod ptk/handle-error :default
|
||||
[error]
|
||||
@ -114,7 +125,7 @@
|
||||
(ptk/handle-error (assoc error :type :assertion))
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||
(show-not-blocking-error cause))))
|
||||
(flash :cause cause :type :unhandled))))
|
||||
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
@ -203,7 +214,7 @@
|
||||
(defmethod ptk/handle-error :assertion
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(show-not-blocking-error cause)
|
||||
(flash :cause cause :type :handled)
|
||||
(ex/print-throwable cause :prefix "Assertion Error")))
|
||||
|
||||
;; ;; All the errors that happens on worker are handled here.
|
||||
@ -307,7 +318,7 @@
|
||||
:else
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "Restriction Error")
|
||||
(show-not-blocking-error cause))))
|
||||
(flash :cause cause :type :unhandled))))
|
||||
|
||||
;; This happens when the backed server fails to process the
|
||||
;; request. This can be caused by an internal assertion or any other
|
||||
@ -333,14 +344,14 @@
|
||||
(set! last-exception cause)
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/schedule #(show-not-blocking-error cause)))))
|
||||
(ts/schedule #(flash :cause cause :type :unhandled)))))
|
||||
|
||||
(on-unhandled-rejection [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(show-not-blocking-error cause))))]
|
||||
(ts/schedule #(flash :cause cause :type :unhandled))))]
|
||||
|
||||
(.addEventListener g/window "error" on-unhandled-error)
|
||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||
|
||||
@ -33,6 +33,8 @@
|
||||
(let [variant
|
||||
(d/nilv variant "primary")
|
||||
|
||||
button-ref (mf/use-ref nil)
|
||||
|
||||
tooltip-id
|
||||
(mf/use-id)
|
||||
|
||||
@ -47,10 +49,12 @@
|
||||
props
|
||||
(mf/spread-props props
|
||||
{:class [class button-class]
|
||||
:ref button-ref
|
||||
:aria-labelledby tooltip-id})]
|
||||
|
||||
[:> tooltip* {:content aria-label
|
||||
:class tooltip-class
|
||||
:trigger-ref button-ref
|
||||
:placement tooltip-placement
|
||||
:id tooltip-id}
|
||||
[:> :button props
|
||||
|
||||
@ -28,7 +28,8 @@
|
||||
{::mf/schema schema:token-option}
|
||||
[{:keys [id name on-click selected ref focused resolved] :rest props}]
|
||||
(let [internal-id (mf/use-id)
|
||||
id (d/nilv id internal-id)]
|
||||
id (d/nilv id internal-id)
|
||||
element-ref (mf/use-ref nil)]
|
||||
[:li {:value id
|
||||
:class (stl/css-case :token-option true
|
||||
:option-with-pill true
|
||||
@ -50,10 +51,12 @@
|
||||
:aria-hidden (when name true)}]
|
||||
[:span {:class (stl/css :icon-placeholder)}])
|
||||
[:> tooltip* {:content name
|
||||
:trigger-ref element-ref
|
||||
:id (dm/str id "-name")
|
||||
:class (stl/css :option-text)}
|
||||
;; Add ellipsis
|
||||
[:span {:aria-labelledby (dm/str id "-name")}
|
||||
;; Add ellipsis
|
||||
[:span {:aria-labelledby (dm/str id "-name")
|
||||
:ref element-ref}
|
||||
name]]
|
||||
(when resolved
|
||||
[:> :span {:class (stl/css :option-pill)}
|
||||
|
||||
@ -84,6 +84,7 @@
|
||||
:on-click on-icon-click}])
|
||||
(if aria-label
|
||||
[:> tooltip* {:content aria-label
|
||||
:trigger-ref (or ref input-ref)
|
||||
:class (stl/css :tooltip-wrapper)
|
||||
:id tooltip-id}
|
||||
[:> "input" props]]
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
(tr "ds.inputs.token-field.no-active-token-option" label))
|
||||
default-id (mf/use-id)
|
||||
id (d/nilv id default-id)
|
||||
pill-ref (mf/use-ref nil)
|
||||
|
||||
focus-wrapper
|
||||
(mf/use-fn
|
||||
@ -53,6 +54,7 @@
|
||||
(dom/focus! (mf/ref-val token-wrapper-ref)))))]
|
||||
[:> tooltip* {:content property
|
||||
:class (stl/css :token-field-wrapper)
|
||||
:trigger-ref token-wrapper-ref
|
||||
:id (dm/str default-id "-input")}
|
||||
[:div {:class [class (stl/css-case :token-field true
|
||||
:with-icon (some? slot-start)
|
||||
@ -70,8 +72,10 @@
|
||||
|
||||
[:div {:class (stl/css :content-wrapper)}
|
||||
[:> tooltip* {:content content
|
||||
:trigger-ref pill-ref
|
||||
:id (dm/str id "-pill")}
|
||||
[:button {:on-click on-click
|
||||
:ref pill-ref
|
||||
:class (stl/css-case :pill true
|
||||
:no-set-pill (not set-active?)
|
||||
:pill-disabled disabled)
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.ui.ds.tooltip.tooltip
|
||||
(:require-macros
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
@ -15,10 +14,10 @@
|
||||
[app.util.timers :as ts]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private ^:const arrow-height 12)
|
||||
(def ^:private ^:const half-arrow-height (/ arrow-height 2))
|
||||
(def ^:private ^:const overlay-offset 32)
|
||||
|
||||
(defonce active-tooltip (atom nil))
|
||||
|
||||
(defn- clear-schedule
|
||||
[ref]
|
||||
(when-let [schedule (mf/ref-val ref)]
|
||||
@ -29,20 +28,6 @@
|
||||
[ref delay f]
|
||||
(mf/set-ref-val! ref (ts/schedule delay f)))
|
||||
|
||||
(defn- show-popover
|
||||
[node]
|
||||
(when (.-isConnected ^js node)
|
||||
(.showPopover ^js node)))
|
||||
|
||||
(defn- hide-popover
|
||||
[node]
|
||||
(when (and (some? node)
|
||||
(fn? (.-hidePopover node)))
|
||||
(dom/unset-css-property! node "block-size")
|
||||
(dom/unset-css-property! node "inset-block-start")
|
||||
(dom/unset-css-property! node "inset-inline-start")
|
||||
(.hidePopover ^js node)))
|
||||
|
||||
(defn- calculate-placement-bounding-rect
|
||||
"Given a placement, calcultates the bounding rect for it taking in
|
||||
account provided tooltip bounding rect and the origin bounding
|
||||
@ -72,18 +57,18 @@
|
||||
:height tooltip-height}
|
||||
|
||||
"left"
|
||||
{:top (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2))
|
||||
:left (- trigger-left tooltip-width arrow-height)
|
||||
{:top (- (+ trigger-top (/ trigger-height 2)) (/ tooltip-height 2))
|
||||
:left (- trigger-left tooltip-width)
|
||||
:right (+ (- trigger-left tooltip-width) tooltip-width)
|
||||
:bottom (+ (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2)) tooltip-height)
|
||||
:bottom (+ (- (+ trigger-top (/ trigger-height 2)) (/ tooltip-height 2)) tooltip-height)
|
||||
:width tooltip-width
|
||||
:height tooltip-height}
|
||||
|
||||
"right"
|
||||
{:top (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2))
|
||||
{:top (- (+ trigger-top (/ trigger-height 2)) (/ tooltip-height 2))
|
||||
:left (+ trigger-right offset)
|
||||
:right (+ trigger-right offset tooltip-width)
|
||||
:bottom (+ (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2)) tooltip-height)
|
||||
:bottom (+ (- (+ trigger-top (/ trigger-height 2)) (/ tooltip-height 2)) tooltip-height)
|
||||
:width tooltip-width
|
||||
:height tooltip-height}
|
||||
|
||||
@ -153,22 +138,6 @@
|
||||
(recur (rest placements))
|
||||
#js [placement placement-brect])))))
|
||||
|
||||
(defn- update-tooltip-position
|
||||
"Update the tooltip position having in account the current window
|
||||
size, placement. It calculates the appropriate placement and updates
|
||||
the dom with the result."
|
||||
[tooltip placement origin-brect offset]
|
||||
(show-popover tooltip)
|
||||
(let [tooltip-brect (dom/get-bounding-rect tooltip)
|
||||
tooltip-brect (assoc tooltip-brect :height (:height tooltip-brect) :width (:width tooltip-brect))
|
||||
window-size (dom/get-window-size)]
|
||||
(when-let [[placement placement-rect] (find-matching-placement placement tooltip-brect origin-brect window-size offset)]
|
||||
(let [height (:height placement-rect)]
|
||||
(dom/set-css-property! tooltip "block-size" (dm/str height "px"))
|
||||
(dom/set-css-property! tooltip "inset-block-start" (dm/str (:top placement-rect) "px"))
|
||||
(dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left placement-rect) "px")))
|
||||
placement)))
|
||||
|
||||
(def ^:private schema:tooltip
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
@ -176,19 +145,26 @@
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
[:content [:or fn? :string map?]]
|
||||
[:trigger-ref {:optional true} [:maybe :any]]
|
||||
[:placement {:optional true}
|
||||
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
|
||||
|
||||
(mf/defc tooltip*
|
||||
{::mf/schema schema:tooltip}
|
||||
[{:keys [class id children content placement offset delay] :rest props}]
|
||||
[{:keys [class id children content placement offset delay trigger-ref aria-label] :rest props}]
|
||||
(let [internal-id
|
||||
(mf/use-id)
|
||||
trigger-ref (mf/use-ref nil)
|
||||
internal-trigger-ref (mf/use-ref nil)
|
||||
trigger-ref (or trigger-ref internal-trigger-ref)
|
||||
|
||||
tooltip-ref (mf/use-ref nil)
|
||||
|
||||
id
|
||||
(d/nilv id internal-id)
|
||||
|
||||
tooltip-id
|
||||
(mf/use-id)
|
||||
|
||||
placement*
|
||||
(mf/use-state #(d/nilv placement "top"))
|
||||
|
||||
@ -201,35 +177,35 @@
|
||||
schedule-ref
|
||||
(mf/use-ref nil)
|
||||
|
||||
visible*
|
||||
(mf/use-state false)
|
||||
visible (deref visible*)
|
||||
|
||||
on-show
|
||||
(mf/use-fn
|
||||
(mf/deps id placement offset)
|
||||
(fn [event]
|
||||
|
||||
(let [current (dom/get-current-target event)
|
||||
related (dom/get-related-target event)
|
||||
is-node? (fn [node] (and node (.-nodeType node)))]
|
||||
(when-not (and related (is-node? related) (.contains current related))
|
||||
(clear-schedule schedule-ref)
|
||||
(when-let [tooltip (dom/get-element id)]
|
||||
(let [origin-brect
|
||||
(dom/get-bounding-rect (mf/ref-val trigger-ref))
|
||||
|
||||
update-position
|
||||
(fn []
|
||||
(let [new-placement (update-tooltip-position tooltip placement origin-brect offset)]
|
||||
(when (not= new-placement placement)
|
||||
(reset! placement* new-placement))))]
|
||||
|
||||
(add-schedule schedule-ref delay update-position)))))))
|
||||
(mf/deps tooltip-id delay)
|
||||
(fn [_]
|
||||
(let [trigger-el (mf/ref-val trigger-ref)]
|
||||
(clear-schedule schedule-ref)
|
||||
(add-schedule schedule-ref (d/nilv delay 300)
|
||||
(fn []
|
||||
(prn tooltip-id)
|
||||
(when-let [active @active-tooltip]
|
||||
(when (not= (:id active) tooltip-id)
|
||||
(when-let [tooltip-el (dom/get-element (:id active))]
|
||||
(dom/set-css-property! tooltip-el "display" "none"))
|
||||
(reset! active-tooltip nil)))
|
||||
(reset! active-tooltip {:id tooltip-id :trigger trigger-el})
|
||||
(reset! visible* true))))))
|
||||
|
||||
on-hide
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(mf/deps tooltip-id)
|
||||
(fn []
|
||||
(when-let [tooltip (dom/get-element id)]
|
||||
(clear-schedule schedule-ref)
|
||||
(hide-popover tooltip))))
|
||||
(clear-schedule schedule-ref)
|
||||
(reset! visible* false)
|
||||
(when (= (:id @active-tooltip) tooltip-id)
|
||||
(reset! active-tooltip nil))))
|
||||
|
||||
handle-key-down
|
||||
(mf/use-fn
|
||||
@ -250,28 +226,62 @@
|
||||
:tooltip-bottom-left (identical? placement "bottom-left")
|
||||
:tooltip-top-left (identical? placement "top-left"))
|
||||
|
||||
content
|
||||
(if (fn? content)
|
||||
(content)
|
||||
content)
|
||||
props
|
||||
(mf/spread-props props
|
||||
{:on-mouse-enter on-show
|
||||
:on-mouse-leave on-hide
|
||||
:on-focus on-show
|
||||
:on-blur on-hide
|
||||
:ref internal-trigger-ref
|
||||
:on-key-down handle-key-down
|
||||
:ref trigger-ref
|
||||
:id id
|
||||
:class [class (stl/css :tooltip-trigger)]
|
||||
:aria-describedby id})
|
||||
content
|
||||
(if (fn? content)
|
||||
(content)
|
||||
content)]
|
||||
:aria-label (if (string? content)
|
||||
content
|
||||
aria-label)})]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps visible placement offset)
|
||||
(fn []
|
||||
(when visible
|
||||
(let [trigger-el (mf/ref-val trigger-ref)
|
||||
tooltip-el (mf/ref-val tooltip-ref)]
|
||||
(when (and trigger-el tooltip-el)
|
||||
(js/requestAnimationFrame
|
||||
(fn []
|
||||
(let [origin-brect (dom/get-bounding-rect trigger-el)
|
||||
tooltip-brect (dom/get-bounding-rect tooltip-el)
|
||||
window-size (dom/get-window-size)]
|
||||
(when-let [[new-placement placement-rect]
|
||||
(find-matching-placement
|
||||
placement
|
||||
tooltip-brect
|
||||
origin-brect
|
||||
window-size
|
||||
offset)]
|
||||
(dom/set-css-property! tooltip-el "inset-block-start"
|
||||
(str (:top placement-rect) "px"))
|
||||
(dom/set-css-property! tooltip-el "inset-inline-start"
|
||||
(str (:left placement-rect) "px"))
|
||||
|
||||
(when (not= new-placement placement)
|
||||
(reset! placement* new-placement)))))))))))
|
||||
|
||||
[:> :div props
|
||||
children
|
||||
[:div {:class (stl/css :tooltip)
|
||||
:id id
|
||||
:popover "auto"
|
||||
:role "tooltip"}
|
||||
[:div {:class tooltip-class}
|
||||
[:div {:class (stl/css :tooltip-content)} content]
|
||||
[:div {:class (stl/css :tooltip-arrow)
|
||||
:id "tooltip-arrow"}]]]]))
|
||||
(when visible
|
||||
(mf/portal
|
||||
(mf/html
|
||||
[:div {:class (stl/css :tooltip)
|
||||
:role "tooltip"
|
||||
:id tooltip-id
|
||||
:ref tooltip-ref}
|
||||
[:div {:class tooltip-class}
|
||||
[:div {:class (stl/css :tooltip-content)} content]
|
||||
[:div {:class (stl/css :tooltip-arrow)
|
||||
:id "tooltip-arrow"}]]])
|
||||
(.-body js/document)))]))
|
||||
|
||||
@ -6,17 +6,19 @@
|
||||
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
$arrow-side: 12px;
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
max-inline-size: $sz-352;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
inline-size: fit-content;
|
||||
block-size: fit-content;
|
||||
z-index: var(--z-index-notifications);
|
||||
}
|
||||
|
||||
.tooltip-content-wrapper {
|
||||
|
||||
@ -96,8 +96,9 @@
|
||||
image (:image background)
|
||||
format (if id? "rounded" "square")
|
||||
element-id (mf/use-id)
|
||||
has-opacity? (and (some? (:color background))
|
||||
(< (:opacity background) 1))
|
||||
has-opacity? (and (some? (:color background))
|
||||
(< (:opacity background) 1))
|
||||
element-ref (mf/use-ref nil)
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps background on-click)
|
||||
@ -120,7 +121,8 @@
|
||||
(mf/spread-props props {:class class
|
||||
:on-click on-click
|
||||
:type button-type
|
||||
:aria-labelledby element-id})
|
||||
:aria-labelledby element-id
|
||||
:ref element-ref})
|
||||
children (mf/html
|
||||
[:> element-type props
|
||||
(cond
|
||||
@ -147,6 +149,7 @@
|
||||
[:> tooltip* {:content (if tooltip-content
|
||||
tooltip-content
|
||||
(color-title background))
|
||||
:trigger-ref element-ref
|
||||
:id element-id}
|
||||
children]
|
||||
|
||||
|
||||
@ -23,11 +23,12 @@
|
||||
|
||||
(mf/defc property-detail-copiable*
|
||||
{::mf/schema schema:property-detail-copiable}
|
||||
[{:keys [color token copied on-click children]}]
|
||||
[{:keys [color token copied on-click children ref]}]
|
||||
[:button {:class (stl/css-case :property-detail-copiable true
|
||||
:property-detail-copied copied
|
||||
:property-detail-copiable-color (some? color))
|
||||
:on-click on-click}
|
||||
:on-click on-click
|
||||
:ref ref}
|
||||
(when color
|
||||
[:> swatch* {:background color
|
||||
:size "small"}])
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
color-image-name (:name color-image)
|
||||
color-image-url (when (some? color-image)
|
||||
(cfg/resolve-file-media color-image))
|
||||
row-ref (mf/use-ref nil)
|
||||
color-opacity (mf/use-memo
|
||||
(mf/deps color)
|
||||
#(dm/str (-> color
|
||||
@ -96,6 +97,7 @@
|
||||
(if token
|
||||
[:> tooltip* {:id (:name token)
|
||||
:class (stl/css :tooltip-token-wrapper)
|
||||
:trigger-ref row-ref
|
||||
:content #(mf/html
|
||||
[:div {:class (stl/css :tooltip-token)}
|
||||
[:div {:class (stl/css :tooltip-token-title)}
|
||||
@ -104,6 +106,7 @@
|
||||
(:resolved-value token)]])}
|
||||
[:> property-detail-copiable* {:color color
|
||||
:token token
|
||||
:ref row-ref
|
||||
:copied copied
|
||||
:on-click copy-attr} formatted-color-value]]
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
copiable-value (if (some? token)
|
||||
(:name token)
|
||||
property)
|
||||
row-ref (mf/use-ref nil)
|
||||
|
||||
copy-attr
|
||||
(mf/use-fn
|
||||
@ -54,6 +55,7 @@
|
||||
(let [token-type (:type token)]
|
||||
[:> tooltip* {:id (:name token)
|
||||
:class (stl/css :tooltip-token-wrapper)
|
||||
:trigger-ref row-ref
|
||||
:content #(mf/html
|
||||
[:div {:class (stl/css :tooltip-token)}
|
||||
[:div {:class (stl/css :tooltip-token-title)}
|
||||
@ -75,6 +77,7 @@
|
||||
(:resolved-value token))]])}
|
||||
[:> property-detail-copiable* {:token token
|
||||
:copied copied
|
||||
:ref row-ref
|
||||
:on-click copy-attr} detail]])
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-attr} detail])
|
||||
|
||||
@ -9,13 +9,12 @@
|
||||
(:require
|
||||
["rxjs" :as rxjs]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :refer [is-authenticated?]]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
@ -448,22 +447,22 @@
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
[{:keys [data] :as props}]
|
||||
(let [type (get data :type)
|
||||
report (mf/with-memo [data]
|
||||
(some-> data ::errors/instance errors/generate-report))
|
||||
cause (get data ::errors/instance)
|
||||
|
||||
report (mf/with-memo [cause]
|
||||
(when (ex/exception? cause)
|
||||
(errors/generate-report cause)))
|
||||
|
||||
props (mf/spread-props props {:report report})]
|
||||
|
||||
(mf/with-effect [data route report]
|
||||
(let [params (:query-params route)
|
||||
params (u/map->query-string params)]
|
||||
(st/emit! (ev/event {::ev/name "exception-page"
|
||||
:type (get data :type :unknown)
|
||||
:href (rt/get-current-href)
|
||||
:hint (get data :hint)
|
||||
:path (get route :path)
|
||||
:report report
|
||||
:params params}))))
|
||||
(mf/with-effect [report type cause]
|
||||
(when (and (ex/exception? cause)
|
||||
(not (contains? #{:not-found :authentication} type)))
|
||||
(errors/submit-report :event-name "exception-page"
|
||||
:report report
|
||||
:hint (ex/get-hint cause))))
|
||||
|
||||
(case type
|
||||
:not-found
|
||||
|
||||
@ -44,12 +44,15 @@
|
||||
(on-token-pill-click event token)))
|
||||
id-tooltip (mf/use-id)
|
||||
resolved (:resolved-value token)
|
||||
color-value (dwta/value->color resolved)]
|
||||
color-value (dwta/value->color resolved)
|
||||
item-ref (mf/use-ref nil)]
|
||||
[:> tooltip* {:id id-tooltip
|
||||
:style {:width "100%"}
|
||||
:trigger-ref item-ref
|
||||
:content (:name token)}
|
||||
[:button {:class (stl/css-case :color-token-item true
|
||||
:color-token-selected selected)
|
||||
:ref item-ref
|
||||
:aria-labelledby id-tooltip
|
||||
:on-click on-click}
|
||||
[:> swatch* {:background color-value
|
||||
|
||||
@ -94,6 +94,7 @@
|
||||
not-active (or (empty? active-tokens)
|
||||
(nil? token))
|
||||
id (dm/str (:id token) "-name")
|
||||
token-name-ref (mf/use-ref nil)
|
||||
swatch-tooltip-content (cond
|
||||
not-active
|
||||
(tr "ds.inputs.token-field.no-active-color.token-option")
|
||||
@ -126,8 +127,11 @@
|
||||
:size "small"}]]
|
||||
[:> tooltip* {:content name-tooltip-content
|
||||
:id id
|
||||
:aria-label (str (tr "workspace.tokens.token-name") ": " applied-token-name)
|
||||
:trigger-ref token-name-ref
|
||||
:class (stl/css :token-tooltip)}
|
||||
[:div {:class (stl/css :token-name)
|
||||
:ref token-name-ref
|
||||
:aria-labelledby id}
|
||||
(or token-name applied-token-name)]]
|
||||
[:div {:class (stl/css :token-actions)}
|
||||
|
||||
@ -94,12 +94,12 @@
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-width-change)
|
||||
(fn [value]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-stroke-width-change index value)
|
||||
(do
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{:stroke-width}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{:stroke-width}
|
||||
:shape-ids ids})))))
|
||||
|
||||
stroke-alignment (or (:stroke-alignment stroke) :center)
|
||||
|
||||
|
||||
@ -4,17 +4,27 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
|
||||
.token-modal-wrapper {
|
||||
@extend .modal-container-base;
|
||||
@include deprecated.menuShadow;
|
||||
border-radius: $br-4;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
min-width: $sz-364;
|
||||
min-height: $sz-192;
|
||||
max-width: $sz-512;
|
||||
max-height: $sz-512;
|
||||
box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark);
|
||||
position: absolute;
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
z-index: 11;
|
||||
z-index: var(--z-index-set);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--sp-xxxl);
|
||||
&.token-modal-large {
|
||||
max-block-size: 95vh;
|
||||
}
|
||||
@ -22,6 +32,6 @@
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: deprecated.$s-6;
|
||||
right: deprecated.$s-6;
|
||||
top: px2rem(6);
|
||||
right: px2rem(6);
|
||||
}
|
||||
|
||||
@ -144,10 +144,11 @@
|
||||
(if (and not-editing? (seq selected-shapes) (not= (:type token) :number))
|
||||
(st/emit! (dwta/toggle-token {:token token
|
||||
:shape-ids selected-ids}))
|
||||
(st/emit! (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 3000}))))))]
|
||||
(when (seq selected-shapes)
|
||||
(st/emit! (ntf/show {:content (tr "workspace.tokens.error-text-edition")
|
||||
:type :toast
|
||||
:level :warning
|
||||
:timeout 3000})))))))]
|
||||
|
||||
[:div {:class (stl/css :token-section-wrapper)
|
||||
:data-testid (dm/str "section-" (name type))}
|
||||
|
||||
@ -164,8 +164,8 @@
|
||||
(obj/without-empty
|
||||
#js {:id (-> id format-id)
|
||||
:style (-> style format-key)
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:offsetX offset-x
|
||||
:offsetY offset-y
|
||||
:blur blur
|
||||
:spread spread
|
||||
:hidden hidden
|
||||
|
||||
@ -147,7 +147,7 @@
|
||||
;; export interface Shadow {
|
||||
;; id?: string;
|
||||
;; style?: 'drop-shadow' | 'inner-shadow';
|
||||
;; offset--y?: number;
|
||||
;; offsetX?: number;
|
||||
;; offsetY?: number;
|
||||
;; blur?: number;
|
||||
;; spread?: number;
|
||||
@ -160,8 +160,8 @@
|
||||
(d/without-nils
|
||||
{:id (-> (obj/get shadow "id") parse-id)
|
||||
:style (-> (obj/get shadow "style") parse-keyword)
|
||||
:offset-x (obj/get shadow "offset-x")
|
||||
:offset-y (obj/get shadow "offset-y")
|
||||
:offset-x (obj/get shadow "offsetX")
|
||||
:offset-y (obj/get shadow "offsetY")
|
||||
:blur (obj/get shadow "blur")
|
||||
:spread (obj/get shadow "spread")
|
||||
:hidden (obj/get shadow "hidden")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user