From 4c605b81515439652ee8a125abe3ded191b1c80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6dl?= Date: Mon, 28 Jul 2025 17:36:06 +0200 Subject: [PATCH] :sparkles: Implement text case token (#6978) --- common/src/app/common/types/token.cljc | 14 +++++++- .../src/app/main/data/style_dictionary.cljs | 19 +++++++++++ .../data/workspace/tokens/application.cljs | 15 ++++++++ .../main/data/workspace/tokens/errors.cljs | 4 +++ .../data/workspace/tokens/propagation.cljs | 1 + .../tokens/management/context_menu.cljs | 2 ++ .../tokens/management/create/form.cljs | 8 +++++ .../tokens/management/create/modals.cljs | 6 ++++ .../ui/workspace/tokens/management/group.cljs | 1 + .../tokens/management/token_pill.cljs | 2 +- .../tokens/logic/token_actions_test.cljs | 34 +++++++++++++++++++ frontend/translations/en.po | 8 +++++ frontend/translations/es.po | 4 +++ 13 files changed, 116 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index b66d30ac8d..5f2f4751d2 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -37,6 +37,7 @@ :font-family "fontFamilies" :font-size "fontSizes" :letter-spacing "letterSpacing" + :text-case "textCase" :number "number" :opacity "opacity" :other "other" @@ -154,7 +155,16 @@ (def font-family-keys (schema-keys schema:font-family)) -(def typography-keys (set/union font-size-keys letter-spacing-keys font-family-keys)) +(def ^:private schema:text-case + [:map + [:text-case {:optional true} token-name-ref]]) + +(def text-case-keys (schema-keys schema:text-case)) + +(def typography-keys (set/union font-size-keys + letter-spacing-keys + font-family-keys + text-case-keys)) ;; TODO: Created to extract the font-size feature from the typography feature flag. ;; Delete this once the typography feature flag is removed. @@ -191,6 +201,7 @@ schema:font-size schema:letter-spacing schema:font-family + schema:text-case schema:dimensions]) (defn shape-attr->token-attrs @@ -221,6 +232,7 @@ (font-size-keys shape-attr) #{shape-attr} (letter-spacing-keys shape-attr) #{shape-attr} (font-family-keys shape-attr) #{shape-attr} + (text-case-keys shape-attr) #{shape-attr} (border-radius-keys shape-attr) #{shape-attr} (sizing-keys shape-attr) #{shape-attr} (opacity-keys shape-attr) #{shape-attr} diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 205cdd7954..56e4406653 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -157,6 +157,24 @@ :else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}))) +(defn- parse-sd-token-text-case-value + "Parses `value` of a text-case `sd-token` into a map like `{:value \"uppercase\"}`. + If the `value` is not parseable and/or has missing references returns a map with `:errors`." + [value] + (let [normalized-value (str/lower (str/trim value)) + valid? (contains? #{"none" "uppercase" "lowercase" "capitalize"} normalized-value) + references (seq (ctob/find-token-value-references value))] + (cond + valid? + {:value normalized-value} + + references + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)] + :references references} + + :else + {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-text-case value)]}))) + (defn process-sd-tokens "Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure. The `get-origin-token` argument should be a function that takes an @@ -199,6 +217,7 @@ :color (parse-sd-token-color-value value) :opacity (parse-sd-token-opacity-value value has-references?) :stroke-width (parse-sd-token-stroke-width-value value has-references?) + :text-case (parse-sd-token-text-case-value value) :number (parse-sd-token-number-value value) (parse-sd-token-general-value value)) output-token (cond (:errors parsed-token-value) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 637e041db7..55e14cc4ff 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -298,6 +298,13 @@ (when (number? value) (generate-text-shape-update {:font-size (str value)} shape-ids page-id)))) +(defn update-text-case + ([value shape-ids attributes] (update-text-case value shape-ids attributes nil)) + ([value shape-ids _attributes page-id] + (when (string? value) + (generate-text-shape-update {:text-transform value} shape-ids page-id)))) + + ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ (defn apply-token @@ -464,6 +471,14 @@ :fields [{:label "Font Family" :key :font-family}]}} + :text-case + {:title "Text Case" + :attributes ctt/text-case-keys + :on-update-shape update-text-case + :modal {:key :tokens/text-case + :fields [{:label "Text Case" + :key :text-case}]}} + :stroke-width {:title "Stroke Width" :attributes ctt/stroke-width-keys diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 7918e74726..52402da22c 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -72,6 +72,10 @@ {:error/code :error.style-dictionary/invalid-token-value-stroke-width :error/fn #(str/join "\n" [(str (tr "workspace.tokens.invalid-value" %) ".") (tr "workspace.tokens.stroke-width-range")])} + :error.style-dictionary/invalid-token-value-text-case + {:error/code :error.style-dictionary/invalid-token-value-text-case + :error/fn #(tr "workspace.tokens.invalid-text-case-token-value" %)} + :error/unknown {:error/code :error/unknown :error/fn #(tr "labels.unknown-error")}}) diff --git a/frontend/src/app/main/data/workspace/tokens/propagation.cljs b/frontend/src/app/main/data/workspace/tokens/propagation.cljs index cd490f2447..fa9174d171 100644 --- a/frontend/src/app/main/data/workspace/tokens/propagation.cljs +++ b/frontend/src/app/main/data/workspace/tokens/propagation.cljs @@ -36,6 +36,7 @@ #{:font-size} dwta/update-font-size #{:letter-spacing} dwta/update-letter-spacing #{:font-family} dwta/update-font-family + #{:text-case} dwta/update-text-case #{:x :y} dwta/update-shape-position #{:p1 :p2 :p3 :p4} dwta/update-layout-padding #{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index fa5ac80996..457d3ccea6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -268,6 +268,7 @@ (let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width") font-size (partial generic-attribute-actions #{:font-size} "Font Size") line-height #(generic-attribute-actions #{:line-height} "Line Height" (assoc % :on-update-shape dwta/update-line-height)) + text-case (partial generic-attribute-actions #{:text-case} "Text Case") border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left" :r2 "Top Right" :r4 "Bottom Left" @@ -291,6 +292,7 @@ (when (seq line-height) line-height)))) :stroke-width stroke-width :font-size font-size + :text-case text-case :dimensions (fn [context-data] (-> (concat (when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}]) 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 32941eae12..dc33e9ce4a 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 @@ -787,10 +787,18 @@ :custom-input-token-value font-picker* :on-value-resolve on-value-resolve})])) +(mf/defc text-case-form* + [{:keys [token] :rest props}] + (let [placeholder (tr "workspace.tokens.text-case-value-enter")] + [:> form* + (mf/spread-props props {:token token + :input-placeholder placeholder})])) + (mf/defc form-wrapper* [{:keys [token token-type] :as props}] (let [token-type' (or (:type token) token-type)] (case token-type' :color [:> color-form* props] :font-family [:> font-family-form* props] + :text-case [:> text-case-form* props] [:> form* props]))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs index a75851bf9c..a0316ef76f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs @@ -197,3 +197,9 @@ ::mf/register-as :tokens/font-family} [properties] [:& token-update-create-modal properties]) + +(mf/defc text-case-modal + {::mf/register modal/components + ::mf/register-as :tokens/text-case} + [properties] + [:& token-update-create-modal properties]) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index b84e363f70..f14b4b8c59 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -30,6 +30,7 @@ :font-family "text-font-family" :font-size "text-font-size" :letter-spacing "text-letterspacing" + :text-case "text-mixed" :opacity "percentage" :number "number" :rotation "rotation" diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs index 52d630bc80..639d51c707 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs @@ -176,7 +176,7 @@ selected-shapes))) (def token-types-with-status-icon - #{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width}) + #{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width :text-case}) (mf/defc token-pill* {::mf/wrap [mf/memo]} diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index 19a89fb774..e66ff0b54f 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -592,6 +592,40 @@ (t/is (= (:font-family (:applied-tokens text-1')) (:name token-target'))) (t/is (= (:font-family style-text-blocks) (:font-id txt/default-text-attrs)))))))))) +(t/deftest test-apply-text-case + (t/testing "applies text-case token and updates the text transform" + (t/async + done + (let [text-case-token {:name "uppercase-case" + :value "uppercase" + :type :text-case} + file (-> (setup-file-with-tokens) + (update-in [:data :tokens-lib] + #(ctob/add-token-in-set % "Set A" (ctob/make-token text-case-token)))) + store (ths/setup-store file) + text-1 (cths/get-shape file :text-1) + events [(dwta/apply-token {:shape-ids [(:id text-1)] + :attributes #{:text-case} + :token (toht/get-token file "uppercase-case") + :on-update-shape dwta/update-text-case})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + token-target' (toht/get-token file' "uppercase-case") + text-1' (cths/get-shape file' :text-1) + style-text-blocks (->> (:content text-1') + (txt/content->text+styles) + (remove (fn [[_ text]] (str/empty? (str/trim text)))) + (mapv (fn [[style text]] + {:styles (merge txt/default-text-attrs style) + :text-content text})) + (first) + (:styles))] + (t/is (some? (:applied-tokens text-1'))) + (t/is (= (:text-case (:applied-tokens text-1')) (:name token-target'))) + (t/is (= (:text-transform style-text-blocks) "uppercase"))))))))) + (t/deftest test-toggle-token-none (t/testing "should apply token to all selected items, where no item has the token applied" (t/async diff --git a/frontend/translations/en.po b/frontend/translations/en.po index b65708bae7..b87261613b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7253,6 +7253,14 @@ msgstr "Multiple files" msgid "workspace.tokens.export.no-tokens-themes-sets" msgstr "There are no tokens, themes or sets to export." +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:602 +msgid "workspace.tokens.text-case-value-enter" +msgstr "Enter text case: none | Uppercase | Lowercase | Capitalize" + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:109 +msgid "workspace.tokens.invalid-text-case-token-value" +msgstr "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted" + #: src/app/main/ui/workspace/tokens/modals/export.cljs:33 msgid "workspace.tokens.export.preview" msgstr "Preview:" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index f08ce1e96a..81a0621e99 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7227,6 +7227,10 @@ msgstr "Múltiples ficheros" msgid "workspace.tokens.export.no-tokens-themes-sets" msgstr "No existen tokens, temas o sets para exportar." +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:602 +msgid "workspace.tokens.text-case-value-enter" +msgstr "Introduce una capitalización: none | Uppercase | Lowercase | Capitalize" + #: src/app/main/ui/workspace/tokens/modals/export.cljs:33 msgid "workspace.tokens.export.preview" msgstr "Previsualizar:"