Add line-height to composite typography token

This commit is contained in:
Florian Schroedl
2025-09-16 15:50:38 +03:00
committed by Andrés Moya
parent c1fd1a3b42
commit c882e8347a
14 changed files with 110 additions and 9 deletions

View File

@@ -58,6 +58,19 @@
"fontSize" :font-size
"fontFamily" :font-family)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
@@ -214,7 +227,8 @@
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys))
typography-token-keys
#{:line-height}))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
@@ -286,6 +300,7 @@
(font-size-keys shape-attr) #{shape-attr :typography}
(letter-spacing-keys shape-attr) #{shape-attr :typography}
(font-family-keys shape-attr) #{shape-attr :typography}
(= :line-height shape-attr) #{:line-height :typography}
(= :text-transform shape-attr) #{:text-case :typography}
(text-decoration-keys shape-attr) #{shape-attr :typography}
(font-weight-keys shape-attr) #{shape-attr :typography}

View File

@@ -1517,7 +1517,7 @@ Will return a value that matches this schema:
[value]
(if (map? value)
(-> value
(set/rename-keys cto/dtcg-token-type->token-type)
(set/rename-keys cto/composite-dtcg-token-type->token-type)
(select-keys cto/typography-keys)
;; Convert font-family values within typography composite tokens
(d/update-when :font-family convert-dtcg-font-family))
@@ -1705,7 +1705,7 @@ Will return a value that matches this schema:
(reduce-kv
(fn [acc k v]
(if (contains? cto/typography-keys k)
(assoc acc (cto/token-type->dtcg-token-type k) v)
(assoc acc (cto/composite-token-type->dtcg-token-type k) v)
acc))
{} value)
value))

View File

@@ -33,6 +33,7 @@
"fontWeight": "bold",
"fontSize": "24px",
"letterSpacing": "0.05em",
"lineHeights": "100%",
"fontFamilies": ["Arial", "sans-serif"],
"textCase": "uppercase"
},

View File

@@ -1625,6 +1625,7 @@
(t/is (= (:value token) {:font-weight "bold"
:font-size "24px"
:letter-spacing "0.05em"
:line-height "100%"
:font-family ["Arial", "sans-serif"]
:text-case "uppercase"}))
(t/is (= (:description token) "A complex typography token"))))

View File

@@ -92,6 +92,7 @@
"~:font-family": ["42dot Sans"],
"~:font-size": "100",
"~:font-weight": "300",
"~:line-height": "2",
"~:letter-spacing": "2",
"~:text-case": "uppercase",
"~:text-decoration": "underline"

View File

@@ -974,9 +974,9 @@ test.describe("Tokens: Themes modal", () => {
).toBeVisible();
await expect(saveButton).toBeDisabled();
// Allow empty fields
// Show error with line-height depending on invalid font-size
await fontSizeField.fill("");
await expect(saveButton).toBeEnabled();
await expect(saveButton).toBeDisabled();
// Fill in values for all fields and verify they persist when switching tabs
await fontSizeField.fill("16");
@@ -985,6 +985,8 @@ test.describe("Tokens: Themes modal", () => {
tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
const lineHeightField =
tokensUpdateCreateModal.getByLabel(/Line Height/i);
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
const textDecorationField =
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
@@ -995,6 +997,7 @@ test.describe("Tokens: Themes modal", () => {
fontFamily: await fontFamilyField.inputValue(),
fontWeight: await fontWeightField.inputValue(),
letterSpacing: await letterSpacingField.inputValue(),
lineHeight: await lineHeightField.inputValue(),
textCase: await textCaseField.inputValue(),
textDecoration: await textDecorationField.inputValue(),
};
@@ -1014,6 +1017,9 @@ test.describe("Tokens: Themes modal", () => {
await expect(letterSpacingField).toHaveValue(
originalValues.letterSpacing,
);
await expect(lineHeightField).toHaveValue(
originalValues.lineHeight,
);
await expect(textCaseField).toHaveValue(originalValues.textCase);
await expect(textDecorationField).toHaveValue(
originalValues.textDecoration,

View File

@@ -225,6 +225,34 @@
:else
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value)]})))
(defn- parse-sd-token-typography-line-height
"Parses `line-height-value` of a composite typography token.
Uses `font-size-value` to calculate the relative line-height value.
Returns an error for an invalid font-size value."
[line-height-value font-size-value font-size-errors]
(let [missing-references (seq (some ctob/find-token-value-references line-height-value))
error
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
(or
(not font-size-value)
(seq font-size-errors))
{:errors [(wte/error-with-value :error.style-dictionary/composite-line-height-needs-font-size font-size-value)]
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
(case unit
"%" (/ value 100)
"px" (/ value font-size-value)
nil value
nil))
(catch :default _ nil))
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value line-height-value)]})))
(defn- parse-sd-token-font-family-value
[value]
(let [missing-references (seq (some ctob/find-token-value-references value))]
@@ -247,6 +275,8 @@
nil))
(defn- parse-composite-typography-value
"Parses composite typography `value` map.
Processes the `:line-height` based on the `:font-size` value in the map."
[value]
(let [missing-references
(when (string? value)
@@ -261,14 +291,34 @@
:else
(let [converted (js->clj value :keywordize-keys true)
add-keyed-errors (fn [typography-map k errors]
(update typography-map :errors concat (map #(assoc % :typography-key k) errors)))
;; Separate line-height to process in an extra step
without-line-height (dissoc converted :line-height)
valid-typography (reduce
(fn [acc [k v]]
(let [{:keys [errors value]} (parse-atomic-typography-value k v)]
(if (seq errors)
(update acc :errors concat (map #(assoc % :typography-key k) errors))
(add-keyed-errors acc k errors)
(assoc-in acc [:value k] (or value v)))))
{:value {}}
converted)]
without-line-height)
;; Calculate line-height based on the resolved font-size and add it back to the map
line-height (when-let [line-height (:line-height converted)]
(-> (parse-sd-token-typography-line-height
line-height
(get-in valid-typography [:value :font-size])
(get-in valid-typography [:errors :font-size]))))
valid-typography (cond
(:errors line-height)
(add-keyed-errors valid-typography :line-height (:errors line-height))
line-height
(assoc-in valid-typography [:value :line-height] line-height)
:else
valid-typography)]
valid-typography))))
(defn collect-typography-errors [token]

View File

@@ -434,7 +434,8 @@
:font-weight update-font-weight
:letter-spacing update-letter-spacing
:text-case update-text-case
:text-decoration update-text-decoration}
:text-decoration update-text-decoration
:line-height update-line-height}
value
[shape-ids attributes page-id])))))

