From 46864fb01a8f805df6495be3955c3dd61b661744 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 4 Feb 2026 12:25:47 +0100 Subject: [PATCH] :tada: Create token combobox --- .../ds/controls/shared/options_dropdown.cljs | 6 +- .../main/ui/ds/controls/utilities/utils.cljs | 95 +++++ .../main/ui/workspace/tokens/management.cljs | 3 +- .../management/forms/border_radius.cljs | 26 ++ .../tokens/management/forms/controls.cljs | 5 +- .../management/forms/controls/combobox.cljs | 391 ++++++++++++++++++ .../management/forms/controls/combobox.scss | 3 + .../management/forms/form_container.cljs | 2 + .../tokens/management/forms/generic_form.cljs | 139 ++++--- 9 files changed, 600 insertions(+), 70 deletions(-) create mode 100644 frontend/src/app/main/ui/ds/controls/utilities/utils.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/border_radius.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index 3633b0145b..4719eb9cd7 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -35,6 +35,7 @@ (def ^:private schema:options-dropdown [:map [:ref {:optional true} fn?] + [:wrapper-ref {:optional true} :any] [:on-click fn?] [:options [:vector schema:option]] [:selected {:optional true} :any] @@ -83,6 +84,7 @@ :name name :resolved (get option :resolved-value) :ref ref + :role "option" :focused (= id focused) :on-click on-click}] @@ -94,6 +96,7 @@ :aria-label (get option :aria-label) :icon (get option :icon) :ref ref + :role "option" :focused (= id focused) :dimmed (true? (:dimmed option)) :on-click on-click}])))) @@ -101,7 +104,7 @@ (mf/defc options-dropdown* {::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 wrapper-ref] :rest props}] (let [align (d/nilv align :left) @@ -110,6 +113,7 @@ {:class (stl/css-case :option-list true :left-align (= align :left) :right-align (= align :right)) + :ref wrapper-ref :tab-index "-1" :role "listbox"}) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/utils.cljs b/frontend/src/app/main/ui/ds/controls/utilities/utils.cljs new file mode 100644 index 0000000000..0214d34506 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/utilities/utils.cljs @@ -0,0 +1,95 @@ +(ns app.main.ui.ds.controls.utilities.utils + (:require + [app.common.data.macros :as dm] + [app.common.types.token :as cto] + [app.util.i18n :refer [tr]] + [cuerdas.core :as str])) + +(defn- token->dropdown-option + [token] + {:id (str (get token :id)) + :type :token + :resolved-value (get token :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- extract-partial-brace-text + [s] + (when-let [start (str/last-index-of s "{")] + (subs s (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- 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 get-token-dropdown-options + [tokens filter-term] + (delay + (let [tokens (if (delay? tokens) @tokens tokens) + + 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?)))) + +(defn filter-tokens-for-input + [raw-tokens input-type] + (delay + (-> (deref raw-tokens) + (select-keys (get cto/tokens-by-input input-type)) + (not-empty)))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 1f124b7b8b..0ff4deafa4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -143,8 +143,7 @@ (let [token-ids (set tokens-in-path-ids) remaining-tokens (filter (fn [token] (not (contains? token-ids (:id token)))) - selected-token-set-tokens) - _ (prn "Remaining tokens:" remaining-tokens)] + selected-token-set-tokens)] (seq remaining-tokens)))) delete-token diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/border_radius.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/border_radius.cljs new file mode 100644 index 0000000000..52e5811019 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/border_radius.cljs @@ -0,0 +1,26 @@ + +;; 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.tokens.management.forms.border-radius + (:require + [app.common.types.token :as cto] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [rumext.v2 :as mf])) + + +(mf/defc form* + [{:keys [token token-type] :rest props}] + (let [token + (mf/with-memo [token] + (if token + (update token :value cto/join-font-family) + {:type token-type})) + props (mf/spread-props props {:token token + :token-type token-type + :input-component token.controls/combobox*})] + [:> generic/form* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs index 70feee56ca..40097461a8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs @@ -2,6 +2,7 @@ (:require [app.common.data.macros :as dm] [app.main.ui.workspace.tokens.management.forms.controls.color-input :as color] + [app.main.ui.workspace.tokens.management.forms.controls.combobox :as combobox] [app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox :as fonts] [app.main.ui.workspace.tokens.management.forms.controls.input :as input] [app.main.ui.workspace.tokens.management.forms.controls.select :as select])) @@ -16,4 +17,6 @@ (dm/export fonts/fonts-combobox*) (dm/export fonts/composite-fonts-combobox*) -(dm/export select/select-indexed*) \ No newline at end of file +(dm/export select/select-indexed*) + +(dm/export combobox/combobox*) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs new file mode 100644 index 0000000000..723c93e1f6 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -0,0 +1,391 @@ +;; 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.tokens.management.forms.controls.combobox + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.types.token :as cto] + [app.common.types.tokens-lib :as ctob] + [app.main.data.style-dictionary :as sd] + [app.main.ui.context :as muc] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :as ds] + [app.main.ui.ds.controls.select :refer [get-option]] + [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]] + [app.main.ui.ds.controls.utilities.utils :as csu] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.forms :as fc] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.object :as obj] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- focusable-option? + [option] + (and (:id option) + (not= :group (:type option)) + (not= :separator (:type option)))) + +(defn- first-focusable-id + [options] + (some #(when (focusable-option? %) (:id %)) options)) + +(defn next-focus-id + [options focused-id direction] + (let [focusable (filter focusable-option? options) + ids (map :id focusable) + idx (.indexOf (clj->js ids) focused-id) + next-idx (case direction + :down (min (dec (count ids)) (inc (if (= idx -1) -1 idx))) + :up (max 0 (dec (if (= idx -1) 0 idx))))] + (nth ids next-idx nil))) + + +(defn extract-partial-token + [value cursor] + (let [text-before (subs value 0 cursor) + last-open (str/last-index-of text-before "{") + last-close (str/last-index-of text-before "}")] + (when (and last-open (or (nil? last-close) (> last-open last-close))) + {:start last-open + :partial (subs text-before (inc last-open))}))) + +(defn replace-active-token + [value cursor new-name] + (let [before (subs value 0 cursor) + start (str/last-index-of before "{")] + (if start + + (let [after-start (subs value start) + close-pos (str/index-of after-start "}") + end (if close-pos + (+ start close-pos 1) + cursor)] + (str (subs value 0 start) + "{" new-name "}" + (subs value end))) + + (str (subs value 0 cursor) + "{" new-name "}")))) + +(defn active-token [value input-node] + (let [cursor (.-selectionStart input-node)] + (extract-partial-token value cursor))) + +(defn- select-option-by-id + [id options-ref input-node value] + (let [cursor (.-selectionStart input-node) + options (mf/ref-val options-ref) + options (if (delay? options) @options options) + + option (get-option options id) + name (:name option) + final-val (replace-active-token value cursor name)] + final-val)) + +(defn- resolve-value + [tokens prev-token token-name value] + (let [valid-token-name? + (and (string? token-name) + (re-matches cto/token-name-validation-regex token-name)) + + token + {:value value + :name (if (or (not valid-token-name?) (str/blank? token-name)) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} + tokens + (-> tokens + ;; Remove previous token when renaming a token + (dissoc (:name prev-token)) + (update (:name token) #(ctob/make-token (merge % prev-token token))))] + + (->> tokens + (sd/resolve-tokens-interactive) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (first errors)})))))))) + +(mf/defc combobox* + [{:keys [name tokens token token-type empty-to-end ref] :rest props}] + + (let [form (mf/use-ctx fc/context) + + input-name name + token-name (get-in @form [:data :name] nil) + + is-open* (mf/use-state false) + is-open (deref is-open*) + + + listbox-id (mf/use-id) + filter-term* (mf/use-state "") + filter-term (deref filter-term*) + + focused-id* (mf/use-state nil) + focused-id (deref focused-id*) + + options-ref (mf/use-ref nil) + dropdown-ref (mf/use-ref nil) + internal-ref (mf/use-ref nil) + nodes-ref (mf/use-ref nil) + icon-button-ref (mf/use-ref nil) + ref (or ref internal-ref) + + touched? + (and (contains? (:data @form) input-name) + (get-in @form [:touched input-name])) + + error + (get-in @form [:errors input-name]) + + value + (get-in @form [:data input-name] "") + + raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type) + + filtered-tokens-by-type + (mf/with-memo [raw-tokens-by-type token-type] + (csu/filter-tokens-for-input raw-tokens-by-type token-type)) + + dropdown-options + (mf/with-memo [filtered-tokens-by-type filter-term] + (csu/get-token-dropdown-options filtered-tokens-by-type (str "{" filter-term))) + + 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)))) + + toggle-dropdown + (mf/use-fn + (mf/deps) + (fn [event] + (dom/prevent-default event) + (swap! is-open* not))) + + + resolve-stream + (mf/with-memo [token] + (if (contains? token :value) + (rx/behavior-subject (:value token)) + (rx/subject))) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name form) + (fn [event] + (let [node (dom/get-target event) + value (dom/get-input-value node) + token (active-token value node)] + + (fm/on-input-change form input-name value) + (rx/push! resolve-stream value) + + (if token + (do + (reset! is-open* true) + (reset! filter-term* (:partial token))) + (do + (reset! is-open* false) + (reset! filter-term* "")))))) + + on-option-click + (mf/use-fn + (mf/deps value resolve-stream ref) + (fn [event] + (let [input-node (mf/ref-val ref) + node (dom/get-current-target event) + id (dom/get-data node "id") + final-val (select-option-by-id id options-ref input-node value)] + + (fm/on-input-change form input-name final-val true) + (rx/push! resolve-stream final-val) + + (reset! filter-term* "") + (reset! is-open* false) + + (dom/focus! input-node) + (let [new-cursor (+ (str/index-of final-val "}") 1)] + (set! (.-selectionStart input-node) new-cursor) + (set! (.-selectionEnd input-node) new-cursor))))) + + on-option-enter + (mf/use-fn + (mf/deps focused-id value resolve-stream) + (fn [_] + (let [input-node (mf/ref-val ref) + final-val (select-option-by-id focused-id options-ref input-node value)] + (fm/on-input-change form input-name final-val true) + (rx/push! resolve-stream final-val) + (reset! filter-term* "") + (reset! is-open* false)))) + + on-key-down + (mf/use-fn + (mf/deps is-open focused-id) + (fn [event] + (let [up? (kbd/up-arrow? event) + down? (kbd/down-arrow? event) + enter? (kbd/enter? event) + esc? (kbd/esc? event) + open-dropdown (kbd/is-key? event "{") + close-dropdown (kbd/is-key? event "}") + options (mf/ref-val options-ref) + options (if (delay? options) @options options)] + + (cond + open-dropdown + (reset! is-open* true) + + close-dropdown + (reset! is-open* false) + + down? + (do + (dom/prevent-default event) + (if is-open + (let [next-id (next-focus-id options focused-id :down)] + (reset! focused-id* next-id)) + (when (some? @filtered-tokens-by-type) + (do + (toggle-dropdown event) + (reset! focused-id* (first-focusable-id options)))))) + + up? + (when is-open + (dom/prevent-default event) + (let [next-id (next-focus-id options focused-id :up)] + (reset! focused-id* next-id))) + + enter? + (do + (dom/prevent-default event) + (if is-open + (on-option-enter event) + (do + (reset! focused-id* (first-focusable-id options)) + (toggle-dropdown event)))) + + esc? + (do + (dom/prevent-default event) + (reset! is-open* false)))))) + + handle-blur + (fn [event] + (let [input-node (mf/ref-val ref) + dropdown-node (mf/ref-val dropdown-ref) + next-focus (.-relatedTarget event)] + + (when (and next-focus + (not (dom/child? next-focus input-node)) + (not (dom/child? next-focus dropdown-node))) + (reset! is-open* false)))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + props + (mf/spread-props props {:on-change on-change + :value value + :variant "comfortable" + :hint-message (:message hint) + :on-key-down on-key-down + :hint-type (:type hint) + :ref ref + :role "combobox" + :aria-activedescendant focused-id + :aria-controls listbox-id + :aria-expanded is-open + :slot-end + (when (some? @filtered-tokens-by-type) + (mf/html + [:> icon-button* + {:variant "action" + :icon i/arrow-down + :ref icon-button-ref + :tooltip-class (stl/css :button-tooltip) + :class (stl/css :invisible-button) + :tab-index "-1" + :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") + :on-click toggle-dropdown}]))}) + props + (if (and error touched?) + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name token-name] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token token-name)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + ((:error/fn error) (:error/value error)))))) + (rx/subs! (fn [{:keys [error value]}] + (let [touched? (get-in @form [:touched input-name])] + (when touched? + (if error + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (reset! hint* {:message error :type "error"})) + (let [message (tr "workspace.tokens.resolved-value" value)] + (swap! form update :extra-errors dissoc input-name) + (reset! hint* {:message message :type "hint"}))))))))] + (fn [] + (rx/dispose! subs)))) + + (mf/with-effect [dropdown-options] + (mf/set-ref-val! options-ref dropdown-options)) + + (mf/with-effect [is-open* ref nodes-ref] + (when @is-open* + (let [handler (fn [event] + (let [input-node (mf/ref-val ref) + dropdown-node (mf/ref-val dropdown-ref) + target (dom/get-target event)] + (when (and input-node dropdown-node + (not (dom/child? target input-node)) + (not (dom/child? target dropdown-node))) + (reset! is-open* false))))] + + (.addEventListener js/document "mousedown" handler) + + (fn [] + (.removeEventListener js/document "mousedown" handler))))) + + [:div {:class (stl/css :combobox-wrapper) + :on-blur handle-blur} + [:> ds/input* props] + (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 + :focused focused-id + :selected nil + :align :right + :style {:top "56px"} + :empty-to-end empty-to-end + :wrapper-ref dropdown-ref + :ref set-option-ref}]))])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss new file mode 100644 index 0000000000..085eabe9c9 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss @@ -0,0 +1,3 @@ +.combobox-wrapper { + position: relative; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index b0aef352ae..a70053e53e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.types.tokens-lib :as ctob] [app.main.refs :as refs] + [app.main.ui.workspace.tokens.management.forms.border-radius :as test] [app.main.ui.workspace.tokens.management.forms.color :as color] [app.main.ui.workspace.tokens.management.forms.font-family :as font-family] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] @@ -49,4 +50,5 @@ :text-case [:> generic/form* text-case-props] :text-decoration [:> generic/form* text-decoration-props] :font-weight [:> generic/form* font-weight-props] + :border-radius [:> test/form* props] [:> generic/form* props]))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index b0f36ec06c..6e5e2a87ec 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -21,6 +21,7 @@ [app.main.data.workspace.tokens.remapping :as remap] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.context :as muc] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] @@ -97,6 +98,10 @@ (and (:name token) (:value token)) (assoc (:name token) token))) + active-tokens-by-type + (mf/with-memo [tokens] + (delay (ctob/group-by-type tokens))) + schema (mf/with-memo [tokens-tree-in-selected-set active-tab] (make-schema tokens-tree-in-selected-set active-tab)) @@ -224,78 +229,80 @@ error-message (first error-messages)] (swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))] - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} + [(mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type} + [:> fc/form* {:class (stl/css :form-wrapper) + :form form + :on-submit on-submit} + [:div {:class (stl/css :token-rows)} - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (if (= action "edit") - (tr "workspace.tokens.edit-token" token-type) - (tr "workspace.tokens.create-token" token-type))] + [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} + (if (= action "edit") + (tr "workspace.tokens.edit-token" token-type) + (tr "workspace.tokens.create-token" token-type))] - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :trim true - :auto-focus true}]] + [:div {:class (stl/css :input-row)} + [:> fc/form-input* {:id "token-name" + :name :name + :label (tr "workspace.tokens.token-name") + :placeholder (tr "workspace.tokens.enter-token-name" token-title) + :max-length max-input-length + :variant "comfortable" + :trim true + :auto-focus true}]] - [:div {:class (stl/css :input-row)} - (case type - :indexed - [:> input-component - {:token token - :tokens tokens - :tab active-tab - :value-subfield value-subfield - :handle-toggle on-toggle-tab}] + [:div {:class (stl/css :input-row)} + (case type + :indexed + [:> input-component + {:token token + :tokens tokens + :tab active-tab + :value-subfield value-subfield + :handle-toggle on-toggle-tab}] - :composite - [:> input-component - {:token token - :tokens tokens - :tab active-tab - :handle-toggle on-toggle-tab}] + :composite + [:> input-component + {:token token + :tokens tokens + :tab active-tab + :handle-toggle on-toggle-tab}] - [:> input-component - {:placeholder (or input-value-placeholder - (tr "workspace.tokens.token-value-enter")) - :label (tr "workspace.tokens.token-value") - :name :value - :token token - :tokens tokens}])] + [:> input-component + {:placeholder (or input-value-placeholder + (tr "workspace.tokens.token-value-enter")) + :label (tr "workspace.tokens.token-value") + :name :value + :token token + :token-type token-type + :tokens tokens}])] - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] + [:div {:class (stl/css :input-row)} + [:> fc/form-input* {:id "token-description" + :name :description + :label (tr "workspace.tokens.token-description") + :placeholder (tr "workspace.tokens.token-description") + :max-length max-input-length + :variant "comfortable" + :is-optional true}]] - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) + [:div {:class (stl/css-case :button-row true + :with-delete (= action "edit"))} + (when (= action "edit") + [:> button* {:on-click on-delete-token + :on-key-down handle-key-down-delete + :class (stl/css :delete-btn) + :type "button" + :icon i/delete + :variant "secondary"} + (tr "labels.delete")]) - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] + [:> button* {:on-click on-cancel + :on-key-down handle-key-down-cancel + :type "button" + :id "token-modal-cancel" + :variant "secondary"} + (tr "labels.cancel")] - [:> fc/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) + [:> fc/form-submit* {:variant "primary" + :on-submit on-submit} + (tr "labels.save")]]]]]))