From d54c5476d8a1405b0de0677940ef24dc5f117c28 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 14:33:45 +0200 Subject: [PATCH 1/5] Add dropdown button --- frontend/src/app/main/refs.cljs | 3 +++ .../src/app/main/ui/workspace/tokens/common.cljs | 6 ++++-- .../main/ui/workspace/tokens/modals/themes.cljs | 14 +++++++++++--- .../main/ui/workspace/tokens/modals/themes.scss | 15 +++++++++++++++ .../app/main/ui/workspace/tokens/token_set.cljs | 8 ++++++++ 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 3e6dbf02c..af5207ffb 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -244,6 +244,9 @@ [id] (l/derived #(wtts/get-workspace-theme id %) st/state)) +(def workspace-token-theme-groups + (l/derived wtts/get-workspace-theme-groups st/state)) + (def workspace-active-theme-ids (l/derived wtts/get-active-theme-ids st/state)) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs index cd955330c..e1f258053 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -34,7 +34,7 @@ (mf/defc labeled-input {::mf/wrap-props false} - [{:keys [label input-props auto-complete? error?]}] + [{:keys [label input-props auto-complete? error? icon render-right]}] (let [input-props (cond-> input-props :always camel-keys ;; Disable auto-complete on form fields for proprietary password managers @@ -45,4 +45,6 @@ [:label {:class (stl/css-case :labeled-input true :labeled-input-error error?)} [:span {:class (stl/css :label)} label] - [:& :input input-props]])) + [:& :input input-props] + (when render-right + [:& render-right])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 91c949826..2d6465c07 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -108,7 +108,7 @@ "Create theme"]]])) (mf/defc edit-theme - [{:keys [token-sets theme on-back on-submit] :as props}] + [{:keys [token-sets theme theme-groups on-back on-submit]}] (let [edit? (some? (:id theme)) theme-state (mf/use-state {:token-sets token-sets :theme theme}) @@ -153,7 +153,13 @@ [:div {:class (stl/css :edit-theme-inputs-wrapper)} [:& labeled-input {:label "Group" :input-props {:default-value (:group theme) - :on-change on-update-group}}] + :on-change on-update-group} + :render-right (mf/fnc [] + [:button {:class (stl/css :group-drop-down-button) + :type "button" + :on-click (fn [e] + (dom/stop-propagation e))} + i/arrow])}] [:& labeled-input {:label "Theme" :input-props {:default-value (:name theme) :on-change on-update-name}}]] @@ -189,10 +195,12 @@ [{:keys [state set-state]}] (let [{:keys [theme-id]} @state token-sets (mf/deref refs/workspace-ordered-token-sets) - theme (mf/deref (refs/workspace-token-theme theme-id))] + theme (mf/deref (refs/workspace-token-theme theme-id)) + theme-groups (mf/deref refs/workspace-token-theme-groups)] [:& edit-theme {:token-sets token-sets :theme theme + :theme-groups theme-groups :on-back #(set-state (constantly {:type :themes-overview})) :on-submit #(st/emit! (wdt/update-token-theme %))}])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss index 43844c392..83ca1ae21 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss @@ -211,3 +211,18 @@ hr { } } } + +.group-drop-down-button { + @include buttonStyle; + width: $s-24; + height: 100%; + + padding: 0; + margin: 0 $s-6; + + svg { + @extend .button-icon-small; + transform: rotate(90deg); + fill: var(--icon-foreground); + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs index 62eb5def4..ee2678c21 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs @@ -18,6 +18,14 @@ (defn get-workspace-themes-index [state] (get-in state [:workspace-data :token-themes-index] {})) +(defn get-workspace-theme-groups [state] + (reduce + (fn [acc {:keys [group]}] + (if group + (conj acc group) + acc)) + #{} (vals (get-workspace-themes-index state)))) + (defn get-workspace-token-set-groups [state] (get-in state [:workspace-data :token-set-groups])) From df16d0c222d6a90f2f73af6e9a53d2632283d852 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 15:16:52 +0200 Subject: [PATCH 2/5] Add abstract dropdown component --- .../app/main/ui/workspace/tokens/common.cljs | 93 ++++++++++++++++++- .../app/main/ui/workspace/tokens/common.scss | 79 ++++++++++++++++ .../ui/workspace/tokens/modals/themes.cljs | 21 ++++- .../ui/workspace/tokens/modals/themes.scss | 4 + 4 files changed, 192 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs index e1f258053..7f1c78779 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -8,8 +8,19 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.data.shortcuts :as dsc] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.globals :as globals] + [app.util.keyboard :as kbd] [cuerdas.core :as str] - [rumext.v2 :as mf])) + [goog.events :as events] + [rumext.v2 :as mf]) + (:import + goog.events.EventType)) ;; Helpers --------------------------------------------------------------------- @@ -30,8 +41,88 @@ [(keyword (str/camel (name k))) v] [k v]))))) +(defn direction-select + "Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`. + + `direction` accepts `:up` or `:down`." + [direction n coll] + (let [last-n (dec (count coll)) + next-n (case direction + :up (dec n) + :down (inc n)) + wrap-around-n (cond + (neg? next-n) last-n + (> next-n last-n) 0 + :else next-n)] + wrap-around-n)) + +(defn use-arrow-highlight [{:keys [shortcuts-key options on-select]}] + (let [highlighted* (mf/use-state nil) + highlighted (deref highlighted*) + on-dehighlight #(reset! highlighted* nil) + on-keyup (fn [event] + (cond + (and (kbd/enter? event) highlighted) (on-select (nth options highlighted)) + (kbd/up-arrow? event) (do + (dom/prevent-default event) + (->> (direction-select :up (or highlighted 0) options) + (reset! highlighted*))) + (kbd/down-arrow? event) (do + (dom/prevent-default event) + (->> (direction-select :down (or highlighted -1) options) + (reset! highlighted*)))))] + (mf/with-effect [highlighted] + (let [shortcuts-key shortcuts-key + keys [(events/listen globals/document EventType.KEYUP on-keyup) + (events/listen globals/document EventType.KEYDOWN dom/prevent-default)]] + (st/emit! (dsc/push-shortcuts shortcuts-key {})) + (fn [] + (doseq [key keys] + (events/unlistenByKey key)) + (st/emit! (dsc/pop-shortcuts shortcuts-key))))) + {:highlighted highlighted + :on-dehighlight on-dehighlight})) + +(defn use-dropdown-open-state [] + (let [open? (mf/use-state false) + on-open (mf/use-fn #(reset! open? true)) + on-close (mf/use-fn #(reset! open? false)) + on-toggle (mf/use-fn #(swap! open? not))] + {:dropdown-open? @open? + :on-open-dropdown on-open + :on-close-dropdown on-close + :on-toggle-dropdown on-toggle})) + ;; Components ------------------------------------------------------------------ +(mf/defc dropdown-select + [{:keys [id _shortcuts-key options on-close element-ref on-select] :as props}] + (let [{:keys [highlighted on-dehighlight]} (use-arrow-highlight props)] + [:& dropdown {:show true + :on-close on-close} + [:> :div {:class (stl/css :dropdown) + :on-mouse-enter on-dehighlight + :ref element-ref} + [:ul {:class (stl/css :dropdown-list)} + (for [[index item] (d/enumerate options)] + (cond + (= :separator item) + [:li {:class (stl/css :separator) + :key (dm/str id "-" index)}] + :else + (let [{:keys [label selected? disabled?]} item + highlighted? (= highlighted index)] + [:li + {:key (str id "-" index) + :class (stl/css-case :dropdown-element true + :is-selected selected? + :is-highlighted highlighted?) + :data-label label + :disabled disabled? + :on-click #(on-select item)} + [:span {:class (stl/css :label)} label] + [:span {:class (stl/css :check-icon)} i/tick]])))]]])) + (mf/defc labeled-input {::mf/wrap-props false} [{:keys [label input-props auto-complete? error? icon render-right]}] diff --git a/frontend/src/app/main/ui/workspace/tokens/common.scss b/frontend/src/app/main/ui/workspace/tokens/common.scss index 9398a2bb2..bb067e683 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.scss +++ b/frontend/src/app/main/ui/workspace/tokens/common.scss @@ -34,3 +34,82 @@ @extend .button-icon; } } + +.dropdown { + @extend .dropdown-wrapper; + max-height: $s-320; + width: 100%; + margin-top: $s-4; + + ul { + margin: 0; + } + + .separator { + margin: 0; + height: $s-12; + } + + .dropdown-element { + @extend .dropdown-element-base; + color: var(--menu-foreground-color-rest); + display: flex; + + & > span { + display: flex; + justify-content: flex-start; + align-content: center; + } + + .label, + .value { + width: fit-content; + } + + .label { + text-transform: unset; + flex: 1; + } + + .value { + text-align: right; + justify-content: flex-end; + flex: 0.6; + } + + .check-icon { + @include flexCenter; + translate: -$s-4 0; + svg { + @extend .button-icon-small; + visibility: hidden; + stroke: var(--icon-foreground); + } + } + + &.is-selected { + color: var(--menu-foreground-color); + .check-icon svg { + stroke: var(--menu-foreground-color); + visibility: visible; + } + } + + &:hover { + background-color: var(--menu-background-color-hover); + color: var(--menu-foreground-color-hover); + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + &.is-highlighted { + background-color: var(--button-primary-background-color-rest); + span { + color: var(--button-primary-foreground-color-rest); + } + .check-icon svg { + stroke: var(--button-primary-foreground-color-rest); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 2d6465c07..520255da9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -13,13 +13,14 @@ [app.main.store :as st] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.icons :as i] - [app.main.ui.workspace.tokens.common :refer [labeled-input]] + [app.main.ui.workspace.tokens.common :refer [labeled-input] :as wtco] [app.main.ui.workspace.tokens.sets :as wts] [app.main.ui.workspace.tokens.token-set :as wtts] [app.util.dom :as dom] [rumext.v2 :as mf] [cuerdas.core :as str] - [app.main.ui.workspace.tokens.sets-context :as sets-context])) + [app.main.ui.workspace.tokens.sets-context :as sets-context] + [app.main.ui.shapes.group :as group])) (def ^:private chevron-icon (i/icon-xref :arrow (stl/css :chevron-icon))) @@ -109,7 +110,9 @@ (mf/defc edit-theme [{:keys [token-sets theme theme-groups on-back on-submit]}] - (let [edit? (some? (:id theme)) + (let [{:keys [dropdown-open? on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state) + + edit? (some? (:id theme)) theme-state (mf/use-state {:token-sets token-sets :theme theme}) disabled? (-> (get-in @theme-state [:theme :name]) @@ -151,6 +154,15 @@ :on-click on-back} chevron-icon "Back"]] [:div {:class (stl/css :edit-theme-inputs-wrapper)} + (when dropdown-open? + [:& wtco/dropdown-select {:id ::groups-dropdown + :shortcuts-key ::groups-dropdown + :options (map (fn [group] + {:label group + :value group}) + theme-groups) + :on-select #(on-update-group (:value %)) + :on-close on-close-dropdown}]) [:& labeled-input {:label "Group" :input-props {:default-value (:group theme) :on-change on-update-group} @@ -158,7 +170,8 @@ [:button {:class (stl/css :group-drop-down-button) :type "button" :on-click (fn [e] - (dom/stop-propagation e))} + (dom/stop-propagation e) + (on-toggle-dropdown))} i/arrow])}] [:& labeled-input {:label "Theme" :input-props {:default-value (:name theme) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss index 83ca1ae21..ca41bd82e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss @@ -212,6 +212,10 @@ hr { } } +.group-input-wrapper { + position: relative; +} + .group-drop-down-button { @include buttonStyle; width: $s-24; From 21f42021d85b332bb7bb027409cbd0536cfa9e49 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 15:37:01 +0200 Subject: [PATCH 3/5] Add groups select --- .../ui/workspace/tokens/modals/themes.cljs | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 520255da9..c693aae55 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -130,6 +130,7 @@ on-change-field (fn [field] (fn [e] (swap! theme-state (fn [st] (assoc-in st field (dom/get-target-val e)))))) + group-input-ref (mf/use-ref) on-update-group (on-change-field [:theme :group]) on-update-name (on-change-field [:theme :name]) on-save-form (mf/use-callback @@ -154,25 +155,29 @@ :on-click on-back} chevron-icon "Back"]] [:div {:class (stl/css :edit-theme-inputs-wrapper)} - (when dropdown-open? - [:& wtco/dropdown-select {:id ::groups-dropdown - :shortcuts-key ::groups-dropdown - :options (map (fn [group] - {:label group - :value group}) - theme-groups) - :on-select #(on-update-group (:value %)) - :on-close on-close-dropdown}]) - [:& labeled-input {:label "Group" - :input-props {:default-value (:group theme) - :on-change on-update-group} - :render-right (mf/fnc [] - [:button {:class (stl/css :group-drop-down-button) - :type "button" - :on-click (fn [e] - (dom/stop-propagation e) - (on-toggle-dropdown))} - i/arrow])}] + [:div {:class (stl/css :group-input-wrapper)} + (when dropdown-open? + [:& wtco/dropdown-select {:id ::groups-dropdown + :shortcuts-key ::groups-dropdown + :options (map (fn [group] + {:label group + :value group}) + theme-groups) + :on-select (fn [{:keys [value]}] + (set! (.-value (mf/ref-val group-input-ref)) value) + (swap! theme-state assoc-in [:theme :group] value)) + :on-close on-close-dropdown}]) + [:& labeled-input {:label "Group" + :input-props {:ref group-input-ref + :default-value (:group theme) + :on-change on-update-group} + :render-right (mf/fnc [] + [:button {:class (stl/css :group-drop-down-button) + :type "button" + :on-click (fn [e] + (dom/stop-propagation e) + (on-toggle-dropdown))} + i/arrow])}]] [:& labeled-input {:label "Theme" :input-props {:default-value (:name theme) :on-change on-update-name}}]] From 281b801112ecb43029cd74dbd039328ea4bc08cc Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 15:42:44 +0200 Subject: [PATCH 4/5] Show dropdown only when groups exist --- .../main/ui/workspace/tokens/modals/themes.cljs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index c693aae55..6557e5ab7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -171,13 +171,14 @@ :input-props {:ref group-input-ref :default-value (:group theme) :on-change on-update-group} - :render-right (mf/fnc [] - [:button {:class (stl/css :group-drop-down-button) - :type "button" - :on-click (fn [e] - (dom/stop-propagation e) - (on-toggle-dropdown))} - i/arrow])}]] + :render-right (when (seq theme-groups) + (mf/fnc [] + [:button {:class (stl/css :group-drop-down-button) + :type "button" + :on-click (fn [e] + (dom/stop-propagation e) + (on-toggle-dropdown))} + i/arrow]))}]] [:& labeled-input {:label "Theme" :input-props {:default-value (:name theme) :on-change on-update-name}}]] From 56374171d6ac79488753e59d4a488d1b5acc0112 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 15:44:11 +0200 Subject: [PATCH 5/5] Fix theme groups not showing up in create state --- frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 6557e5ab7..47e00588d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -226,10 +226,12 @@ (mf/defc create-theme [{:keys [set-state]}] (let [token-sets (mf/deref refs/workspace-ordered-token-sets) - theme {:name "" :sets #{}}] + theme {:name "" :sets #{}} + theme-groups (mf/deref refs/workspace-token-theme-groups)] [:& edit-theme {:token-sets token-sets :theme theme + :theme-groups theme-groups :on-back #(set-state (constantly {:type :themes-overview})) :on-submit #(st/emit! (wdt/create-token-theme %))}]))