mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
✨ Add line-height to composite typography token
This commit is contained in:
committed by
Andrés Moya
parent
c1fd1a3b42
commit
c882e8347a
@@ -58,6 +58,19 @@
|
|||||||
"fontSize" :font-size
|
"fontSize" :font-size
|
||||||
"fontFamily" :font-family)))
|
"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
|
(def token-types
|
||||||
(into #{} (keys token-type->dtcg-token-type)))
|
(into #{} (keys token-type->dtcg-token-type)))
|
||||||
|
|
||||||
@@ -214,7 +227,8 @@
|
|||||||
text-case-keys
|
text-case-keys
|
||||||
text-decoration-keys
|
text-decoration-keys
|
||||||
font-weight-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.
|
;; 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.
|
||||||
@@ -286,6 +300,7 @@
|
|||||||
(font-size-keys shape-attr) #{shape-attr :typography}
|
(font-size-keys shape-attr) #{shape-attr :typography}
|
||||||
(letter-spacing-keys shape-attr) #{shape-attr :typography}
|
(letter-spacing-keys shape-attr) #{shape-attr :typography}
|
||||||
(font-family-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-transform shape-attr) #{:text-case :typography}
|
||||||
(text-decoration-keys shape-attr) #{shape-attr :typography}
|
(text-decoration-keys shape-attr) #{shape-attr :typography}
|
||||||
(font-weight-keys shape-attr) #{shape-attr :typography}
|
(font-weight-keys shape-attr) #{shape-attr :typography}
|
||||||
|
|||||||
@@ -1517,7 +1517,7 @@ Will return a value that matches this schema:
|
|||||||
[value]
|
[value]
|
||||||
(if (map? value)
|
(if (map? value)
|
||||||
(-> 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)
|
(select-keys cto/typography-keys)
|
||||||
;; Convert font-family values within typography composite tokens
|
;; Convert font-family values within typography composite tokens
|
||||||
(d/update-when :font-family convert-dtcg-font-family))
|
(d/update-when :font-family convert-dtcg-font-family))
|
||||||
@@ -1705,7 +1705,7 @@ Will return a value that matches this schema:
|
|||||||
(reduce-kv
|
(reduce-kv
|
||||||
(fn [acc k v]
|
(fn [acc k v]
|
||||||
(if (contains? cto/typography-keys k)
|
(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))
|
acc))
|
||||||
{} value)
|
{} value)
|
||||||
value))
|
value))
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"fontWeight": "bold",
|
"fontWeight": "bold",
|
||||||
"fontSize": "24px",
|
"fontSize": "24px",
|
||||||
"letterSpacing": "0.05em",
|
"letterSpacing": "0.05em",
|
||||||
|
"lineHeights": "100%",
|
||||||
"fontFamilies": ["Arial", "sans-serif"],
|
"fontFamilies": ["Arial", "sans-serif"],
|
||||||
"textCase": "uppercase"
|
"textCase": "uppercase"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1625,6 +1625,7 @@
|
|||||||
(t/is (= (:value token) {:font-weight "bold"
|
(t/is (= (:value token) {:font-weight "bold"
|
||||||
:font-size "24px"
|
:font-size "24px"
|
||||||
:letter-spacing "0.05em"
|
:letter-spacing "0.05em"
|
||||||
|
:line-height "100%"
|
||||||
:font-family ["Arial", "sans-serif"]
|
:font-family ["Arial", "sans-serif"]
|
||||||
:text-case "uppercase"}))
|
:text-case "uppercase"}))
|
||||||
(t/is (= (:description token) "A complex typography token"))))
|
(t/is (= (:description token) "A complex typography token"))))
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"~:font-family": ["42dot Sans"],
|
"~:font-family": ["42dot Sans"],
|
||||||
"~:font-size": "100",
|
"~:font-size": "100",
|
||||||
"~:font-weight": "300",
|
"~:font-weight": "300",
|
||||||
|
"~:line-height": "2",
|
||||||
"~:letter-spacing": "2",
|
"~:letter-spacing": "2",
|
||||||
"~:text-case": "uppercase",
|
"~:text-case": "uppercase",
|
||||||
"~:text-decoration": "underline"
|
"~:text-decoration": "underline"
|
||||||
|
|||||||
@@ -974,9 +974,9 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
|
|
||||||
// Allow empty fields
|
// Show error with line-height depending on invalid font-size
|
||||||
await fontSizeField.fill("");
|
await fontSizeField.fill("");
|
||||||
await expect(saveButton).toBeEnabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
|
|
||||||
// Fill in values for all fields and verify they persist when switching tabs
|
// Fill in values for all fields and verify they persist when switching tabs
|
||||||
await fontSizeField.fill("16");
|
await fontSizeField.fill("16");
|
||||||
@@ -985,6 +985,8 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
tokensUpdateCreateModal.getByLabel(/Font Weight/i);
|
tokensUpdateCreateModal.getByLabel(/Font Weight/i);
|
||||||
const letterSpacingField =
|
const letterSpacingField =
|
||||||
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
|
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
|
||||||
|
const lineHeightField =
|
||||||
|
tokensUpdateCreateModal.getByLabel(/Line Height/i);
|
||||||
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
|
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
|
||||||
const textDecorationField =
|
const textDecorationField =
|
||||||
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
|
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
|
||||||
@@ -995,6 +997,7 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
fontFamily: await fontFamilyField.inputValue(),
|
fontFamily: await fontFamilyField.inputValue(),
|
||||||
fontWeight: await fontWeightField.inputValue(),
|
fontWeight: await fontWeightField.inputValue(),
|
||||||
letterSpacing: await letterSpacingField.inputValue(),
|
letterSpacing: await letterSpacingField.inputValue(),
|
||||||
|
lineHeight: await lineHeightField.inputValue(),
|
||||||
textCase: await textCaseField.inputValue(),
|
textCase: await textCaseField.inputValue(),
|
||||||
textDecoration: await textDecorationField.inputValue(),
|
textDecoration: await textDecorationField.inputValue(),
|
||||||
};
|
};
|
||||||
@@ -1014,6 +1017,9 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
await expect(letterSpacingField).toHaveValue(
|
await expect(letterSpacingField).toHaveValue(
|
||||||
originalValues.letterSpacing,
|
originalValues.letterSpacing,
|
||||||
);
|
);
|
||||||
|
await expect(lineHeightField).toHaveValue(
|
||||||
|
originalValues.lineHeight,
|
||||||
|
);
|
||||||
await expect(textCaseField).toHaveValue(originalValues.textCase);
|
await expect(textCaseField).toHaveValue(originalValues.textCase);
|
||||||
await expect(textDecorationField).toHaveValue(
|
await expect(textDecorationField).toHaveValue(
|
||||||
originalValues.textDecoration,
|
originalValues.textDecoration,
|
||||||
|
|||||||
@@ -225,6 +225,34 @@
|
|||||||
:else
|
:else
|
||||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value)]})))
|
{: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
|
(defn- parse-sd-token-font-family-value
|
||||||
[value]
|
[value]
|
||||||
(let [missing-references (seq (some ctob/find-token-value-references value))]
|
(let [missing-references (seq (some ctob/find-token-value-references value))]
|
||||||
@@ -247,6 +275,8 @@
|
|||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn- parse-composite-typography-value
|
(defn- parse-composite-typography-value
|
||||||
|
"Parses composite typography `value` map.
|
||||||
|
Processes the `:line-height` based on the `:font-size` value in the map."
|
||||||
[value]
|
[value]
|
||||||
(let [missing-references
|
(let [missing-references
|
||||||
(when (string? value)
|
(when (string? value)
|
||||||
@@ -261,14 +291,34 @@
|
|||||||
|
|
||||||
:else
|
:else
|
||||||
(let [converted (js->clj value :keywordize-keys true)
|
(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
|
valid-typography (reduce
|
||||||
(fn [acc [k v]]
|
(fn [acc [k v]]
|
||||||
(let [{:keys [errors value]} (parse-atomic-typography-value k v)]
|
(let [{:keys [errors value]} (parse-atomic-typography-value k v)]
|
||||||
(if (seq errors)
|
(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)))))
|
(assoc-in acc [:value k] (or value v)))))
|
||||||
{:value {}}
|
{: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))))
|
valid-typography))))
|
||||||
|
|
||||||
(defn collect-typography-errors [token]
|
(defn collect-typography-errors [token]
|
||||||
|
|||||||
@@ -434,7 +434,8 @@
|
|||||||
:font-weight update-font-weight
|
:font-weight update-font-weight
|
||||||
:letter-spacing update-letter-spacing
|
:letter-spacing update-letter-spacing
|
||||||
:text-case update-text-case
|
:text-case update-text-case
|
||||||
:text-decoration update-text-decoration}
|
:text-decoration update-text-decoration
|
||||||
|
:line-height update-line-height}
|
||||||
value
|
value
|
||||||
[shape-ids attributes page-id])))))
|
[shape-ids attributes page-id])))))
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,10 @@
|
|||||||
{:error/code :error.style-dictionary/invalid-token-value-typography
|
{:error/code :error.style-dictionary/invalid-token-value-typography
|
||||||
:error/fn #(tr "workspace.tokens.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/unknown
|
||||||
{:error/code :error/unknown
|
{:error/code :error/unknown
|
||||||
:error/fn #(tr "labels.unknown-error")}})
|
:error/fn #(tr "labels.unknown-error")}})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:font-size "Font Size"
|
:font-size "Font Size"
|
||||||
:font-family "Font Family"
|
:font-family "Font Family"
|
||||||
:font-weight "Font Weight"
|
:font-weight "Font Weight"
|
||||||
|
:line-height "Line Height"
|
||||||
:letter-spacing "Letter Spacing"
|
:letter-spacing "Letter Spacing"
|
||||||
:text-case "Text Case"
|
:text-case "Text Case"
|
||||||
:text-decoration "Text Decoration"})
|
:text-decoration "Text Decoration"})
|
||||||
|
|||||||
@@ -912,6 +912,10 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
|
|||||||
{:label "Font Weight"
|
{:label "Font Weight"
|
||||||
:icon i/text-font-weight
|
:icon i/text-font-weight
|
||||||
:placeholder (tr "workspace.tokens.font-weight-value-enter")}
|
: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
|
:letter-spacing
|
||||||
{:label "Letter Spacing"
|
{:label "Letter Spacing"
|
||||||
:icon i/text-letterspacing
|
:icon i/text-letterspacing
|
||||||
|
|||||||
@@ -874,6 +874,7 @@
|
|||||||
:value {:font-size "24px"
|
:value {:font-size "24px"
|
||||||
:font-weight "bold"
|
:font-weight "bold"
|
||||||
:font-family [(:font-id txt/default-text-attrs) "Arial" "sans-serif"]
|
:font-family [(:font-id txt/default-text-attrs) "Arial" "sans-serif"]
|
||||||
|
:line-height "24px"
|
||||||
:letter-spacing "2"
|
:letter-spacing "2"
|
||||||
:text-case "uppercase"
|
:text-case "uppercase"
|
||||||
:text-decoration "underline"}
|
:text-decoration "underline"}
|
||||||
@@ -895,7 +896,6 @@
|
|||||||
(fn [new-state]
|
(fn [new-state]
|
||||||
(let [file' (ths/get-file-from-state new-state)
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
text-1' (cths/get-shape file' :text-1)
|
text-1' (cths/get-shape file' :text-1)
|
||||||
text-1' (def text-1' text-1')
|
|
||||||
style-text-blocks (->> (:content text-1')
|
style-text-blocks (->> (:content text-1')
|
||||||
(txt/content->text+styles)
|
(txt/content->text+styles)
|
||||||
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
||||||
@@ -909,6 +909,7 @@
|
|||||||
|
|
||||||
(t/is (= (:font-size style-text-blocks) "24"))
|
(t/is (= (:font-size style-text-blocks) "24"))
|
||||||
(t/is (= (:font-weight style-text-blocks) "700"))
|
(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 (= (:font-family style-text-blocks) "sourcesanspro"))
|
||||||
(t/is (= (:letter-spacing style-text-blocks) "2"))
|
(t/is (= (:letter-spacing style-text-blocks) "2"))
|
||||||
(t/is (= (:text-transform style-text-blocks) "uppercase"))
|
(t/is (= (:text-transform style-text-blocks) "uppercase"))
|
||||||
|
|||||||
@@ -7466,6 +7466,10 @@ msgstr ""
|
|||||||
msgid "workspace.tokens.font-weight-value-enter"
|
msgid "workspace.tokens.font-weight-value-enter"
|
||||||
msgstr "Enter a value (300, Bold, Regular Italic...) or an {alias}"
|
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
|
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:229
|
||||||
msgid "workspace.tokens.gaps"
|
msgid "workspace.tokens.gaps"
|
||||||
msgstr "Gaps"
|
msgstr "Gaps"
|
||||||
@@ -7662,6 +7666,10 @@ msgstr "Invalid token value. The resolved value is too large: %s"
|
|||||||
msgid "workspace.tokens.opacity-range"
|
msgid "workspace.tokens.opacity-range"
|
||||||
msgstr "Opacity must be between 0 and 100% or 0 and 1 (e.g. 50% or 0.5)."
|
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
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:145
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "workspace.tokens.original-value"
|
msgid "workspace.tokens.original-value"
|
||||||
|
|||||||
@@ -7445,6 +7445,10 @@ msgstr ""
|
|||||||
msgid "workspace.tokens.font-weight-value-enter"
|
msgid "workspace.tokens.font-weight-value-enter"
|
||||||
msgstr "Introduce un valor (300, Bold, Regular Italic...) o un {alias}"
|
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
|
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
|
||||||
#, unused
|
#, unused
|
||||||
msgid "workspace.tokens.generic-error"
|
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"
|
msgid "workspace.tokens.opacity-range"
|
||||||
msgstr "La opacidad debe estar entre 0 y 100% o 0 y 1 (p.e. 50% o 0.5)."
|
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
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:145
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "workspace.tokens.original-value"
|
msgid "workspace.tokens.original-value"
|
||||||
|
|||||||
Reference in New Issue
Block a user