🚧 Add dropdown to component

This commit is contained in:
Eva Marco 2025-10-21 13:43:05 +02:00
parent 942149ae87
commit f22633d18b
10 changed files with 346 additions and 117 deletions

View File

@ -100,7 +100,8 @@
(mf/defc options-dropdown* (mf/defc options-dropdown*
{::mf/schema schema:options-dropdown} ;; TODO: Review schema
;; {::mf/schema schema:options-dropdown}
[{:keys [ref on-click options selected focused empty-to-end align] :rest props}] [{:keys [ref on-click options selected focused empty-to-end align] :rest props}]
(let [align (let [align
(d/nilv align :left) (d/nilv align :left)

View File

@ -25,7 +25,7 @@
[:focused {:optional true} :boolean]]) [:focused {:optional true} :boolean]])
(mf/defc token-option* (mf/defc token-option*
{::mf/schema schema:token-option} ;; {::mf/schema schema:token-option}
[{:keys [id name on-click selected ref focused resolved] :rest props}] [{:keys [id name on-click selected ref focused resolved] :rest props}]
(let [internal-id (mf/use-id) (let [internal-id (mf/use-id)
id (d/nilv id internal-id)] id (d/nilv id internal-id)]
@ -56,5 +56,12 @@
[:span {:aria-labelledby (dm/str id "-name")} [:span {:aria-labelledby (dm/str id "-name")}
name]] name]]
(when resolved (when resolved
[:> :span {:class (stl/css :option-pill)} (cond
resolved])])) (map? resolved)
(for [[k v] resolved]
[:div {:key (str k)}
[:span (dm/str (d/name k) ": ")]
[:strong (str v)]])
:else
[:span {:class (stl/css :option-pill)}
resolved]))]))

View File

