diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs new file mode 100644 index 0000000000..a65b80d206 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs @@ -0,0 +1,166 @@ +;; 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.create.combobox-token-fonts + (: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.fonts :as fonts] + [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.forms :as fc] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [clojure.core :as c] + [rumext.v2 :as mf])) + + +(defn- resolve-value + [tokens prev-token value] + (let [token + {:value (cto/split-font-family value) + :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + + 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 font-picker-combobox* + [{:keys [token tokens name] :rest props}] + (let [form (mf/use-ctx fc/context) + input-name name + + resolved-input-name + (mf/with-memo [input-name] + (keyword (str "resolved-" (c/name input-name)))) + + 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] "") + + font (fonts/find-font-family value) + + resolve-stream + (mf/with-memo [token] + (if-let [value (:value token)] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + font-selector-open* (mf/use-state false) + font-selector-open? (deref font-selector-open*) + + on-click-dropdown-button + (mf/use-fn + (mf/deps font-selector-open?) + (fn [e] + (dom/prevent-default e) + (reset! font-selector-open* (not font-selector-open?)))) + + font-selector-button + (mf/html + [:> icon-button* + {:on-click on-click-dropdown-button + :aria-label (tr "workspace.tokens.token-font-family-select") + :icon i/arrow-down + :variant "action" + :type "button"}]) + + on-close-font-selector + (mf/use-fn + (fn [] + (reset! font-selector-open* false))) + + on-select-font + (mf/use-fn + (mf/deps font) + (fn [{:keys [family] :as font}] + (when (not= value family) + (fm/on-input-change form input-name family true) + (rx/push! resolve-stream family)))) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [value (-> event dom/get-target dom/get-input-value)] + (fm/on-input-change form input-name value false) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + ;; TODO: Review this value vs default-value + :value (or value "") + :hint-message (:message hint) + :slot-end font-selector-button + :hint-type (:type hint)}) + + 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] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + ((:error/fn error) (:error/value error)))))) + (rx/subs! (fn [{:keys [error value]}] + (if error + (do + (swap! form assoc-in [:errors input-name] {:message error}) + (swap! form assoc-in [:errors resolved-input-name] {:message error}) + (swap! form update :data dissoc resolved-input-name) + (reset! hint* {:message error :type "error"})) + (let [message (tr "workspace.tokens.resolved-value" (cto/join-font-family value))] + (swap! form update :errors dissoc input-name resolved-input-name) + (swap! form update :data assoc resolved-input-name value) + (reset! hint* {:message message :type "hint"}))))))] + + (fn [] + (rx/dispose! subs)))) + + [:* + [:> input* props] + (when font-selector-open? + [:div {:class (stl/css :font-select-wrapper)} + [:> font-selector* {:current-font font + :on-select on-select-font + :on-close on-close-font-selector + :full-size true}]])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.scss new file mode 100644 index 0000000000..a94c8baa8a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.scss @@ -0,0 +1,14 @@ +// 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/_utils.scss" as *; + +.font-select-wrapper { + position: absolute; + width: 100%; + height: px2rem(115); + top: px2rem(54); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs new file mode 100644 index 0000000000..0741d36750 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs @@ -0,0 +1,223 @@ +;; 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.create.font-family + (:require-macros [app.main.style :as stl]) + (:require + [app.common.files.tokens :as cft] + [app.common.schema :as sm] + [app.common.types.token :as cto] + [app.common.types.tokens-lib :as ctob] + [app.main.constants :refer [max-input-length]] + [app.main.data.modal :as modal] + [app.main.data.workspace.tokens.application :as dwta] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.data.workspace.tokens.propagation :as dwtp] + [app.main.refs :as refs] + [app.main.store :as st] + [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*]] + [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] + [app.main.ui.forms :as fc] + [app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-combobox*]] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as k] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- make-schema + [tokens-tree] + (sm/schema + [:and + [:map + [:name + [:and + [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + #(not (cft/token-name-path-exists? % tokens-tree))]]] + + [:value ::sm/text] + + [:resolved-value ::sm/any] + + [:description {:optional true} + [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] + + [:fn {:error/field :value + :error/fn #(tr "workspace.tokens.self-reference")} + (fn [{:keys [name value]}] + (when (and name value) + (nil? (cto/token-value-self-reference? name value))))]])) + + +(mf/defc form* + [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] + + (let [token + (mf/with-memo [token] + (if token + (update token :value cto/join-font-family) + {:type :font-family})) + + token-type + (get token :type) + + token-properties + (dwta/get-token-properties token) + + token-title (str/lower (:title token-properties)) + + tokens + (mf/deref refs/workspace-active-theme-sets-tokens) + + tokens + (mf/with-memo [tokens] + ;; Ensure that the resolved value uses the currently editing token + ;; even if the name has been overriden by a token with the same name + ;; in another set below. + (cond-> tokens + (and (:name token) (:value token)) + (assoc (:name token) token))) + + schema + (mf/with-memo [tokens-tree-in-selected-set] + (make-schema tokens-tree-in-selected-set)) + + initial + (mf/with-memo [token] + {:name (:name token "") + :value (:value token "") + :description (:description token "")}) + + form + (fm/use-form :schema schema + :initial initial) + + warning-name-change? + (not= (get-in @form [:data :name]) + (:name initial)) + + on-cancel + (mf/use-fn + (fn [e] + (dom/prevent-default e) + (modal/hide!))) + + on-delete-token + (mf/use-fn + (mf/deps selected-token-set-id token) + (fn [e] + (dom/prevent-default e) + (modal/hide!) + (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) + + handle-key-down-delete + (mf/use-fn + (mf/deps on-delete-token) + (fn [e] + (when (or (k/enter? e) (k/space? e)) + (on-delete-token e)))) + + handle-key-down-cancel + (mf/use-fn + (mf/deps on-cancel) + (fn [e] + (when (or (k/enter? e) (k/space? e)) + (on-cancel e)))) + + on-submit + (mf/use-fn + (mf/deps validate-token token tokens token-type) + (fn [form _event] + (let [name (get-in @form [:clean-data :name]) + description (get-in @form [:clean-data :description]) + value (get-in @form [:clean-data :value])] + (->> (validate-token {:token-value value + :token-name name + :token-description description + :prev-token token + :tokens tokens}) + (rx/subs! + (fn [valid-token] + (st/emit! + (if is-create + (dwtl/create-token (ctob/make-token {:name name + :type token-type + :value (:value valid-token) + :description description})) + + (dwtl/update-token (:id token) + {:name name + :value (:value valid-token) + :description description})) + (dwtp/propagate-workspace-tokens) + (modal/hide))))))))] + + [:> 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)} + (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" + :auto-focus true}] + + (when (and warning-name-change? (= action "edit")) + [:div {:class (stl/css :warning-name-change-notification-wrapper)} + [:> context-notification* + {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] + + [:div {:class (stl/css :input-row)} + [:> font-picker-combobox* + {:placeholder (tr "workspace.tokens.token-value-enter") + :label (tr "workspace.tokens.token-value") + :name :value + :token token + :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-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")] + + [:> fc/form-submit* {:variant "primary" + :on-submit on-submit} + (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.scss new file mode 100644 index 0000000000..38ce932694 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.scss @@ -0,0 +1,59 @@ +// 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/_sizes.scss" as *; +@use "ds/_borders.scss" as *; + +.form-wrapper { + width: $sz-384; + position: relative; +} + +.token-rows { + display: flex; + flex-direction: column; + gap: var(--sp-l); +} + +.input-row { + display: flex; + flex-direction: column; + gap: var(--sp-xs); + position: relative; +} + +.title-bar { + display: grid; + grid-template-columns: 1fr auto; +} + +.form-modal-title { + @include t.use-typography("headline-medium"); + color: var(--color-foreground-primary); + display: flex; + align-items: center; +} + +.button-row { + display: grid; + grid-template-columns: auto auto; + justify-content: end; + gap: var(--sp-m); + padding-block-start: var(--sp-s); +} + +.with-delete { + grid-template-columns: 1fr auto auto; +} + +.warning-name-change-notification-wrapper { + margin-block-start: var(--sp-l); +} + +.delete-btn { + justify-self: start; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs index 224d29a93f..7bf6018a76 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs @@ -38,6 +38,7 @@ [app.main.ui.workspace.tokens.management.create.border-radius :as border-radius] [app.main.ui.workspace.tokens.management.create.color :as color] [app.main.ui.workspace.tokens.management.create.dimensions :as dimensions] + [app.main.ui.workspace.tokens.management.create.font-family :as font-family] [app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]] [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token* token-value-hint*]] [app.main.ui.workspace.tokens.management.create.text-case :as text-case] @@ -1437,13 +1438,14 @@ (mf/spread-props props {:token-type token-type :validate-token default-validate-token :tokens-tree-in-selected-set tokens-tree-in-selected-set - :token token})] + :token token}) + font-family-props (mf/spread-props props {:validate-token validate-font-family-token})] (case token-type :color [:> color/form* props] :typography [:> typography-form* props] :shadow [:> shadow-form* props] - :font-family [:> font-family-form* props] + :font-family [:> font-family/form* font-family-props] :text-case [:> text-case/form* props] :text-decoration [:> text-decoration-form* props] :font-weight [:> font-weight-form* props]