mirror of
https://github.com/penpot/penpot.git
synced 2025-05-04 23:45:54 +02:00
Merge pull request #277 from tokens-studio/group-select-themes
Themes & Sets: Add groups select to modal
This commit is contained in:
commit
f5ab6e65fc
6 changed files with 243 additions and 12 deletions
|
@ -244,6 +244,9 @@
|
||||||
[id]
|
[id]
|
||||||
(l/derived #(wtts/get-workspace-theme id %) st/state))
|
(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
|
(def workspace-active-theme-ids
|
||||||
(l/derived wtts/get-active-theme-ids st/state))
|
(l/derived wtts/get-active-theme-ids st/state))
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,19 @@
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[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]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[goog.events :as events]
|
||||||
|
[rumext.v2 :as mf])
|
||||||
|
(:import
|
||||||
|
goog.events.EventType))
|
||||||
|
|
||||||
;; Helpers ---------------------------------------------------------------------
|
;; Helpers ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -30,11 +41,91 @@
|
||||||
[(keyword (str/camel (name k))) v]
|
[(keyword (str/camel (name k))) v]
|
||||||
[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 ------------------------------------------------------------------
|
;; 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/defc labeled-input
|
||||||
{::mf/wrap-props false}
|
{::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
|
(let [input-props (cond-> input-props
|
||||||
:always camel-keys
|
:always camel-keys
|
||||||
;; Disable auto-complete on form fields for proprietary password managers
|
;; Disable auto-complete on form fields for proprietary password managers
|
||||||
|
@ -45,4 +136,6 @@
|
||||||
[:label {:class (stl/css-case :labeled-input true
|
[:label {:class (stl/css-case :labeled-input true
|
||||||
:labeled-input-error error?)}
|
:labeled-input-error error?)}
|
||||||
[:span {:class (stl/css :label)} label]
|
[:span {:class (stl/css :label)} label]
|
||||||
[:& :input input-props]]))
|
[:& :input input-props]
|
||||||
|
(when render-right
|
||||||
|
[:& render-right])]))
|
||||||
|
|
|
@ -34,3 +34,82 @@
|
||||||
@extend .button-icon;
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,13 +13,14 @@
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||||
[app.main.ui.icons :as i]
|
[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.sets :as wts]
|
||||||
[app.main.ui.workspace.tokens.token-set :as wtts]
|
[app.main.ui.workspace.tokens.token-set :as wtts]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[rumext.v2 :as mf]
|
[rumext.v2 :as mf]
|
||||||
[cuerdas.core :as str]
|
[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
|
(def ^:private chevron-icon
|
||||||
(i/icon-xref :arrow (stl/css :chevron-icon)))
|
(i/icon-xref :arrow (stl/css :chevron-icon)))
|
||||||
|
@ -108,8 +109,10 @@
|
||||||
"Create theme"]]]))
|
"Create theme"]]]))
|
||||||
|
|
||||||
(mf/defc edit-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))
|
(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-state (mf/use-state {:token-sets token-sets
|
||||||
:theme theme})
|
:theme theme})
|
||||||
disabled? (-> (get-in @theme-state [:theme :name])
|
disabled? (-> (get-in @theme-state [:theme :name])
|
||||||
|
@ -127,6 +130,7 @@
|
||||||
on-change-field (fn [field]
|
on-change-field (fn [field]
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(swap! theme-state (fn [st] (assoc-in st field (dom/get-target-val 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-group (on-change-field [:theme :group])
|
||||||
on-update-name (on-change-field [:theme :name])
|
on-update-name (on-change-field [:theme :name])
|
||||||
on-save-form (mf/use-callback
|
on-save-form (mf/use-callback
|
||||||
|
@ -151,9 +155,30 @@
|
||||||
:on-click on-back}
|
:on-click on-back}
|
||||||
chevron-icon "Back"]]
|
chevron-icon "Back"]]
|
||||||
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
|
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
|
||||||
[:& labeled-input {:label "Group"
|
[:div {:class (stl/css :group-input-wrapper)}
|
||||||
:input-props {:default-value (:group theme)
|
(when dropdown-open?
|
||||||
:on-change on-update-group}}]
|
[:& 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 (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"
|
[:& labeled-input {:label "Theme"
|
||||||
:input-props {:default-value (:name theme)
|
:input-props {:default-value (:name theme)
|
||||||
:on-change on-update-name}}]]
|
:on-change on-update-name}}]]
|
||||||
|
@ -189,20 +214,24 @@
|
||||||
[{:keys [state set-state]}]
|
[{:keys [state set-state]}]
|
||||||
(let [{:keys [theme-id]} @state
|
(let [{:keys [theme-id]} @state
|
||||||
token-sets (mf/deref refs/workspace-ordered-token-sets)
|
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
|
[:& edit-theme
|
||||||
{:token-sets token-sets
|
{:token-sets token-sets
|
||||||
:theme theme
|
:theme theme
|
||||||
|
:theme-groups theme-groups
|
||||||
:on-back #(set-state (constantly {:type :themes-overview}))
|
:on-back #(set-state (constantly {:type :themes-overview}))
|
||||||
:on-submit #(st/emit! (wdt/update-token-theme %))}]))
|
:on-submit #(st/emit! (wdt/update-token-theme %))}]))
|
||||||
|
|
||||||
(mf/defc create-theme
|
(mf/defc create-theme
|
||||||
[{:keys [set-state]}]
|
[{:keys [set-state]}]
|
||||||
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
|
(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
|
[:& edit-theme
|
||||||
{:token-sets token-sets
|
{:token-sets token-sets
|
||||||
:theme theme
|
:theme theme
|
||||||
|
:theme-groups theme-groups
|
||||||
:on-back #(set-state (constantly {:type :themes-overview}))
|
:on-back #(set-state (constantly {:type :themes-overview}))
|
||||||
:on-submit #(st/emit! (wdt/create-token-theme %))}]))
|
:on-submit #(st/emit! (wdt/create-token-theme %))}]))
|
||||||
|
|
||||||
|
|
|
@ -211,3 +211,22 @@ hr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,14 @@
|
||||||
(defn get-workspace-themes-index [state]
|
(defn get-workspace-themes-index [state]
|
||||||
(get-in state [:workspace-data :token-themes-index] {}))
|
(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]
|
(defn get-workspace-token-set-groups [state]
|
||||||
(get-in state [:workspace-data :token-set-groups]))
|
(get-in state [:workspace-data :token-set-groups]))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue