diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 74f36981a4..cd4a4d8195 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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} diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index fa9d634082..c472a75d3a 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -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)) diff --git a/common/test/common_tests/types/data/tokens-typography-example.json b/common/test/common_tests/types/data/tokens-typography-example.json index 614c7cf1e4..94a48ecb80 100644 --- a/common/test/common_tests/types/data/tokens-typography-example.json +++ b/common/test/common_tests/types/data/tokens-typography-example.json @@ -33,6 +33,7 @@ "fontWeight": "bold", "fontSize": "24px", "letterSpacing": "0.05em", + "lineHeights": "100%", "fontFamilies": ["Arial", "sans-serif"], "textCase": "uppercase" }, diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index d155ba1089..7fc2ac6053 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -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")))) diff --git a/frontend/playwright/data/workspace/get-file-typography-tokens.json b/frontend/playwright/data/workspace/get-file-typography-tokens.json index 35ed171867..8e3497af70 100644 --- a/frontend/playwright/data/workspace/get-file-typography-tokens.json +++ b/frontend/playwright/data/workspace/get-file-typography-tokens.json @@ -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" diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index f71397ce28..482117facd 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -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, diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 19df3a7207..5c506440ba 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -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] diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index a919c33b86..3dc3a8930c 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -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]))))) diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 4173481916..8ec4c667c9 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -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")}}) diff --git a/frontend/src/app/main/data/workspace/tokens/format.cljs b/frontend/src/app/main/data/workspace/tokens/format.cljs index 43a26c9c20..e9ce1e84ae 100644 --- a/frontend/src/app/main/data/workspace/tokens/format.cljs +++ b/frontend/src/app/main/data/workspace/tokens/format.cljs @@ -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"}) 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 0b7ad911ce..a018af13c6 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 @@ -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 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 1329c9ea8b..3410dcb6c7 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -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")) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a4da54468f..aeb26299b5 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b9b84ce344..04138d226b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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"