From 01896501c14868b0ab19c7010d2799fa7df5b376 Mon Sep 17 00:00:00 2001
From: Xaviju
Date: Thu, 24 Jul 2025 09:37:38 +0200
Subject: [PATCH 01/17] :bug: Remove image type from inspect tab panels (#6959)
---
frontend/src/app/main/ui/inspect/attributes.cljs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend/src/app/main/ui/inspect/attributes.cljs b/frontend/src/app/main/ui/inspect/attributes.cljs
index 661b808f0a..1e86b08eaa 100644
--- a/frontend/src/app/main/ui/inspect/attributes.cljs
+++ b/frontend/src/app/main/ui/inspect/attributes.cljs
@@ -27,13 +27,12 @@
[rumext.v2 :as mf]))
(def type->options
- {:multiple [:fill :stroke :image :text :shadow :blur :layout-element]
+ {:multiple [:fill :stroke :text :shadow :blur :layout-element]
:frame [:visibility :geometry :fill :stroke :shadow :blur :layout :layout-element]
:group [:visibility :geometry :svg :layout-element]
:rect [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:circle [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:path [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
- :image [:visibility :image :geometry :fill :stroke :shadow :blur :svg :layout-element]
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})
From d08c94d5a6053f61d626963d11647eb9f5d0adb2 Mon Sep 17 00:00:00 2001
From: Andrey Antukh
Date: Tue, 8 Jul 2025 15:27:23 +0200
Subject: [PATCH 02/17] :sparkles: Change default status filtering for logical
deletion
---
backend/src/app/features/logical_deletion.clj | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/backend/src/app/features/logical_deletion.clj b/backend/src/app/features/logical_deletion.clj
index 8a06f3f30f..a8407cdf62 100644
--- a/backend/src/app/features/logical_deletion.clj
+++ b/backend/src/app/features/logical_deletion.clj
@@ -10,18 +10,19 @@
[app.config :as cf]
[app.util.time :as dt]))
+(def ^:private canceled-status
+ #{"canceled" "unpaid"})
+
(defn get-deletion-delay
"Calculate the next deleted-at for a resource (file, team, etc) in function
of team settings"
[team]
- (if-let [subscription (get team :subscription)]
+ (if-let [{:keys [type status]} (get team :subscription)]
(cond
- (and (= (:type subscription) "unlimited")
- (= (:status subscription) "active"))
+ (and (= "unlimited" type) (not (contains? canceled-status status)))
(dt/duration {:days 30})
- (and (= (:type subscription) "enterprise")
- (= (:status subscription) "active"))
+ (and (= "enterprise" type) (not (contains? canceled-status status)))
(dt/duration {:days 90})
:else
From 1f15e9b81e5a87b9591e6e2a681b2882ffaefe7b Mon Sep 17 00:00:00 2001
From: Florian Schroedl
Date: Wed, 23 Jul 2025 14:52:39 +0200
Subject: [PATCH 03/17] :sparkles: Fix spacing token for frame children
---
common/src/app/common/types/token.cljc | 19 +-
.../data/workspace/tokens/application.cljs | 199 ++++++++++--------
.../main/ui/workspace/tokens/management.cljs | 7 +
.../tokens/management/context_menu.cljs | 44 ++--
.../ui/workspace/tokens/management/group.cljs | 3 +-
.../tokens/management/token_pill.cljs | 16 +-
6 files changed, 182 insertions(+), 106 deletions(-)
diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc
index 9ef1571e53..afd7279808 100644
--- a/common/src/app/common/types/token.cljc
+++ b/common/src/app/common/types/token.cljc
@@ -92,19 +92,32 @@
(def opacity-keys (schema-keys schema:opacity))
-(def ^:private schema:spacing
+(def ^:private schema:spacing-gap
[:map
[:row-gap {:optional true} token-name-ref]
- [:column-gap {:optional true} token-name-ref]
+ [:column-gap {:optional true} token-name-ref]])
+
+(def ^:private schema:spacing-padding
+ [:map
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
- [:p4 {:optional true} token-name-ref]
+ [:p4 {:optional true} token-name-ref]])
+
+(def ^:private schema:spacing-margin
+ [:map
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
+(def ^:private schema:spacing
+ (reduce mu/union [schema:spacing-gap
+ schema:spacing-padding
+ schema:spacing-margin]))
+
+(def spacing-margin-keys (schema-keys schema:spacing-margin))
+
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions
diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs
index 0ae82fd980..b457963c01 100644
--- a/frontend/src/app/main/data/workspace/tokens/application.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/application.cljs
@@ -31,88 +31,6 @@
(declare token-properties)
-;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
-
-(defn apply-token
- "Apply `attributes` that match `token` for `shape-ids`.
-
- Optionally remove attributes from `attributes-to-remove`,
- this is useful for applying a single attribute from an attributes set
- while removing other applied tokens from this set."
- [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
- (ptk/reify ::apply-token
- ptk/WatchEvent
- (watch [_ state _]
- ;; We do not allow to apply tokens while text editor is open.
- (when (empty? (get state :workspace-editor-state))
- (when-let [tokens (some-> (dsh/lookup-file-data state)
- (get :tokens-lib)
- (ctob/get-tokens-in-active-sets))]
- (->> (sd/resolve-tokens tokens)
- (rx/mapcat
- (fn [resolved-tokens]
- (let [undo-id (js/Symbol)
- objects (dsh/lookup-page-objects state)
-
- shape-ids (or (->> (select-keys objects shape-ids)
- (filter (fn [[_ shape]]
- (ctt/any-appliable-attr? attributes (:type shape))))
- (keys))
- [])
-
- resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
- tokenized-attributes (cft/attributes-map attributes token)]
- (rx/of
- (st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
- (dwu/start-undo-transaction undo-id)
- (dwsh/update-shapes shape-ids (fn [shape]
- (cond-> shape
- attributes-to-remove
- (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
- :always
- (update :applied-tokens merge tokenized-attributes))))
- (when on-update-shape
- (on-update-shape resolved-value shape-ids attributes))
- (dwu/commit-undo-transaction undo-id)))))))))))
-
-(defn unapply-token
- "Removes `attributes` that match `token` for `shape-ids`.
-
- Doesn't update shape attributes."
- [{:keys [attributes token shape-ids] :as _props}]
- (ptk/reify ::unapply-token
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of
- (let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
- (dwsh/update-shapes
- shape-ids
- (fn [shape]
- (update shape :applied-tokens remove-token))))))))
-
-(defn toggle-token
- [{:keys [token shapes]}]
- (ptk/reify ::on-toggle-token
- ptk/WatchEvent
- (watch [_ _ _]
- (let [{:keys [attributes all-attributes on-update-shape]}
- (get token-properties (:type token))
-
- unapply-tokens?
- (cft/shapes-token-applied? token shapes (or all-attributes attributes))
-
- shape-ids (map :id shapes)]
- (if unapply-tokens?
- (rx/of
- (unapply-token {:attributes (or all-attributes attributes)
- :token token
- :shape-ids shape-ids}))
- (rx/of
- (apply-token {:attributes attributes
- :token token
- :shape-ids shape-ids
- :on-update-shape on-update-shape})))))))
-
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
;; (note that dwsh/update-shapes function returns an event)
@@ -380,6 +298,123 @@
{:ignore-touched true
:page-id page-id})))))
+;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
+
+(defn apply-token
+ "Apply `attributes` that match `token` for `shape-ids`.
+
+ Optionally remove attributes from `attributes-to-remove`,
+ this is useful for applying a single attribute from an attributes set
+ while removing other applied tokens from this set."
+ [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
+ (ptk/reify ::apply-token
+ ptk/WatchEvent
+ (watch [_ state _]
+ ;; We do not allow to apply tokens while text editor is open.
+ (when (empty? (get state :workspace-editor-state))
+ (when-let [tokens (some-> (dsh/lookup-file-data state)
+ (get :tokens-lib)
+ (ctob/get-tokens-in-active-sets))]
+ (->> (sd/resolve-tokens tokens)
+ (rx/mapcat
+ (fn [resolved-tokens]
+ (let [undo-id (js/Symbol)
+ objects (dsh/lookup-page-objects state)
+ selected-shapes (select-keys objects shape-ids)
+
+ shape-ids (or (->> selected-shapes
+ (filter (fn [[_ shape]]
+ (or
+ (and (ctsl/any-layout-immediate-child? objects shape)
+ (some ctt/spacing-margin-keys attributes))
+ (ctt/any-appliable-attr? attributes (:type shape)))))
+ (keys))
+ [])
+
+ resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
+ tokenized-attributes (cft/attributes-map attributes token)]
+ (rx/of
+ (st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
+ (dwu/start-undo-transaction undo-id)
+ (dwsh/update-shapes shape-ids (fn [shape]
+ (cond-> shape
+ attributes-to-remove
+ (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
+ :always
+ (update :applied-tokens merge tokenized-attributes))))
+ (when on-update-shape
+ (on-update-shape resolved-value shape-ids attributes))
+ (dwu/commit-undo-transaction undo-id)))))))))))
+
+(defn apply-spacing-token
+ "Handles edge-case for spacing token when applying token via toggle button.
+ Splits out `shape-ids` into seperate default actions:
+ - Layouts take the `default` update function
+ - Shapes inside layout will only take margin"
+ [{:keys [token shapes]}]
+ (ptk/reify ::apply-spacing-token
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [objects (dsh/lookup-page-objects state)
+
+ {:keys [attributes on-update-shape]}
+ (get token-properties (:type token))
+
+ {:keys [other frame-children]}
+ (group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)]
+
+ (rx/of
+ (apply-token {:attributes attributes
+ :token token
+ :shape-ids (map :id other)
+ :on-update-shape on-update-shape})
+ (apply-token {:attributes ctt/spacing-margin-keys
+ :token token
+ :shape-ids (map :id frame-children)
+ :on-update-shape update-layout-item-margin}))))))
+
+(defn unapply-token
+ "Removes `attributes` that match `token` for `shape-ids`.
+
+ Doesn't update shape attributes."
+ [{:keys [attributes token shape-ids] :as _props}]
+ (ptk/reify ::unapply-token
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of
+ (let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
+ (dwsh/update-shapes
+ shape-ids
+ (fn [shape]
+ (update shape :applied-tokens remove-token))))))))
+
+(defn toggle-token
+ [{:keys [token shapes]}]
+ (ptk/reify ::on-toggle-token
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [{:keys [attributes all-attributes on-update-shape]}
+ (get token-properties (:type token))
+
+ unapply-tokens?
+ (cft/shapes-token-applied? token shapes (or all-attributes attributes))
+
+ shape-ids (map :id shapes)]
+ (if unapply-tokens?
+ (rx/of
+ (unapply-token {:attributes (or all-attributes attributes)
+ :token token
+ :shape-ids shape-ids}))
+ (rx/of
+ (case (:type token)
+ :spacing
+ (apply-spacing-token {:token token
+ :shapes shapes})
+ (apply-token {:attributes attributes
+ :token token
+ :shape-ids shape-ids
+ :on-update-shape on-update-shape}))))))))
+
;; Map token types to different properties used along the cokde ---------------------------------------------
;; FIXME: the values should be lazy evaluated, probably a function,
diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs
index bfc533364b..19d606c955 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs
@@ -2,6 +2,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
@@ -61,6 +62,10 @@
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
+ is-selected-inside-layout
+ (mf/with-memo [selected-shapes objects]
+ (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
+
active-theme-tokens
(mf/with-memo [tokens-lib]
(if tokens-lib
@@ -148,6 +153,7 @@
:is-open (get open-status type false)
:type type
:selected-shapes selected-shapes
+ :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens tokens}]))
@@ -155,5 +161,6 @@
[:> token-group* {:key (name type)
:type type
:selected-shapes selected-shapes
+ :is-selected-inside-layout :is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens []}])]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
index bc96b1ad76..86a054e336 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
+ [app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
@@ -34,11 +35,13 @@
(some #(contains? m %) ks))
(defn clean-separators
- "Cleans up `:separator` inside of `items`
- Will clean consecutive items like `[:separator :separator {}]`
- And will return nil for lists consisting only of `:separator` items."
+ "Cleans up `:separator` inside of `items` with these rules:
+ - Clean consecutive items like `[:separator :separator {}]`
+ - Returns nil for lists consisting only of `:separator` items.
+ - Removes `:separator` at the beginning of the `items`"
[items]
- (let [items' (dedupe items)]
+ (let [items' (->> (dedupe items)
+ (drop-while #(= % :separator)))]
(when-not (every? #(= % :separator) items')
items')))
@@ -190,7 +193,7 @@
-(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}]
+(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes is-selected-inside-layout] :as context-data}]
(let [padding-attr-labels {:p1 "Padding top"
:p2 "Padding right"
:p3 "Padding bottom"
@@ -209,7 +212,9 @@
:m2 "Margin right"
:m3 "Margin bottom"
:m4 "Margin left"}
- margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels)
+ margin-items (when (or
+ is-selected-inside-layout
+ (key-in-map? allowed-shape-attributes margin-attr-labels))
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels margin-attr-labels
@@ -224,11 +229,13 @@
:hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-spacing}
context-data)]
- (concat gap-items
- (when padding-items [:separator])
- padding-items
- (when margin-items [:separator])
- margin-items)))
+ (->> (concat
+ gap-items
+ [:separator]
+ padding-items
+ [:separator]
+ margin-items)
+ (clean-separators))))
(defn sizing-attribute-actions [context-data]
(->>
@@ -446,9 +453,17 @@
(mf/defc token-context-menu-tree
[{:keys [width errors] :as mdata}]
- (let [objects (mf/deref refs/workspace-page-objects)
+ (let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
- selected-shapes (into [] (keep (d/getf objects)) selected)
+
+ selected-shapes
+ (mf/with-memo [selected objects]
+ (into [] (keep (d/getf objects)) selected))
+
+ is-selected-inside-layout
+ (mf/with-memo [selected-shapes objects]
+ (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
+
token-name (:token-name mdata)
token (mf/deref (refs/workspace-token-in-selected-set token-name))
selected-token-set-name (mf/deref refs/selected-token-set-name)]
@@ -457,7 +472,8 @@
:token token
:errors errors
:selected-token-set-name selected-token-set-name
- :selected-shapes selected-shapes}]]))
+ :selected-shapes selected-shapes
+ :is-selected-inside-layout is-selected-inside-layout}]]))
(mf/defc token-context-menu
[]
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
index c96ed54cdf..2a55e65960 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
@@ -42,7 +42,7 @@
(mf/defc token-group*
{::mf/private true}
- [{:keys [type tokens selected-shapes active-theme-tokens is-open]}]
+ [{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open]}]
(let [{:keys [modal title]}
(get dwta/token-properties type)
editing-ref (mf/deref refs/workspace-editor-state)
@@ -115,6 +115,7 @@
{:key (:name token)
:token token
:selected-shapes selected-shapes
+ :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])]])]]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs
index fa4890d9f0..669eb6056d 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs
@@ -21,6 +21,7 @@
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
+ [clojure.set :as set]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -164,17 +165,20 @@
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
- [selected-shapes attrs]
- (some (fn [shape]
- (ctt/any-appliable-attr? attrs (:type shape)))
- selected-shapes))
+ [selected-shapes attrs & {:keys [selected-inside-layout?]}]
+ (or
+ ;; Edge-case for allowing margin attribute on shapes inside layout parent
+ (and selected-inside-layout? (set/subset? ctt/spacing-margin-keys attrs))
+ (some (fn [shape]
+ (ctt/any-appliable-attr? attrs (:type shape)))
+ selected-shapes)))
(def token-types-with-status-icon
#{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width})
(mf/defc token-pill*
{::mf/wrap [mf/memo]}
- [{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}]
+ [{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes))
@@ -201,7 +205,7 @@
has-selected?
(not applied?)
(not half-applied?)
- (not (attributes-match-selection? selected-shapes attributes)))
+ (not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
;; FIXME: move to context or props
can-edit? (:can-edit (deref refs/permissions))
From 8c96a617be5e252b1ad48f8d72257e06a303af20 Mon Sep 17 00:00:00 2001
From: Florian Schroedl
Date: Wed, 23 Jul 2025 15:35:52 +0200
Subject: [PATCH 04/17] :sparkles: Add test for spacing token application rules
---
.../tokens/management/context_menu.cljs | 13 +++---
.../tokens/logic/token_actions_test.cljs | 45 +++++++++++++++++++
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
index 86a054e336..c0d5a2cc69 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
@@ -456,17 +456,20 @@
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
+ token-name (:token-name mdata)
+ token (mf/deref (refs/workspace-token-in-selected-set token-name))
+ token-type (:type token)
+ selected-token-set-name (mf/deref refs/selected-token-set-name)
+
selected-shapes
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
- (mf/with-memo [selected-shapes objects]
- (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
+ (mf/with-memo [token-type selected-shapes objects]
+ (when (= :spacing token-type)
+ (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))]
- token-name (:token-name mdata)
- token (mf/deref (refs/workspace-token-in-selected-set token-name))
- selected-token-set-name (mf/deref refs/selected-token-set-name)]
[:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width
:token token
diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
index 8c87194ddd..19b66579c8 100644
--- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
+++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
@@ -645,6 +645,51 @@
(t/is (= (:r1 (:applied-tokens rect-without-token')) (:name target-token)))
(t/is (= (:r1 (:applied-tokens rect-with-other-token-2')) (:name target-token)))))))))))
+(t/deftest test-toggle-spacing-token
+ (t/testing "applies spacing token only to layouts and layout children"
+ (t/async
+ done
+ (let [spacing-token {:name "spacing.md"
+ :value "16"
+ :type :spacing}
+ file (-> (setup-file-with-tokens)
+ (ctho/add-frame-with-child :frame-layout :rect-in-layout
+ {:frame-params {:layout :grid}})
+ (ctho/add-rect :rect-regular)
+ (update-in [:data :tokens-lib]
+ #(ctob/add-token-in-set % "Set A" (ctob/make-token spacing-token))))
+ store (ths/setup-store file)
+ frame-layout (cths/get-shape file :frame-layout)
+ rect-in-layout (cths/get-shape file :rect-in-layout)
+ rect-regular (cths/get-shape file :rect-regular)
+ events [(dwta/toggle-token {:token (toht/get-token file "spacing.md")
+ :shapes [frame-layout rect-in-layout rect-regular]})]]
+ (tohs/run-store-async
+ store done events
+ (fn [new-state]
+ (let [file' (ths/get-file-from-state new-state)
+ frame-layout' (cths/get-shape file' :frame-layout)
+ rect-in-layout' (cths/get-shape file' :rect-in-layout)
+ rect-regular' (cths/get-shape file' :rect-regular)]
+
+ (t/testing "frame with layout gets all spacing attributes"
+ (t/is (= "spacing.md" (:column-gap (:applied-tokens frame-layout'))))
+ (t/is (= "spacing.md" (:row-gap (:applied-tokens frame-layout'))))
+ (t/is (= 16 (get-in frame-layout' [:layout-gap :column-gap])))
+ (t/is (= 16 (get-in frame-layout' [:layout-gap :row-gap]))))
+
+ (t/testing "shape inside layout frame gets only margin attributes"
+ (t/is (= "spacing.md" (:m1 (:applied-tokens rect-in-layout'))))
+ (t/is (= "spacing.md" (:m2 (:applied-tokens rect-in-layout'))))
+ (t/is (= "spacing.md" (:m3 (:applied-tokens rect-in-layout'))))
+ (t/is (= "spacing.md" (:m4 (:applied-tokens rect-in-layout'))))
+ (t/is (nil? (:column-gap (:applied-tokens rect-in-layout'))))
+ (t/is (nil? (:row-gap (:applied-tokens rect-in-layout'))))
+ (t/is (= {:m1 16, :m2 16, :m3 16, :m4 16} (get rect-in-layout' :layout-item-margin))))
+
+ (t/testing "regular shape doesn't get spacing attributes"
+ (t/is (nil? (:applied-tokens rect-regular')))))))))))
+
(t/deftest test-detach-styles-color
(t/testing "applying a color token to a shape with color styles should detach the styles"
(t/async
From 019bc2f1832df77e672e34076ef98ebb7e97a504 Mon Sep 17 00:00:00 2001
From: Andrey Antukh
Date: Mon, 7 Jul 2025 15:56:37 +0200
Subject: [PATCH 05/17] :sparkles: Add migrations handling on file snapshots
---
CHANGES.md | 1 +
backend/src/app/migrations.clj | 5 +-
.../sql/0140-mod-file-change-table.sql | 2 +
.../src/app/rpc/commands/files_snapshot.clj | 52 ++++++++++++++-----
common/src/app/common/files/migrations.cljc | 2 +-
5 files changed, 46 insertions(+), 16 deletions(-)
create mode 100644 backend/src/app/migrations/sql/0140-mod-file-change-table.sql
diff --git a/CHANGES.md b/CHANGES.md
index e3806a46e4..9106360ef3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -57,6 +57,7 @@
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
- Fix UI theme selection from main menu [Taiga #11567](https://tree.taiga.io/project/penpot/issue/11567)
+- Add missing migration information to file snapshots [Github #686](https://github.com/penpot/penpot/pull/6864)
## 2.8.0
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 795a9bea5c..1c51365a9c 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -438,7 +438,10 @@
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
{:name "0139-mod-file-change-table.sql"
- :fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
+ :fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}
+
+ {:name "0140-mod-file-change-table.sql"
+ :fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]
diff --git a/backend/src/app/migrations/sql/0140-mod-file-change-table.sql b/backend/src/app/migrations/sql/0140-mod-file-change-table.sql
new file mode 100644
index 0000000000..6189edb140
--- /dev/null
+++ b/backend/src/app/migrations/sql/0140-mod-file-change-table.sql
@@ -0,0 +1,2 @@
+ALTER TABLE file_change
+ ADD COLUMN migrations text[];
diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj
index 71689560a5..bcfbad9428 100644
--- a/backend/src/app/rpc/commands/files_snapshot.clj
+++ b/backend/src/app/rpc/commands/files_snapshot.clj
@@ -8,6 +8,7 @@
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
+ [app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
@@ -15,6 +16,7 @@
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
+ [app.features.file-migrations :refer [reset-migrations!]]
[app.main :as-alias main]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
@@ -27,6 +29,13 @@
[app.util.time :as dt]
[cuerdas.core :as str]))
+(defn decode-row
+ [{:keys [migrations] :as row}]
+ (when row
+ (cond-> row
+ (some? migrations)
+ (assoc :migrations (db/decode-pgarray migrations)))))
+
(def sql:get-file-snapshots
"WITH changes AS (
SELECT id, label, revn, created_at, created_by, profile_id
@@ -74,10 +83,7 @@
(assert (#{:system :user :admin} created-by)
"expected valid keyword for created-by")
- (let [conn
- (db/get-connection cfg)
-
- created-by
+ (let [created-by
(name created-by)
deleted-at
@@ -101,12 +107,15 @@
(blob/encode (:data file))
features
- (db/encode-pgarray (:features file) conn "text")]
+ (into-array (:features file))
- (l/debug :hint "creating file snapshot"
- :file-id (str (:id file))
- :id (str snapshot-id)
- :label label)
+ migrations
+ (into-array (:migrations file))]
+
+ (l/dbg :hint "creating file snapshot"
+ :file-id (str (:id file))
+ :id (str snapshot-id)
+ :label label)
(db/insert! cfg :file-change
{:id snapshot-id
@@ -114,6 +123,7 @@
:data data
:version (:version file)
:features features
+ :migrations migrations
:profile-id profile-id
:file-id (:id file)
:label label
@@ -159,7 +169,17 @@
{:file-id file-id
:id snapshot-id}
{::db/for-share true})
- (feat.fdata/resolve-file-data cfg))]
+ (feat.fdata/resolve-file-data cfg)
+ (decode-row))
+
+ ;; If snapshot has tracked applied migrations, we reuse them,
+ ;; if not we take a safest set of migrations as starting
+ ;; point. This is because, at the time of implementing
+ ;; snapshots, migrations were not taken into account so we
+ ;; need to make this backward compatible in some way.
+ file (assoc file :migrations
+ (or (:migrations snapshot)
+ (fmg/generate-migrations-from-version 67)))]
(when-not snapshot
(ex/raise :type :not-found
@@ -180,12 +200,16 @@
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
- ;; If the file was already offloaded, on restring the snapshot
- ;; we are going to replace the file data, so we need to touch
- ;; the old referenced storage object and avoid possible leaks
+ ;; If the file was already offloaded, on restoring the snapshot we
+ ;; are going to replace the file data, so we need to touch the old
+ ;; referenced storage object and avoid possible leaks
(when (feat.fdata/offloaded? file)
(sto/touch-object! storage (:data-ref-id file)))
+ ;; In the same way, on reseting the file data, we need to restore
+ ;; the applied migrations on the moment of taking the snapshot
+ (reset-migrations! conn file)
+
(db/update! conn :file
{:data (:data snapshot)
:revn (inc (:revn file))
@@ -253,7 +277,7 @@
:deleted-at nil}
{:id snapshot-id}
{::db/return-keys true})
- (dissoc :data :features)))
+ (dissoc :data :features :migrations)))
(defn- get-snapshot
"Get a minimal snapshot from database and lock for update"
diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc
index 5973451a94..a603e9b922 100644
--- a/common/src/app/common/files/migrations.cljc
+++ b/common/src/app/common/files/migrations.cljc
@@ -81,7 +81,7 @@
(update :migrations set/union diff)
(vary-meta assoc ::migrated (not-empty diff)))))
-(defn- generate-migrations-from-version
+(defn generate-migrations-from-version
"A function that generates new format migration from the old,
version based migration system"
[version]
From f6b97af148fc816e8d01908ad7e0632158de7ad2 Mon Sep 17 00:00:00 2001
From: Florian Schroedl
Date: Thu, 24 Jul 2025 15:09:09 +0200
Subject: [PATCH 06/17] :bug: Fix spacing menu not available in dimensions
token
---
.../app/main/ui/workspace/tokens/management/context_menu.cljs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
index c0d5a2cc69..7fe47addf4 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
@@ -467,7 +467,7 @@
is-selected-inside-layout
(mf/with-memo [token-type selected-shapes objects]
- (when (= :spacing token-type)
+ (when (contains? #{:spacing :dimensions} token-type)
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))]
[:ul {:class (stl/css :context-list)}
From 58a843ea2339a9c5cf739701d6a55dd38cd75af4 Mon Sep 17 00:00:00 2001
From: Florian Schroedl
Date: Thu, 24 Jul 2025 15:38:32 +0200
Subject: [PATCH 07/17] :sparkles: Remove token when applying tyopgraphic asset
style
---
common/src/app/common/logic/shapes.cljc | 12 +++-
.../data/workspace/tokens/application.cljs | 56 +++++++------------
2 files changed, 30 insertions(+), 38 deletions(-)
diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc
index 6590c7bed8..1f41a71c54 100644
--- a/common/src/app/common/logic/shapes.cljc
+++ b/common/src/app/common/logic/shapes.cljc
@@ -11,6 +11,7 @@
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.logic.variant-properties :as clvp]
+ [app.common.text :as ct]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.pages-list :as ctpl]
@@ -21,9 +22,12 @@
[app.common.uuid :as uuid]
[clojure.set :as set]))
+(def text-typography-attrs (set ct/text-typography-attrs))
+
(defn- generate-unapply-tokens
"When updating attributes that have a token applied, we must unapply it, because the value
- of the attribute now has been given directly, and does not come from the token."
+ of the attribute now has been given directly, and does not come from the token.
+ When applying a typography asset style we also unapply any typographic tokens."
[changes objects changed-sub-attr]
(let [new-objects (pcb/get-objects changes)
mod-obj-changes (->> (:redo-changes changes)
@@ -32,7 +36,11 @@
text-changed-attrs
(fn [shape]
(let [new-shape (get new-objects (:id shape))
- attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))]
+ attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))
+ ;; Unapply token when applying typography asset style
+ attrs (if (set/intersection text-typography-attrs attrs)
+ (into attrs cto/typography-keys)
+ attrs)]
(apply set/union (map cto/shape-attr->token-attrs attrs))))
check-attr (fn [shape changes attr]
diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs
index b457963c01..c011cabcc4 100644
--- a/frontend/src/app/main/data/workspace/tokens/application.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/application.cljs
@@ -250,53 +250,37 @@
(dwsl/update-layout-child shape-ids props {:ignore-touched true
:page-id page-id}))))))))
+(defn generate-text-shape-update
+ [txt-attrs shape-ids page-id]
+ (let [update-node? (fn [node]
+ (or (txt/is-text-node? node)
+ (txt/is-paragraph-node? node)))
+ update-fn (fn [node _]
+ (-> node
+ (d/txt-merge txt-attrs)
+ (cty/remove-typography-from-node)))]
+ (dwsh/update-shapes shape-ids
+ #(txt/update-text-content % update-node? update-fn nil)
+ {:ignore-touched true
+ :page-id page-id})))
+
(defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
([value shape-ids _attributes page-id]
- (let [update-node? (fn [node]
- (or (txt/is-text-node? node)
- (txt/is-paragraph-node? node)))
- update-fn (fn [node _]
- (-> node
- (d/txt-merge {:line-height value})
- (cty/remove-typography-from-node)))]
- (when (number? value)
- (dwsh/update-shapes shape-ids
- #(txt/update-text-content % update-node? update-fn nil)
- {:ignore-touched true
- :page-id page-id})))))
+ (when (number? value)
+ (generate-text-shape-update {:line-height value} shape-ids page-id))))
(defn update-letter-spacing
([value shape-ids attributes] (update-letter-spacing value shape-ids attributes nil))
([value shape-ids _attributes page-id]
- (let [update-node? (fn [node]
- (or (txt/is-text-node? node)
- (txt/is-paragraph-node? node)))
- update-fn (fn [node _]
- (-> node
- (d/txt-merge {:letter-spacing (str value)})
- (cty/remove-typography-from-node)))]
- (when (number? value)
- (dwsh/update-shapes shape-ids
- #(txt/update-text-content % update-node? update-fn nil)
- {:ignore-touched true
- :page-id page-id})))))
+ (when (number? value)
+ (generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
([value shape-ids _attributes page-id]
- (let [update-node? (fn [node]
- (or (txt/is-text-node? node)
- (txt/is-paragraph-node? node)))
- update-fn (fn [node _]
- (-> node
- (d/txt-merge {:font-size (str value)})
- (cty/remove-typography-from-node)))]
- (when (number? value)
- (dwsh/update-shapes shape-ids
- #(txt/update-text-content % update-node? update-fn nil)
- {:ignore-touched true
- :page-id page-id})))))
+ (when (number? value)
+ (generate-text-shape-update {:font-size (str value)} shape-ids page-id))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
From 5ae4dde222203caadf59d964bf07e297c14db96b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?=
Date: Fri, 25 Jul 2025 12:30:56 +0200
Subject: [PATCH 08/17] :books: Add font size token doc (#6972)
---
docs/user-guide/design-tokens/index.njk | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/user-guide/design-tokens/index.njk b/docs/user-guide/design-tokens/index.njk
index 3de6a9dbae..df0d4d7698 100644
--- a/docs/user-guide/design-tokens/index.njk
+++ b/docs/user-guide/design-tokens/index.njk
@@ -205,6 +205,10 @@ title: 10· Design Tokens
Y Position (dimension)
The Y property specifies the position of the element on the Y axis of the canvas.
+Font Size
+Font size tokens allow you to define and standardize font-size values across your design system. These tokens can be applied to the font-size property in text layers, ensuring consistent typography throughout your designs.
+Font size token values are always computed as px (pixels).
+
Opacity
Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.
Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with `%` sign at the end of the value. For example, you can use 45% which would resolve to .45 .
From b7a8747f005249d7c393992878876b8bb1666a3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?=
Date: Fri, 25 Jul 2025 13:20:39 +0200
Subject: [PATCH 09/17] :books: Add doc for tokens zip file import option
(#6973)
---
docs/user-guide/design-tokens/index.njk | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/user-guide/design-tokens/index.njk b/docs/user-guide/design-tokens/index.njk
index df0d4d7698..186de28280 100644
--- a/docs/user-guide/design-tokens/index.njk
+++ b/docs/user-guide/design-tokens/index.njk
@@ -382,7 +382,15 @@ title: 10· Design Tokens
Import Options
-Single file
+
+ZIP file
+You can import tokens from a .zip file. This file can either contain a single JSON file or a folder structure with multiple files. The ZIP import option provides flexibility for organizing your tokens before importing them into Penpot.
+
+ If the ZIP contains a single JSON file, it will be imported as a single set of tokens.
+ If the ZIP contains a folder structure, each file and folder will be interpreted as separate token sets, following the same rules as the multifile import.
+
+
+Single JSON file
You can import a JSON file comprising all tokens, token sets and token themes.
When importing a single file, the first-level keys of the json file will be interpreted as the set name.
From 54fcd58531ab0f20ef2404a906332747c6920ff8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?=
Date: Fri, 25 Jul 2025 13:20:52 +0200
Subject: [PATCH 10/17] :books: Add doc for resizing text (#6974)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* :books: Add doc for resizing text
* :books: Update docs for text resizing
Co-authored-by: Madalena Melo
Signed-off-by: andrés gonzález
---------
Signed-off-by: andrés gonzález
Co-authored-by: Madalena Melo
---
docs/user-guide/objects/index.njk | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/user-guide/objects/index.njk b/docs/user-guide/objects/index.njk
index 5faba1e1ca..9653ec3670 100644
--- a/docs/user-guide/objects/index.njk
+++ b/docs/user-guide/objects/index.njk
@@ -142,6 +142,11 @@ a design.
+Tips for resizing
+
+ Double-click on the right side of the bounding box to set the resize setting to auto-width.
+ Double-click on the bottom side of the bounding box to set the resize setting to auto-height.
+
Edit and style text content
Press Enter with a text layer selected to start editing the text content. You can style parts of the text content as rich text.
From 97fc7702b801be0580c76688617891ad751a9195 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?=
Date: Fri, 25 Jul 2025 14:53:32 +0200
Subject: [PATCH 11/17] :books: Improve and clarify 'Hide and lock layers'
section (#6975)
---
docs/user-guide/layer-basics/index.njk | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/docs/user-guide/layer-basics/index.njk b/docs/user-guide/layer-basics/index.njk
index 317857423c..1667d8886e 100644
--- a/docs/user-guide/layer-basics/index.njk
+++ b/docs/user-guide/layer-basics/index.njk
@@ -34,7 +34,13 @@ desc: Master layer basics with Penpot's user guide! Learn to create, manipulate,
Layers are displayed from the bottom to the top of the layer stack, with layers above on the stack being shown on top in the image.
Hide and lock layers
-Click on the eye icon to change the visibility of a layer. Click on the lock icon to lock or unlock a layer. A locked layer can not be modified.
+
+Hide and show layers
+You can control the visibility of any layer by clicking the eye icon next to it in the Layers panel. When a layer is hidden, it will not appear on the canvas, but you can still select it in the Layers panel, move its order, or modify its properties. The eye icon always indicates whether a layer is visible or hidden, making it easy to manage complex designs.
+
+Lock and unlock layers
+Locking a layer helps prevent accidental changes or movement on the canvas. When a layer is locked, it cannot be moved or edited directly in the canvas area. However, you can still select a locked layer in the Layers panel and adjust its properties, such as color, effects, or name. The lock icon next to the layer’s name shows its locked status, helping you keep your design organized and protected.
+
From ade5eecf80ecaff1d5c9fee4dbef3201f6dcc77e Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 23 Jul 2025 12:00:13 +0200
Subject: [PATCH 12/17] :bug: Fix component changes not propagated
---
.../app/main/data/workspace/libraries.cljs | 27 ++++++++++++-------
.../app/main/data/workspace/shape_layout.cljs | 9 ++++---
2 files changed, 23 insertions(+), 13 deletions(-)
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 4a06dd72c5..bc147df275 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -1095,15 +1095,24 @@
(when (seq (:redo-changes changes))
(rx/of (dch/commit-changes changes)))
(when-not (empty? updated-frames)
- (rx/merge
- (rx/of (ptk/data-event :layout/update {:ids (map :id updated-frames) :undo-group undo-group}))
- (->> (rx/from updated-frames)
- (rx/mapcat
- (fn [shape]
- (rx/of
- (dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
- (when-not (= (:frame-id shape) uuid/zero)
- (dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame"))))))))
+ (let [frames-by-page (->> updated-frames
+ (group-by :page-id))]
+ (rx/merge
+ ;; Emit one layout/update event for each page
+ (rx/from
+ (map (fn [[page-id frames]]
+ (ptk/data-event :layout/update
+ {:page-id page-id
+ :ids (map :id frames)
+ :undo-group undo-group}))
+ frames-by-page))
+ (->> (rx/from updated-frames)
+ (rx/mapcat
+ (fn [shape]
+ (rx/of
+ (dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
+ (when-not (= (:frame-id shape) uuid/zero)
+ (dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame")))))))))
(when (not= file-id library-id)
;; When we have just updated the library file, give some time for the
diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs
index 9880d68293..1d1ac195d4 100644
--- a/frontend/src/app/main/data/workspace/shape_layout.cljs
+++ b/frontend/src/app/main/data/workspace/shape_layout.cljs
@@ -131,11 +131,12 @@
;; they are process together. It will get a better performance.
(rx/buffer-time 100)
(rx/filter #(d/not-empty? %))
- (rx/map
+ (rx/mapcat
(fn [data]
- (let [page-id (->> data (keep :page-id) first)
- ids (reduce #(into %1 (:ids %2)) #{} data)]
- (update-layout-positions {:page-id page-id :ids ids}))))
+ (->> (group-by :page-id data)
+ (map (fn [[page-id items]]
+ (let [ids (reduce #(into %1 (:ids %2)) #{} items)]
+ (update-layout-positions {:page-id page-id :ids ids})))))))
(rx/take-until stopper))))))
(defn finalize-shape-layout
From 46c440fef2d80b1bff971c3d69845ee9d4526467 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Mon, 28 Jul 2025 17:48:05 +0200
Subject: [PATCH 13/17] :bug: Fix remove color button in the gradient editor
(#6993)
---
frontend/src/app/main/ui/workspace/colorpicker.cljs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs
index bb40c905c3..e96947174a 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs
@@ -275,10 +275,8 @@
handle-gradient-remove-stop
(mf/use-fn
- (mf/deps state)
(fn [index]
- (when (> (count (:stops state)) 2)
- (st/emit! (dc/remove-gradient-stop index)))))
+ (st/emit! (dc/remove-gradient-stop index))))
handle-stop-edit-start
(mf/use-fn
From f5298f51e727ab8593fcb080c14d0b39b39c3506 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Tue, 29 Jul 2025 09:50:55 +0200
Subject: [PATCH 14/17] :bug: Fix the context menu always closes after any
action (#6944)
---
CHANGES.md | 1 +
frontend/src/app/main/ui/workspace/context_menu.cljs | 12 ++++++++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 9106360ef3..361112031b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -50,6 +50,7 @@
- Fix problem when changing between flex/grid layout [Taiga #11625](https://tree.taiga.io/project/penpot/issue/11625)
- Fix opacity on stroke gradients [Taiga #11646](https://tree.taiga.io/project/penpot/issue/11646)
- Fix change from gradient to solid color [Taiga #11648](https://tree.taiga.io/project/penpot/issue/11648)
+- Fix the context menu always closes after any action [Taiga #11624](https://tree.taiga.io/project/penpot/issue/11624)
## 2.8.1 (Unreleased)
diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs
index dc86ee4764..99e81678cc 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/context_menu.cljs
@@ -59,6 +59,14 @@
on-unmount children is-selected icon disabled value]}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
+
+ on-click'
+ (mf/use-fn
+ (mf/deps on-click)
+ (fn [event]
+ (st/emit! dw/hide-context-menu)
+ (when on-click (on-click event))))
+
on-pointer-enter
(mf/use-fn
(fn []
@@ -96,7 +104,7 @@
:disabled disabled
:data-value value
:ref set-dom-node
- :on-click on-click
+ :on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span
@@ -110,7 +118,7 @@
:disabled disabled
:ref set-dom-node
:data-value value
- :on-click on-click
+ :on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span {:class (stl/css :title)} title]
From c195c07a3f4750891642e72e13790c66d26a7641 Mon Sep 17 00:00:00 2001
From: Yamila Moreno
Date: Tue, 29 Jul 2025 10:33:10 +0200
Subject: [PATCH 15/17] :whale: Update Imagemagick version
---
docker/images/Dockerfile.backend | 2 +-
manage.sh | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend
index b8d59818e9..3535cffd0c 100644
--- a/docker/images/Dockerfile.backend
+++ b/docker/images/Dockerfile.backend
@@ -125,7 +125,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
-COPY --from=penpotapp/imagemagick:7.1.1-47 /opt/imagick /opt/imagick
+COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
USER penpot:penpot
diff --git a/manage.sh b/manage.sh
index 90f69239b9..32b348ffe7 100755
--- a/manage.sh
+++ b/manage.sh
@@ -7,7 +7,7 @@ export DEVENV_PNAME="penpotdev";
export CURRENT_USER_ID=$(id -u);
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD);
-export IMAGEMAGICK_VERSION=7.1.1-47
+export IMAGEMAGICK_VERSION=7.1.2-0
# Safe directory to avoid ownership errors with Git
git config --global --add safe.directory /home/penpot/penpot || true
From efaf6573bd2b3020e64a233842ed7345c2a1235f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marina=20L=C3=B3pez?=
Date: Tue, 29 Jul 2025 12:42:11 +0200
Subject: [PATCH 16/17] :paperclip: Update monetization texts (#7002)
---
.../src/app/main/ui/dashboard/subscription.cljs | 2 +-
.../src/app/main/ui/settings/subscription.cljs | 12 ++++++------
frontend/translations/en.po | 14 ++++----------
frontend/translations/es.po | 14 ++++----------
4 files changed, 15 insertions(+), 27 deletions(-)
diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs
index e751e27443..d320a687e4 100644
--- a/frontend/src/app/main/ui/dashboard/subscription.cljs
+++ b/frontend/src/app/main/ui/dashboard/subscription.cljs
@@ -75,7 +75,7 @@
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.unlimited-plan")
- :bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom", subscription-href)
+ :bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-text", subscription-href)
:has-dropdown true}])
"enterprise"
diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs
index e72c4f9b86..e693da8f33 100644
--- a/frontend/src/app/main/ui/settings/subscription.cljs
+++ b/frontend/src/app/main/ui/settings/subscription.cljs
@@ -365,17 +365,17 @@
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
- :benefits [(tr "subscription.settings.enterprise.security"),
+ :benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
- (tr "subscription.settings.enterprise.unlimited-storage")]
+ (tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
- :benefits [(tr "subscription.settings.enterprise.security"),
+ :benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
- (tr "subscription.settings.enterprise.unlimited-storage")]
+ (tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]))
@@ -425,9 +425,9 @@
:price-value "$950"
:price-period (tr "subscription.settings.price-organization-month")
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
- :benefits [(tr "subscription.settings.enterprise.security"),
+ :benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
- (tr "subscription.settings.enterprise.unlimited-storage")]
+ (tr "subscription.settings.enterprise.autosave")]
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "enterprise" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information")
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index fd363eb1f1..fd85f1db7c 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -4313,11 +4313,6 @@ msgstr "Please upgrade to match your usage. Contact with the team owner: %s"
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Enterprise plan"
-#: src/app/main/ui/dashboard/subscription.cljs:77
-#, unused
-msgid "subscription.dashboard.power-up.enterprise.description"
-msgstr "Advanced security, activity logs, dedicated support and more."
-
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom"
@@ -4357,9 +4352,9 @@ msgstr "Enterprise plan (trial)"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
-msgid "subscription.dashboard.power-up.unlimited.bottom"
+msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
-"Get extra editors, more storage and backup, advanced security and more. "
+"Get extra editors, more backup, unlimited storage and more. "
"[Take a look to the Enterprise plan.|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
@@ -4417,9 +4412,8 @@ msgstr "Enterprise (trial)"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Unlimited storage and 90-day autosave versions and file recovery"
-#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
-msgid "subscription.settings.enterprise.security"
-msgstr "Advanced security"
+msgid "subscription.settings.enterprise.autosave"
+msgstr "90-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.capped-bill"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index f08ce1e96a..fe1e7d8280 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -4340,11 +4340,6 @@ msgstr "Por favor, mejóralo para adaptarlo a tu uso. Contacta con el "
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Plan Enterprise"
-#: src/app/main/ui/dashboard/subscription.cljs:77
-#, unused
-msgid "subscription.dashboard.power-up.enterprise.description"
-msgstr "Seguridad avanzada, registros de actividad, asistencia dedicada y mucho más."
-
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom"
@@ -4388,9 +4383,9 @@ msgstr "Plan Unlimited"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
-msgid "subscription.dashboard.power-up.unlimited.bottom"
+msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
-"Consigue editores adicionales, más almacenamiento y copias de seguridad, seguridad avanzada y mucho más. "
+"Consigue editores adicionales, copias de seguridad, almacenamiento ilimitado y mucho más. "
"[Echa un ojo al Plan Enterprise|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
@@ -4445,9 +4440,8 @@ msgstr "Enterprise (prueba)"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Almacenamiento ilimitado y versiones de autoguardado de 90 días y recuperación de archivos"
-#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
-msgid "subscription.settings.enterprise.security"
-msgstr "Seguridad avanzada"
+msgid "subscription.settings.enterprise.autosave"
+msgstr "Versiones guardadas automáticamente cada 90 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.capped-bill"
From 708a40bff1d468459d40801ba27c7ca2b7207bcc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?=
Date: Tue, 29 Jul 2025 13:12:54 +0200
Subject: [PATCH 17/17] :bug: Fix font selector highlight inconsistency (#6990)
* :bug: Fix font selector highlight inconsistency
* :zap: Add minor performance enhancements
---------
Co-authored-by: Andrey Antukh
---
CHANGES.md | 1 +
.../sidebar/options/menus/typography.cljs | 100 ++++++++----------
2 files changed, 48 insertions(+), 53 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 361112031b..c993f75721 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -51,6 +51,7 @@
- Fix opacity on stroke gradients [Taiga #11646](https://tree.taiga.io/project/penpot/issue/11646)
- Fix change from gradient to solid color [Taiga #11648](https://tree.taiga.io/project/penpot/issue/11648)
- Fix the context menu always closes after any action [Taiga #11624](https://tree.taiga.io/project/penpot/issue/11624)
+- Fix font selector highlight inconsistency when using keyboard navigation [Taiga #11668](https://tree.taiga.io/project/penpot/issue/11668)
## 2.8.1 (Unreleased)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
index 92fab80bad..609e4a822e 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
@@ -10,7 +10,6 @@
["react-virtualized" :as rvt]
[app.common.data :as d]
[app.common.data.macros :as dm]
- [app.common.exceptions :as ex]
[app.common.text :as txt]
[app.main.constants :refer [max-input-length]]
[app.main.data.common :as dcm]
@@ -41,25 +40,9 @@
""
(ust/format-precision value 2)))
-(defn- get-next-font
- [{:keys [id] :as current} fonts]
- (if (seq fonts)
- (let [index (d/index-of-pred fonts #(= (:id %) id))
- index (or index -1)
- next (ex/ignoring (nth fonts (inc index)))]
- (or next (first fonts)))
- current))
-
-(defn- get-prev-font
- [{:keys [id] :as current} fonts]
- (if (seq fonts)
- (let [index (d/index-of-pred fonts #(= (:id %) id))
- next (ex/ignoring (nth fonts (dec index)))]
- (or next (peek fonts)))
- current))
-
(mf/defc font-item*
- {::mf/wrap [mf/memo]}
+ {::mf/wrap [mf/memo]
+ ::mf/private true}
[{:keys [font is-current on-click style]}]
(let [item-ref (mf/use-ref)
on-click (mf/use-fn (mf/deps font) #(on-click font))]
@@ -83,7 +66,7 @@
(declare row-renderer)
-(defn filter-fonts
+(defn- filter-fonts
[{:keys [term backends]} fonts]
(let [term (str/lower term)
xform (cond-> (map identity)
@@ -96,8 +79,7 @@
(mf/defc font-selector*
[{:keys [on-select on-close current-font show-recent full-size]}]
- (let [selected (mf/use-state current-font)
- state* (mf/use-state
+ (let [state* (mf/use-state
#(do {:term "" :backends #{}}))
state (deref state*)
@@ -112,23 +94,41 @@
recent-fonts (mf/with-memo [state recent-fonts]
(filter-fonts state recent-fonts))
- full-size? (boolean (and full-size show-recent))
+ ;; Combine recent fonts with filtered fonts, avoiding duplicates
+ combined-fonts
+ (mf/with-memo [recent-fonts fonts]
+ (let [recent-ids (into #{} d/xf:map-id recent-fonts)]
+ (into recent-fonts (remove #(contains? recent-ids (:id %))) fonts)))
+
+ ;; Initialize selected with current font index
+ selected-index
+ (mf/use-state
+ (fn []
+ (or (some (fn [[idx font]]
+ (when (= (:id current-font) (:id font)) idx))
+ (map-indexed vector combined-fonts))
+ 0)))
+
+ full-size?
+ (boolean (and full-size show-recent))
select-next
(mf/use-fn
- (mf/deps fonts)
+ (mf/deps combined-fonts)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
- (swap! selected get-next-font fonts)))
+ (let [next-idx (mod (inc @selected-index) (count combined-fonts))]
+ (reset! selected-index next-idx))))
select-prev
(mf/use-fn
- (mf/deps fonts)
+ (mf/deps combined-fonts)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
- (swap! selected get-prev-font fonts)))
+ (let [prev-idx (mod (dec @selected-index) (count combined-fonts))]
+ (reset! selected-index prev-idx))))
on-select-and-close
(mf/use-fn
@@ -139,35 +139,32 @@
on-key-down
(mf/use-fn
- (mf/deps fonts)
+ (mf/deps combined-fonts)
(fn [event]
(cond
(kbd/up-arrow? event) (select-prev event)
(kbd/down-arrow? event) (select-next event)
(kbd/esc? event) (on-close)
- (kbd/enter? event) (do (on-select-and-close @selected))
+ (kbd/enter? event) (do
+ (let [selected-font (nth combined-fonts @selected-index)]
+ (on-select-and-close selected-font)))
:else (dom/focus! (mf/ref-val input)))))
on-filter-change
(mf/use-fn
(fn [event]
- (swap! state* assoc :term event)))
-
- on-select-and-close
- (mf/use-fn
- (mf/deps on-select on-close)
- (fn [font]
- (on-select font)
- (on-close)))]
+ (swap! state* assoc :term event)
+ ;; Reset selection to first item when filter changes
+ (reset! selected-index 0)))]
(mf/with-effect [fonts]
(let [key (events/listen js/document "keydown" on-key-down)]
#(events/unlistenByKey key)))
- (mf/with-effect [@selected]
+ (mf/with-effect [@selected-index]
(when-let [inst (mf/ref-val flist)]
- (when-let [index (:index @selected)]
- (.scrollToRow ^js inst index))))
+ (when (and (>= @selected-index 0) (< @selected-index (count combined-fonts)))
+ (.scrollToRow ^js inst @selected-index))))
(mf/with-effect []
(st/emit! (dsc/push-shortcuts :typography {}))
@@ -175,15 +172,12 @@
(st/emit! (dsc/pop-shortcuts :typography))))
(mf/with-effect []
- (let [index (d/index-of-pred fonts #(= (:id %) (:id current-font)))
+ (let [index (d/index-of-pred combined-fonts #(= (:id %) (:id current-font)))
inst (mf/ref-val flist)]
- (tm/schedule
- #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
- (.scrollToPosition ^js inst offset)))))
-
- (mf/with-effect [(:term state) fonts]
- (when (and (seq fonts) (not= (:id @selected) (:id (first fonts))))
- (reset! selected (first fonts))))
+ (when (and index (>= index 0))
+ (tm/schedule
+ #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
+ (.scrollToPosition ^js inst offset))))))
[:div {:class (stl/css :font-selector)}
[:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)}
@@ -200,7 +194,7 @@
:font font
:style {}
:on-click on-select-and-close
- :is-current (= (:id font) (:id @selected))}])])]
+ :is-current (= idx @selected-index)}])])]
[:div {:class (stl/css-case :fonts-list true
:fonts-list-full-size full-size?)}
@@ -208,17 +202,17 @@
(fn [props]
(let [width (unchecked-get props "width")
height (unchecked-get props "height")
- render #(row-renderer fonts @selected on-select-and-close %)]
+ render #(row-renderer combined-fonts @selected-index on-select-and-close %)]
(mf/html
[:> rvt/List #js {:height height
:ref flist
:width width
- :rowCount (count fonts)
+ :rowCount (count combined-fonts)
:rowHeight 36
:rowRenderer render}])))]]]]))
(defn row-renderer
- [fonts selected on-select props]
+ [fonts selected-index on-select props]
(let [index (unchecked-get props "index")
key (unchecked-get props "key")
style (unchecked-get props "style")
@@ -228,7 +222,7 @@
:font font
:style style
:on-click on-select
- :is-current (= (:id font) (:id selected))}])))
+ :is-current (= index selected-index)}])))
(mf/defc font-options
{::mf/wrap-props false}