@ -120,35 +120,33 @@
sorted-tokens) sorted-tokens)
no-sets? (nil? sorted-tokens)] no-sets? (nil? sorted-tokens)]
(generate-dropdown-options options no-sets?)))) (generate-dropdown-options options no-sets?))))
on-option-click on-option-click
(mf/use-fn (mf/use-fn
(mf/deps ) (mf/deps)
(fn [event] (fn [event]))
)) focused-id* (mf/use-state nil)
focused-id* (mf/use-state nil)
focused-id (deref focused-id*) focused-id (deref focused-id*)
selected-id* selected-id*
(mf/use-state (fn [] (mf/use-state (fn []))
)) empty-to-end (d/nilv empty-to-end false)
empty-to-end (d/nilv empty-to-end false) selected-id
selected-id
(deref selected-id*) (deref selected-id*)
listbox-id (mf/use-id) listbox-id (mf/use-id)
nodes-ref (mf/use-ref nil) nodes-ref (mf/use-ref nil)
set-option-ref set-option-ref
(mf/use-fn (mf/use-fn
(fn [node] (fn [node]
(let [state (mf/ref-val nodes-ref) (let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {}) state (d/nilv state #js {})
id (dom/get-data node "id") id (dom/get-data node "id")
state (obj/set! state id node)] state (obj/set! state id node)]
(mf/set-ref-val! nodes-ref state) (mf/set-ref-val! nodes-ref state)
(fn [] (fn []
(let [state (mf/ref-val nodes-ref) (let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {}) state (d/nilv state #js {})
id (dom/get-data node "id") id (dom/get-data node "id")
state (obj/unset! state id)] state (obj/unset! state id)]
(mf/set-ref-val! nodes-ref state)))))) (mf/set-ref-val! nodes-ref state))))))
props (mf/spread-props props {:type type props (mf/spread-props props {:type type
:id id :id id
:has-hint has-hint :has-hint has-hint

View File

@ -155,61 +155,65 @@
tabs-action-button tabs-action-button
(mf/with-memo [] (mf/with-memo []
(mf/html [:> collapse-button* {}]))] (mf/html [:> collapse-button* {}]))
[:> (mf/provider muc/sidebar) {:value :left} active-tokens-by-type
[:aside {:ref parent-ref (mf/with-memo [resolved-active-tokens]
:id "left-sidebar-aside" (delay (ctob/group-by-type resolved-active-tokens)))]
:data-testid "left-sidebar"
:data-left-sidebar-width (str width)
:class aside-class
:style {:--left-sidebar-width (dm/str width "px")}}
[:> left-header* [:> (mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type}
{:file file [:> (mf/provider muc/sidebar) {:value :left}
:layout layout [:aside {:ref parent-ref
:project project :id "left-sidebar-aside"
:page-id page-id :data-testid "left-sidebar"
:class (stl/css :left-header)}] :data-left-sidebar-width (str width)
:class aside-class
:style {:--left-sidebar-width (dm/str width "px")}}
[:div {:on-pointer-down on-pointer-down [:> left-header*
:on-lost-pointer-capture on-lost-pointer-capture {:file file
:on-pointer-move on-pointer-move :layout layout
:class (stl/css :resize-area)}] :project project
:page-id page-id
:class (stl/css :left-header)}]
(cond [:div {:on-pointer-down on-pointer-down
(true? shortcuts?) :on-lost-pointer-capture on-lost-pointer-capture
[:> shortcuts-container* {:class (stl/css :settings-bar-content)}] :on-pointer-move on-pointer-move
:class (stl/css :resize-area)}]
(true? show-debug?) (cond
[:> debug-panel* {:class (stl/css :settings-bar-content)}] (true? shortcuts?)
[:> shortcuts-container* {:class (stl/css :settings-bar-content)}]
:else (true? show-debug?)
[:div {:class (stl/css :settings-bar-content)} [:> debug-panel* {:class (stl/css :settings-bar-content)}]
[:> tab-switcher* {:tabs tabs
:default "layers"
:selected (name section)
:on-change on-tab-change
:class (stl/css :left-sidebar-tabs)
:action-button-position "start"
:action-button tabs-action-button}
(case section :else
:assets [:div {:class (stl/css :settings-bar-content)}
[:> assets-toolbox* [:> tab-switcher* {:tabs tabs
{:size (- width 58) :default "layers"
:file-id file-id}] :selected (name section)
:on-change on-tab-change
:class (stl/css :left-sidebar-tabs)
:action-button-position "start"
:action-button tabs-action-button}
:tokens (case section
[:> tokens-sidebar-tab* :assets
{:tokens-lib tokens-lib [:> assets-toolbox*
:active-tokens active-tokens {:size (- width 58)
:resolved-active-tokens resolved-active-tokens}] :file-id file-id}]
:layers :tokens
[:> layers-content* [:> tokens-sidebar-tab*
{:layout layout {:tokens-lib tokens-lib
:width width}])]])]])) :active-tokens active-tokens}]
:layers
[:> layers-content*
{:layout layout
:width width}])]])]]]))
;; --- Right Sidebar (Component) ;; --- Right Sidebar (Component)

View File

@ -11,6 +11,7 @@
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
@ -115,7 +116,9 @@
[empty-group filled-group] [empty-group filled-group]
(mf/with-memo [tokens-by-type] (mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))] (get-sorted-token-groups tokens-by-type))
active-theme-tokens (mf/use-ctx muc/active-tokens-by-type)]
(mf/with-effect [tokens-lib selected-token-set-id] (mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib (when (and tokens-lib
@ -150,7 +153,7 @@
:selected-ids selected :selected-ids selected
:selected-shapes selected-shapes :selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens resolved-active-tokens :active-theme-tokens active-theme-tokens
:tokens tokens}])) :tokens tokens}]))
(for [type empty-group] (for [type empty-group]
@ -158,5 +161,5 @@
:type type :type type
:selected-shapes selected-shapes :selected-shapes selected-shapes
:is-selected-inside-layout :is-selected-inside-layout :is-selected-inside-layout :is-selected-inside-layout
:active-theme-tokens resolved-active-tokens :active-theme-tokens active-theme-tokens
:tokens []}])])) :tokens []}])]))

View File

