Implement font-weight token (#7089)

This commit is contained in:
Florian Schrödl
2025-08-08 11:11:18 +02:00
committed by GitHub
parent a9f4fe84fa
commit c29a8cb0c4
12 changed files with 169 additions and 3 deletions

View File

@@ -46,7 +46,8 @@
:string "string" :string "string"
:stroke-width "borderWidth" :stroke-width "borderWidth"
:text-case "textCase" :text-case "textCase"
:text-decoration "textDecoration"}) :text-decoration "textDecoration"
:font-weight "fontWeights"})
(def dtcg-token-type->token-type (def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type)) (set/map-invert token-type->dtcg-token-type))
@@ -171,6 +172,12 @@
(def text-case-keys (schema-keys schema:text-case)) (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 (def ^:private schema:text-decoration
[:map [:map
[:text-decoration {:optional true} token-name-ref]]) [:text-decoration {:optional true} token-name-ref]])
@@ -180,8 +187,10 @@
(def typography-keys (set/union font-size-keys (def typography-keys (set/union font-size-keys
letter-spacing-keys letter-spacing-keys
font-family-keys font-family-keys
font-weight-keys
text-case-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. ;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed. ;; Delete this once the typography feature flag is removed.
@@ -253,6 +262,7 @@
(font-family-keys shape-attr) #{shape-attr} (font-family-keys shape-attr) #{shape-attr}
(text-case-keys shape-attr) #{shape-attr} (text-case-keys shape-attr) #{shape-attr}
(text-decoration-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} (border-radius-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr} (sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr} (opacity-keys shape-attr) #{shape-attr}
@@ -381,3 +391,35 @@
(let [normalized-value (str/lower (str/trim value))] (let [normalized-value (str/lower (str/trim value))]
(when (contains? text-decoration-values normalized-value) (when (contains? text-decoration-values normalized-value)
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")))))

View File

@@ -193,6 +193,23 @@
:else :else
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-text-decoration value)]}))) {: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 (defn process-sd-tokens
"Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure. "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 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?) :stroke-width (parse-sd-token-stroke-width-value value has-references?)
:text-case (parse-sd-token-text-case-value value) :text-case (parse-sd-token-text-case-value value)
:text-decoration (parse-sd-token-text-decoration-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) :number (parse-sd-token-number-value value)
(parse-sd-token-general-value value)) (parse-sd-token-general-value value))
output-token (cond (:errors parsed-token-value) output-token (cond (:errors parsed-token-value)

View File

@@ -326,6 +326,33 @@
(st/emit! (ptk/data-event :expand-text-more-options)) (st/emit! (ptk/data-event :expand-text-more-options))
(update-text-decoration value shape-ids attributes page-id)))) (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 ------------------------------------------------------------ ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token (defn apply-token
@@ -503,6 +530,14 @@
:fields [{:label "Text Case" :fields [{:label "Text Case"
:key :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 :text-decoration
{:title "Text Decoration" {:title "Text Decoration"
:attributes ctt/text-decoration-keys :attributes ctt/text-decoration-keys

View File

@@ -80,6 +80,10 @@
{:error/code :error.style-dictionary/invalid-token-value-text-decoration {:error/code :error.style-dictionary/invalid-token-value-text-decoration
:error/fn #(tr "workspace.tokens.invalid-text-decoration-token-value" %)} :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/unknown
{:error/code :error/unknown {:error/code :error/unknown
:error/fn #(tr "labels.unknown-error")}}) :error/fn #(tr "labels.unknown-error")}})

View File

@@ -38,6 +38,7 @@
#{:font-family} dwta/update-font-family #{:font-family} dwta/update-font-family
#{:text-case} dwta/update-text-case #{:text-case} dwta/update-text-case
#{:text-decoration} dwta/update-text-decoration #{:text-decoration} dwta/update-text-decoration
#{:font-weight} dwta/update-font-weight
#{:x :y} dwta/update-shape-position #{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding #{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin #{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin

View File

@@ -272,6 +272,7 @@
line-height #(generic-attribute-actions #{:line-height} "Line Height" (assoc % :on-update-shape dwta/update-line-height)) 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-case (partial generic-attribute-actions #{:text-case} "Text Case")
text-decoration (partial generic-attribute-actions #{:text-decoration} "Text Decoration") 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" border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left"
:r2 "Top Right" :r2 "Top Right"
:r4 "Bottom Left" :r4 "Bottom Left"
@@ -300,6 +301,7 @@
:letter-spacing letter-spacing :letter-spacing letter-spacing
:text-case text-case :text-case text-case
:text-decoration text-decoration :text-decoration text-decoration
:font-weight font-weight
:dimensions (fn [context-data] :dimensions (fn [context-data]
(-> (concat (-> (concat
(when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}]) (when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}])

View File

@@ -799,6 +799,12 @@
(mf/spread-props props {:token token (mf/spread-props props {:token token
:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})]) :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* (mf/defc form-wrapper*
[{:keys [token token-type] :as props}] [{:keys [token token-type] :as props}]
(let [token-type' (or (:type token) token-type)] (let [token-type' (or (:type token) token-type)]
@@ -807,4 +813,5 @@
:font-family [:> font-family-form* props] :font-family [:> font-family-form* props]
:text-case [:> text-case-form* props] :text-case [:> text-case-form* props]
:text-decoration [:> text-decoration-form* props] :text-decoration [:> text-decoration-form* props]
:font-weight [:> font-weight-form* props]
[:> form* props]))) [:> form* props])))

View File

@@ -209,3 +209,9 @@
::mf/register-as :tokens/text-decoration} ::mf/register-as :tokens/text-decoration}
[properties] [properties]
[:& token-update-create-modal 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])

View File

@@ -32,6 +32,7 @@
:letter-spacing "text-letterspacing" :letter-spacing "text-letterspacing"
:text-case "text-mixed" :text-case "text-mixed"
:text-decoration "text-underlined" :text-decoration "text-underlined"
:font-weight "text-font-weight"
:opacity "percentage" :opacity "percentage"
:number "number" :number "number"
:rotation "rotation" :rotation "rotation"

View File

@@ -660,6 +660,40 @@
(t/is (= (:text-decoration (:applied-tokens text-1')) (:name token-target'))) (t/is (= (:text-decoration (:applied-tokens text-1')) (:name token-target')))
(t/is (= (:text-decoration style-text-blocks) "underline"))))))))) (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/deftest test-toggle-token-none
(t/testing "should apply token to all selected items, where no item has the token applied" (t/testing "should apply token to all selected items, where no item has the token applied"
(t/async (t/async

View File

@@ -7289,6 +7289,14 @@ msgstr "Invalid token value: only none, Uppercase, Lowercase or Capitalize are a
msgid "workspace.tokens.text-decoration-value-enter" msgid "workspace.tokens.text-decoration-value-enter"
msgstr "Enter text decoration: none | underline | strike-through" 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 #: src/app/main/ui/workspace/tokens/management/create/form.cljs:81
msgid "workspace.tokens.invalid-text-decoration-token-value" msgid "workspace.tokens.invalid-text-decoration-token-value"
msgstr "Invalid token value: only none, underline and strike-through are accepted" msgstr "Invalid token value: only none, underline and strike-through are accepted"

View File

@@ -7240,6 +7240,14 @@ msgstr "Introduce: none | uppercase | lowercase | capitalize o {alias}"
msgid "workspace.tokens.text-decoration-value-enter" msgid "workspace.tokens.text-decoration-value-enter"
msgstr "Introduce text decoration: none | underline | strike-through" 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 #: src/app/main/ui/workspace/tokens/modals/export.cljs:33
msgid "workspace.tokens.export.preview" msgid "workspace.tokens.export.preview"
msgstr "Previsualizar:" msgstr "Previsualizar:"