View File

@@ -92,6 +92,10 @@
{:error/code :error.style-dictionary/invalid-token-value-typography
:error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}
:error.style-dictionary/composite-line-height-needs-font-size
{:error/code :error.style-dictionary/composite-line-height-needs-font-size
:error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size" %)}
:error/unknown
{:error/code :error/unknown
:error/fn #(tr "labels.unknown-error")}})

View File

@@ -12,6 +12,7 @@
:font-size "Font Size"
:font-family "Font Family"
:font-weight "Font Weight"
:line-height "Line Height"
:letter-spacing "Letter Spacing"
:text-case "Text Case"
:text-decoration "Text Decoration"})

View File

@@ -912,6 +912,10 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
{:label "Font Weight"
:icon i/text-font-weight
:placeholder (tr "workspace.tokens.font-weight-value-enter")}
:line-height
{:label "Line Height"
:icon i/text-lineheight
:placeholder (tr "workspace.tokens.line-height-value-enter")}
:letter-spacing
{:label "Letter Spacing"
:icon i/text-letterspacing

View File

@@ -874,6 +874,7 @@
:value {:font-size "24px"
:font-weight "bold"
:font-family [(:font-id txt/default-text-attrs) "Arial" "sans-serif"]
:line-height "24px"
:letter-spacing "2"
:text-case "uppercase"
:text-decoration "underline"}
@@ -895,7 +896,6 @@
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
text-1' (cths/get-shape file' :text-1)
text-1' (def text-1' text-1')
style-text-blocks (->> (:content text-1')
(txt/content->text+styles)
(remove (fn [[_ text]] (str/empty? (str/trim text))))
@@ -909,6 +909,7 @@
(t/is (= (:font-size style-text-blocks) "24"))
(t/is (= (:font-weight style-text-blocks) "700"))
(t/is (= (:line-height style-text-blocks) 1))
(t/is (= (:font-family style-text-blocks) "sourcesanspro"))
(t/is (= (:letter-spacing style-text-blocks) "2"))
(t/is (= (:text-transform style-text-blocks) "uppercase"))

View File

@@ -7466,6 +7466,10 @@ msgstr ""
msgid "workspace.tokens.font-weight-value-enter"
msgstr "Enter a value (300, Bold, Regular Italic...) or an {alias}"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:918
msgid "workspace.tokens.line-height-value-enter"
msgstr "Enter line height — multiplier, px, %, or {alias}"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:229
msgid "workspace.tokens.gaps"
msgstr "Gaps"
@@ -7662,6 +7666,10 @@ msgstr "Invalid token value. The resolved value is too large: %s"
msgid "workspace.tokens.opacity-range"
msgstr "Opacity must be between 0 and 100% or 0 and 1 (e.g. 50% or 0.5)."
#: frontend/src/app/main/data/workspace/tokens/errors.cljs:99
msgid "workspace.tokens.composite-line-height-needs-font-size"
msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved value."
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:145
#, fuzzy
msgid "workspace.tokens.original-value"

View File

@@ -7445,6 +7445,10 @@ msgstr ""
msgid "workspace.tokens.font-weight-value-enter"
msgstr "Introduce un valor (300, Bold, Regular Italic...) o un {alias}"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:918
msgid "workspace.tokens.line-height-value-enter"
msgstr "Introduce line height — multiplicador, px o % — o {alias}"
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
#, unused
msgid "workspace.tokens.generic-error"
@@ -7596,6 +7600,10 @@ msgstr "Valor de token no valido. El valor resuelto es muy grande: %s"
msgid "workspace.tokens.opacity-range"
msgstr "La opacidad debe estar entre 0 y 100% o 0 y 1 (p.e. 50% o 0.5)."
#: frontend/src/app/main/data/workspace/tokens/errors.cljs:99
msgid "workspace.tokens.composite-line-height-needs-font-size"
msgstr "El Line Height depende del Font Size. Añade un Font Size para obtener el valor computado."
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:145
#, fuzzy
msgid "workspace.tokens.original-value"