From c29a8cb0c4cb8fe8af03f510e0ed56d9ca689af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6dl?= Date: Fri, 8 Aug 2025 11:11:18 +0200 Subject: [PATCH] :sparkles: Implement font-weight token (#7089) --- common/src/app/common/types/token.cljc | 46 ++++++++++++++++++- .../src/app/main/data/style_dictionary.cljs | 18 ++++++++ .../data/workspace/tokens/application.cljs | 35 ++++++++++++++ .../main/data/workspace/tokens/errors.cljs | 4 ++ .../data/workspace/tokens/propagation.cljs | 1 + .../tokens/management/context_menu.cljs | 2 + .../tokens/management/create/form.cljs | 7 +++ .../tokens/management/create/modals.cljs | 6 +++ .../ui/workspace/tokens/management/group.cljs | 1 + .../tokens/logic/token_actions_test.cljs | 34 ++++++++++++++ frontend/translations/en.po | 8 ++++ frontend/translations/es.po | 10 +++- 12 files changed, 169 insertions(+), 3 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index f6c93d117c..4cc143eed2 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -46,7 +46,8 @@ :string "string" :stroke-width "borderWidth" :text-case "textCase" - :text-decoration "textDecoration"}) + :text-decoration "textDecoration" + :font-weight "fontWeights"}) (def dtcg-token-type->token-type (set/map-invert token-type->dtcg-token-type)) @@ -171,6 +172,12 @@ (def text-case-keys (schema-keys schema:text-case)) +(def ^:private schema:font-weight + [:map + [:font-weight {:optional true} token-name-ref]]) + +(def font-weight-keys (schema-keys schema:font-weight)) + (def ^:private schema:text-decoration [:map [:text-decoration {:optional true} token-name-ref]]) @@ -180,8 +187,10 @@ (def typography-keys (set/union font-size-keys letter-spacing-keys font-family-keys + font-weight-keys text-case-keys - text-decoration-keys)) + text-decoration-keys + font-weight-keys)) ;; TODO: Created to extract the font-size feature from the typography feature flag. ;; Delete this once the typography feature flag is removed. @@ -253,6 +262,7 @@ (font-family-keys shape-attr) #{shape-attr} (text-case-keys shape-attr) #{shape-attr} (text-decoration-keys shape-attr) #{shape-attr} + (font-weight-keys shape-attr) #{shape-attr} (border-radius-keys shape-attr) #{shape-attr} (sizing-keys shape-attr) #{shape-attr} (opacity-keys shape-attr) #{shape-attr} @@ -381,3 +391,35 @@ (let [normalized-value (str/lower (str/trim value))] (when (contains? text-decoration-values normalized-value) normalized-value))) + +(def font-weight-aliases + {"100" #{"thin" "hairline"}, + "200" #{"ultra light" "extralight" "extraleicht" "extra-light" "ultra-light" "ultralight" "extra light"}, + "300" #{"light" "leicht"}, + "400" #{"book" "normal" "buch" "regular"}, + "500" #{"kräftig" "medium" "kraeftig"}, + "600" #{"demi-bold" "halbfett" "demibold" "demi bold" "semibold" "semi bold" "semi-bold"}, + "700" #{"dreiviertelfett" "bold"}, + "800" #{"extrabold" "fett" "extra-bold" "ultrabold" "ultra-bold" "extra bold" "ultra bold"}, + "900" #{"heavy" "black" "extrafett"}, + "950" #{"extra-black" "extra black" "ultra-black" "ultra black"}}) + +(def font-weight-values (into #{} (keys font-weight-aliases))) + +(def font-weight-map + "A map of font-weight aliases that map to their number equivalent used by penpot fonts per `:weight`." + (->> font-weight-aliases + (reduce (fn [acc [k vs]] + (into acc (zipmap vs (repeat k)))) {}))) + +(defn valid-font-weight-variant + "Converts font-weight token value to a map like `{:weight \"100\" :style \"italic\"}`. + Converts a weight alias like `regular` to a number, needs to be a regular number. + Adds `italic` style when found in the `value` string." + [value] + (let [[weight style] (->> (str/split value #"\s+") + (map str/lower)) + weight (get font-weight-map weight weight)] + (when (font-weight-values weight) + (cond-> {:weight weight} + (= style "italic") (assoc :style "italic"))))) diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index a692204f50..fbb53d8719 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -193,6 +193,23 @@ :else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-text-decoration value)]}))) +(defn- parse-sd-token-font-weight-value + "Parses `value` of a font-weight `sd-token` into a map like `{:value \"700\"}` or `{:value \"700 Italic\"}`. + If the `value` is not parseable and/or has missing references returns a map with `:errors`." + [value] + (let [valid-font-weight (ctt/valid-font-weight-variant value) + references (seq (ctob/find-token-value-references value))] + (cond + valid-font-weight + {:value 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-font-weight 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 @@ -237,6 +254,7 @@ :stroke-width (parse-sd-token-stroke-width-value value has-references?) :text-case (parse-sd-token-text-case-value value) :text-decoration (parse-sd-token-text-decoration-value value) + :font-weight (parse-sd-token-font-weight-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 5db37804e1..13b01d0302 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -326,6 +326,33 @@ (st/emit! (ptk/data-event :expand-text-more-options)) (update-text-decoration value shape-ids attributes page-id)))) +(defn- generate-font-weight-text-shape-update + [font-variant shape-ids page-id] + (let [update-node? (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node))) + update-fn (fn [node _] + (let [font (fonts/get-font-data (:font-id node)) + font-variant-id (or + (fonts/find-variant font font-variant) + ;; When variant with matching weight but not with matching style (italic) is found, use that one + (fonts/find-variant font (dissoc font-variant :style)))] + (if font-variant-id + (-> node + (d/txt-merge (assoc font-variant :font-variant-id (:id font-variant-id))) + (cty/remove-typography-from-node)) + node)))] + (dwsh/update-shapes shape-ids + #(txt/update-text-content % update-node? update-fn nil) + {:ignore-touched true + :page-id page-id}))) + +(defn update-font-weight + ([value shape-ids attributes] (update-font-weight value shape-ids attributes nil)) + ([value shape-ids _attributes page-id] + (when-let [font-variant (ctt/valid-font-weight-variant value)] + (generate-font-weight-text-shape-update font-variant shape-ids page-id)))) + ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ (defn apply-token @@ -503,6 +530,14 @@ :fields [{:label "Text Case" :key :text-case}]}} + :font-weight + {:title "Font Weight" + :attributes ctt/font-weight-keys + :on-update-shape update-font-weight + :modal {:key :tokens/font-weight + :fields [{:label "Font Weight" + :key :font-weight}]}} + :text-decoration {:title "Text Decoration" :attributes ctt/text-decoration-keys diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 85bb653281..ec8ad988b1 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -80,6 +80,10 @@ {:error/code :error.style-dictionary/invalid-token-value-text-decoration :error/fn #(tr "workspace.tokens.invalid-text-decoration-token-value" %)} + :error.style-dictionary/invalid-token-value-font-weight + {:error/code :error.style-dictionary/invalid-token-value-font-weight + :error/fn #(tr "workspace.tokens.invalid-font-weight-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 0f5cc1c001..21f256288f 100644 --- a/frontend/src/app/main/data/workspace/tokens/propagation.cljs +++ b/frontend/src/app/main/data/workspace/tokens/propagation.cljs @@ -38,6 +38,7 @@ #{:font-family} dwta/update-font-family #{:text-case} dwta/update-text-case #{:text-decoration} dwta/update-text-decoration + #{:font-weight} dwta/update-font-weight #{: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 f4c103a7bf..0fa23958a0 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 @@ -272,6 +272,7 @@ 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") text-decoration (partial generic-attribute-actions #{:text-decoration} "Text Decoration") + font-weight (partial generic-attribute-actions #{:font-weight} "Font Weight") border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left" :r2 "Top Right" :r4 "Bottom Left" @@ -300,6 +301,7 @@ :letter-spacing letter-spacing :text-case text-case :text-decoration text-decoration + :font-weight font-weight :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 19d13c8f23..4f02201f0a 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 @@ -799,6 +799,12 @@ (mf/spread-props props {:token token :input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})]) +(mf/defc font-weight-form* + [{:keys [token] :rest props}] + [:> form* + (mf/spread-props props {:token token + :input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})]) + (mf/defc form-wrapper* [{:keys [token token-type] :as props}] (let [token-type' (or (:type token) token-type)] @@ -807,4 +813,5 @@ :font-family [:> font-family-form* props] :text-case [:> text-case-form* props] :text-decoration [:> text-decoration-form* props] + :font-weight [:> font-weight-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 aa910ffb59..4871886229 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 @@ -209,3 +209,9 @@ ::mf/register-as :tokens/text-decoration} [properties] [:& token-update-create-modal properties]) + +(mf/defc font-weight-modal + {::mf/register modal/components + ::mf/register-as :tokens/font-weight} + [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 8b76a51e4d..bb6f5b3b15 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -32,6 +32,7 @@ :letter-spacing "text-letterspacing" :text-case "text-mixed" :text-decoration "text-underlined" + :font-weight "text-font-weight" :opacity "percentage" :number "number" :rotation "rotation" 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 d9e7c9d93c..1cd6a815c3 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -660,6 +660,40 @@ (t/is (= (:text-decoration (:applied-tokens text-1')) (:name token-target'))) (t/is (= (:text-decoration style-text-blocks) "underline"))))))))) +(t/deftest test-apply-font-weight + (t/testing "applies font-weight token and updates the font weight" + (t/async + done + (let [font-weight-token {:name "font-weight" + :value "regular" + :type :font-weight} + file (-> (setup-file-with-tokens) + (update-in [:data :tokens-lib] + #(ctob/add-token-in-set % "Set A" (ctob/make-token font-weight-token)))) + store (ths/setup-store file) + text-1 (cths/get-shape file :text-1) + events [(dwta/apply-token {:shape-ids [(:id text-1)] + :attributes #{:font-weight} + :token (toht/get-token file "font-weight") + :on-update-shape dwta/update-font-weight})]] + (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' "font-weight") + 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 (= (:font-weight (:applied-tokens text-1')) (:name token-target'))) + (t/is (= (:font-weight style-text-blocks) "400"))))))))) + (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 76bbd5a1ff..9fca23c57e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7289,6 +7289,14 @@ msgstr "Invalid token value: only none, Uppercase, Lowercase or Capitalize are a msgid "workspace.tokens.text-decoration-value-enter" msgstr "Enter text decoration: none | underline | strike-through" +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:109 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'" + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:602 +msgid "workspace.tokens.font-weight-value-enter" +msgstr "Enter: 400, Bold, 700 Italic, or {alias}" + #: src/app/main/ui/workspace/tokens/management/create/form.cljs:81 msgid "workspace.tokens.invalid-text-decoration-token-value" msgstr "Invalid token value: only none, underline and strike-through are accepted" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 23ef139dda..f37fe5a0c5 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7240,6 +7240,14 @@ msgstr "Introduce: none | uppercase | lowercase | capitalize o {alias}" msgid "workspace.tokens.text-decoration-value-enter" msgstr "Introduce text decoration: none | underline | strike-through" +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:109 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "Valor de peso de fuente inválido: usa valores numéricos (100-950) o nombres estándar (thin, light, regular, bold, etc.) opcionalmente seguidos de 'Italic'" + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:602 +msgid "workspace.tokens.font-weight-value-enter" +msgstr "Introduce: 400, Bold, 700 Italic, o {alias}" + #: src/app/main/ui/workspace/tokens/modals/export.cljs:33 msgid "workspace.tokens.export.preview" msgstr "Previsualizar:" @@ -7852,4 +7860,4 @@ msgid "labels.sources" msgstr "Recursos" msgid "labels.pinned-projects" -msgstr "Proyectos fijados" \ No newline at end of file +msgstr "Proyectos fijados"