diff --git a/CHANGES.md b/CHANGES.md index 98ff1b78af..37cd31218b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### :boom: Breaking changes ### :sparkles: New features + +- Added selected colors widget in right sidebar [Taiga #2485](https://tree.taiga.io/project/penpot/us/2485) + ### :bug: Bugs fixed ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index e9f702f00d..5820b7658c 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -1534,3 +1534,44 @@ margin-right: $size-2; } } + +.expand-colors { + cursor: pointer; + display: flex; + + .text { + color: $color-gray-30; + font-size: 0.75rem; + padding-left: 10px; + } + + svg { + width: 16px; + height: 16px; + fill: $color-gray-20; + stroke: $color-gray-20; + } +} + +.selected-colors { + .color-data { + margin-bottom: 0; + padding-bottom: 5px; + + svg { + visibility: hidden; + } + + .percentil { + margin-bottom: 0; + } + } + + .color-data:hover { + background-color: $color-gray-60; + + svg { + visibility: visible; + } + } +} diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 97deb5a069..e5e5f491bd 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -390,3 +390,32 @@ (update [_ state] (-> state (assoc-in [:workspace-global :editing-stop] spot))))) + +(defn color-att->text + [color] + {:fill-color (:color color) + :fill-opacity (:opacity color) + :fill-color-ref-id (:id color) + :fill-color-ref-file (:file-id color) + :fill-color-gradient (:gradient color)}) + +(defn change-text-color + [old-color new-color index node] + (let [fills (:fills node) + parsed-color (d/without-nils (color-att->text old-color)) + parsed-new-color (d/without-nils (color-att->text new-color)) + has-color? (d/index-of fills parsed-color)] + (cond-> node + (some? has-color?) + (assoc-in [:fills index] parsed-new-color)))) + +(defn change-color-in-selected + [new-color shapes-by-color old-color] + (ptk/reify ::change-color-in-selected + ptk/WatchEvent + (watch [_ _ _] + (->> (rx/from shapes-by-color) + (rx/map (fn [shape] (case (:prop shape) + :fill (change-fill [(:shape-id shape)] new-color (:index shape)) + :stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) + :content (dwt/update-text-with-function (:shape-id shape) (partial change-text-color old-color new-color (:index shape)))))))))) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index f927d398c2..dd41cc7b63 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -236,5 +236,13 @@ (events/listen globals/window EventType.CLICK on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) + + (mf/use-layout-effect + (mf/deps handle-mouse-wheel) + (fn [] + (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:pasive false})]] + #(doseq [key keys] + (events/unlistenByKey key))))) + [:> :input props])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs new file mode 100644 index 0000000000..a1418a655f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -0,0 +1,189 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.menus.color-selection + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.text :as txt] + [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.common :as dwc] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(defn fill->color-att + [fill] + (let [attrs (d/without-nils {:color (:fill-color fill) + :opacity (:fill-opacity fill) + :id (:fill-color-ref-id fill) + :file-id (:fill-color-ref-file fill) + :gradient (:fill-color-gradient fill)})] + {:attrs attrs + :prop :fill + :shape-id (:shape-id fill) + :index (:index fill)})) + +(defn stroke->color-att + [stroke] + (let [attrs (d/without-nils {:color (:stroke-color stroke) + :opacity (:stroke-opacity stroke) + :id (:stroke-color-ref-id stroke) + :file-id (:stroke-color-ref-file stroke) + :gradient (:stroke-color-gradient stroke)})] + {:attrs attrs + :prop :stroke + :shape-id (:shape-id stroke) + :index (:index stroke)})) + +(defn text->color-att + [fill] + (let [attrs (d/without-nils {:color (:fill-color fill) + :opacity (:fill-opacity fill) + :id (:fill-color-ref-id fill) + :file-id (:fill-color-ref-file fill) + :gradient (:fill-color-gradient fill)})] + {:attrs attrs + :prop :content + :shape-id (:shape-id fill) + :index (:index fill)})) + +(defn treat-node + [node shape-id] + (map-indexed #(assoc %2 :shape-id shape-id :index %1) node)) + +(defn extract-text-colors + [text] + (let [content (txt/node-seq txt/is-text-node? (:content text)) + content-filtered (map :fills content) + indexed (mapcat #(treat-node % (:id text)) content-filtered)] + (map text->color-att indexed))) + +(defn- extract-all-colors + [shapes] + (reduce + (fn [list shape] + (let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape)) + stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape))] + (if (= :text (:type shape)) + (-> list + (into (map stroke->color-att) stroke-obj) + (into (extract-text-colors shape))) + + (-> list + (into (map fill->color-att) fill-obj) + (into (map stroke->color-att) stroke-obj))))) + [] + shapes)) + +(defn- prepare-colors + [shapes] + (let [data (extract-all-colors shapes) + grouped-colors (group-by :attrs data) + all-colors (distinct (mapv :attrs data)) + + tmp (group-by #(some? (:id %)) all-colors) + library-colors (get tmp true) + colors (get tmp false)] + + {:grouped-colors grouped-colors + :all-colors all-colors + :colors colors + :library-colors library-colors})) + +(mf/defc color-selection-menu + {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} + [{:keys [shapes] :as props}] + (let [{:keys [all-colors grouped-colors library-colors colors]} (mf/with-memo [shapes] + (prepare-colors shapes)) + expand-lib-color (mf/use-state false) + expand-color (mf/use-state false) + + grouped-colors* (mf/use-var nil) + prev-color* (mf/use-var nil) + + on-change + (mf/use-fn + (fn [new-color old-color] + (let [old-color (-> (or @prev-color* old-color) + (dissoc :name) + (dissoc :path) + (d/without-nils)) + shapes-by-color (get @grouped-colors* old-color)] + (reset! prev-color* new-color) + (st/emit! (dc/change-color-in-selected new-color shapes-by-color old-color))))) + + on-open (mf/use-fn + (fn [color] + (reset! prev-color* color))) + + on-detach + (mf/use-fn + (fn [color] + (let [shapes-by-color (get @grouped-colors* color) + new-color (assoc color :id nil :file-id nil)] + (st/emit! (dc/change-color-in-selected new-color shapes-by-color color))))) + + select-only + (mf/use-fn + (fn [color] + (let [shapes-by-color (get @grouped-colors* color) + ids (into (d/ordered-set) (map :shape-id) shapes-by-color)] + (st/emit! (dwc/select-shapes ids)))))] + + (mf/with-effect [grouped-colors] + (reset! grouped-colors* grouped-colors)) + + (when (> (count all-colors) 1) + [:div.element-set + [:div.element-set-title + [:span (tr "workspace.options.selection-color")]] + [:div.element-set-content + [:div.selected-colors + (for [[index color] (d/enumerate (take 3 library-colors))] + [:& color-row {:key (dm/str "color-" index) + :color color + :index index + :on-detach on-detach + :select-only select-only + :on-change #(on-change % color) + :on-open on-open}]) + (when (and (false? @expand-lib-color) (< 3 (count library-colors))) + [:div.expand-colors {:on-click #(reset! expand-lib-color true)} + [:span i/actions] + [:span.text (tr "workspace.options.more-lib-colors")]]) + (when @expand-lib-color + (for [[index color] (d/enumerate (drop 3 library-colors))] + [:& color-row {:key (dm/str "color-" index) + :color color + :index index + :on-detach on-detach + :select-only select-only + :on-change #(on-change % color) + :on-open on-open}]))] + + [:div.selected-colors + (for [[index color] (d/enumerate (take 3 colors))] + [:& color-row {:key (dm/str "color-" index) + :color color + :index index + :select-only select-only + :on-change #(on-change % color) + :on-open on-open}]) + (when (and (false? @expand-color) (< 3 (count colors))) + [:div.expand-colors {:on-click #(reset! expand-color true)} + [:span i/actions] + [:span.text (tr "workspace.options.more-colors")]]) + (when @expand-color + (for [[index color] (d/enumerate (drop 3 colors))] + [:& color-row {:key (dm/str "color-" index) + :color color + :index index + :select-only select-only + :on-change #(on-change % color) + :on-open on-open}]))]]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 41ce9d1d15..3efb15080f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -14,6 +14,7 @@ [app.main.ui.components.color-input :refer [color-input]] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.context :as ctx] + [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.util.color :as uc] @@ -45,7 +46,7 @@ :on-change handle-change-color :on-close handle-close :data color}] - (handle-open) + (handle-open color) (modal/show! :colorpicker props)))) (defn opacity->string [opacity] @@ -53,13 +54,16 @@ "" (str (-> opacity (d/coalesce 1) - (* 100))))) + (* 100) + (fmt/format-number))))) (defn remove-multiple [v] (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove disable-drag select-all on-blur]}] + [{:keys [index color disable-gradient disable-opacity on-change + on-reorder on-detach on-open on-close title on-remove + disable-drag select-all on-blur select-only]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -92,7 +96,11 @@ handle-pick-color (fn [color] (when on-change (on-change (merge uc/empty-color color)))) - handle-open (fn [] (when on-open (on-open))) + handle-select (fn [] + (select-only color)) + + handle-open (fn [color] + (when on-open (on-open (merge uc/empty-color color)))) handle-close (fn [value opacity id file-id] (when on-close (on-close value opacity id file-id))) @@ -104,14 +112,12 @@ handle-opacity-change (fn [value] (change-opacity (/ value 100))) - handle-click-color (mf/use-callback - (mf/deps color) - (color-picker-callback color - disable-gradient - disable-opacity - handle-pick-color - handle-open - handle-close)) + handle-click-color (color-picker-callback color + disable-gradient + disable-opacity + handle-pick-color + handle-open + handle-close) prev-color (h/use-previous color) @@ -155,14 +161,23 @@ {:on-mouse-enter #(reset! hover-detach true) :on-mouse-leave #(reset! hover-detach false) :on-click detach-value} - (if @hover-detach i/unchain i/chain)])] + (if @hover-detach i/unchain i/chain)]) + + (when select-only + [:div.element-set-actions-button {:on-click handle-select} + i/pointer-inner])] ;; Rendering a gradient (and (not (uc/multiple? color)) (:gradient color) (get-in color [:gradient :type])) - [:div.color-info - [:div.color-name (cb/gradient-type->string (get-in color [:gradient :type]))]] + [:* + [:div.color-info + [:div.color-name (cb/gradient-type->string (get-in color [:gradient :type]))]] + (when select-only + [:div.element-set-actions-button {:on-click handle-select} + i/pointer-inner])] + ;; Rendering a plain color/opacity :else @@ -186,7 +201,10 @@ :on-blur on-blur :on-change handle-opacity-change :min 0 - :max 100}]])]) + :max 100}]]) + (when select-only + [:div.element-set-actions-button {:on-click handle-select} + i/pointer-inner])]) (when (some? on-remove) [:div.element-set-actions-button.remove {:on-click on-remove} i/minus])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 1683aa087e..f74b444930 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] @@ -49,15 +50,19 @@ (when-not (empty? fill-ids) [:& fill-menu {:type type :ids fill-ids :values fill-values}]) + (when-not (empty? stroke-ids) + [:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) + + (when (> (count objects) 2) + [:& color-selection-menu {:type type + :shapes (vals objects)}]) + (when-not (empty? shadow-ids) [:& shadow-menu {:type type :ids shadow-ids :values shadow-values}]) (when-not (empty? blur-ids) [:& blur-menu {:type type :ids blur-ids :values blur-values}]) - (when-not (empty? stroke-ids) - [:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) - (when-not (empty? text-ids) [:& ot/text-menu {:type type :ids text-ids :values text-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 7b4754e02e..c0d636fe56 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -13,6 +13,7 @@ [app.common.text :as txt] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-attrs exports-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] @@ -280,6 +281,9 @@ (when-not (empty? stroke-ids) [:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values}]) + + (when-not (empty? shapes) + [:& color-selection-menu {:type type :shapes (vals objects-no-measures)}]) (when-not (empty? shadow-ids) [:& shadow-menu {:type type :ids shadow-ids :values shadow-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 93324181a2..02b23b67cd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -10,6 +10,7 @@ [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu fill-attrs]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] @@ -58,7 +59,6 @@ :attrs text-attrs}))] [:* - [:& measures-menu {:ids ids :type type @@ -82,6 +82,9 @@ :type type :values stroke-values}] + (when (= :multiple (:fills fill-values)) + [:& color-selection-menu {:type type :shapes [shape]}]) + [:& shadow-menu {:ids ids :values (select-keys shape [:shadow])}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ce91504364..971395c86b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3011,6 +3011,18 @@ msgstr "Selection fill" msgid "workspace.options.selection-stroke" msgstr "Selection stroke" +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color" +msgstr "Selected colors" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-colors" +msgstr "More colors" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-lib-colors" +msgstr "More library colors" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.blur" msgstr "Blur" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index c09e0e91b4..6b3091e857 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3024,6 +3024,18 @@ msgstr "Relleno de selección" msgid "workspace.options.selection-stroke" msgstr "Borde de selección" +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color" +msgstr "Colores seleccionados" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-colors" +msgstr "Más colores" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.more-lib-colors" +msgstr "Más colores de la biblioteca" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.blur" msgstr "Desenfoque"