From d26c08f8e2842717aaa64ba70e312e9e431ae8d2 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 20 Nov 2025 11:04:39 +0100 Subject: [PATCH] :recycle: Replace token forms (#7759) * :tada: Create dimensions form * :tada: Create text-case form * :tada: Create color form * :recycle: Remove unused code on form file --- frontend/playwright/ui/specs/tokens.spec.js | 5 +- .../src/app/main/ui/ds/utilities/swatch.cljs | 58 +++-- .../tokens/management/create/color.cljs | 220 ++++++++++++++++ .../tokens/management/create/color.scss | 58 +++++ .../tokens/management/create/dimensions.cljs | 219 ++++++++++++++++ .../tokens/management/create/dimensions.scss | 58 +++++ .../tokens/management/create/form.cljs | 52 +--- .../create/form_color_input_token.cljs | 244 ++++++++++++++++++ .../tokens/management/create/text_case.cljs | 220 ++++++++++++++++ .../tokens/management/create/text_case.scss | 58 +++++ 10 files changed, 1119 insertions(+), 73 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/color.scss create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index 7300800292..f19600670b 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -307,7 +307,7 @@ test.describe("Tokens: Tokens Tab", () => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.pressSequentially(".changed"); - await nameField.press("Enter"); + await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click(); await expect(tokensUpdateCreateModal).not.toBeVisible(); @@ -501,8 +501,9 @@ test.describe("Tokens: Tokens Tab", () => { // Clearing the input field should pick hex await valueField.fill(""); + // TODO: We need to fix this translation await expect( - tokensUpdateCreateModal.getByText("Token value cannot be empty"), + tokensUpdateCreateModal.getByText("Empty field"), ).toBeVisible(); await valueSaturationSelector.click({ position: { x: 50, y: 50 } }); await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/); diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs index 83d732a9a7..3f1a605efa 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.cljs +++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs @@ -64,11 +64,13 @@ [:size {:optional true} [:enum "small" "medium" "large"]] [:active {:optional true} ::sm/boolean] [:has-errors {:optional true} [:maybe ::sm/boolean]] + [:show-tooltip {:optional true} [:maybe ::sm/boolean]] + [:tooltip-content {:optional true} ::sm/any] [:on-click {:optional true} ::sm/fn]]) (mf/defc swatch* {::mf/schema (sm/schema schema:swatch)} - [{:keys [background on-click size active class tooltip-content has-errors] + [{:keys [background class size active has-errors tooltip-content on-click show-tooltip] :rest props}] (let [;; NOTE: this code is only relevant for storybook, because ;; storybook is unable to pass in a comfortable way a complex @@ -84,6 +86,7 @@ id? (some? (:ref-id background)) element-type (if read-only? "div" "button") button-type (if (not read-only?) "button" nil) + show-tooltip (if (some? show-tooltip) show-tooltip true) size (or size "small") active (or active false) gradient-type (-> background :gradient :type) @@ -117,29 +120,34 @@ (mf/spread-props props {:class class :on-click on-click :type button-type - :aria-labelledby element-id})] + :aria-labelledby element-id}) + children (mf/html + [:> element-type props + (cond + (some? gradient-type) + [:div {:class (stl/css :swatch-gradient) + :style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}] - [:> tooltip* {:content (if tooltip-content - tooltip-content - (color-title background)) - :id element-id} - [:> element-type props - (cond - (some? gradient-type) - [:div {:class (stl/css :swatch-gradient) - :style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}] + (some? image) + (let [uri (cfg/resolve-file-media image)] + [:div {:class (stl/css :swatch-image) + :style {:background-image (str/ffmt "url(%)" uri)}}]) + has-errors + [:div {:class (stl/css :swatch-error)}] + :else + [:div {:class (stl/css :swatch-opacity)} + [:div {:class (stl/css :swatch-solid-side) + :style {:background (uc/color->background (assoc background :opacity 1))}}] + [:div {:class (stl/css-case :swatch-opacity-side true + :swatch-opacity-side-transparency has-opacity? + :swatch-opacity-side-solid-color (not has-opacity?)) + :style {"--solid-color-overlay" (str (uc/color->background background))}}]])])] - (some? image) - (let [uri (cfg/resolve-file-media image)] - [:div {:class (stl/css :swatch-image) - :style {:background-image (str/ffmt "url(%)" uri)}}]) - has-errors - [:div {:class (stl/css :swatch-error)}] - :else - [:div {:class (stl/css :swatch-opacity)} - [:div {:class (stl/css :swatch-solid-side) - :style {:background (uc/color->background (assoc background :opacity 1))}}] - [:div {:class (stl/css-case :swatch-opacity-side true - :swatch-opacity-side-transparency has-opacity? - :swatch-opacity-side-solid-color (not has-opacity?)) - :style {"--solid-color-overlay" (str (uc/color->background background))}}]])]])) + (if show-tooltip + [:> tooltip* {:content (if tooltip-content + tooltip-content + (color-title background)) + :id element-id} + children] + + children))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs new file mode 100644 index 0000000000..90eb0c78a3 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs @@ -0,0 +1,220 @@ +;; 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.color + (: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.form-color-input-token :refer [form-color-input-token*]] + [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] + (or token {:type :color})) + + 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)} + [:> form-color-input-token* + {: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/color.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/color.scss new file mode 100644 index 0000000000..6593b3c30a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/color.scss @@ -0,0 +1,58 @@ +// 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); +} + +.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/dimensions.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs new file mode 100644 index 0000000000..aa429de437 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs @@ -0,0 +1,219 @@ +;; 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.dimensions + (: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.form-input-token :refer [form-input-token*]] + [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] + (or token {:type :dimensions})) + + 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)} + [:> form-input-token* + {: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/dimensions.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss new file mode 100644 index 0000000000..6593b3c30a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss @@ -0,0 +1,58 @@ +// 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); +} + +.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 bb9bbc3f10..224d29a93f 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 @@ -8,7 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.files.tokens :as cft] [app.common.schema :as sm] [app.common.types.color :as c] @@ -37,8 +36,11 @@ [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] [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.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] [app.util.dom :as dom] [app.util.functions :as uf] [app.util.i18n :refer [tr]] @@ -97,9 +99,6 @@ (when (cto/token-value-self-reference? token-name token-value) (wte/get-error-code :error.token/direct-self-reference))) -(defn check-token-self-reference [token] - (check-self-reference (:name token) (:value token))) - (defn validate-resolve-token [token prev-token tokens] (let [token (cond-> token @@ -911,40 +910,6 @@ :on-change on-change'}]) [:> token-value-hint* {:result token-resolve-result}]])) -(mf/defc color-form* - [{:keys [token on-display-colorpicker] :rest props}] - (let [color* (mf/use-state (:value token)) - color (deref color*) - on-value-resolve (mf/use-fn - (mf/deps color) - (fn [value] - (reset! color* value) - value)) - - custom-input-token-value-props - (mf/use-memo - (mf/deps color on-display-colorpicker) - (fn [] - {:color color - :on-display-colorpicker on-display-colorpicker})) - - on-get-token-value - (mf/use-fn - (fn [e] - (let [value (dom/get-target-val e)] - (if (tinycolor/hex-without-hash-prefix? value) - (let [hex-value (dm/str "#" value)] - (dom/set-value! (dom/get-target e) hex-value) - hex-value) - value))))] - - [:> form* - (mf/spread-props props {:token token - :on-get-token-value on-get-token-value - :on-value-resolve on-value-resolve - :custom-input-token-value color-picker* - :custom-input-token-value-props custom-input-token-value-props})])) - (mf/defc shadow-color-picker-wrapper* "Wrapper for color-picker* that passes shadow color state from parent. Similar to color-form* but receives color state from shadow-value-inputs*." @@ -1309,12 +1274,6 @@ :on-value-resolve on-value-resolve :validate-token validate-font-family-token})])) -(mf/defc text-case-form* - [{:keys [token] :rest props}] - [:> form* - (mf/spread-props props {:token token - :input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})]) - (mf/defc text-decoration-form* [{:keys [token] :rest props}] [:> form* @@ -1481,12 +1440,13 @@ :token token})] (case token-type - :color [:> color-form* props] + :color [:> color/form* props] :typography [:> typography-form* props] :shadow [:> shadow-form* props] :font-family [:> font-family-form* props] - :text-case [:> text-case-form* props] + :text-case [:> text-case/form* props] :text-decoration [:> text-decoration-form* props] :font-weight [:> font-weight-form* props] :border-radius [:> border-radius/form* props] + :dimensions [:> dimensions/form* props] [:> form* props]))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs new file mode 100644 index 0000000000..1e31503c53 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs @@ -0,0 +1,244 @@ +;; 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.form-color-input-token + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.types.color :as cl] + [app.common.types.tokens-lib :as ctob] + [app.main.data.style-dictionary :as sd] + [app.main.data.tinycolor :as tinycolor] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.utilities.swatch :refer [swatch*]] + [app.main.ui.forms :as fc] + [app.main.ui.workspace.colorpicker :as colorpicker] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-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 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)})))))))) + +(defn- hex->color-obj + [hex] + (when-let [tc (tinycolor/valid-color hex)] + (let [hex (tinycolor/->hex-string tc) + alpha (tinycolor/alpha tc) + [r g b] (cl/hex->rgb hex) + [h s v] (cl/hex->hsv hex)] + {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha alpha}))) + +(mf/defc ramp* + [{:keys [color on-change]}] + (let [wrapper-node-ref (mf/use-ref nil) + dragging-ref (mf/use-ref false) + + on-start-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref true)) + + on-finish-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref false)) + + internal-color* + (mf/use-state #(hex->color-obj color)) + + internal-color + (deref internal-color*) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [{:keys [hex alpha] :as selector-color}] + (let [dragging? (mf/ref-val dragging-ref)] + (when-not (and dragging? hex) + (reset! internal-color* selector-color) + (on-change hex alpha)))))] + (mf/use-effect + (mf/deps color) + (fn [] + ;; Update internal color when user changes input value + (when-let [color (tinycolor/valid-color color)] + (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) + (reset! internal-color* (hex->color-obj color)))))) + + (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) + [:div {:ref wrapper-node-ref} + [:> ramp-selector* + {:color internal-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag + :on-change on-change'}]])) + +(mf/defc form-color-input-token* + [{:keys [name tokens token] :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] "") + + resolved-value + (get-in @form [:data resolved-input-name] "") + + hex (if (tinycolor/valid-color resolved-value) + (tinycolor/->hex-string (tinycolor/valid-color resolved-value)) + "#8f9da3") + + alpha (if (tinycolor/valid-color resolved-value) + (tinycolor/alpha (tinycolor/valid-color resolved-value)) + 1) + + resolve-stream + (mf/with-memo [token] + (if-let [value (:value token)] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + color-ramp-open* (mf/use-state false) + color-ramp-open? (deref color-ramp-open*) + + on-click-swatch + (mf/use-fn + (mf/deps color-ramp-open?) + (fn [] + (let [open? (not color-ramp-open?)] + (reset! color-ramp-open* open?)))) + + swatch + (mf/html + [:> swatch* + {:background {:color hex :opacity alpha} + :show-tooltip false + :data-testid "token-form-color-bullet" + :class (stl/css :slot-start) + :on-click on-click-swatch}]) + + on-change-value + (mf/use-fn + (mf/deps resolve-stream input-name value) + (fn [hex alpha] + (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field + prev-input-color (some-> value + (tinycolor/valid-color)) + ;; If the input is a reference we will take the format from the computed value + prev-computed-color (when-not prev-input-color + (some-> value (tinycolor/valid-color))) + prev-format (some-> (or prev-input-color prev-computed-color) + (tinycolor/color-format)) + to-rgba? (and + (< alpha 1) + (or (= prev-format "hex") (not prev-format))) + to-hex? (and (not prev-format) (= alpha 1)) + format (cond + to-rgba? "rgba" + to-hex? "hex" + prev-format prev-format + :else "hex") + color-value (-> (tinycolor/valid-color hex) + (tinycolor/set-alpha (or alpha 1)) + (tinycolor/->string format))] + (when (not= value color-value) + (fm/on-input-change form input-name color-value true) + (rx/push! resolve-stream color-value))))) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [raw-value (-> event dom/get-target dom/get-input-value) + value (if (tinycolor/hex-without-hash-prefix? raw-value) + (dm/str "#" raw-value) + raw-value)] + (fm/on-input-change form input-name value true) + (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-start swatch + :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" 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 color-ramp-open? + [:> ramp* {:color value :on-change on-change-value}])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs new file mode 100644 index 0000000000..2eb3776d54 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs @@ -0,0 +1,220 @@ +;; 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.text-case + (: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.form-input-token :refer [form-input-token*]] + [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] + (or token {:type :text-case})) + + 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)} + [:> form-input-token* + {:placeholder (tr "workspace.tokens.text-case-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/text_case.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss new file mode 100644 index 0000000000..6593b3c30a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss @@ -0,0 +1,58 @@ +// 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); +} + +.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; +}