From 5b1766835ff395f82e5b7a286b25474690d661c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 21 Jan 2026 09:26:48 +0100 Subject: [PATCH 1/8] :bug: Fix broken selection on duplicated shapes on new pages --- frontend/src/app/render_wasm/shape.cljs | 10 ++++++++-- frontend/src/app/worker/index.cljs | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 6475fb9e2f..7809d5ca0f 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -227,7 +227,7 @@ :svg-attrs (do (api/set-shape-svg-attrs v) - ;; Always update fills/blur/shadow to clear previous state if filters disappear + ;; Always update fills/blur/shadow to clear previous state if filters disappear (api/set-shape-fills id (:fills shape) false) (api/set-shape-blur (:blur shape)) (api/set-shape-shadows (:shadow shape))) @@ -397,12 +397,18 @@ (next es)) (throw (js/Error. "conj on a map takes map entries or seqables of map entries")))))))) +(def ^:private xf:without-id-and-type + (remove (fn [kvpair] + (let [k (key kvpair)] + (or (= k :id) + (= k :type)))))) + (defn create-shape "Instanciate a shape from a map" [attrs] (ShapeProxy. (:id attrs) (:type attrs) - (dissoc attrs :id :type))) + (into {} xf:without-id-and-type attrs))) (t/add-handlers! ;; We only add a write handler, read handler uses the dynamic dispatch diff --git a/frontend/src/app/worker/index.cljs b/frontend/src/app/worker/index.cljs index 0de440f7c1..c40f0b6fd8 100644 --- a/frontend/src/app/worker/index.cljs +++ b/frontend/src/app/worker/index.cljs @@ -58,6 +58,8 @@ (swap! state update ::snap snap/update-page old-page new-page) (swap! state update ::selection selection/update-page old-page new-page)) + (catch :default cause + (log/error :hint "error updating page index" :id page-id :cause cause)) (finally (let [elapsed (tpoint)] (log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true)))) From a1a3966d7b59f19533bc1f10f872fb2b6f5a47f8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 22 Jan 2026 08:24:13 +0100 Subject: [PATCH 2/8] =?UTF-8?q?:bug:=20Editing=20the=20text=20inside=20a?= =?UTF-8?q?=20text=20object=20doesn=E2=80=99t=20update=20the=20text=20laye?= =?UTF-8?q?r=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/workspace/shapes/text/v2_editor.cljs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index 410498e13f..ac44b9720b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -90,7 +90,8 @@ instance (dwt/create-editor editor-node canvas-node options) - update-name? (nil? content) + ;; Store original content to compare name later + original-content content on-key-up (fn [event] @@ -101,10 +102,22 @@ on-blur (fn [] (when-let [content (content/dom->cljs (dwt/get-editor-root instance))] - (st/emit! (dwt/v2-update-text-shape-content shape-id content - :update-name? update-name? - :name (gen-name instance) - :finalize? true))) + (let [state @st/state + objects (dsh/lookup-page-objects state) + shape (get objects shape-id) + current-name (:name shape) + generated-name (gen-name instance) + ;; Update name if: (1) it's a new shape (nil original content), or + ;; (2) the current name matches the generated name from original content + ;; (meaning it was never manually renamed) + update-name? (or (nil? original-content) + (and (some? current-name) + (some? original-content) + (= current-name (txt/generate-shape-name (txt/content->text original-content)))))] + (st/emit! (dwt/v2-update-text-shape-content shape-id content + :update-name? update-name? + :name generated-name + :finalize? true)))) (let [container-node (mf/ref-val container-ref)] (dom/set-style! container-node "opacity" 0))) From 835ea97be77d86da514dc45267bacf1d70c194b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 22 Jan 2026 13:27:05 +0100 Subject: [PATCH 3/8] :bug: Fix blur applied when clicking in the active page --- .../src/app/main/ui/workspace/sidebar/sitemap.cljs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index d578ccfbc5..3701bb505d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -61,7 +61,7 @@ (mf/defc page-item {::mf/wrap-props false} - [{:keys [page index deletable? selected? editing? hovering?]}] + [{:keys [page index deletable? selected? editing? hovering? current-page-id]}] (let [input-ref (mf/use-ref) id (:id page) delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) @@ -72,8 +72,10 @@ (mf/use-fn (mf/deps id) (fn [] - ;; when using the wasm renderer, apply a blur effect to the viewport canvas - (if (features/active-feature? @st/state "render-wasm/v1") + ;; For the wasm renderer, apply a blur effect to the viewport canvas + ;; when we navigate to a different page. + (if (and (features/active-feature? @st/state "render-wasm/v1") + (not= id current-page-id)) (do (wasm.api/capture-canvas-pixels) (wasm.api/apply-canvas-blur) @@ -203,12 +205,13 @@ (mf/defc page-item-wrapper {::mf/wrap-props false} - [{:keys [page-id index deletable? selected? editing?]}] + [{:keys [page-id index deletable? selected? editing? current-page-id]}] (let [page-ref (mf/with-memo [page-id] (make-page-ref page-id)) page (mf/deref page-ref)] [:& page-item {:page page :index index + :current-page-id current-page-id :deletable? deletable? :selected? selected? :editing? editing?}])) @@ -231,6 +234,7 @@ :deletable? deletable? :editing? (= page-id editing-page-id) :selected? (= page-id current-page-id) + :current-page-id current-page-id :key page-id}])]])) ;; --- Sitemap Toolbox From 3d0c6ad4213fc3e2f4393b600e25048510c2209d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 22 Jan 2026 16:00:24 +0100 Subject: [PATCH 4/8] :sparkles: Blur board titles and outlines when switching pages --- frontend/src/app/main/ui/workspace/viewport/outline.cljs | 2 +- frontend/src/app/main/ui/workspace/viewport/widgets.cljs | 2 +- frontend/src/app/main/ui/workspace/viewport_wasm.cljs | 3 ++- frontend/src/app/render_wasm/api.cljs | 5 +++-- frontend/src/app/render_wasm/api/webgl.cljs | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index bbc7931d2d..a5ba8aba4f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -144,7 +144,7 @@ modifiers (hooks/use-equal-memo modifiers) shapes (hooks/use-equal-memo shapes)] - [:g.outlines + [:g.outlines.blurrable [:& shape-outlines-render {:shapes shapes :zoom zoom :modifiers modifiers}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index c329b483a8..17da584434 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -252,7 +252,7 @@ edition (mf/deref refs/selected-edition) grid-edition? (ctl/grid-layout? objects edition)] - [:g.frame-titles + [:g.frame-titles.blurrable (for [{:keys [id parent-id] :as shape} shapes] (when (and (not= id uuid/zero) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 0530b21a06..3e516e923f 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -424,6 +424,7 @@ :xmlnsXlink "http://www.w3.org/1999/xlink" :preserveAspectRatio "xMidYMid meet" :key (str "viewport" page-id) + :id "viewport-controls" :view-box (utils/format-viewbox vbox) :ref on-viewport-ref :class (dm/str @cursor (when drawing-tool " drawing") " " (stl/css :viewport-controls)) @@ -473,7 +474,7 @@ :zoom zoom}] (when (ctl/any-layout? outlined-frame) - [:g.ghost-outline + [:g.ghost-outline.blurrable [:& outline/shape-outlines {:objects base-objects :selected selected diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 24e054cfc0..2aa0dd4ff1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1429,8 +1429,9 @@ (defn apply-canvas-blur [] - (when wasm/canvas - (dom/set-style! wasm/canvas "filter" "blur(4px)"))) + (when wasm/canvas (dom/set-style! wasm/canvas "filter" "blur(4px)")) + (let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")] + (run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur))) (defn init-wasm-module diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index 764da6dfa4..d2da5df87a 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -151,6 +151,8 @@ void main() { (.clear ^js context (.-DEPTH_BUFFER_BIT ^js context)) (.clear ^js context (.-STENCIL_BUFFER_BIT ^js context))) (dom/set-style! wasm/canvas "filter" "none") + (let [controls-to-unblur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")] + (run! #(dom/set-style! % "filter" "none") controls-to-unblur)) (set! wasm/canvas-pixels nil))) (defn capture-canvas-pixels From d112c0a33b3902119bb968d1e116cff0f0a99bf3 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 22 Jan 2026 08:37:36 +0100 Subject: [PATCH 5/8] :bug: Fix text boxes with auto-height don't update height when resized by dragging side handles --- render-wasm/src/shapes/modifiers.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index f8e0c6428a..d0db679cf7 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -188,10 +188,20 @@ fn propagate_transform( || !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()) { if let Type::Text(text_content) = &mut shape.shape_type.clone() { + let resized_selrect = math::Rect::from_xywh( + shape.selrect.left(), + shape.selrect.top(), + shape_bounds_after.width(), + shape_bounds_after.height(), + ); match text_content.grow_type() { GrowType::AutoHeight => { - if text_content.needs_update_layout() { - text_content.update_layout(shape.selrect); + // For auto-height, always update layout when width changes + // because the new width affects how text wraps + let width_changed = + !is_close_to(shape_bounds_before.width(), shape_bounds_after.width()); + if width_changed || text_content.needs_update_layout() { + text_content.update_layout(resized_selrect); } let height = text_content.size.height; let resize_transform = math::resize_matrix( @@ -204,8 +214,12 @@ fn propagate_transform( transform.post_concat(&resize_transform); } GrowType::AutoWidth => { - if text_content.needs_update_layout() { - text_content.update_layout(shape.selrect); + // For auto-width, always update layout when height changes + // because the new height affects how text flows + let height_changed = + !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()); + if height_changed || text_content.needs_update_layout() { + text_content.update_layout(resized_selrect); } let width = text_content.width(); let height = text_content.size.height; From e53f33520419bf912a15d3fe18c26196df141290 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 23 Jan 2026 09:35:53 +0100 Subject: [PATCH 6/8] :bug: Fix unhandled error on tokens modal (#8165) --- backend/src/app/email.clj | 2 -- .../src/app/main/data/style_dictionary.cljs | 11 +++++----- .../src/app/main/ui/dashboard/file_menu.cljs | 1 - .../forms/controls/color_input.cljs | 17 ++++++++------ .../forms/controls/fonts_combobox.cljs | 17 ++++++++------ .../management/forms/controls/input.cljs | 22 +++++++++++-------- .../management/forms/form_container.cljs | 13 ++++++----- .../tokens/management/forms/generic_form.cljs | 19 ++++++++-------- 8 files changed, 54 insertions(+), 48 deletions(-) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 43361039d9..44d5cd7e67 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -124,8 +124,6 @@ (throw (IllegalArgumentException. "invalid email body provided"))) (doseq [[name content] attachments] - - (prn "attachment" name) (let [attachment-part (MimeBodyPart.)] (.setFileName attachment-part ^String name) (.setContent attachment-part ^String content (str "text/plain; charset=" charset)) diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 1bf2de0d0a..53405e3073 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -126,7 +126,7 @@ If the `value` is not parseable and/or has missing references returns a map with `:errors`. If the `value` is parseable but is out of range returns a map with `warnings`." [value] - (let [missing-references? (seq (seq (cto/find-token-value-references value))) + (let [missing-references? (seq (cto/find-token-value-references value)) parsed-value (cft/parse-token-value value) out-of-scope (not (<= 0 (:value parsed-value) 1)) references (seq (cto/find-token-value-references value))] @@ -152,15 +152,14 @@ [value] (let [missing-references? (seq (cto/find-token-value-references value)) parsed-value (cft/parse-token-value value) - out-of-scope (< (:value parsed-value) 0) - references (seq (cto/find-token-value-references value))] + out-of-scope (< (:value parsed-value) 0)] (cond (and parsed-value (not out-of-scope)) parsed-value - references - {:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)] - :references references} + missing-references? + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references?)] + :references missing-references?} (and (not missing-references?) out-of-scope) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)]} diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index c2e825c2a5..dfecbc779b 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -202,7 +202,6 @@ on-restore-immediately (fn [] - (prn files) (st/emit! (modal/show {:type :confirm :title (tr "dashboard-restore-file-confirmation.title") diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index a4bfbf1b0c..9f9d395013 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -53,10 +53,12 @@ (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens @@ -131,6 +133,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? @@ -260,10 +263,10 @@ :else props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -309,7 +312,7 @@ (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value value-subfield index input-name]) @@ -422,10 +425,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index ba6a8348c2..80f2d91133 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -49,10 +49,12 @@ ;; validate data within the form state. (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value (cto/split-font-family value) - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens @@ -73,6 +75,7 @@ [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? (and (contains? (:data @form) input-name) @@ -152,10 +155,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name touched?] + (mf/with-effect [resolve-stream tokens token input-name touched? token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -200,7 +203,7 @@ [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value input-name]) @@ -276,10 +279,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index 2e57f197be..0f1b2a79b1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -139,10 +139,12 @@ (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens ;; Remove previous token when renaming a token @@ -163,6 +165,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? (and (contains? (:data @form) input-name) @@ -206,11 +209,11 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -252,6 +255,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value input-name]) @@ -298,10 +302,10 @@ (mf/spread-props props {:hint-formated true}) props)] - (mf/with-effect [resolve-stream tokens token input-name name] + (mf/with-effect [resolve-stream tokens token input-name name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -365,7 +369,7 @@ (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value value-subfield index input-name]) @@ -410,10 +414,10 @@ (mf/spread-props props {:hint-formated true}) props)] - (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index af394eadee..70797979c6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -23,19 +23,20 @@ (let [token-type (or (:type token) token-type) + tokens-in-selected-set + (mf/deref refs/workspace-all-tokens-in-selected-set) + token-path (mf/with-memo [token] (cft/token-name->path (:name token))) - all-tokens (mf/deref refs/workspace-all-tokens-map) - - all-tokens - (mf/with-memo [token-path all-tokens] - (-> (ctob/tokens-tree all-tokens) + tokens-tree-in-selected-set + (mf/with-memo [token-path tokens-in-selected-set] + (-> (ctob/tokens-tree tokens-in-selected-set) (d/dissoc-in token-path))) props (mf/spread-props props {:token-type token-type - :all-token-tree all-tokens + :tokens-tree-in-selected-set tokens-tree-in-selected-set :token token}) text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index 1a4a6b14b9..a260540a92 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -86,7 +86,7 @@ action is-create selected-token-set-id - all-token-tree + tokens-tree-in-selected-set token-type make-schema input-component @@ -111,8 +111,7 @@ token-title (str/lower (:title token-properties)) - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) + tokens (mf/deref refs/workspace-all-tokens-map) tokens (mf/with-memo [tokens token] @@ -124,8 +123,8 @@ (assoc (:name token) token))) schema - (mf/with-memo [all-token-tree active-tab] - (make-schema all-token-tree active-tab)) + (mf/with-memo [tokens-tree-in-selected-set active-tab] + (make-schema tokens-tree-in-selected-set active-tab)) initial (mf/with-memo [token] @@ -208,11 +207,11 @@ :value (:value valid-token) :description description})) (dwtp/propagate-workspace-tokens) - (modal/hide))))) - (fn [{:keys [errors]}] - (let [error-messages (wte/humanize-errors errors) - error-message (first error-messages)] - (swap! form assoc-in [:extra-errors :value] {:message error-message}))))))] + (modal/hide))) + (fn [{:keys [errors]}] + (let [error-messages (wte/humanize-errors errors) + error-message (first error-messages)] + (swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))] [:> fc/form* {:class (stl/css :form-wrapper) :form form From 5146221513c45142825d45f89f73e10738e1ea29 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 23 Jan 2026 09:50:36 +0100 Subject: [PATCH 7/8] :bug: Fix allow negative spread values on shadow token creation (#8167) * :bug: Fix allow negative spread values on shadow token creation * :tada: Add test --- CHANGES.md | 1 + frontend/playwright/ui/specs/tokens.spec.js | 186 ++++++++++++++++++ .../src/app/main/data/style_dictionary.cljs | 2 +- .../tokens/management/forms/shadow.cljs | 7 +- 4 files changed, 189 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a6e50eb8f1..fe518a5597 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,7 @@ - Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959) - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865) - Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110) +- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167) ## 2.12.1 diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index f8502b5ecb..d7715a7f4c 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -1256,6 +1256,192 @@ test.describe("Tokens: Tokens Tab", () => { ).toBeEnabled(); }); + test("User creates shadow token with negative spread", async ({ page }) => { + const emptyNameError = "Name should be at least 1 character"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]}); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Shadow`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter a value or alias with {alias}", + ), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + const offsetXField = tokensUpdateCreateModal.getByRole("textbox", { + name: "X", + }); + const offsetYField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Y", + }); + const blurField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Blur", + }); + const spreadField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Spread", + }); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Check default values + await expect(offsetXField).toHaveValue("4"); + await expect(offsetYField).toHaveValue("4"); + await expect(blurField).toHaveValue("4"); + await expect(spreadField).toHaveValue("0"); + + // 2. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 3. Invalid color → disabled + error message + await colorField.fill("1"); + + await expect( + tokensUpdateCreateModal.getByText("Invalid color value: 1"), + ).toBeVisible(); + + await expect(submitButton).toBeDisabled(); + + await colorField.fill("{missing-reference}"); + + await expect( + tokensUpdateCreateModal.getByText( + "Missing token references: missing-reference", + ), + ).toBeVisible(); + + // 4. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL FIELDS ------- + // + + // 5. Valid color → resolved + + await colorField.fill("red"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: #ff0000"), + ).toBeVisible(); + const colorSwatch = tokensUpdateCreateModal.getByTestId( + "token-form-color-bullet", + ); + await colorSwatch.click(); + const rampSelector = tokensUpdateCreateModal.getByTestId( + "value-saturation-selector", + ); + await expect(rampSelector).toBeVisible(); + await rampSelector.click({ position: { x: 50, y: 50 } }); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value:"), + ).toBeVisible(); + + const sliderOpacity = tokensUpdateCreateModal.getByTestId("slider-opacity"); + await sliderOpacity.click({ position: { x: 50, y: 0 } }); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Color" }), + ).toHaveValue(/rgba\s*\([^)]*\)/); + + // 6. Valid offset → resolved + await offsetXField.fill("3 + 3"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 6"), + ).toBeVisible(); + + await offsetYField.fill("3 + 7"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 10"), + ).toBeVisible(); + + // 7. Valid blur → resolved + + await blurField.fill("3 + 1"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 4"), + ).toBeVisible(); + + // 8. Valid spread → resolved + + await spreadField.fill("3 - 3"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 0"), + ).toBeVisible(); + + await spreadField.fill("1 - 3"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: -2"), + ).toBeVisible(); + + await nameField.fill("my-token"); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + const compositeToggle = + tokensUpdateCreateModal.getByTestId("composite-opt"); + await referenceToggle.click(); + + const referenceInput = tokensUpdateCreateModal.getByPlaceholder( + "Enter a token shadow alias", + ); + await expect(referenceInput).toBeVisible(); + + await compositeToggle.click(); + await expect(colorField).toBeVisible(); + + await referenceToggle.click(); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); + await referenceField.fill("{my-token}"); + await expect( + tokensUpdateCreateModal.getByText( + "Resolved value: - X: 6 - Y: 10 - Blur: 4 - Spread: -2", + ), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + }); + test("User creates typography token", async ({ page }) => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 53405e3073..0ed5fd68d9 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -364,7 +364,7 @@ "Parses shadow spread value (non-negative number)." [value] (let [parsed (parse-sd-token-general-value value) - valid? (and (:value parsed) (>= (:value parsed) 0))] + valid? (:value parsed)] (cond valid? parsed diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs index 53c6d8e402..c569c81e30 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -282,12 +282,7 @@ (let [n (d/parse-double blur)] (or (nil? n) (not (< n 0)))))]]] [:spread {:optional true} - [:and - [:maybe :string] - [:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")} - (fn [spread] - (let [n (d/parse-double spread)] - (or (nil? n) (not (< n 0)))))]]] + [:maybe :string]] [:color {:optional true} [:maybe :string]] [:color-result {:optional true} ::sm/any] [:inset {:optional true} [:maybe :boolean]]]]] From 9c9b672e3edd1d6705c7c84c149c924e3d3aa1e0 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 23 Jan 2026 10:05:20 +0100 Subject: [PATCH 8/8] :bug: Fix spanish translations on import export token modal (#8172) --- CHANGES.md | 1 + frontend/translations/es.po | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fe518a5597..10aca874cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,7 @@ - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865) - Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110) - Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167) +- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171) ## 2.12.1 diff --git a/frontend/translations/es.po b/frontend/translations/es.po index d69baf6480..40fd3fd30f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7594,6 +7594,10 @@ msgstr "Error al importar: No se pudo procesar el JSON." msgid "workspace.tokens.export" msgstr "Exportar" +#: src/app/main/ui/workspace/tokens/export/modal.cljs:125 +msgid "workspace.tokens.export-tokens" +msgstr "Exportar tokens" + #: src/app/main/ui/workspace/tokens/export/modal.cljs:118 msgid "workspace.tokens.export.multiple-files" msgstr "Múltiples ficheros" @@ -7638,10 +7642,26 @@ msgstr "Nombre del grupo" msgid "workspace.tokens.grouping-set-alert" msgstr "La agrupación de sets aun no está soportada." +#: src/app/main/ui/workspace/tokens/import/modal.cljs:233 +msgid "workspace.tokens.import-button-prefix" +msgstr "Importar %s" + #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 msgid "workspace.tokens.import-error" msgstr "Error al importar:" +#: src/app/main/ui/workspace/tokens/import/modal.cljs:273 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "Carpeta" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:271 +msgid "workspace.tokens.import-menu-json-option" +msgstr "Archivo JSON único" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:272 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "Archivo ZIP" + #: src/app/main/ui/workspace/tokens/import/modal.cljs:241 msgid "workspace.tokens.import-multiple-files" msgstr "" @@ -7656,7 +7676,7 @@ msgstr "" #: src/app/main/ui/workspace/tokens/import/modal.cljs:237 msgid "workspace.tokens.import-tokens" -msgstr "Import tokens" +msgstr "Importar tokens" #: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415 #, unused