From efe74e62e82971954e11bade2c66653b371d7f2e Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 1 Dec 2025 11:17:25 +0100 Subject: [PATCH] :tada: Replace font family form (#7825) --- frontend/playwright/ui/specs/tokens.spec.js | 3 +- .../app/main/ui/components/radio_buttons.cljs | 3 +- .../src/app/main/ui/ds/controls/input.cljs | 10 +- .../src/app/main/ui/ds/controls/input.scss | 4 + frontend/src/app/main/ui/forms.cljs | 3 +- .../app/main/ui/workspace/colorpicker.cljs | 4 +- .../workspace/sidebar/assets/components.cljs | 5 +- .../sidebar/options/drawing/frame.cljs | 5 +- .../workspace/sidebar/options/menus/bool.cljs | 9 +- .../sidebar/options/menus/component.cljs | 5 +- .../sidebar/options/menus/constraints.cljs | 2 +- .../sidebar/options/menus/constraints.scss | 4 +- .../sidebar/options/menus/grid_cell.cljs | 17 +- .../sidebar/options/menus/interactions.cljs | 8 +- .../options/menus/layout_container.cljs | 124 ++--- .../sidebar/options/menus/measures.cljs | 4 +- .../workspace/sidebar/options/menus/text.cljs | 28 +- .../sidebar/options/menus/typography.cljs | 7 +- .../management/create/border_radius.cljs | 4 +- .../tokens/management/create/color.cljs | 4 +- .../create/combobox_token_fonts.cljs | 167 ++++++- .../tokens/management/create/dimensions.cljs | 4 +- .../tokens/management/create/font_family.cljs | 4 +- .../tokens/management/create/form.cljs | 15 +- .../create/form_color_input_token.cljs | 34 +- .../management/create/form_input_token.cljs | 135 +++++- .../tokens/management/create/text_case.cljs | 4 +- .../tokens/management/create/typography.cljs | 427 ++++++++++++++++++ .../tokens/management/create/typography.scss | 74 +++ .../workspace/tokens/themes/create_modal.cljs | 5 +- frontend/src/app/util/forms.cljs | 67 ++- 31 files changed, 968 insertions(+), 221 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index e1bbebbacb..c2d2796caf 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -303,7 +303,7 @@ test.describe("Tokens: Tokens Tab", () => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.pressSequentially(".changed"); - await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click(); + await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click(); await expect(tokensUpdateCreateModal).not.toBeVisible(); @@ -1070,6 +1070,7 @@ test.describe("Tokens: Apply token", () => { // Fill in values for all fields and verify they persist when switching tabs await fontSizeField.fill("16"); + await expect(saveButton).toBeEnabled(); const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i); const letterSpacingField = diff --git a/frontend/src/app/main/ui/components/radio_buttons.cljs b/frontend/src/app/main/ui/components/radio_buttons.cljs index 85ec06a793..d8bc4c969e 100644 --- a/frontend/src/app/main/ui/components/radio_buttons.cljs +++ b/frontend/src/app/main/ui/components/radio_buttons.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.formats :as fmt] [app.util.dom :as dom] [rumext.v2 :as mf])) @@ -46,7 +47,7 @@ :disabled disabled)} (if (some? icon) - [:span {:class icon-class} icon] + [:> icon* {:icon-id icon :class icon-class :aria-hidden true}] [:span {:class (stl/css :title-name)} value]) [:input {:id id diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs index 0a3735703b..29ae0cc804 100644 --- a/frontend/src/app/main/ui/ds/controls/input.cljs +++ b/frontend/src/app/main/ui/ds/controls/input.cljs @@ -26,14 +26,19 @@ [:max-length {:optional true} :int] [:variant {:optional true} [:maybe [:enum "seamless" "dense" "comfortable"]]] [:hint-message {:optional true} [:maybe :string]] - [:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]]) + [:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]] + [:hint-formated {:optional true} :boolean]]) (mf/defc input* {::mf/forward-ref true ::mf/schema schema:input} - [{:keys [id class label is-optional type max-length variant hint-message hint-type] :rest props} ref] + [{:keys [id class label is-optional type max-length variant hint-message hint-type hint-formated] :rest props} ref] (let [id (or id (mf/use-id)) variant (d/nilv variant "dense") + hint-class (if (and (not= "error" hint-type) + hint-formated) + (stl/css :hint-formated) + "") is-optional (d/nilv is-optional false) type (d/nilv type "text") max-length (d/nilv max-length max-input-length) @@ -56,6 +61,7 @@ [:> input-field* props] (when has-hint [:> hint-message* {:id id + :class hint-class :message hint-message :type hint-type}])])) diff --git a/frontend/src/app/main/ui/ds/controls/input.scss b/frontend/src/app/main/ui/ds/controls/input.scss index 3de9b95466..ab5da17513 100644 --- a/frontend/src/app/main/ui/ds/controls/input.scss +++ b/frontend/src/app/main/ui/ds/controls/input.scss @@ -12,3 +12,7 @@ gap: var(--sp-xs); inline-size: 100%; } + +.hint-formated { + white-space: pre; +} diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index e95128580c..4988f589d4 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -34,10 +34,9 @@ (let [value (-> event dom/get-target dom/get-input-value)] (fm/on-input-change form input-name value true)))) - props (mf/spread-props props {:on-change on-change - :default-value value}) + :value value}) props (if (and error touched?) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 4da60706d0..3e930e9f81 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -418,11 +418,11 @@ [:& radio-buttons {:selected color-style :on-change toggle-token-color :name "color-style"} - [:& radio-button {:icon deprecated-icon/swatches + [:& radio-button {:icon i/swatches :value :direct-color :title (tr "labels.color") :id "opt-color"}] - [:& radio-button {:icon deprecated-icon/tokens + [:& radio-button {:icon i/tokens :value :token-color :title (tr "workspace.colorpicker.color-tokens") :id "opt-token-color"}]])] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index b7f8cc121d..077742d71d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -28,7 +28,6 @@ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as h] - [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.sidebar.assets.groups :as grp] [app.util.dom :as dom] @@ -567,11 +566,11 @@ [:& radio-buttons {:selected (if is-listing-thumbs "grid" "list") :on-change toggle-list-style :name "listing-style"} - [:& radio-button {:icon deprecated-icon/view-as-list + [:& radio-button {:icon i/view-as-list :value "list" :title (tr "workspace.assets.list-view") :id "opt-list"}] - [:& radio-button {:icon deprecated-icon/flex-grid + [:& radio-button {:icon i/flex-grid :value "grid" :title (tr "workspace.assets.grid-view") :id "opt-grid"}]]]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs index afff809444..275ad11e8d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs @@ -13,6 +13,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -99,10 +100,10 @@ :name "frame-orientation" :wide true :class (stl/css :radio-buttons)} - [:& radio-button {:icon deprecated-icon/size-vertical + [:& radio-button {:icon i/size-vertical :value "vertical" :id "size-vertical"}] - [:& radio-button {:icon deprecated-icon/size-horizontal + [:& radio-button {:icon i/size-horizontal :value "horizontal" :id "size-horizontal"}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs index 7181bf9d36..b2ffa16cb8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.cljs @@ -16,6 +16,7 @@ [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -78,22 +79,22 @@ :class (stl/css :boolean-radio-btn) :on-change on-change :name "bool-options"} - [:& radio-button {:icon deprecated-icon/boolean-union + [:& radio-button {:icon i/boolean-union :value "union" :disabled disabled-bool-btns :title (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")") :id "bool-opt-union"}] - [:& radio-button {:icon deprecated-icon/boolean-difference + [:& radio-button {:icon i/boolean-difference :value "difference" :disabled disabled-bool-btns :title (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")") :id "bool-opt-differente"}] - [:& radio-button {:icon deprecated-icon/boolean-intersection + [:& radio-button {:icon i/boolean-intersection :value "intersection" :disabled disabled-bool-btns :title (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")") :id "bool-opt-intersection"}] - [:& radio-button {:icon deprecated-icon/boolean-exclude + [:& radio-button {:icon i/boolean-exclude :value "exclude" :disabled disabled-bool-btns :title (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 42f7456ba0..0098eea40e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -42,7 +42,6 @@ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]] [app.main.ui.hooks :as h] - [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.sidebar.options.menus.variants-help-modal] [app.util.debug :as dbg] @@ -798,10 +797,10 @@ [:& radio-buttons {:selected (if (:listing-thumbs? filters) "grid" "list") :on-change toggle-list-style :name "swap-listing-style"} - [:& radio-button {:icon deprecated-icon/view-as-list + [:& radio-button {:icon i/view-as-list :value "list" :id "swap-opt-list"}] - [:& radio-button {:icon deprecated-icon/flex-grid + [:& radio-button {:icon i/flex-grid :value "grid" :id "swap-opt-grid"}]]] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs index b589b2a8d9..a56843dce2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -206,7 +206,7 @@ :data-value "bottom" :on-click on-constraint-button-clicked} [:span {:class (stl/css :resalted-area)}]]]] - [:div {:class (stl/css :contraints-selects)} + [:div {:class (stl/css :constraints-selects)} [:div {:class (stl/css :horizontal-select) :data-testid "constraint-h-select"} [:& select {:default-value (if (not= constraints-h :multiple) (d/nilv (d/name constraints-h) "scale") "") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss index 50fffa78ac..5f7578afe1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss @@ -120,7 +120,9 @@ } .constraints-selects { - @include deprecated.flexColumn; + display: flex; + flex-direction: column; + gap: 4px; } .horizontal-select, diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs index 883bf4ca78..25a92ac854 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs @@ -18,6 +18,7 @@ [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.title-bar :refer [title-bar*]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] @@ -52,29 +53,29 @@ :name (dm/str "flex-align-items-" type)} [:& radio-button {:value "start" :icon (if is-col? - deprecated-icon/align-self-row-left - deprecated-icon/align-self-column-top) + i/align-self-row-left + i/align-self-column-top) :title "Align self start" :id (dm/str "align-self-start-" type)}] [:& radio-button {:value "center" :icon (if is-col? - deprecated-icon/align-self-row-center - deprecated-icon/align-self-column-center) + i/align-self-row-center + i/align-self-column-center) :title "Align self center" :id (dm/str "align-self-center-" type)}] [:& radio-button {:value "end" :icon (if is-col? - deprecated-icon/align-self-row-right - deprecated-icon/align-self-column-bottom) + i/align-self-row-right + i/align-self-column-bottom) :title "Align self end" :id (dm/str "align-self-end-" type)}] [:& radio-button {:value "stretch" :icon (if is-col? - deprecated-icon/align-self-row-stretch - deprecated-icon/align-self-column-stretch) + i/align-self-row-stretch + i/align-self-column-stretch) :title "Align self stretch" :id (dm/str "align-self-stretch-" type)}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 2abb8c4973..2f8271e4d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -645,19 +645,19 @@ [:& radio-buttons {:selected (d/name direction) :on-change change-direction :name "animation-direction"} - [:& radio-button {:icon deprecated-icon/column + [:& radio-button {:icon i/column :icon-class (stl/css :right) :value "right" :id "animation-right"}] - [:& radio-button {:icon deprecated-icon/column + [:& radio-button {:icon i/column :icon-class (stl/css :left) :id "animation-left" :value "left"}] - [:& radio-button {:icon deprecated-icon/column + [:& radio-button {:icon i/column :icon-class (stl/css :down) :id "animation-down" :value "down"}] - [:& radio-button {:icon deprecated-icon/column + [:& radio-button {:icon i/column :icon-class (stl/css :up) :id "animation-up" :value "up"}]]]]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index ca32217c40..320abd7d18 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -42,10 +42,10 @@ (defn- dir-icons-refactor [val] (case val - :row deprecated-icon/grid-row - :row-reverse deprecated-icon/row-reverse - :column deprecated-icon/column - :column-reverse deprecated-icon/column-reverse)) + :row i/grid-row + :row-reverse i/row-reverse + :column i/column + :column-reverse i/column-reverse)) (mf/defc numeric-input-wrapper* @@ -111,63 +111,63 @@ :align-items (if column? (case val - :start deprecated-icon/align-items-column-start - :end deprecated-icon/align-items-column-end - :center deprecated-icon/align-items-column-center) + :start i/align-items-column-start + :end i/align-items-column-end + :center i/align-items-column-center) (case val - :start deprecated-icon/align-items-row-start - :end deprecated-icon/align-items-row-end - :center deprecated-icon/align-items-row-center)) + :start i/align-items-row-start + :end i/align-items-row-end + :center i/align-items-row-center)) :justify-content (if column? (case val - :start deprecated-icon/justify-content-column-start - :end deprecated-icon/justify-content-column-end - :center deprecated-icon/justify-content-column-center - :space-around deprecated-icon/justify-content-column-around - :space-evenly deprecated-icon/justify-content-column-evenly - :space-between deprecated-icon/justify-content-column-between) + :start i/justify-content-column-start + :end i/justify-content-column-end + :center i/justify-content-column-center + :space-around i/justify-content-column-around + :space-evenly i/justify-content-column-evenly + :space-between i/justify-content-column-between) (case val - :start deprecated-icon/justify-content-row-start - :end deprecated-icon/justify-content-row-end - :center deprecated-icon/justify-content-row-center - :space-around deprecated-icon/justify-content-row-around - :space-evenly deprecated-icon/justify-content-row-evenly - :space-between deprecated-icon/justify-content-row-between)) + :start i/justify-content-row-start + :end i/justify-content-row-end + :center i/justify-content-row-center + :space-around i/justify-content-row-around + :space-evenly i/justify-content-row-evenly + :space-between i/justify-content-row-between)) :align-content (if column? (case val - :start deprecated-icon/align-content-column-start - :end deprecated-icon/align-content-column-end - :center deprecated-icon/align-content-column-center - :space-around deprecated-icon/align-content-column-around - :space-evenly deprecated-icon/align-content-column-evenly - :space-between deprecated-icon/align-content-column-between + :start i/align-content-column-start + :end i/align-content-column-end + :center i/align-content-column-center + :space-around i/align-content-column-around + :space-evenly i/align-content-column-evenly + :space-between i/align-content-column-between :stretch nil) (case val - :start deprecated-icon/align-content-row-start - :end deprecated-icon/align-content-row-end - :center deprecated-icon/align-content-row-center - :space-around deprecated-icon/align-content-row-around - :space-evenly deprecated-icon/align-content-row-evenly - :space-between deprecated-icon/align-content-row-between + :start i/align-content-row-start + :end i/align-content-row-end + :center i/align-content-row-center + :space-around i/align-content-row-around + :space-evenly i/align-content-row-evenly + :space-between i/align-content-row-between :stretch nil)) :align-self (if column? (case val - :auto deprecated-icon/remove-icon - :start deprecated-icon/align-self-row-left - :end deprecated-icon/align-self-row-right - :center deprecated-icon/align-self-row-center) + :auto i/remove + :start i/align-self-row-left + :end i/align-self-row-right + :center i/align-self-row-center) (case val - :auto deprecated-icon/remove-icon - :start deprecated-icon/align-self-column-top - :end deprecated-icon/align-self-column-bottom - :center deprecated-icon/align-self-column-center)))) + :auto i/remove + :start i/align-self-column-top + :end i/align-self-column-bottom + :center i/align-self-column-center)))) (defn get-layout-grid-icon [type val ^boolean column?] @@ -175,32 +175,32 @@ :align-items (if column? (case val - :auto deprecated-icon/remove-icon - :start deprecated-icon/align-self-row-left - :end deprecated-icon/align-self-row-right - :center deprecated-icon/align-self-row-center) + :auto i/remove + :start i/align-self-row-left + :end i/align-self-row-right + :center i/align-self-row-center) (case val - :auto deprecated-icon/remove-icon - :start deprecated-icon/align-self-column-top - :end deprecated-icon/align-self-column-bottom - :center deprecated-icon/align-self-column-center)) + :auto i/remove + :start i/align-self-column-top + :end i/align-self-column-bottom + :center i/align-self-column-center)) :justify-items (if (not column?) (case val - :start deprecated-icon/align-content-column-start - :center deprecated-icon/align-content-column-center - :end deprecated-icon/align-content-column-end - :space-around deprecated-icon/align-content-column-around - :space-between deprecated-icon/align-content-column-between - :stretch deprecated-icon/align-content-column-stretch) + :start i/align-content-column-start + :center i/align-content-column-center + :end i/align-content-column-end + :space-around i/align-content-column-around + :space-between i/align-content-column-between + :stretch i/align-content-column-stretch) (case val - :start deprecated-icon/align-content-row-start - :center deprecated-icon/align-content-row-center - :end deprecated-icon/align-content-row-end - :space-around deprecated-icon/align-content-row-around - :space-between deprecated-icon/align-content-row-between - :stretch deprecated-icon/align-content-row-stretch)))) + :start i/align-content-row-start + :center i/align-content-row-center + :end i/align-content-row-end + :space-around i/align-content-row-around + :space-between i/align-content-row-between + :stretch i/align-content-row-stretch)))) (mf/defc direction-row-flex {::mf/props :obj diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index cb1d12c606..813faf0847 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -455,10 +455,10 @@ :name "frame-orientation" :wide true :class (stl/css :radio-buttons)} - [:& radio-button {:icon deprecated-icon/size-vertical + [:& radio-button {:icon i/size-vertical :value "vert" :id "size-vertical"}] - [:& radio-button {:icon deprecated-icon/size-horizontal + [:& radio-button {:icon i/size-horizontal :value "horiz" :id "size-horizontal"}]] [:> icon-button* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 69f2362722..c2f9226250 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -53,19 +53,19 @@ [:& radio-button {:value "left" :id "text-align-left" :title (tr "workspace.options.text-options.text-align-left") - :icon deprecated-icon/text-align-left}] + :icon i/text-align-left}] [:& radio-button {:value "center" :id "text-align-center" :title (tr "workspace.options.text-options.text-align-center") - :icon deprecated-icon/text-align-center}] + :icon i/text-align-center}] [:& radio-button {:value "right" :id "text-align-right" :title (tr "workspace.options.text-options.text-align-right") - :icon deprecated-icon/text-align-right}] + :icon i/text-align-right}] [:& radio-button {:value "justify" :id "text-align-justify" :title (tr "workspace.options.text-options.text-align-justify") - :icon deprecated-icon/text-justify}]]])) + :icon i/text-justify}]]])) (mf/defc text-direction-options [{:keys [values on-change on-blur] :as props}] @@ -88,12 +88,12 @@ :type "checkbox" :id "ltr-text-direction" :title (tr "workspace.options.text-options.direction-ltr") - :icon deprecated-icon/text-ltr}] + :icon i/text-ltr}] [:& radio-button {:value "rtl" :type "checkbox" :id "rtl-text-direction" :title (tr "workspace.options.text-options.direction-rtl") - :icon deprecated-icon/text-rtl}]]])) + :icon i/text-rtl}]]])) (mf/defc vertical-align [{:keys [values on-change on-blur] :as props}] @@ -113,15 +113,15 @@ [:& radio-button {:value "top" :id "vertical-text-align-top" :title (tr "workspace.options.text-options.align-top") - :icon deprecated-icon/text-top}] + :icon i/text-top}] [:& radio-button {:value "center" :id "vertical-text-align-center" :title (tr "workspace.options.text-options.align-middle") - :icon deprecated-icon/text-middle}] + :icon i/text-middle}] [:& radio-button {:value "bottom" :id "vertical-text-align-bottom" :title (tr "workspace.options.text-options.align-bottom") - :icon deprecated-icon/text-bottom}]]])) + :icon i/text-bottom}]]])) (mf/defc grow-options [{:keys [ids values on-blur] :as props}] @@ -150,15 +150,15 @@ [:& radio-button {:value "fixed" :id "text-fixed-grow" :title (tr "workspace.options.text-options.grow-fixed") - :icon deprecated-icon/text-fixed}] + :icon i/text-fixed}] [:& radio-button {:value "auto-width" :id "text-auto-width-grow" :title (tr "workspace.options.text-options.grow-auto-width") - :icon deprecated-icon/text-auto-width}] + :icon i/text-auto-width}] [:& radio-button {:value "auto-height" :id "text-auto-height-grow" :title (tr "workspace.options.text-options.grow-auto-height") - :icon deprecated-icon/text-auto-height}]]])) + :icon i/text-auto-height}]]])) (mf/defc text-decoration-options [{:keys [values on-change on-blur] :as props}] @@ -180,12 +180,12 @@ :type "checkbox" :id "underline-text-decoration" :title (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) - :icon deprecated-icon/text-underlined}] + :icon i/text-underlined}] [:& radio-button {:value "line-through" :type "checkbox" :id "line-through-text-decoration" :title (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) - :icon deprecated-icon/text-stroked}]]])) + :icon i/text-stroked}]]])) (mf/defc text-menu {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index e8312bd19c..76a8883794 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -26,6 +26,7 @@ [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.select :refer [select]] [app.main.ui.context :as ctx] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -418,17 +419,17 @@ [:& radio-buttons {:selected text-transform :on-change handle-change :name "text-transform"} - [:& radio-button {:icon deprecated-icon/text-uppercase + [:& radio-button {:icon i/text-uppercase :type "checkbox" :title (tr "inspect.attributes.typography.text-transform.uppercase") :value "uppercase" :id "text-transform-uppercase"}] - [:& radio-button {:icon deprecated-icon/text-mixed + [:& radio-button {:icon i/text-mixed :type "checkbox" :value "capitalize" :title (tr "inspect.attributes.typography.text-transform.capitalize") :id "text-transform-capitalize"}] - [:& radio-button {:icon deprecated-icon/text-lowercase + [:& radio-button {:icon i/text-lowercase :type "checkbox" :title (tr "inspect.attributes.typography.text-transform.lowercase") :value "lowercase" diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs index fb1473a8e1..d3f5842390 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs @@ -52,8 +52,6 @@ [:value [::sm/text {:error/fn token-value-error-fn}]] - [:resolved-value ::sm/any] - [:description {:optional true} [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] @@ -82,7 +80,7 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens] + (mf/with-memo [tokens token] ;; 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. 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 index e5f70ed99f..e953d8206e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs @@ -52,8 +52,6 @@ [:value [::sm/text {:error/fn token-value-error-fn}]] - [:resolved-value ::sm/any] - [:description {:optional true} [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] @@ -82,7 +80,7 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens] + (mf/with-memo [tokens token] ;; 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. 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 index a65b80d206..712b244ed3 100644 --- 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 @@ -21,7 +21,7 @@ [app.util.forms :as fm] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] - [clojure.core :as c] + [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -51,10 +51,6 @@ (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])) @@ -125,6 +121,7 @@ :value (or value "") :hint-message (:message hint) :slot-end font-selector-button + :variant "comfortable" :hint-type (:type hint)}) props @@ -133,6 +130,131 @@ :hint-message (:message error)}) props)] + (mf/with-effect [resolve-stream tokens token input-name touched?] + (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]}] + (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)))) + + [:* + [:> 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}]])])) + +(defn- on-composite-combobox-token-change + ([form field value] + (on-composite-combobox-token-change form field value false)) + ([form field value trim?] + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) + +(mf/defc font-picker-composite-combobox* + [{:keys [token tokens name] :rest props}] + (let [form (mf/use-ctx fc/context) + input-name name + + error + (get-in @form [:errors :value input-name]) + + value + (get-in @form [:data :value input-name] "") + + font (fonts/find-font-family value) + + resolve-stream + (mf/with-memo [token] + (if-let [value (get-in token [:value input-name])] + (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) + (on-composite-combobox-token-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)] + (on-composite-combobox-token-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 + :variant "comfortable" + :hint-type (:type hint)}) + + props + (if error + (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) @@ -141,17 +263,30 @@ (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"}))))))] + (rx/subs! + (fn [{:keys [error value]}] + (cond + (and error (str/empty? (:error/value error))) + (do + (swap! form update-in [:errors :value] dissoc input-name) + (swap! form update-in [:data :value] dissoc input-name) + (swap! form update :extra-errors dissoc :value) + (reset! hint* {})) + + + (some? error) + (let [error' (:message error)] + (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})) + + :else + (let [message (tr "workspace.tokens.resolved-value" value) + input-value (get-in @form [:data :value input-name] "")] + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (if (or (empty? value) (= input-value value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"})))))))] (fn [] (rx/dispose! subs)))) 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 index ebefb01d88..fa0c64b501 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs @@ -52,8 +52,6 @@ [:value [::sm/text {:error/fn token-value-error-fn}]] - [:resolved-value ::sm/any] - [:description {:optional true} [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] @@ -82,7 +80,7 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens] + (mf/with-memo [tokens token] ;; 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. 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 index 0741d36750..7e104e8f0f 100644 --- 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 @@ -46,8 +46,6 @@ [:value ::sm/text] - [:resolved-value ::sm/any] - [:description {:optional true} [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] @@ -79,7 +77,7 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens] + (mf/with-memo [tokens token] ;; 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. 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 7bf6018a76..49e9c1e202 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 @@ -31,7 +31,6 @@ [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.icons :as deprecated-icon] [app.main.ui.workspace.colorpicker :as colorpicker] [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] @@ -42,6 +41,7 @@ [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.main.ui.workspace.tokens.management.create.typography :as typography] [app.util.dom :as dom] [app.util.functions :as uf] [app.util.i18n :refer [tr]] @@ -736,11 +736,11 @@ :selected (if reference-tab-active? "reference" "composite") :on-change on-toggle-tab :name "reference-composite-tab"} - [:& radio-button {:icon deprecated-icon/layers + [:& radio-button {:icon i/layers :value "composite" :title (tr "workspace.tokens.individual-tokens") :id "composite-opt"}] - [:& radio-button {:icon deprecated-icon/tokens + [:& radio-button {:icon i/tokens :value "reference" :title (tr "workspace.tokens.use-reference") :id "reference-opt"}]]] @@ -975,11 +975,11 @@ :name (str "inset-select-" shadow-idx)} [:& radio-button {:value "false" :title "false" - :icon "❌" + :icon i/close :id (str "inset-default-" shadow-idx)}] [:& radio-button {:value "true" :title "true" - :icon "✅" + :icon i/tick :id (str "inset-false-" shadow-idx)}]]])) (mf/defc shadow-input* @@ -1439,11 +1439,12 @@ :validate-token default-validate-token :tokens-tree-in-selected-set tokens-tree-in-selected-set :token token}) - font-family-props (mf/spread-props props {:validate-token validate-font-family-token})] + font-family-props (mf/spread-props props {:validate-token validate-font-family-token}) + typography-props (mf/spread-props props {:validate-token validate-typography-token})] (case token-type :color [:> color/form* props] - :typography [:> typography-form* props] + :typography [:> typography/form* typography-props] :shadow [:> shadow-form* props] :font-family [:> font-family/form* font-family-props] :text-case [:> text-case/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 index a6c1db54cf..c6c667281e 100644 --- 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 @@ -22,7 +22,6 @@ [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 @@ -105,9 +104,6 @@ (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) @@ -119,15 +115,12 @@ 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)) + hex (if (tinycolor/valid-color value) + (tinycolor/->hex-string (tinycolor/valid-color value)) "#8f9da3") - alpha (if (tinycolor/valid-color resolved-value) - (tinycolor/alpha (tinycolor/valid-color resolved-value)) + alpha (if (tinycolor/valid-color value) + (tinycolor/alpha (tinycolor/valid-color value)) 1) resolve-stream @@ -224,16 +217,15 @@ (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"}))))))] + (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)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs index 2147c38f98..90c681beb6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs @@ -9,13 +9,14 @@ [app.common.data :as d] [app.common.types.tokens-lib :as ctob] [app.main.data.style-dictionary :as sd] + [app.main.data.workspace.tokens.format :as dwtf] [app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.forms :as fc] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] - [clojure.core :as c] + [cuerdas.core :as str] [rumext.v2 :as mf])) (defn- resolve-value @@ -23,7 +24,6 @@ (let [token {:value value :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} - tokens (-> tokens ;; Remove previous token when renaming a token @@ -45,10 +45,6 @@ (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])) @@ -61,8 +57,8 @@ resolve-stream (mf/with-memo [token] - (if-let [value (:value token)] - (rx/behavior-subject value) + (if (contains? token :value) + (rx/behavior-subject (:value token)) (rx/subject))) hint* @@ -82,10 +78,9 @@ props (mf/spread-props props {:on-change on-change :default-value value - :hint-message (:message hint) :variant "comfortable" + :hint-message (:message hint) :hint-type (:type hint)}) - props (if (and error touched?) (mf/spread-props props {:hint-type "error" @@ -101,18 +96,116 @@ (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"}))))))] + (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)))) - [:> input* props])) \ No newline at end of file + [:> input* props])) + +(defn- on-composite-input-token-change + ([form field value] + (on-composite-input-token-change form field value false)) + ([form field value trim?] + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) + +(mf/defc token-composite-value-input* + [{:keys [name tokens token] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + error + (get-in @form [:errors :value input-name]) + + value + (get-in @form [:data :value input-name] "") + + resolve-stream + (mf/with-memo [token] + (if-let [value (get-in token [:value input-name])] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [value (-> event dom/get-target dom/get-input-value)] + (on-composite-input-token-change form input-name value true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + :default-value value + :variant "comfortable" + :hint-message (:message hint) + :hint-type (:type hint)}) + props + (if error + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props) + + props (if (and (not error) (= input-name :reference)) + (mf/spread-props props {:hint-formated true}) + 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] + (assoc error :message ((:error/fn error) (:error/value error))))))) + + (rx/subs! + (fn [{:keys [error value]}] + (cond + (and error (str/empty? (:error/value error))) + (do + (swap! form update-in [:errors :value] dissoc input-name) + (swap! form update-in [:data :value] dissoc input-name) + (swap! form update :extra-errors dissoc :value) + (reset! hint* {})) + + (some? error) + (let [error' (:message error)] + (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})) + + :else + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) + input-value (get-in @form [:data :value input-name] "")] + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (if (= input-value (str value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"})))))))] + (fn [] + (rx/dispose! subs)))) + + [:> input* props])) 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 index ba0ca455aa..82a2d5ba2c 100644 --- 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 @@ -52,8 +52,6 @@ [:value [::sm/text {:error/fn token-value-error-fn}]] - [:resolved-value ::sm/any] - [:description {:optional true} [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] @@ -82,7 +80,7 @@ (mf/deref refs/workspace-active-theme-sets-tokens) tokens - (mf/with-memo [tokens] + (mf/with-memo [tokens token] ;; 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. diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs new file mode 100644 index 0000000000..ce39829c71 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs @@ -0,0 +1,427 @@ +;; 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.typography + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [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.components.radio-buttons :refer [radio-button radio-buttons]] + [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 forms] + [app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-composite-combobox*]] + [app.main.ui.workspace.tokens.management.create.form-input-token :refer [token-composite-value-input*]] + [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])) + +(mf/defc composite-form* + [{:keys [token tokens] :as props}] + (let [letter-spacing-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :letter-spacing + :value (cto/join-font-family (get value :letter-spacing))} + {:type :letter-spacing})) + + font-family-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-family + :value (get value :font-family)} + {:type :font-family})) + + font-size-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-size + :value (get value :font-size)} + {:type :font-size})) + + font-weight-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-weight + :value (get value :font-weight)} + {:type :font-weight})) + + ;; TODO: Review this type + line-height-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :number + :value (get value :line-height)} + {:type :number})) + + text-case-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :text-case + :value (get value :text-case)} + {:type :text-case})) + + text-decoration-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :text-decoration + :value (get value :text-decoration)} + {:type :text-decoration}))] + + [:* + [:div {:class (stl/css :input-row)} + [:> font-picker-composite-combobox* + {:icon i/text-font-family + :placeholder (tr "workspace.tokens.token-font-family-value-enter") + :aria-label (tr "workspace.tokens.token-font-family-value") + :name :font-family + :token font-family-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Font Size" + :icon i/text-font-size + :placeholder (tr "workspace.tokens.font-size-value-enter") + :name :font-size + :token font-size-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Font Weight" + :icon i/text-font-weight + :placeholder (tr "workspace.tokens.font-weight-value-enter") + :name :font-weight + :token font-weight-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Line Height" + :icon i/text-lineheight + :placeholder (tr "workspace.tokens.line-height-value-enter") + :name :line-height + :token line-height-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Letter Spacing" + :icon i/text-letterspacing + :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite") + :name :letter-spacing + :token letter-spacing-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Text Case" + :icon i/text-mixed + :placeholder (tr "workspace.tokens.text-case-value-enter") + :name :text-case + :token text-case-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:aria-label "Text Decoration" + :icon i/text-underlined + :placeholder (tr "workspace.tokens.text-decoration-value-enter") + :name :text-decoration + :token text-decoration-sub-token + :tokens tokens}]]])) + +(mf/defc reference-form* + [{:keys [token tokens] :as props}] + [:div {:class (stl/css :input-row)} + [:> token-composite-value-input* + {:placeholder (tr "workspace.tokens.reference-composite") + :aria-label (tr "labels.reference") + :icon i/text-typography + :name :reference + :token token + :tokens tokens}]]) + +(defn- make-schema + [tokens-tree active-tab] + (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 + [:map + [:font-family {:optional true} [:maybe :string]] + [:font-size {:optional true} [:maybe :string]] + [:font-weight {:optional true} [:maybe :string]] + [:line-height {:optional true} [:maybe :string]] + [:letter-spacing {:optional true} [:maybe :string]] + [:text-case {:optional true} [:maybe :string]] + [:text-decoration {:optional true} [:maybe :string]] + (if (= active-tab :reference) + [:reference {:optional false} ::sm/text] + [:reference {:optional true} [:maybe :string]])]] + + [:description {:optional true} + [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] + + [:fn {:error/field [:value :reference] + :error/fn #(tr "workspace.tokens.self-reference")} + (fn [{:keys [name value]}] + (let [reference (get value :reference)] + (if (and reference name) + (not (cto/token-value-self-reference? name reference)) + true)))] + + [:fn {:error/field [:value :line-height] + :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")} + (fn [{:keys [value]}] + (let [line-heigh (get value :line-height) + font-size (get value :font-size)] + (if (and line-heigh (not font-size)) + false + true)))] + + ;; This error does not shown on interface, it's just to avoid saving empty composite tokens + ;; We don't need to translate it. + [:fn {:error/fn (fn [_] "At least one composite field must be set") + :error/field :value} + (fn [attrs] + (let [result (reduce-kv (fn [_ _ v] + (if (str/empty? v) + false + (reduced true))) + false + (get attrs :value))] + result))]])) + +(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 :typography})) + + active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite)) + active-tab (deref active-tab*) + + 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 token] + ;; 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 active-tab] + (make-schema tokens-tree-in-selected-set active-tab)) + + initial + (mf/with-memo [token] + (let [value (:value token) + processed-value + (cond + (string? value) + {:reference value} + + (map? value) + (let [value (cond-> value + (:font-family value) + (update :font-family cto/join-font-family))] + (select-keys value + [:font-family + :font-size + :font-weight + :line-height + :letter-spacing + :text-case + :text-decoration])) + :else + {})] + + {:name (:name token "") + :value processed-value + :description (:description token "")})) + + form + (fm/use-form :schema schema + :initial initial) + + warning-name-change? + (not= (get-in @form [:data :name]) + (:name initial)) + + on-toggle-tab + (mf/use-fn + (mf/deps) + (fn [new-tab] + (let [new-tab (keyword new-tab)] + (reset! active-tab* new-tab)))) + + 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 (if (contains? value :reference) + (get value :reference) + 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))))))))] + + [:> forms/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)} + [:> forms/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 :title-bar)} + [:div {:class (stl/css :title)} (tr "labels.typography")] + [:& radio-buttons {:class (stl/css :listing-options) + :selected (d/name active-tab) + :on-change on-toggle-tab + :name "reference-composite-tab"} + [:& radio-button {:icon i/layers + :value "composite" + :title (tr "workspace.tokens.individual-tokens") + :id "composite-opt"}] + [:& radio-button {:icon i/tokens + :value "reference" + :title (tr "workspace.tokens.use-reference") + :id "reference-opt"}]]] + [:div {:class (stl/css :inputs-wrapper)} + (if (= active-tab :composite) + [:> composite-form* {:token token + :tokens tokens}] + + [:> reference-form* {:token token + :tokens tokens}])] + + [:div {:class (stl/css :input-row)} + [:> forms/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")] + + [:> forms/form-submit* {:variant "primary" + :on-submit on-submit} + (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss new file mode 100644 index 0000000000..d129320f10 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss @@ -0,0 +1,74 @@ +// 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); +} + +.inputs-wrapper { + display: flex; + flex-direction: column; + gap: var(--sp-m); + border-inline-start: $b-1 solid var(--color-accent-primary-muted); + padding-inline-start: var(--sp-m); +} + +.input-row { + position: relative; + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.title-bar { + display: grid; + grid-template-columns: 1fr auto; +} + +.title { + @include t.use-typography("body-small"); + color: var(--color-foreground-primary); + display: flex; + align-items: center; +} + +.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/themes/create_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs index a2c9e1447b..7df19b02c8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs @@ -25,7 +25,6 @@ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] - [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.tokens.sets.lists :as wts] [app.util.dom :as dom] [app.util.i18n :refer [tr]] @@ -94,11 +93,11 @@ :name name} [:& radio-button {:id :on :value :on - :icon deprecated-icon/tick + :icon i/tick :label ""}] [:& radio-button {:id :off :value :off - :icon deprecated-icon/close + :icon i/close :label ""}]])) (mf/defc themes-overview diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index 968c319e7f..1fa7feb469 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -48,7 +48,11 @@ (let [props (m/properties schema) tprops (m/type-properties schema) field (or (first in) - (:error/field props))] + (:error/field props)) + + field (if (vector? field) + field + [field])] (if (contains? acc field) acc @@ -58,30 +62,30 @@ (or (= type :malli.core/missing-key) (nil? value)) - (assoc acc field {:message (tr "errors.field-missing")}) + (assoc-in acc field {:message (tr "errors.field-missing")}) ;; --- CHECK on schema props (contains? props :error/fn) - (assoc acc field (handle-error-fn props problem)) + (assoc-in acc field (handle-error-fn props problem)) (contains? props :error/message) - (assoc acc field (handle-error-message props)) + (assoc-in acc field (handle-error-message props)) (contains? props :error/code) - (assoc acc field (handle-error-code props)) + (assoc-in acc field (handle-error-code props)) ;; --- CHECK on type props (contains? tprops :error/fn) - (assoc acc field (handle-error-fn tprops problem)) + (assoc-in acc field (handle-error-fn tprops problem)) (contains? tprops :error/message) - (assoc acc field (handle-error-message tprops)) + (assoc-in acc field (handle-error-message tprops)) (contains? tprops :error/code) - (assoc acc field (handle-error-code tprops)) + (assoc-in acc field (handle-error-code tprops)) :else - (assoc acc field {:message (tr "errors.invalid-data")}))))) + (assoc-in acc field {:message (tr "errors.invalid-data")}))))) (defn- use-rerender-fn [] @@ -114,20 +118,25 @@ [f {:keys [schema validators]}] (fn [& args] (let [state (apply f args) - cleaned (sm/decode schema (:data state) sm/string-transformer) + cleaned (sm/decode schema (:data state) sm/json-transformer) valid? (sm/validate schema cleaned) - errors (when-not valid? - (collect-schema-errors schema validators state))] + + errors + (when-not valid? + (collect-schema-errors schema validators state)) + + extra-errors + (not-empty (:extra-errors state))] (assoc state :errors errors :clean-data (when valid? cleaned) - :valid (and (not errors) valid?))))) + :valid (and (not errors) + (not extra-errors) + valid?))))) (defn- create-form-mutator [internal-state rerender-fn wrap-update-fn initial opts] - (mf/set-ref-val! internal-state initial) - (reify IDeref (-deref [_] @@ -162,7 +171,7 @@ (rerender-fn))))) (defn use-form - [& {:keys [initial] :as opts}] + [& {:keys [initial schema validators] :as opts}] (let [rerender-fn (use-rerender-fn) initial @@ -175,8 +184,15 @@ (mf/use-ref nil) form-mutator - (mf/with-memo [initial] - (create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))] + (mf/with-memo [initial schema validators] + (let [mutator (create-form-mutator internal-state rerender-fn wrap-update-schema-fn + initial + (select-keys opts [:schema :validators]))] + (swap! mutator identity) + mutator))] + + (mf/with-effect [initial] + (mf/set-ref-val! internal-state initial)) ;; Initialize internal state once (mf/with-layout-effect [] @@ -191,11 +207,16 @@ ([form field value] (on-input-change form field value false)) ([form field value trim?] - (swap! form (fn [state] - (-> state - (assoc-in [:touched field] true) - (assoc-in [:data field] (if trim? (str/trim value) value)) - (update :errors dissoc field)))))) + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:touched field] true) + (assoc-in [:data field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) (defn update-input-value! [form field value]