From aecaf519538e3b1cf0b14d87803440d4c299d5bc Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 11 Sep 2025 09:13:43 +0200 Subject: [PATCH] :sparkles: Add color token on colorpicker (#7197) * :sparkles: Add token aplication to colorpicker * :bug: Change fn name * :bug: Change scss from file * :bug: Change color for direct-color * :bug: Remove vector from fns * :bug: Fix CI * :bug: Change color-option name * :bug: Fix comments * :bug: Remove sets without color tokens --- common/src/app/common/flags.cljc | 1 + .../data/workspace/tokens/application.cljs | 31 +- .../src/app/main/ui/components/title_bar.scss | 2 +- frontend/src/app/main/ui/ds/_sizes.scss | 3 + .../main/ui/ds/controls/numeric_input.scss | 1 - .../src/app/main/ui/ds/tooltip/tooltip.cljs | 4 +- .../src/app/main/ui/ds/utilities/swatch.cljs | 41 +- .../src/app/main/ui/ds/utilities/swatch.scss | 2 +- frontend/src/app/main/ui/icons.cljs | 1 + .../app/main/ui/workspace/colorpicker.cljs | 373 +++++++++++++----- .../app/main/ui/workspace/colorpicker.scss | 72 ++-- .../workspace/colorpicker/color_inputs.scss | 1 - .../workspace/colorpicker/color_tokens.cljs | 295 ++++++++++++++ .../workspace/colorpicker/color_tokens.scss | 94 +++++ .../ui/workspace/colorpicker/gradients.cljs | 1 + .../ui/workspace/colorpicker/libraries.scss | 1 - .../ui/workspace/sidebar/assets/colors.cljs | 2 + .../options/menus/color_selection.cljs | 56 ++- .../workspace/sidebar/options/menus/fill.cljs | 1 + .../sidebar/options/menus/frame_grid.cljs | 2 + .../sidebar/options/menus/shadow.cljs | 1 + .../ui/workspace/sidebar/options/page.cljs | 1 + .../sidebar/options/rows/color_row.cljs | 17 +- .../sidebar/options/rows/stroke_row.cljs | 3 +- .../ui/workspace/tokens/management/group.cljs | 33 +- .../main/ui/workspace/tokens/sets/lists.cljs | 1 + frontend/translations/en.po | 14 + frontend/translations/es.po | 18 + 28 files changed, 885 insertions(+), 187 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs create mode 100644 frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index ff272afffc..6b307baab7 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -120,6 +120,7 @@ :tiered-file-data-storage :token-units :token-base-font-size + :token-color :token-typography-types :token-typography-composite :transit-readable-response diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index fc384e7ee0..87ad3e7e81 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -115,7 +115,7 @@ (f shape-ids {:color hex :opacity opacity} 0 {:ignore-touched true :page-id page-id})))) -(defn- value->color +(defn value->color "Transform a token color value into penpot color data structure" [color] (when-let [tc (tinycolor/valid-color color)] @@ -584,6 +584,35 @@ :shape-ids shape-ids :on-update-shape on-update-shape})))))))) + +(defn apply-token-on-selected + [color-operations token] + (ptk/reify ::apply-token-on-selected + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (->> (rx/from color-operations) + (rx/map + (fn [cop] + (let [shape-ids [(:shape-id cop)]] + (case (:prop cop) + :fill (apply-token {:attributes #{:fill} + :token token + :shape-ids shape-ids + :on-update-shape update-fill}) + :stroke (apply-token {:attributes #{:stroke-color} + :token token + :shape-ids shape-ids + :on-update-shape update-stroke-color}) + ;; Text + :content (apply-token {:attributes #{:fill} + :token token + :shape-ids shape-ids + :on-update-shape update-fill}) + :shadow (rx/empty)))))) + (rx/of (dwu/commit-undo-transaction undo-id))))))) ;; 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/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index 08ec58ebbb..72e8a81af7 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -7,6 +7,7 @@ @import "refactor/common-refactor.scss"; .title-bar { + @include headlineSmallTypography; display: flex; align-items: center; justify-content: space-between; @@ -20,7 +21,6 @@ .title, .title-only, .inspect-title { - @include headlineSmallTypography; display: grid; align-items: center; justify-content: flex-start; diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss index 89db4c9700..835dfaed0f 100644 --- a/frontend/src/app/main/ui/ds/_sizes.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -7,9 +7,11 @@ @use "./utils.scss" as *; // TODO: create actual tokens once we have them from design +$sz-1: px2rem(1); $sz-6: px2rem(6); $sz-16: px2rem(16); $sz-24: px2rem(24); +$sz-28: px2rem(28); $sz-32: px2rem(32); $sz-36: px2rem(36); $sz-40: px2rem(40); @@ -26,6 +28,7 @@ $sz-318: px2rem(318); $sz-352: px2rem(352); $sz-384: px2rem(384); $sz-400: px2rem(400); +$sz-430: px2rem(430); $sz-480: px2rem(480); $sz-500: px2rem(500); $sz-964: px2rem(964); diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index a44717f826..14b8b194a5 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -7,7 +7,6 @@ @use "../spacing.scss" as *; @use "../borders.scss" as *; @use "../sizes.scss" as *; -@use "../typography.scss" as t; .input-wrapper { --opacity-button: 0; diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index ac89d1fe24..30476df1c5 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -172,7 +172,9 @@ [:id {:optional true} :string] [:offset {:optional true} :int] [:delay {:optional true} :int] - [:content [:or fn? :string [:fn mf/element?]]] + ;; TODO: Review why html element crash schema + ;; https://tree.taiga.io/project/penpot/task/12039 + ;; [:content [:or fn? :string [:fn mf/element?]]] [:placement {:optional true} [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]]) diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs index bd8d086fd2..214166d275 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.cljs +++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs @@ -14,6 +14,7 @@ [app.common.schema :as sm] [app.common.types.color :as ct] [app.config :as cfg] + [app.main.ui.ds.tooltip :refer [tooltip*]] [app.util.color :as uc] [app.util.i18n :refer [tr]] [cuerdas.core :as str] @@ -66,7 +67,7 @@ (mf/defc swatch* {::mf/schema (sm/schema schema:swatch)} - [{:keys [background on-click size active class] + [{:keys [background on-click size active class tooltip-content] :rest props}] (let [;; NOTE: this code is only relevant for storybook, because ;; storybook is unable to pass in a comfortable way a complex @@ -90,6 +91,7 @@ :stops gradient-stops} image (:image background) format (if id? "rounded" "square") + element-id (mf/use-id) class (dm/str class " " (stl/css-case @@ -106,23 +108,26 @@ (mf/spread-props props {:class class :on-click on-click :type button-type - :title (color-title background)})] + :aria-labelledby element-id})] - [:> element-type props - (cond + [:> tooltip* {:content (if tooltip-content + tooltip-content + (color-title background)) + :id element-id} + [:> element-type props + (cond + (some? gradient-type) + [:span {:class (stl/css :swatch-gradient) + :style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}] - (some? gradient-type) - [:span {:class (stl/css :swatch-gradient) - :style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}] + (some? image) + (let [uri (cfg/resolve-file-media image)] + [:span {:class (stl/css :swatch-image) + :style {:background-image (str/ffmt "url(%)" uri)}}]) - (some? image) - (let [uri (cfg/resolve-file-media image)] - [:span {:class (stl/css :swatch-image) - :style {:background-image (str/ffmt "url(%)" uri)}}]) - - :else - [:span {:class (stl/css :swatch-opacity)} - [:span {:class (stl/css :swatch-solid-side) - :style {:background (uc/color->background (assoc background :opacity 1))}}] - [:span {:class (stl/css :swatch-opacity-side) - :style {:background (uc/color->background background)}}]])])) + :else + [:span {:class (stl/css :swatch-opacity)} + [:span {:class (stl/css :swatch-solid-side) + :style {:background (uc/color->background (assoc background :opacity 1))}}] + [:span {:class (stl/css :swatch-opacity-side) + :style {:background (uc/color->background background)}}]])]])) diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss index edf3d3fb6b..6fb070bfc0 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.scss +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -17,7 +17,7 @@ --checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%); --checkerboard-size: 0.5rem 0.5rem; - border: 1px solid var(--border-color); + border: $b-1 solid var(--border-color); border-radius: var(--border-radius); overflow: hidden; diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 5e3849adbd..8f3eca9479 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -263,6 +263,7 @@ (def ^:icon tokens (icon-xref :tokens)) (def ^:icon to-corner (icon-xref :to-corner)) (def ^:icon to-curve (icon-xref :to-curve)) +(def ^:icon tokens (icon-xref :tokens)) (def ^:icon tree (icon-xref :tree)) (def ^:icon unlock (icon-xref :unlock)) (def ^:icon user (icon-xref :user)) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index dec18b21de..a6653558ea 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -13,6 +13,7 @@ [app.common.geom.point :as gpt] [app.common.types.color :as cc] [app.common.types.fills :as types.fills] + [app.common.types.tokens-lib :as ctob] [app.config :as cfg] [app.main.data.event :as-alias ev] [app.main.data.modal :as modal] @@ -26,12 +27,14 @@ [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]] [app.main.ui.components.select :refer [select]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] + [app.main.ui.workspace.colorpicker.color-tokens :refer [token-section*]] [app.main.ui.workspace.colorpicker.gradients :refer [gradients*]] [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]] [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] @@ -90,12 +93,20 @@ (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) (mf/defc colorpicker - {::mf/props :obj} - [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}] + [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change]}] (let [state (mf/deref refs/colorpicker) node-ref (mf/use-ref) should-update? (mf/use-var true) + token-color (contains? cfg/flags :token-color) + color-style* (mf/use-state :direct-color) + color-style (deref color-style*) + toggle-token-color + (mf/use-fn + (mf/deps color-style) + (fn [] + (let [new-style (if (= :direct-color color-style) :token-color :direct-color)] + (reset! color-style* new-style)))) ;; TODO: I think we need to put all this picking state under ;; the same object for avoid creating adhoc refs for each @@ -382,8 +393,10 @@ :ref node-ref :style {:touch-action "none"}} [:div {:class (stl/css :top-actions)} + [:div {:class (stl/css :top-actions-right)} - (when (= :gradient selected-mode) + (when (and (= color-style :direct-color) + (= :gradient selected-mode)) [:div {:class (stl/css :opacity-input-wrapper)} [:span {:class (stl/css :icon-text)} "%"] [:> numeric-input* @@ -394,118 +407,143 @@ :min 0 :max 100}]]) - (when (or (not disable-gradient) (not disable-image)) + (when (and (= color-style :direct-color) + (or (not disable-gradient) (not disable-image))) [:div {:class (stl/css :select)} [:& select {:default-value selected-mode :options options - :on-change handle-change-mode}]])] + :on-change handle-change-mode}]]) - (when (not= selected-mode :image) + (when (and (= origin :sidebar) token-color) + [:& radio-buttons {:selected color-style + :on-change toggle-token-color + :name "color-style"} + [:& radio-button {:icon deprecated-icon/swatches + :value :direct-color + :title (tr "labels.color") + :id "opt-color"}] + [:& radio-button {:icon deprecated-icon/tokens + :value :token-color + :title (tr "workspace.colorpicker.color-tokens") + :id "opt-token-color"}]])] + + (when (and (not= selected-mode :image) + (= color-style :direct-color)) [:button {:class (stl/css-case :picker-btn true :selected picking-color?) :on-click handle-click-picker} - deprecated-icon/picker])] + deprecated-icon/picker]) - (when (= selected-mode :gradient) - [:> gradients* - {:type (:type state) - :stops (if cap-stops? (vec (take types.fills/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) - :editing-stop (:editing-stop state) - :on-stop-edit-start handle-stop-edit-start - :on-stop-edit-finish handle-stop-edit-finish - :on-select-stop handle-change-gradient-selected-stop - :on-change-type handle-change-gradient-type - :on-change-stop handle-gradient-change-stop - :on-add-stop-auto handle-gradient-add-stop-auto - :on-add-stop-preview handle-gradient-add-stop-preview - :on-remove-stop handle-gradient-remove-stop - :on-rotate-stops handle-rotate-stops - :on-reverse-stops handle-reverse-stops - :on-reorder-stops handle-reorder-stops}]) - - (if (= selected-mode :image) - (let [uri (cfg/resolve-file-media (:image current-color)) - keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)] - [:div {:class (stl/css :select-image)} - [:div {:class (stl/css :content)} - (when (:image current-color) - [:img {:src uri}])] - - (when (some? (:image current-color)) - [:div {:class (stl/css :checkbox-option)} - [:label {:for "keep-aspect-ratio" - :class (stl/css-case :global/checked keep-aspect-ratio?)} - [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)} - (when keep-aspect-ratio? - deprecated-icon/status-tick)] - (tr "media.keep-aspect-ratio") - [:input {:type "checkbox" - :id "keep-aspect-ratio" - :checked keep-aspect-ratio? - :on-change handle-change-keep-aspect-ratio}]]]) - [:button - {:class (stl/css :choose-image) - :title (tr "media.choose-image") - :aria-label (tr "media.choose-image") - :on-click on-fill-image-click} - (tr "media.choose-image") - [:& file-uploader - {:input-id "fill-image-upload" - :accept "image/jpeg,image/png" - :multi false - :ref fill-image-ref - :on-selected on-fill-image-selected}]]]) + (when (= color-style :token-color) + [:div {:class (stl/css :token-color-title)} + (tr "workspace.colorpicker.color-tokens")])] + (if (= color-style :direct-color) [:* - [:div {:class (stl/css :colorpicker-tabs)} - [:> tab-switcher* {:tabs tabs - :selected active-color-tab - :on-change on-change-tab} - (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] + (when (= selected-mode :gradient) + [:> gradients* + {:type (:type state) + :stops (if cap-stops? (vec (take types.fills/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) + :editing-stop (:editing-stop state) + :on-stop-edit-start handle-stop-edit-start + :on-stop-edit-finish handle-stop-edit-finish + :on-select-stop handle-change-gradient-selected-stop + :on-change-type handle-change-gradient-type + :on-change-stop handle-gradient-change-stop + :on-add-stop-auto handle-gradient-add-stop-auto + :on-add-stop-preview handle-gradient-add-stop-preview + :on-remove-stop handle-gradient-remove-stop + :on-rotate-stops handle-rotate-stops + :on-reverse-stops handle-reverse-stops + :on-reorder-stops handle-reorder-stops}]) + + (if (= selected-mode :image) + (let [uri (cfg/resolve-file-media (:image current-color)) + keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)] + [:div {:class (stl/css :select-image)} + [:div {:class (stl/css :content)} + (when (:image current-color) + [:img {:src uri}])] + + (when (some? (:image current-color)) + [:div {:class (stl/css :checkbox-option)} + [:label {:for "keep-aspect-ratio" + :class (stl/css-case :global/checked keep-aspect-ratio?)} + [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)} + (when keep-aspect-ratio? + deprecated-icon/status-tick)] + (tr "media.keep-aspect-ratio") + [:input {:type "checkbox" + :id "keep-aspect-ratio" + :checked keep-aspect-ratio? + :on-change handle-change-keep-aspect-ratio}]]]) + [:button + {:class (stl/css :choose-image) + :title (tr "media.choose-image") + :aria-label (tr "media.choose-image") + :on-click on-fill-image-click} + (tr "media.choose-image") + [:& file-uploader + {:input-id "fill-image-upload" + :accept "image/jpeg,image/png" + :multi false + :ref fill-image-ref + :on-selected on-fill-image-selected}]]]) + + [:* + [:div {:class (stl/css :colorpicker-tabs)} + [:> tab-switcher* {:tabs tabs + :selected active-color-tab + :on-change on-change-tab} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] - (case active-color-tab - "ramp" - [:> ramp-selector* - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] + (case active-color-tab + "ramp" + [:> ramp-selector* + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] - "harmony" - [:& harmony-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag}] + "harmony" + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag}] - "hsva" - [:& hsva-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]))]] + "hsva" + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]))]] - [:& color-inputs - {:type type - :disable-opacity disable-opacity - :color current-color - :on-change handle-change-color}] + [:& color-inputs + {:type type + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:& libraries - {:state state - :current-color current-color - :disable-gradient disable-gradient - :disable-opacity disable-opacity - :disable-image disable-image - :on-select-color on-select-library-color - :on-add-library-color on-add-library-color}]])] + [:& libraries + {:state state + :current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :disable-image disable-image + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}]])] + + [:> token-section* {:combined-tokens combined-tokens + :on-token-change on-token-change + :color-origin color-origin}])] (when (fn? on-accept) [:div {:class (stl/css :actions)} [:button {:class (stl/css-case @@ -561,6 +599,115 @@ :top top-offset :maxHeight max-height-top})))) +(defn- group-sets + "Groups sets by their parent path (everything before the last '/') if present. + The set name is always the last part of the path. + + Input: + [{:set \"brand/subgroup/one\" :tokens [{:name \"background\"}]} + {:set \"brand/subgroup/two\" :tokens [{:name \"foreground\"}]} + {:set \"primitives\" :tokens [{:name \"blue-100\"}]}] + + Output: + [{:group \"brand/subgroup\" + :sets [\"one\" \"two\"] + :tokens [\"background\" \"foreground\"]} + {:group nil + :sets [\"primitives\"] + :tokens [\"blue-100\"]}]" + + [sets] + (->> sets + (group-by (fn [{:keys [set]}] + (when (str/includes? set "/") + (str/join "/" (butlast (str/split set #"/")))))) + (map (fn [[group grouped-sets]] + (if group + {:group group + :sets (map #(last (str/split (:set %) #"/")) grouped-sets) + :tokens (->> grouped-sets + (mapcat :tokens) + (map :name) + distinct)} + (map (fn [{:keys [set tokens]}] + {:group nil + :sets [set] + :tokens (map :name tokens)}) + grouped-sets)))) + flatten)) + +(defn- combine-groups-with-resolved + "Replaces token names in grouped sets with their full resolved token objects. + + Input: + - groups: [{:group \"brand\" + :sets [\"light\" \"dark\"] + :tokens [\"background\" \"foreground\"]} ...] + - resolved-tokens: [{:name \"background\" :type \"color\" :value \"{red-100}\" ...} ...] + + Output: + [{:group \"brand\" + :sets [\"light\" \"dark\"] + :tokens [{:name \"background\" :type \"color\" :value \"{red-100}\" ...} + {:name \"foreground\" :type \"color\" :value \"{green-100}\" ...}]}]" + + [groups resolved-tokens] + (let [token-map (into {} (map (juxt :name identity) resolved-tokens))] + (map (fn [{:keys [group sets tokens]}] + {:group group + :sets sets + :tokens (->> tokens + (map #(get token-map %)) + (remove nil?) + vec)}) + groups))) + +(defn- filter-non-empty-sets + "Removes sets that have no tokens. + + Input: + [{:set \"brand/light\" :tokens []} + {:set \"brand/dark\" :tokens [{:name \"background\"}]}] + + Output: + [{:set \"brand/dark\" :tokens [{:name \"background\"}]}]" + [sets] + (filter (fn [{:keys [tokens]}] + (seq tokens)) + sets)) + +(defn- add-tokens-to-sets + "Extracts set name and its tokens from raw set objects. + + Input: + A vector of set objects (raw domain type), each compatible with: + {:id ... :name \"brand/light\" :tokens {...}} + + Output: + A vector of simplified maps: + [{:set \"brand/light\" :tokens [{:name \"background\" ...} ...]}]" + [sets] + (map (fn [s] + {:set (ctob/get-name s) + :tokens (ctob/get-tokens s)}) + sets)) + +(defn- filter-active-sets + "Filters sets to only include those whose :set value is in active-set-names. + + Input: + - sets: [{:set \"brand/light\" :tokens [...]}, + {:set \"brand/dark\" :tokens [...]}, + {:set \"primitivos\" :tokens [...]}, + ...] + - active-set-names: #{\"brand/light\" \"primitivos\"} + + Output: + [{:set \"brand/light\" :tokens [...]} + {:set \"primitivos\" :tokens [...]}]" + [sets active-set-names] + (filter #(contains? active-set-names (:set %)) sets)) + (mf/defc colorpicker-modal {::mf/register modal/components ::mf/register-as :colorpicker @@ -569,7 +716,11 @@ disable-gradient disable-opacity disable-image + active-tokens on-change + origin + color-origin + on-token-change on-close on-accept]}] (let [vport (mf/deref viewport) @@ -587,7 +738,27 @@ (reset! last-change new-data) (if (fn? on-change) (on-change new-data) - (st/emit! (dc/update-colorpicker new-data))))))] + (st/emit! (dc/update-colorpicker new-data)))))) + + tokens-lib + (mf/deref refs/tokens-lib) + + active-sets-names + (mf/with-memo [tokens-lib] + (some-> tokens-lib + (ctob/get-active-themes-set-names))) + + color-tokens (:color active-tokens) + + grouped-tokens-by-set + (mf/with-memo [tokens-lib active-sets-names color-tokens] + (some-> tokens-lib + (ctob/get-sets) + (add-tokens-to-sets) + (filter-active-sets active-sets-names) + (filter-non-empty-sets) + (group-sets) + (combine-groups-with-resolved color-tokens)))] (mf/with-effect [] (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts))) @@ -601,8 +772,12 @@ :style style} [:& colorpicker {:data data + :combined-tokens grouped-tokens-by-set :disable-gradient disable-gradient :disable-opacity disable-opacity :disable-image disable-image + :on-token-change on-token-change :on-change on-change' + :origin origin + :color-origin color-origin :on-accept on-accept}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index 033b06e4f5..e78c4a28c8 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -4,8 +4,11 @@ // // Copyright (c) KALEIDOS INC +@use "../ds/typography.scss" as t; +@use "../ds/spacing.scss"; +@use "../ds/_borders.scss" as *; +@use "../ds/_sizes.scss" as *; @import "refactor/common-refactor.scss"; -@import "../ds/_sizes.scss"; .colorpicker-tooltip { @extend .modal-background; @@ -23,22 +26,17 @@ overflow: hidden; } -.colorpicker-tabs { - padding: 0 var(--sp-m); -} - .top-actions { display: flex; align-items: flex-start; flex-direction: row-reverse; justify-content: space-between; height: $s-40; - padding: 0 var(--sp-m); } .top-actions-right { display: flex; - gap: $s-8; + gap: var(--sp-s); } .opacity-input-wrapper { @@ -52,12 +50,12 @@ @include flexCenter; border-radius: $br-8; background-color: transparent; - border: $s-1 solid transparent; - height: $s-20; - width: $s-20; + border: $b-1 solid transparent; + height: var(--sp-xl); + width: var(--sp-xl); border-radius: $br-4; padding: 0; - margin-top: $s-4; + margin-top: var(--sp-xs); svg { @extend .button-icon; stroke: var(--button-tertiary-foreground-color-rest); @@ -76,7 +74,7 @@ } &:active { outline: none; - border: $s-1 solid transparent; + border: $b-1 solid transparent; svg { stroke: var(--button-tertiary-foreground-color-active); } @@ -91,17 +89,17 @@ .gradient-buttons { display: flex; align-items: center; - gap: $s-8; + gap: var(--sp-s); } .gradient-btn { @extend .button-tertiary; - height: $s-20; - width: $s-20; + height: var(--sp-xl); + width: var(--sp-xl); border-radius: $br-4; - border: $s-2 solid transparent; + border: $b-2 solid transparent; &:hover { - border: $s-2 solid var(--colorpicker-details-color-selected); + border: $b-2 solid var(--colorpicker-details-color-selected); } } @@ -109,7 +107,7 @@ background: linear-gradient(180deg, var(--color-foreground-secondary), transparent); &.selected { background: linear-gradient(to bottom, rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); - border: $s-2 solid var(--colorpicker-details-color-selected); + border: $b-2 solid var(--colorpicker-details-color-selected); } } @@ -117,38 +115,38 @@ background: radial-gradient(transparent, var(--color-foreground-secondary)); &.selected { background: radial-gradient(rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); - border: $s-2 solid var(--colorpicker-details-color-selected); + border: $b-2 solid var(--colorpicker-details-color-selected); } } .actions { display: flex; - gap: $s-4; + gap: var(--sp-xs); } .accept-color { @include uppercaseTitleTipography; @extend .button-primary; width: 100%; - height: $s-32; - margin-top: $s-8; + height: var(--sp-xxxl); + margin-top: var(--sp-s); } .picker-detail-wrapper { @include flexCenter; position: relative; - margin: $s-12 0 $s-8 0; + margin: var(--sp-m) 0 var(--sp-s) 0; } .center-circle { - width: $s-24; - height: $s-24; - border: $s-2 solid var(--colorpicker-details-color); + width: var(--sp-xxl); + height: var(--sp-xxl); + border: $b-2 solid var(--colorpicker-details-color); border-radius: $br-circle; position: absolute; left: 50%; top: 50%; - transform: translate(calc(-1 * $s-12), calc(-1 * $s-12)); + transform: translate(calc(-1 * var(--sp-m)), calc(-1 * var(--sp-m))); } .picker-detail { @@ -161,7 +159,7 @@ } .select-image { - margin-top: $s-4; + margin-top: var(--sp-xs); } .content { @@ -172,8 +170,8 @@ background-position: center; background-size: auto $s-140; height: $s-140; - margin-bottom: $s-6; - margin-right: $s-1; + margin-bottom: $sz-6; + margin-right: $sz-1; img { height: fit-content; width: fit-content; @@ -187,11 +185,19 @@ @extend .button-secondary; @include uppercaseTitleTipography; width: 100%; - margin-top: $s-12; - height: $s-32; + margin-top: var(--sp-m); + height: var(--sp-xxxl); } .checkbox-option { @extend .input-checkbox; - margin: $s-16 0 0 0; + margin: var(--sp-l) 0 0 0; +} + +.token-color-title { + @include t.use-typography("title-small"); + color: var(--color-foreground-secondary); + display: flex; + align-items: center; + height: var(--sp-xxxl); } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss index 349da89a7d..b6d52d6490 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -9,7 +9,6 @@ .color-values { @include flexColumn; margin-top: $s-8; - padding: 0 var(--sp-m); &.disable-opacity { grid-template-columns: 3.5rem repeat(3, 1fr); diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs new file mode 100644 index 0000000000..a2bf6f4d59 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs @@ -0,0 +1,295 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.colorpicker.color-tokens + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.constants :refer [max-input-length]] + [app.main.data.event :as-alias ev] + [app.main.data.workspace.tokens.application :as dwta] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.title-bar :refer [title-bar*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.tooltip :refer [tooltip*]] + [app.main.ui.ds.utilities.swatch :refer [swatch*]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc token-empty-state* + {::mf/private true} + [] + [:div {:class (stl/css :color-token-empty-state)} + (tr "color-token.empty-state")]) + +(mf/defc list-item* + {::mf/private true} + [{:keys [token on-token-pill-click selected] :rest props}] + (let [on-click + (mf/use-fn + (mf/deps token on-token-pill-click) + (fn [event] + (on-token-pill-click event token))) + id-tooltip (mf/use-id) + resolved (:resolved-value token) + color-value (dwta/value->color resolved)] + [:> tooltip* {:id id-tooltip + :style {:width "100%"} + :content (:name token)} + [:button {:class (stl/css-case :color-token-item true + :color-token-selected selected) + :aria-labelledby id-tooltip + :on-click on-click} + [:> swatch* {:background color-value + :tooltip-content (tr "workspace.tokens.resolved-value" resolved) + :size "small"}] + [:div {:class (stl/css :token-name)} + (:name token)] + (when selected + [:> i/icon* {:icon-id i/tick + :size "s" + :class (stl/css :token-selected-icon)}])]])) + +(mf/defc grid-item* + {::mf/private true} + [{:keys [token on-token-pill-click selected] :rest props}] + (let [on-click + (mf/use-fn + (mf/deps token on-token-pill-click) + (fn [event] + (on-token-pill-click event token))) + resolved (:resolved-value token) + token-name (:name token) + color-value (dwta/value->color resolved)] + [:div {:class (stl/css-case :color-token-item-grid true + :color-token-selected-grid selected)} + [:> swatch* {:background color-value + :tooltip-content + (mf/html + [:* + [:div (dm/str (tr "workspace.tokens.token-name") ": " token-name)] + [:div (tr "workspace.tokens.resolved-value" resolved)]]) + :on-click on-click + :size "medium"}]])) + +(defn group->set-name + "Given a group structure, returns a representative set name. + + Input: + {:group \"brand\" + :sets [\"light\" \"dark\"] + :tokens [...]} + + Output: + - If :group exists → \"brand/light\" (first set in :sets) + - If :group is nil → the first (and only) value of :sets" + [{:keys [group sets]}] + (if group + (str group "/" (first sets)) + (first sets))) + +(mf/defc set-section* + {::mf/private true} + [{:keys [collapsed toggle-sets-open set name color-origin on-token-change] :rest props}] + + (let [list-style* (mf/use-state :list) + list-style (deref list-style*) + toggle-list-style + (mf/use-fn + (mf/deps list-style) + (fn [] + (let [new-style (if (= :list list-style) :grid :list)] + (reset! list-style* new-style)))) + + toggle-set + (mf/use-fn + (mf/deps name toggle-sets-open) + (fn [] + (toggle-sets-open name))) + + objects (mf/deref refs/workspace-page-objects) + selected (mf/deref refs/selected-shapes) + + selected-shapes + (mf/with-memo [selected objects] + (into [] (keep (d/getf objects)) selected)) + first-shape (first selected-shapes) + applied-tokens (:applied-tokens first-shape) + has-color-tokens? (get applied-tokens :fill) + has-stroke-tokens? (get applied-tokens :stroke-color) + + on-token-pill-click + (mf/use-fn + (mf/deps selected-shapes selected color-origin) + (fn [event token] + (dom/stop-propagation event) + (when (seq selected-shapes) + (if (= :color-selection color-origin) + (on-token-change event token) + (let [attributes (if (= color-origin :stroke) #{:stroke-color} #{:fill}) + shape-ids (into #{} (map :id selected-shapes))] + (if (or + (= (:name token) has-stroke-tokens?) + (= (:name token) has-color-tokens?)) + (st/emit! (dwta/unapply-token {:attributes attributes + :token token + :shape-ids shape-ids})) + (st/emit! (dwta/apply-token {:shape-ids shape-ids + :attributes attributes + :token token + :on-update-shape dwta/update-fill-stroke})))))))) + + create-token-on-set + (mf/use-fn + (mf/deps set) + (fn [_] + (let [first-set-name (group->set-name set) + set-item-id (dm/str "token-set-item-" first-set-name) + set-element (dom/get-element set-item-id)] + (when set-element + (dom/click set-element) + (tm/schedule-on-idle + (let [button-element (dom/get-element "add-token-button-Color")] + #(dom/click button-element)))))))] + + [:div {:class (stl/css :color-token-set)} + [:> title-bar* {:collapsable true + :collapsed collapsed + :all-clickable true + :on-collapsed toggle-set + :class (stl/css :set-title-bar) + :title name} + + (when (not collapsed) + [:div {:class (stl/css :set-actions)} + [:> icon-button* {:on-click toggle-list-style + :variant "action" + :aria-label (if (= :list list-style) + (tr "workspace.assets.grid-view") + (tr "workspace.assets.list-view")) + :icon (if (= :list list-style) + i/flex-grid + i/view-as-list)}] + [:> icon-button* {:on-click create-token-on-set + :variant "action" + :aria-label (tr "workspace.tokens.add-token" "color") + :icon i/add}]])] + + (when (not collapsed) + [:div {:class (stl/css-case :color-token-list true + :list-view (= list-style :list) + :grid-view (= list-style :grid))} + + (for [token (:tokens set)] + (let [selected? (if (= color-origin :fill) + (= has-color-tokens? (:name token)) + (= has-stroke-tokens? (:name token)))] + (if (= :grid list-style) + [:> grid-item* {:key (str "token-grid-" (:id token)) + :on-token-pill-click on-token-pill-click + :selected selected? + :token token}] + [:> list-item* {:key (str "token-list-" (:id token)) + :on-token-pill-click on-token-pill-click + :selected selected? + :token token}])))])])) +(defn- label-group-or-set [{:keys [group sets]}] + (if group + (str group " (" (str/join ", " sets) ")") + (first sets))) + +(defn- filter-combined-tokens + "Filters the combined-tokens structure by token name. + Removes sets or groups if they end up with no tokens. + + Input: + [{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"background\"} {:name \"foreground\"}]} + {:group nil, :sets [\"primitivos\"], :tokens [{:name \"blue-100\"} {:name \"red-100\"}]}] + + (filter-combined-tokens ... \"blue\") + Output: + [{:group nil, :sets [\"primitivos\"], :tokens [{:name \"blue-100\"}]}] + => keeps only tokens matching \"blue\", and removes sets/groups if no tokens match." + + [combined-tokens term] + (let [term (str/lower (str/trim term))] + (if (str/blank? term) + combined-tokens + (->> combined-tokens + (map (fn [{:keys [tokens] :as entry}] + (let [filtered (filter #(str/includes? + (str/lower (:name %)) + term) + tokens)] + (when (seq filtered) + (assoc entry :tokens filtered))))) + (remove nil?))))) + +(defn- sort-combined-tokens + "Sorts tokens alphabetically by :name inside each group/set. + Input: + [{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"foreground\"} {:name \"background\"}]}] + + Output: + [{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"background\"} {:name \"foreground\"}]}]" + [combined-tokens] + (map (fn [entry] + (update entry :tokens #(sort-by :name %))) + combined-tokens)) + +(mf/defc token-section* + {} + [{:keys [combined-tokens color-origin on-token-change] :rest props}] + (let [sets (set (mapv label-group-or-set combined-tokens)) + filter-term* (mf/use-state "") + filter-term (deref filter-term*) + open-sets* (mf/use-state sets) + open-sets (deref open-sets*) + toggle-sets-open + (mf/use-fn + (mf/deps open-sets) + (fn [name] + (if (contains? open-sets name) + (swap! open-sets* disj name) + (swap! open-sets* conj name)))) + + on-filter-tokens + (mf/use-fn + (mf/deps filter-term) + (fn [event] + (let [value (-> event (dom/get-target) + (dom/get-value))] + (reset! filter-term* value)))) + filtered-combined (filter-combined-tokens combined-tokens filter-term) + sorted-tokens (sort-combined-tokens filtered-combined)] + (if combined-tokens + [:div {:class (stl/css :color-tokens-section)} + [:> input* {:placeholder "Search by token name" + :icon i/search + :max-length max-input-length + :variant "comfortable" + :class (stl/css :search-input) + :default-value filter-term + :on-change on-filter-tokens}] + (for [combined-sets sorted-tokens] + (let [name (label-group-or-set combined-sets)] + [:> set-section* + {:collapsed (not (contains? open-sets name)) + :key (str "set-" name) + :toggle-sets-open toggle-sets-open + :color-origin color-origin + :on-token-change on-token-change + :name name + :set combined-sets}]))] + [:> token-empty-state*]))) + diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss new file mode 100644 index 0000000000..e296acc562 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss @@ -0,0 +1,94 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../../ds/typography.scss" as t; +@use "../../ds/_borders.scss" as *; +@use "../../ds/_sizes.scss" as *; + +.color-token-list { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.color-token-item { + --color-token-background: var(--color-background-primary); + background-color: var(--color-token-background); + color: var(--color-foreground-primary); + text-align: left; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: var(--sp-xs); + width: 100%; + border-radius: $br-8; + padding: var(--sp-xs); + height: var(--sp-xxl); + border: none; + cursor: pointer; + &:hover { + --color-token-background: var(--color-background-tertiary); + } +} + +.color-token-empty-state { + @include t.use-typography("body-small"); + padding: var(--sp-s) var(--sp-xxl); + text-align: center; + color: var(--color-foreground-secondary); +} + +.color-token-selected { + background-color: var(--color-background-tertiary); +} + +.color-token-selected-grid { + border: $b-1 solid var(--color-accent-primary); + border-radius: $br-4; + width: fit-content; +} + +.token-selected-icon { + color: var(--color-foreground-secondary); +} + +.token-name { + @include t.use-typography("body-small"); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.color-tokens-section { + max-height: $sz-430; + overflow: auto; +} + +.set-actions { + display: flex; +} + +.grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--sp-xxl), 1fr)); + justify-items: start; + gap: var(--sp-s); +} + +.list-view { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.set-title-bar { + @include t.use-typography("title-small"); + text-transform: none; +} + +.search-input { + padding: $sz-1; +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 1db83d6c01..39263a1d5d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -158,6 +158,7 @@ :disable-picker true :color stop :index index + :origin :gradient :on-change handle-change-stop-color :on-remove handle-remove-stop :on-focus handle-focus-stop-color diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss index 9b510d97fe..8b401448db 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss @@ -9,7 +9,6 @@ .libraries { margin-top: $s-8; width: 100%; - padding: 0 var(--sp-m); } .selected-colors { diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index 25cfffa002..bf2c5fb75b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -128,6 +128,7 @@ {:x (.-clientX ^js event) :y (.-clientY ^js event) :on-accept edit-color + :origin :assets :data color :position :right}))) @@ -407,6 +408,7 @@ {:x x-position :y y-position :on-accept add-color + :origin :assets :data {:color "#406280" :opacity 1} :position :right}))))) 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 index 64a34297a1..10e70c3bee 100644 --- 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 @@ -10,6 +10,7 @@ [app.common.data :as d] [app.main.data.workspace.colors :as dwc] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.tokens.application :as dwta] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.hooks :as h] @@ -34,6 +35,18 @@ (def xf:map-shape-id (map :shape-id)) +(defn- generate-color-operations + [groups old-color prev-colors] + (let [old-color (-> old-color + (dissoc :name :path) + (d/without-nils)) + prev-color (d/seek (partial get groups) prev-colors) + color-operations-old (get groups old-color) + color-operations-prev (get groups prev-colors) + color-operations (or color-operations-prev color-operations-old) + old-color (or prev-color old-color)] + [color-operations old-color])) + (mf/defc color-selection-menu* {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} [{:keys [shapes file-id libraries]}] @@ -57,22 +70,13 @@ on-change (mf/use-fn (fn [new-color old-color from-picker?] - (let [old-color (-> old-color - (dissoc :name :path) - (d/without-nils)) - - ;; When dragging on the color picker sometimes all + (let [;; When dragging on the color picker sometimes all ;; the shapes hasn't updated the color to the prev ;; value so we need this extra calculation groups (mf/ref-val groups-ref) prev-colors (mf/ref-val prev-colors-ref) - prev-color (d/seek (partial get groups) prev-colors) - - cops-old (get groups old-color) - cops-prev (get groups prev-colors) - cops (or cops-prev cops-old) - old-color (or prev-color old-color)] + [color-operations old-color] (generate-color-operations groups old-color prev-colors)] (when from-picker? (let [color (-> new-color @@ -81,7 +85,7 @@ (mf/set-ref-val! prev-colors-ref (conj prev-colors color)))) - (st/emit! (dwc/change-color-in-selected cops new-color old-color))))) + (st/emit! (dwc/change-color-in-selected color-operations new-color old-color))))) on-open (mf/use-fn #(mf/set-ref-val! prev-colors-ref [])) @@ -93,17 +97,31 @@ (mf/use-fn (fn [color] (let [groups (mf/ref-val groups-ref) - cops (get groups color) + color-operations (get groups color) color' (dissoc color :id :file-id)] - (st/emit! (dwc/change-color-in-selected cops color' color))))) + (st/emit! (dwc/change-color-in-selected color-operations color' color))))) select-only (mf/use-fn (fn [color] (let [groups (mf/ref-val groups-ref) - cops (get groups color) - ids (into (d/ordered-set) xf:map-shape-id cops)] - (st/emit! (dws/select-shapes ids)))))] + color-operations (get groups color) + ids (into (d/ordered-set) xf:map-shape-id color-operations)] + (st/emit! (dws/select-shapes ids))))) + on-token-change + (mf/use-fn + (fn [_ token old-color] + (let [groups (mf/ref-val groups-ref) + prev-colors (mf/ref-val prev-colors-ref) + resolved-value (:resolved-value token) + new-color (dwta/value->color resolved-value) + color (-> new-color + (dissoc :name :path) + (d/without-nils)) + [color-operations _] (generate-color-operations groups old-color prev-colors)] + (mf/set-ref-val! prev-colors-ref + (conj prev-colors color)) + (st/emit! (dwta/apply-token-on-selected color-operations token)))))] [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} @@ -132,7 +150,9 @@ :on-detach on-detach :select-only select-only :on-change #(on-change %1 color %2) + :on-token-change #(on-token-change %1 %2 color) :on-open on-open + :origin :color-selection-library :on-close on-close}])) (when (and (false? @expand-lib-color) (< 3 (count library-colors))) [:button {:class (stl/css :more-colors-btn) @@ -147,6 +167,8 @@ :index index :select-only select-only :on-change #(on-change %1 color %2) + :origin :color-selection + :on-token-change #(on-token-change %1 %2 color) :on-open on-open :on-close on-close}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index f49bbc6104..6d6293c451 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -226,6 +226,7 @@ :on-remove on-remove :disable-drag disable-drag? :on-focus on-focus + :origin :fill :select-on-focus (not disable-drag?) :on-blur on-blur}]))]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index edda70d353..67cf41449b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -201,6 +201,7 @@ :title (tr "workspace.options.grid.params.color") :disable-gradient true :disable-image true + :origin :guides :on-change handle-change-color :on-detach handle-detach-color}] [:button {:class (stl/css-case :show-more-options true @@ -242,6 +243,7 @@ :title (tr "workspace.options.grid.params.color") :disable-gradient true :disable-image true + :origin :guides :on-change handle-change-color :on-detach handle-detach-color}]]] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index afc6061ae1..0ce1ce95a6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -232,6 +232,7 @@ :title (tr "workspace.options.shadow-options.color") :disable-gradient true :disable-image true + :origin :shadow :on-change on-update-color :on-detach on-detach-color :on-open on-open-row diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index f68a90f356..740750b0cf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -50,6 +50,7 @@ :title (tr "workspace.options.canvas-background") :color color :on-change on-change + :origin :canvas :on-open on-open :on-close on-close}]]])) 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 b2bd704263..9e1dca9dcc 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 @@ -19,6 +19,7 @@ [app.main.ui.components.color-input :refer [color-input*]] [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]] + [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.formats :as fmt] @@ -47,10 +48,11 @@ (mf/defc color-row* [{:keys [index color class disable-gradient disable-opacity disable-image disable-picker hidden - on-change on-reorder on-detach on-open on-close on-remove - disable-drag on-focus on-blur select-only select-on-focus]}] + on-change on-reorder on-detach on-open on-close on-remove origin + disable-drag on-focus on-blur select-only select-on-focus on-token-change]}] (let [libraries (mf/deref refs/files) on-change (h/use-ref-callback on-change) + on-token-change (h/use-ref-callback on-token-change) file-id (or (:ref-file color) (:file-id color)) color-id (or (:ref-id color) (:id color)) @@ -70,6 +72,11 @@ class (if (some? class) (dm/str class " ") "") + active-tokens* (mf/use-ctx ctx/active-tokens-by-type) + active-tokens (if active-tokens* + @active-tokens* + {}) + opacity? (and (not multiple-colors?) (not library-color?) @@ -133,7 +140,7 @@ handle-click-color (mf/use-fn - (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open) + (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open active-tokens) (fn [color event] (let [color (cond multiple-colors? @@ -157,9 +164,13 @@ :disable-image disable-image ;; on-change second parameter means if the source is the color-picker :on-change #(on-change % index) + :on-token-change on-token-change :on-close (fn [value opacity id file-id] (when on-close (on-close value opacity id file-id))) + :active-tokens active-tokens + :color-origin origin + :origin :sidebar :data color}] (when (fn? on-open) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 9d084a2cb9..323ac06283 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -157,10 +157,11 @@ :on-remove on-remove :disable-drag disable-drag :on-focus on-focus + :origin :stroke :select-on-focus select-on-focus :on-blur on-blur}] - ;; Stroke Width, Alignment & Style + ;; Stroke Width, Alignment & Style [:div {:class (stl/css :stroke-options)} [:div {:class (stl/css :stroke-width-input-element) :title (tr "workspace.options.stroke-width")} 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 dfdd2f202d..cb7137e601 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -81,15 +81,29 @@ (fn [event] (dom/stop-propagation event) (st/emit! (dwtl/set-token-type-section-open type true) - ;; FIXME: use dom/get-client-position - (modal/show (:key modal) - {:x (.-clientX ^js event) - :y (.-clientY ^js event) - :position :right - :fields (:fields modal) - :title title - :action "create" - :token-type type})))) + ;; Normally the modal position is calculated by client-position, + ;; but in some cases it is opened programmatically (not by a user click), + ;; so we need to set its position explicitly. + (let [pos (dom/get-client-position event) + window-size (dom/get-window-size) + left-sidebar (dom/get-element "left-sidebar-aside") + x-size (dom/get-data left-sidebar "size") + modal-size {:width 452 + :height 392} + x (if (= 0 (:x pos)) + (- (int x-size) 30) + (:x pos)) + y (if (= 0 (:y pos)) + (- (/ (:height window-size) 2) (/ (:height modal-size) 2)) + (:y pos))] + (modal/show (:key modal) + {:x x + :y y + :position :right + :fields (:fields modal) + :title title + :action "create" + :token-type type}))))) on-token-pill-click (mf/use-fn @@ -111,6 +125,7 @@ [:> icon-button* {:on-click on-popover-open-click :variant "ghost" :icon i/add + :id (str "add-token-button-" title) :aria-label (tr "workspace.tokens.add-token" title)}])] (when is-open [:& cmm/asset-section-block {:role :content} diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs index 3b772f2009..9467890257 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs @@ -286,6 +286,7 @@ [:div {:ref dref :role "button" :data-testid "tokens-set-item" + :id (str "token-set-item-" (str/join "/" tree-path)) :style {"--tree-depth" tree-depth} :class (stl/css-case :set-item-container true :selected-set is-selected diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f1e39655a9..4adfab8ae6 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1972,6 +1972,10 @@ msgstr "Close" msgid "labels.collapse" msgstr "Collapse" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "labels.color" +msgstr "Color" + #: src/app/main/ui/comments.cljs:913 msgid "labels.comment" msgstr "Comment" @@ -2777,6 +2781,12 @@ msgstr "Radial" msgid "media.solid" msgstr "Solid" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "color-token.empty-state" +msgstr "" +"No available color tokens. " +"Check active sets/themes or add new tokens." + #: src/app/main/data/common.cljs:128 msgid "modals.add-shared-confirm-empty.hint" msgstr "" @@ -7850,6 +7860,10 @@ msgstr "Invalid value: Units are not allowed." msgid "workspace.tokens.warning-name-change" msgstr "Renaming this token will break any reference to its old name." +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.colorpicker.color-tokens" +msgstr "Color tokens" + #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:145 msgid "workspace.toolbar.assets" msgstr "Assets" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index bab450d86e..6f9da5ad86 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1978,6 +1978,10 @@ msgstr "Cerrar" msgid "labels.collapse" msgstr "Colapsar" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "labels.color" +msgstr "Color" + #: src/app/main/ui/comments.cljs:913 msgid "labels.comment" msgstr "Comentario" @@ -2653,6 +2657,10 @@ msgstr "Visibilidad" msgid "labels.svg" msgstr "SVG" +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:518 +msgid "labels.variant" +msgstr "Variante" + #: src/app/main/ui/ds/product/loader.cljs:21 msgid "loader.tips.01.message" msgstr "" @@ -2777,6 +2785,12 @@ msgstr "Radial" msgid "media.solid" msgstr "Sólido" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "color-token.empty-state" +msgstr "" +"No hay tokens disponibles. " +"Comprueba los sets/themes activos o crea nuevos tokens de color." + #: src/app/main/data/common.cljs:128 msgid "modals.add-shared-confirm-empty.hint" msgstr "" @@ -7737,6 +7751,10 @@ msgstr "Valor no válido: No se permiten unidades." msgid "workspace.tokens.warning-name-change" msgstr "Al renombrar este token se romperán las referencias al nombre anterior" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.colorpicker.color-tokens" +msgstr "Tokens de color" + #: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:145 msgid "workspace.toolbar.assets" msgstr "Recursos"