@ -294,7 +294,7 @@
selected-token-set-id selected-token-set-id
action action
input-value-placeholder input-value-placeholder
tokens
;; Callbacks ;; Callbacks
validate-token validate-token
on-value-resolve on-value-resolve
@ -622,6 +622,8 @@
:label label :label label
:default-value default-value :default-value default-value
:ref ref :ref ref
:tokens tokens
:type token-type
:on-blur on-update-value :on-blur on-update-value
:on-change on-update-value :on-change on-update-value
:token-resolve-result token-resolve-result}]))] :token-resolve-result token-resolve-result}]))]
@ -662,13 +664,15 @@
;; Tabs Component -------------------------------------------------------------- ;; Tabs Component --------------------------------------------------------------
(mf/defc composite-reference-input* (mf/defc composite-reference-input*
[{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}] [{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn tokens]}]
[:> input-token* [:> input-token*
{:aria-label (tr "labels.reference") {:aria-label (tr "labels.reference")
:placeholder reference-label :placeholder reference-label
:icon reference-icon :icon reference-icon
:default-value (when (is-reference-fn default-value) default-value) :default-value (when (is-reference-fn default-value) default-value)
:on-blur on-blur :on-blur on-blur
:tokens tokens
:type "composite-reference"
:on-change on-update-value :on-change on-update-value
:token-resolve-result (when (or :token-resolve-result (when (or
(:errors token-resolve-result) (:errors token-resolve-result)
@ -681,6 +685,7 @@
on-external-update-value on-external-update-value
on-value-resolve on-value-resolve
clear-resolve-value clear-resolve-value
tokens
custom-input-token-value-props] custom-input-token-value-props]
:rest props}] :rest props}]
(let [;; Active Tab State (let [;; Active Tab State
@ -751,6 +756,7 @@
:on-update-value on-update-value' :on-update-value on-update-value'
:reference-icon reference-icon :reference-icon reference-icon
:reference-label reference-label :reference-label reference-label
:tokens tokens
:is-reference-fn is-reference-fn})] :is-reference-fn is-reference-fn})]
[:> composite-tab [:> composite-tab
(mf/spread-props props {:default-value default-value (mf/spread-props props {:default-value default-value
@ -760,7 +766,7 @@
(mf/defc composite-form* (mf/defc composite-form*
"Wrapper around form* that manages composite/reference tab state. "Wrapper around form* that manages composite/reference tab state.
Takes the same props as form* plus a function to determine if a token value is a reference." Takes the same props as form* plus a function to determine if a token value is a reference."
[{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value] :rest props}] [{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value tokens] :rest props}]
(let [active-tab* (mf/use-state (if (is-reference-fn (:value token)) :reference :composite)) (let [active-tab* (mf/use-state (if (is-reference-fn (:value token)) :reference :composite))
active-tab (deref active-tab*) active-tab (deref active-tab*)
@ -772,6 +778,7 @@
:set-active-tab #(reset! active-tab* %) :set-active-tab #(reset! active-tab* %)
:composite-tab composite-tab :composite-tab composite-tab
:reference-icon reference-icon :reference-icon reference-icon
:tokens tokens
:reference-label (tr "workspace.tokens.reference-composite") :reference-label (tr "workspace.tokens.reference-composite")
:title title :title title
:update-composite-backup-value update-composite-backup-value :update-composite-backup-value update-composite-backup-value
@ -848,8 +855,8 @@
:on-change on-change'}]])) :on-change on-change'}]]))
(mf/defc color-picker* (mf/defc color-picker*
[{:keys [placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}] [{:keys [ placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
(let [{:keys [color on-display-colorpicker]} custom-input-token-value-props (let [{:keys [color on-display-colorpicker tokens]} custom-input-token-value-props
color-ramp-open* (mf/use-state false) color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*) color-ramp-open? (deref color-ramp-open*)
@ -903,6 +910,8 @@
:default-value default-value :default-value default-value
:ref input-ref :ref input-ref
:on-blur on-blur :on-blur on-blur
:tokens tokens
:type "color"
:on-change on-update-value :on-change on-update-value
:slot-start swatch}] :slot-start swatch}]
(when color-ramp-open? (when color-ramp-open?
@ -912,7 +921,7 @@
[:> token-value-hint* {:result token-resolve-result}]])) [:> token-value-hint* {:result token-resolve-result}]]))
(mf/defc color-form* (mf/defc color-form*
[{:keys [token on-display-colorpicker] :rest props}] [{:keys [token on-display-colorpicker tokens] :rest props}]
(let [color* (mf/use-state (:value token)) (let [color* (mf/use-state (:value token))
color (deref color*) color (deref color*)
on-value-resolve (mf/use-fn on-value-resolve (mf/use-fn
@ -926,6 +935,7 @@
(mf/deps color on-display-colorpicker) (mf/deps color on-display-colorpicker)
(fn [] (fn []
{:color color {:color color
:tokens tokens
:on-display-colorpicker on-display-colorpicker})) :on-display-colorpicker on-display-colorpicker}))
on-get-token-value on-get-token-value
@ -1233,7 +1243,7 @@
:full-size true}]])) :full-size true}]]))
(mf/defc font-picker-combobox* (mf/defc font-picker-combobox*
[{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}] [{:keys [tokens default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}]
(let [font* (mf/use-state (fonts/find-font-family default-value)) (let [font* (mf/use-state (fonts/find-font-family default-value))
font (deref font*) font (deref font*)
set-font (mf/use-fn set-font (mf/use-fn
@ -1287,6 +1297,8 @@
:ref input-ref :ref input-ref
:on-blur on-blur :on-blur on-blur
:on-change on-update-value' :on-change on-update-value'
:tokens tokens
:type "font-family"
:icon i/text-font-family :icon i/text-font-family
:slot-end font-selector-button :slot-end font-selector-button
:token-resolve-result token-resolve-result}] :token-resolve-result token-resolve-result}]
@ -1359,7 +1371,7 @@
:placeholder (tr "workspace.tokens.text-decoration-value-enter")})) :placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
(mf/defc typography-value-inputs* (mf/defc typography-value-inputs*
[{:keys [default-value on-blur on-update-value token-resolve-result]}] [{:keys [default-value on-blur on-update-value token-resolve-result tokens]}]
(let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) (let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
typography-inputs (mf/use-memo typography-inputs) typography-inputs (mf/use-memo typography-inputs)
errors-by-key (sd/collect-typography-errors token-resolve-result)] errors-by-key (sd/collect-typography-errors token-resolve-result)]
@ -1407,6 +1419,7 @@
:input-ref input-ref :input-ref input-ref
:default-value (when value (cto/join-font-family value)) :default-value (when value (cto/join-font-family value))
:on-blur on-blur :on-blur on-blur
:tokens tokens
:on-update-value on-change :on-update-value on-change
:on-external-update-value on-external-update-value :on-external-update-value on-external-update-value
:token-resolve-result token-prop}] :token-resolve-result token-prop}]
@ -1415,12 +1428,14 @@
:placeholder placeholder :placeholder placeholder
:default-value value :default-value value
:on-blur on-blur :on-blur on-blur
:tokens tokens
:icon icon :icon icon
:type "typography-subvalue"
:on-change on-change :on-change on-change
:token-resolve-result token-prop}])]))])) :token-resolve-result token-prop}])]))]))
(mf/defc typography-form* (mf/defc typography-form*
[{:keys [token] :rest props}] [{:keys [token tokens] :rest props}]
(let [on-get-token-value (let [on-get-token-value
(mf/use-fn (mf/use-fn
(fn [e prev-composite-value] (fn [e prev-composite-value]
@ -1451,13 +1466,15 @@
:is-reference-fn cto/typography-composite-token-reference? :is-reference-fn cto/typography-composite-token-reference?
:title (tr "labels.typography") :title (tr "labels.typography")
:validate-token validate-typography-token :validate-token validate-typography-token
:tokens tokens
:on-get-token-value on-get-token-value :on-get-token-value on-get-token-value
:update-composite-backup-value update-composite-backup-value})])) :update-composite-backup-value update-composite-backup-value})]))
(mf/defc form-wrapper* (mf/defc form-wrapper*
[{:keys [token token-type] :rest props}] [{:keys [token token-type tokens] :rest props}]
(let [token-type' (or (:type token) token-type) (let [token-type' (or (:type token) token-type)
props (mf/spread-props props {:token-type token-type' props (mf/spread-props props {:token-type token-type'
:tokens tokens
:token token})] :token token})]
(case token-type' (case token-type'
:color [:> color-form* props] :color [:> color-form* props]

View File

@ -7,18 +7,117 @@
(ns app.main.ui.workspace.tokens.management.create.input-tokens-value (ns app.main.ui.workspace.tokens.management.create.input-tokens-value
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.format :as dwtf] [app.main.data.workspace.tokens.format :as dwtf]
[app.main.data.workspace.tokens.warnings :as wtw] [app.main.data.workspace.tokens.warnings :as wtw]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]] [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]] [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]] [app.main.ui.ds.controls.utilities.label :refer [label*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]] [app.main.ui.ds.foundations.assets.icon :refer [icon-list] :as i]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.object :as obj]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def token-type->reference-types
{:color #{:color}
:dimensions #{:dimensions}
:spacing #{:spacing :dimensions}
:border-radius #{:border-radius :dimensions :sizing}
:font-family #{:font-family}
:font-size #{:font-size :sizing :dimension}
:opacity #{:opacity :number}
:rotation #{:rotation :number}
:stroke-width #{:stroke-width :dimension :sizing}
:sizing #{:sizing :dimensions}
:number #{:number}
:letter-spacing #{:letter-spacing}
:typography #{:typography}
:text-case #{:text-case}
:text-decoration #{:text-decoration}
:line-height #{:number}})
;; TODO; duplucated code with numeric-input.cljs, consider refactoring
(defn- sort-groups-and-tokens
"Sorts both the groups and the tokens inside them alphabetically.
Input:
A map where:
- keys are groups (keywords or strings, e.g. :dimensions, :colors)
- values are vectors of token maps, each containing at least a :name key
Example input:
{:dimensions [{:name \"tres\"} {:name \"quini\"}]
:colors [{:name \"azul\"} {:name \"rojo\"}]}
Output:
A sorted map where:
- groups are ordered alphabetically by key
- tokens inside each group are sorted alphabetically by :name
Example output:
{:colors [{:name \"azul\"} {:name \"rojo\"}]
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
[groups->tokens]
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
(for [[group tokens] groups->tokens]
[group (sort-by :name tokens)])))
(defn- extract-partial-brace-text
[string]
(when-let [start (str/last-index-of string "{")]
(subs string (inc start))))
(defn- filter-token-groups-by-name
[tokens filter-text]
(let [lc-filter (str/lower filter-text)]
(into {}
(keep (fn [[group tokens]]
(let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
(when (seq filtered)
[group filtered]))))
tokens)))
(defn- token->dropdown-option
[token]
{:id (str (get token :id))
:type :token
:resolved-value (get token :resolved-value)
:name (get token :name)})
(defn- generate-dropdown-options
[tokens no-sets]
(if (empty? tokens)
[{:type :empty
:label (if no-sets
(tr "ds.inputs.numeric-input.no-applicable-tokens")
(tr "ds.inputs.numeric-input.no-matches"))}]
(->> tokens
(map (fn [[type items]]
(cons {:group true
:type :group
:id (dm/str "group-" (name type))
:name (name type)}
(map token->dropdown-option items))))
(interpose [{:separator true
:id "separator"
:type :separator}])
(apply concat)
(vec)
(not-empty))))
(defn get-option
[options id]
(let [options (if (delay? options) @options options)]
(or (d/seek #(= id (get % :id)) options)
(nth options 0))))
(def ^:private schema::input-token (def ^:private schema::input-token
[:map [:map
[:label {:optional true} [:maybe :string]] [:label {:optional true} [:maybe :string]]
@ -58,21 +157,118 @@
(mf/defc input-token* (mf/defc input-token*
{::mf/forward-ref true {::mf/forward-ref true
::mf/schema schema::input-token} ::mf/schema schema::input-token}
[{:keys [class label token-resolve-result] :rest props} ref] [{:keys [class label token-resolve-result tokens empty-to-end type] :rest props} ref]
(let [error (not (nil? (:errors token-resolve-result))) (let [error (not (nil? (:errors token-resolve-result)))
id (mf/use-id) id (mf/use-id)
input-ref (mf/use-ref)
is-open* (mf/use-state false)
is-open (deref is-open*)
filter-term* (mf/use-state "")
filter-term (deref filter-term*)
listbox-id (mf/use-id)
focused-id* (mf/use-state nil)
focused-id (deref focused-id*)
selected-id* (mf/use-state (fn []))
selected-id (deref selected-id*)
empty-to-end (d/nilv empty-to-end false)
internal-ref (mf/use-ref nil)
ref (or ref internal-ref)
nodes-ref (mf/use-ref nil)
open-dropdown-ref (mf/use-ref nil)
options-ref (mf/use-ref nil)
set-option-ref
(mf/use-fn
(fn [node]
(let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {})
id (dom/get-data node "id")
state (obj/set! state id node)]
(mf/set-ref-val! nodes-ref state)
(fn []
(let [state (mf/ref-val nodes-ref)
state (d/nilv state #js {})
id (dom/get-data node "id")
state (obj/unset! state id)]
(mf/set-ref-val! nodes-ref state))))))
dropdown-options
(mf/with-memo [tokens filter-term type]
(delay
(let [tokens (if (delay? tokens) @tokens tokens)
allowed (get token-type->reference-types (keyword type) #{})
tokens (select-keys tokens allowed)
sorted-tokens (sort-groups-and-tokens tokens)
partial (extract-partial-brace-text filter-term)
options (if (seq partial)
(filter-token-groups-by-name sorted-tokens partial)
sorted-tokens)
no-sets? (nil? sorted-tokens)]
(generate-dropdown-options options no-sets?))))
update-input
(mf/use-fn
(mf/deps ref)
(fn [new-value]
(when-let [node (mf/ref-val ref)]
(dom/set-value! node new-value)
(reset! is-open* false))))
on-option-click
(mf/use-fn
(mf/deps options-ref update-input)
(fn [event]
(let [node (dom/get-current-target event)
id (dom/get-data node "id")
options (mf/ref-val options-ref)
options (if (delay? options) @options options)
option (get-option options id)
name (get option :name)
new-value (str "{" name "}")]
(update-input new-value))))
open-dropdown
(mf/use-fn
(fn [_]
(swap! is-open* not)))
props (mf/spread-props props {:id id props (mf/spread-props props {:id id
:type "text" :type "text"
:class (stl/css :input) :class (stl/css :input)
:variant "comfortable" :variant "comfortable"
:hint-type (when error "error") :hint-type (when error "error")
:ref (or ref input-ref)})] :slot-end (when (some? tokens)
(mf/html [:> icon-button* {:variant "action"
:icon i/tokens
:class (stl/css :invisible-button)
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:ref open-dropdown-ref
:on-click open-dropdown}]))
:ref ref})]
(mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options))
[:* [:*
[:div {:class (dm/str class " " (stl/css-case :wrapper true [:div {:class [class (stl/css-case :wrapper true
:input-error error))} :input-error error)]}
(when label (when label
[:> label* {:for id} label]) [:> label* {:for id} label])
[:> input-field* props]] [:> input-field* props]]
(when token-resolve-result (when token-resolve-result
[:> token-value-hint* {:result token-resolve-result}])])) [:> token-value-hint* {:result token-resolve-result}])
(when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected-id
:focused focused-id
:align :left
:empty-to-end empty-to-end
:ref set-option-ref}]))]))

View File

@ -68,8 +68,8 @@
(clj->js)))) (clj->js))))
(mf/defc token-update-create-modal (mf/defc token-update-create-modal
{::mf/wrap-props false} { ::mf/props :obj}
[{:keys [x y position token token-type action selected-token-set-id] :as _args}] [{:keys [x y position token token-type action selected-token-set-id tokens] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color)) (let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
modal-size-large* (mf/use-state (= token-type :typography)) modal-size-large* (mf/use-state (= token-type :typography))
modal-size-large? (deref modal-size-large*) modal-size-large? (deref modal-size-large*)
@ -94,6 +94,7 @@
:action action :action action
:selected-token-set-id selected-token-set-id :selected-token-set-id selected-token-set-id
:token-type token-type :token-type token-type
:tokens tokens
:on-display-colorpicker update-modal-size}]])) :on-display-colorpicker update-modal-size}]]))
;; Modals ---------------------------------------------------------------------- ;; Modals ----------------------------------------------------------------------

View File

@ -77,7 +77,7 @@
on-popover-open-click on-popover-open-click
(mf/use-fn (mf/use-fn
(mf/deps type title modal) (mf/deps type title modal active-theme-tokens)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dwtl/set-token-type-section-open type true) (st/emit! (dwtl/set-token-type-section-open type true)
@ -86,6 +86,7 @@
{:x (:x pos) {:x (:x pos)
:y (:y pos) :y (:y pos)
:position :right :position :right
:tokens active-theme-tokens
:fields (:fields modal) :fields (:fields modal)
:title title :title title
:action "create" :action "create"

View File

@ -16,7 +16,7 @@
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.color :as dwtc] [app.main.data.workspace.tokens.color :as dwtc]
[app.main.data.workspace.tokens.format :as dwtf] [app.main.data.workspace.tokens.format :as dwtf]
[app.main.refs :as refs] [app.main.ui.context :as ctx]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]] [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
[app.main.ui.ds.utilities.swatch :refer [swatch*]] [app.main.ui.ds.utilities.swatch :refer [swatch*]]
@ -27,7 +27,6 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Translation dictionaries ;; Translation dictionaries
(def ^:private attribute-dictionary (def ^:private attribute-dictionary
{:rotation "Rotation" {:rotation "Rotation"
:opacity "Opacity" :opacity "Opacity"
@ -77,8 +76,7 @@
:y :y}) :y :y})
;; Helper functions ;; Helper functions
(defn- partially-applied-attr
(defn partially-applied-attr
"Translates partially applied attributes based on the dictionary." "Translates partially applied attributes based on the dictionary."
[app-token-keys is-applied {:keys [attributes all-attributes]}] [app-token-keys is-applied {:keys [attributes all-attributes]}]
(let [filtered-keys (if all-attributes (let [filtered-keys (if all-attributes
@ -87,7 +85,7 @@
(when is-applied (when is-applied
(str/join ", " (map attribute-dictionary filtered-keys))))) (str/join ", " (map attribute-dictionary filtered-keys)))))
(defn translate-and-format (defn- translate-and-format
"Translates and formats grouped values by category." "Translates and formats grouped values by category."
[grouped-values] [grouped-values]
(str/join "\n" (str/join "\n"
@ -98,6 +96,18 @@
(str/join ", " (map attribute-dictionary values)) "."))) (str/join ", " (map attribute-dictionary values)) ".")))
grouped-values))) grouped-values)))
(defn- token-exists?
"Returns true if any token in the grouped token map has a name matching `token-name`."
[tokens-by-type token-name]
(let [clean-name (-> token-name
(str/trim)
(str/replace #"^\{" "")
(str/replace #"\}$" "")
(str/lower))]
(some (fn [[_ tokens]]
(some #(= clean-name (:name %)) tokens))
tokens-by-type)))
(defn- generate-tooltip (defn- generate-tooltip
"Generates a tooltip for a given token" "Generates a tooltip for a given token"
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set] [is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
@ -142,14 +152,6 @@
;; Otherwise only show the base title ;; Otherwise only show the base title
:else base-title))) :else base-title)))
;; FIXME: the token thould already have precalculated references, so
;; we don't need to perform this regex operation on each rerender
(defn contains-reference-value?
"Extracts the value between `{}` in a string and checks if it's in the provided vector."
[text active-tokens]
(let [match (second (re-find #"\{([^}]+)\}" text))]
(contains? active-tokens match)))
(def ^:private (def ^:private
xf:map-id xf:map-id
(map :id)) (map :id))
@ -176,7 +178,6 @@
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}] [{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token (let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes)) has-selected? (pos? (count selected-shapes))
is-reference? (cft/is-reference? token) is-reference? (cft/is-reference? token)
contains-path? (str/includes? name ".") contains-path? (str/includes? name ".")
@ -203,14 +204,14 @@
(not half-applied?) (not half-applied?)
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout}))) (not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
;; FIXME: move to context or props can-edit?
can-edit? (:can-edit (deref refs/permissions)) (mf/use-ctx ctx/can-edit?)
is-viewer? (not can-edit?) is-viewer? (not can-edit?)
ref-not-in-active-set ref-not-in-active-set
(and is-reference? (and is-reference?
(not (contains-reference-value? value active-theme-tokens))) (not (token-exists? @active-theme-tokens value)))
no-valid-value (seq errors) no-valid-value (seq errors)