Allow references to other typography tokens (#7251)

This commit is contained in:
Florian Schrödl
2025-09-08 16:45:18 +02:00
committed by GitHub
parent 0e23c9f6ab
commit 8aed47dad3
10 changed files with 305 additions and 67 deletions

View File

@@ -442,3 +442,8 @@
(when (font-weight-values weight) (when (font-weight-values weight)
(cond-> {:weight weight} (cond-> {:weight weight}
italic? (assoc :style "italic"))))) italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))

View File

@@ -972,10 +972,141 @@ test.describe("Tokens: Themes modal", () => {
await fontSizeField.fill(""); await fontSizeField.fill("");
await expect(saveButton).toBeEnabled(); await expect(saveButton).toBeEnabled();
// Fill in values for all fields and verify they persist when switching tabs
await fontSizeField.fill("16");
const fontFamilyField = tokensUpdateCreateModal
.getByLabel(/Font Family/i)
.first();
const fontWeightField =
tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
const textDecorationField =
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
// Capture all values before switching tabs
const originalValues = {
fontSize: await fontSizeField.inputValue(),
fontFamily: await fontFamilyField.inputValue(),
fontWeight: await fontWeightField.inputValue(),
letterSpacing: await letterSpacingField.inputValue(),
textCase: await textCaseField.inputValue(),
textDecoration: await textDecorationField.inputValue(),
};
// Switch to reference tab and back to composite tab
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Reference",
});
await referenceTabButton.click();
const compositeTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Composite",
});
await compositeTabButton.click();
// Verify all values are preserved after switching tabs
await expect(fontSizeField).toHaveValue(originalValues.fontSize);
await expect(fontFamilyField).toHaveValue(originalValues.fontFamily);
await expect(fontWeightField).toHaveValue(originalValues.fontWeight);
await expect(letterSpacingField).toHaveValue(
originalValues.letterSpacing,
);
await expect(textCaseField).toHaveValue(originalValues.textCase);
await expect(textDecorationField).toHaveValue(
originalValues.textDecoration,
);
await saveButton.click(); await saveButton.click();
// Modal should close, token should be visible (with new name) in sidebar // Modal should close, token should be visible (with new name) in sidebar
await expect(tokensUpdateCreateModal).not.toBeVisible(); await expect(tokensUpdateCreateModal).not.toBeVisible();
}); });
test("User cant submit empty typography token or reference", async ({
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("typography.empty");
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
// Insert a value and then delete it
await valueField.fill("1");
await valueField.fill("");
// Submit button should be disabled when field is empty
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeDisabled();
// Switch to reference tab, should not be submittable either
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Reference",
});
await referenceTabButton.click();
await expect(submitButton).toBeDisabled();
});
test("User adds typography token with reference", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const newTokenTitle = "NewReference";
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newTokenTitle);
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Reference",
});
referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{Full}");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
const resolvedValue =
await tokensUpdateCreateModal.getByText("Resolved value:");
await expect(resolvedValue).toBeVisible();
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
await expect(resolvedValue).toContainText("Font Size: 100");
await expect(resolvedValue).toContainText("Font Weight: 300");
await expect(resolvedValue).toContainText("Letter Spacing: 2");
await expect(resolvedValue).toContainText("Text Case: uppercase");
await expect(resolvedValue).toContainText("Text Decoration: underline");
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
const newToken = tokensSidebar.getByRole("button", {
name: newTokenTitle,
});
await expect(newToken).toBeVisible();
});
}); });
}); });

View File

@@ -88,6 +88,10 @@
{:error/code :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/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
: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/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

@@ -0,0 +1,32 @@
(ns app.main.data.workspace.tokens.format
(:require
[cuerdas.core :as str]))
(def category-dictionary
{:stroke-width "Stroke Width"
:spacing "Spacing"
:sizing "Sizing"
:border-radius "Border Radius"
:x "X"
:y "Y"
:font-size "Font Size"
:font-family "Font Family"
:font-weight "Font Weight"
:letter-spacing "Letter Spacing"
:text-case "Text Case"
:text-decoration "Text Decoration"})
(defn format-token-value
"Converts token value of any shape to a string."
[token-value]
(cond
(map? token-value)
(->> (map (fn [[k v]] (str "- " (category-dictionary k) ": " (format-token-value v))) token-value)
(str/join "\n")
(str "\n"))
(sequential? token-value)
(str/join ", " token-value)
:else
(str token-value)))

View File

@@ -102,19 +102,19 @@
;; Validation ------------------------------------------------------------------ ;; Validation ------------------------------------------------------------------
(defn invalidate-empty-value [token-value] (defn check-empty-value [token-value]
(when (empty? (str/trim token-value)) (when (empty? (str/trim token-value))
(wte/get-error-code :error.token/empty-input))) (wte/get-error-code :error.token/empty-input)))
(defn invalidate-token-empty-value [token] (defn check-token-empty-value [token]
(invalidate-empty-value (:value token))) (check-empty-value (:value token)))
(defn invalidate-self-reference [token-name token-value] (defn check-self-reference [token-name token-value]
(when (ctob/token-value-self-reference? token-name token-value) (when (ctob/token-value-self-reference? token-name token-value)
(wte/get-error-code :error.token/direct-self-reference))) (wte/get-error-code :error.token/direct-self-reference)))
(defn invalidate-token-self-reference [token] (defn check-token-self-reference [token]
(invalidate-self-reference (:name token) (:value token))) (check-self-reference (:name token) (:value token)))
(defn validate-resolve-token (defn validate-resolve-token
[token prev-token tokens] [token prev-token tokens]
@@ -146,7 +146,7 @@
(rx/of token))) (rx/of token)))
(def default-validators (def default-validators
[invalidate-token-empty-value invalidate-token-self-reference]) [check-token-empty-value check-self-reference])
(defn default-validate-token (defn default-validate-token
"Validates a token by confirming a list of `validator` predicates and resolving the token using `tokens` with StyleDictionary. "Validates a token by confirming a list of `validator` predicates and resolving the token using `tokens` with StyleDictionary.
@@ -176,14 +176,14 @@
;; Resolving token via StyleDictionary ;; Resolving token via StyleDictionary
(rx/mapcat #(validate-resolve-token % prev-token tokens))))) (rx/mapcat #(validate-resolve-token % prev-token tokens)))))
(defn invalidate-coll-self-reference (defn check-coll-self-reference
"Invalidate a collection of `token-vals` for a self-refernce against `token-name`.," "Invalidate a collection of `token-vals` for a self-refernce against `token-name`.,"
[token-name token-vals] [token-name token-vals]
(when (some #(ctob/token-value-self-reference? token-name %) token-vals) (when (some #(ctob/token-value-self-reference? token-name %) token-vals)
(wte/get-error-code :error.token/direct-self-reference))) (wte/get-error-code :error.token/direct-self-reference)))
(defn invalidate-font-family-token-self-reference [token] (defn check-font-family-token-self-reference [token]
(invalidate-coll-self-reference (:name token) (:value token))) (check-coll-self-reference (:name token) (:value token)))
(defn validate-font-family-token (defn validate-font-family-token
[props] [props]
@@ -192,30 +192,42 @@
(assoc :validators [(fn [token] (assoc :validators [(fn [token]
(when (empty? (:value token)) (when (empty? (:value token))
(wte/get-error-code :error.token/empty-input))) (wte/get-error-code :error.token/empty-input)))
invalidate-font-family-token-self-reference]) check-font-family-token-self-reference])
(default-validate-token))) (default-validate-token)))
(defn invalidate-typography-token-self-reference (defn check-typography-token-self-reference
"Invalidate token when any of the attributes in token value have a self refernce." "Check token when any of the attributes in token value have a self-reference."
[token] [token]
(let [token-name (:name token) (let [token-name (:name token)
token-values (:value token)] token-values (:value token)]
(some (fn [[k v]] (some (fn [[k v]]
(when-let [err (case k (when-let [err (case k
:font-family (invalidate-coll-self-reference token-name v) :font-family (check-coll-self-reference token-name v)
(invalidate-self-reference token-name v))] (check-self-reference token-name v))]
(assoc err :typography-key k))) (assoc err :typography-key k)))
token-values))) token-values)))
(defn check-empty-typography-token [token]
(when (empty? (:value token))
(wte/get-error-code :error.token/empty-input)))
(defn validate-typography-token (defn validate-typography-token
[props] [{:keys [token-value] :as props}]
(-> props (cond
(update :token-value ;; Entering form without a value - show no error just resolve nil
(fn [v] (nil? token-value) (rx/of nil)
(-> (or v {}) ;; Validate refrence string
(d/update-when :font-family #(if (string? %) (ctt/split-font-family %) %))))) (ctt/typography-composite-token-reference? token-value) (default-validate-token props)
(assoc :validators [invalidate-typography-token-self-reference]) ;; Validate composite token
(default-validate-token))) :else
(-> props
(update :token-value
(fn [v]
(-> (or v {})
(d/update-when :font-family #(if (string? %) (ctt/split-font-family %) %)))))
(assoc :validators [check-empty-typography-token
check-typography-token-self-reference])
(default-validate-token))))
(defn use-debonced-resolve-callback (defn use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`. "Resolves a token values using `StyleDictionary`.
@@ -365,6 +377,11 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
token-resolve-result* (mf/use-state (get resolved-tokens (cft/token-identifier token))) token-resolve-result* (mf/use-state (get resolved-tokens (cft/token-identifier token)))
token-resolve-result (deref token-resolve-result*) token-resolve-result (deref token-resolve-result*)
clear-resolve-value
(mf/use-fn
(fn []
(reset! token-resolve-result* nil)))
set-resolve-value set-resolve-value
(mf/use-fn (mf/use-fn
(mf/deps on-value-resolve) (mf/deps on-value-resolve)
@@ -399,7 +416,6 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
(mf/use-fn (mf/use-fn
(mf/deps on-update-value-debounced) (mf/deps on-update-value-debounced)
(fn [next-value] (fn [next-value]
(dom/set-value! (mf/ref-val value-input-ref) next-value)
(mf/set-ref-val! value-ref next-value) (mf/set-ref-val! value-ref next-value)
(on-update-value-debounced next-value))) (on-update-value-debounced next-value)))
@@ -576,7 +592,8 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
:on-update-value on-update-value :on-update-value on-update-value
:on-external-update-value on-external-update-value :on-external-update-value on-external-update-value
:custom-input-token-value-props custom-input-token-value-props :custom-input-token-value-props custom-input-token-value-props
:token-resolve-result token-resolve-result}] :token-resolve-result token-resolve-result
:clear-resolve-value clear-resolve-value}]
[:> input-tokens-value* [:> input-tokens-value*
{:placeholder placeholder {:placeholder placeholder
:label label :label label
@@ -716,6 +733,7 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
color-value (-> (tinycolor/valid-color hex-value) color-value (-> (tinycolor/valid-color hex-value)
(tinycolor/set-alpha (or alpha 1)) (tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))] (tinycolor/->string format))]
(dom/set-value! (mf/ref-val input-ref) color-value)
(on-external-update-value color-value))))] (on-external-update-value color-value))))]
[:* [:*
@@ -897,7 +915,7 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
{:label "Text Decoration" {:label "Text Decoration"
:placeholder (tr "workspace.tokens.text-decoration-value-enter")})) :placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
(mf/defc typography-inputs* (mf/defc typography-value-inputs*
[{:keys [default-value on-blur on-update-value token-resolve-result]}] [{:keys [default-value on-blur on-update-value token-resolve-result]}]
(let [typography-inputs (mf/use-memo typography-inputs) (let [typography-inputs (mf/use-memo typography-inputs)
errors-by-key (sd/collect-typography-errors token-resolve-result)] errors-by-key (sd/collect-typography-errors token-resolve-result)]
@@ -928,12 +946,12 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
(-> (obj/set! e "tokenType" k) (-> (obj/set! e "tokenType" k)
(on-update-value))))] (on-update-value))))]
[:div {:class (stl/css :input-row)} [:div {:key (str k)
:class (stl/css :input-row)}
(case k (case k
:font-family :font-family
[:> font-picker* [:> font-picker*
{:key (str k) {:label label
:label label
:placeholder placeholder :placeholder placeholder
:input-ref input-ref :input-ref input-ref
:default-value (when value (ctt/join-font-family value)) :default-value (when value (ctt/join-font-family value))
@@ -942,24 +960,84 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
:on-external-update-value on-external-update-value :on-external-update-value on-external-update-value
:token-resolve-result (when (seq token-resolve-result) token-resolve-result)}] :token-resolve-result (when (seq token-resolve-result) token-resolve-result)}]
[:> input-tokens-value* [:> input-tokens-value*
{:key (str k) {:label label
:label label
:placeholder placeholder :placeholder placeholder
:default-value value :default-value value
:on-blur on-blur :on-blur on-blur
:on-change on-change :on-change on-change
:token-resolve-result (when (seq token-resolve-result) token-resolve-result)}])]))])) :token-resolve-result (when (seq token-resolve-result) token-resolve-result)}])]))]))
(mf/defc typography-reference-input*
[{:keys [default-value on-blur on-update-value token-resolve-result]}]
[:> input-tokens-value*
{:label "Reference"
:placeholder "Reference"
:default-value (when (ctt/typography-composite-token-reference? default-value) default-value)
:on-blur on-blur
:on-change on-update-value
:token-resolve-result (when (or
(:errors token-resolve-result)
(string? (:value token-resolve-result)))
token-resolve-result)}])
(mf/defc typography-inputs*
[{:keys [default-value on-update-value on-external-update-value on-value-resolve clear-resolve-value] :rest props}]
(let [;; Active Tab State
active-tab* (mf/use-state (if (ctt/typography-composite-token-reference? default-value) :reference :composite))
active-tab (deref active-tab*)
reference-tab-active? (= :reference active-tab)
;; Backup value ref
;; Used to restore the previously entered value when switching tabs
;; Uses ref to not trigger state updates during update
backup-state-ref (mf/use-var
(if reference-tab-active?
{:reference default-value}
{:composite default-value}))
default-value (get @backup-state-ref active-tab)
on-toggle-tab
(mf/use-fn
(mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value)
(fn []
(let [next-tab (if (= active-tab :composite) :reference :composite)]
;; Clear the resolved value so it wont show up before the next-tab value has resolved
(clear-resolve-value)
;; Restore the internal value from backup
(on-external-update-value (get @backup-state-ref next-tab))
(reset! active-tab* next-tab))))
;; Store token value in the backup-state-ref
on-update-reference-value
(mf/use-fn
(mf/deps on-update-value active-tab)
(fn [e]
(if reference-tab-active?
(swap! backup-state-ref assoc :reference (dom/get-target-val e))
(swap! backup-state-ref assoc-in [:composite (obj/get e "tokenType")] (dom/get-target-val e)))
(on-update-value e)))
input-props (mf/spread-props props {:default-value default-value
:on-update-value on-update-reference-value})]
[:div {:class (stl/css :nested-input-row)}
[:button {:on-click on-toggle-tab :type "button"}
(if reference-tab-active? "Composite" "Reference")]
(if reference-tab-active?
[:> typography-reference-input* input-props]
[:> typography-value-inputs* input-props])]))
(mf/defc typography-form* (mf/defc typography-form*
[{:keys [token] :rest props}] [{:keys [token] :rest props}]
(let [on-get-token-value (let [on-get-token-value
(mf/use-callback (mf/use-callback
(fn [e prev-value] (fn [e prev-value]
(let [token-type (obj/get e "tokenType") (let [token-type (obj/get e "tokenType")
input-value (dom/get-target-val e)] input-value (dom/get-target-val e)
(if (empty? input-value) reference-value-input? (not token-type)]
(dissoc prev-value token-type) (cond
(assoc prev-value token-type input-value)))))] reference-value-input? input-value
(empty? input-value) (dissoc prev-value token-type)
:else (assoc prev-value token-type input-value)))))]
[:> form* [:> form*
(mf/spread-props props {:token token (mf/spread-props props {:token token
:custom-input-token-value typography-inputs* :custom-input-token-value typography-inputs*

View File

@@ -9,6 +9,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.data.workspace.tokens.warnings :as wtw] [app.main.data.workspace.tokens.warnings :as wtw]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]] [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]] [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
@@ -41,7 +42,7 @@
(str/join "\n")) (str/join "\n"))
errors (->> (wte/humanize-errors errors) errors (->> (wte/humanize-errors errors)
(str/join "\n")) (str/join "\n"))
:else (tr "workspace.tokens.resolved-value" (or resolved-value result))) :else (tr "workspace.tokens.resolved-value" (dwtf/format-token-value (or resolved-value result))))
type (cond type (cond
empty-message? "hint" empty-message? "hint"
errors "error" errors "error"

View File

@@ -31,3 +31,7 @@
@include textEllipsis; @include textEllipsis;
color: var(--label-color); color: var(--label-color);
} }
.resolved-value {
white-space: pre;
}

View File

@@ -15,6 +15,7 @@
[app.common.types.token :as ctt] [app.common.types.token :as ctt]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.color :as dwtc] [app.main.data.workspace.tokens.color :as dwtc]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.components.color-bullet :refer [color-bullet]] [app.main.ui.components.color-bullet :refer [color-bullet]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
@@ -26,6 +27,7 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Translation dictionaries ;; Translation dictionaries
(def ^:private attribute-dictionary (def ^:private attribute-dictionary
{:rotation "Rotation" {:rotation "Rotation"
:opacity "Opacity" :opacity "Opacity"
@@ -74,20 +76,6 @@
:x :x :x :x
:y :y}) :y :y})
(def ^:private category-dictionary
{:stroke-width "Stroke Width"
:spacing "Spacing"
:sizing "Sizing"
:border-radius "Border Radius"
:x "X"
:y "Y"
:font-size "Font Size"
:font-family "Font Family"
:font-weight "Font Weight"
:letter-spacing "Letter Spacing"
:text-case "Text Case"
:text-decoration "Text Decoration"})
;; Helper functions ;; Helper functions
(defn partially-applied-attr (defn partially-applied-attr
@@ -105,24 +93,11 @@
(str/join "\n" (str/join "\n"
(map (fn [[category values]] (map (fn [[category values]]
(if (#{:x :y} category) (if (#{:x :y} category)
(dm/str "- " (category-dictionary category)) (dm/str "- " (dwtf/category-dictionary category))
(dm/str "- " (category-dictionary category) ": " (dm/str "- " (dwtf/category-dictionary category) ": "
(str/join ", " (map attribute-dictionary values)) "."))) (str/join ", " (map attribute-dictionary values)) ".")))
grouped-values))) grouped-values)))
(defn format-token-value [token-value]
(cond
(map? token-value)
(->> (map (fn [[k v]] (str "- " (category-dictionary k) ": " (format-token-value v))) token-value)
(str/join "\n")
(str "\n"))
(sequential? token-value)
(str/join "," token-value)
:else
(str token-value)))
(defn- generate-tooltip (defn- generate-tooltip
"Generates a tooltip for a given token" "Generates a tooltip for a given token"
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set] [is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
@@ -142,8 +117,8 @@
grouped-values (group-by dimensions-dictionary app-token-keys) grouped-values (group-by dimensions-dictionary app-token-keys)
base-title (dm/str "Token: " name "\n" base-title (dm/str "Token: " name "\n"
(tr "workspace.tokens.original-value" (format-token-value value)) "\n" (tr "workspace.tokens.original-value" (dwtf/format-token-value value)) "\n"
(tr "workspace.tokens.resolved-value" (format-token-value resolved-value)) (tr "workspace.tokens.resolved-value" (dwtf/format-token-value resolved-value))
(when (= (:type token) :number) (when (= (:type token) :number)
(dm/str "\n" (tr "workspace.tokens.more-options"))))] (dm/str "\n" (tr "workspace.tokens.more-options"))))]

View File

@@ -7531,6 +7531,10 @@ msgstr ""
"Invalid font weight value: use numeric values (100-950) or standard names " "Invalid font weight value: use numeric values (100-950) or standard names "
"(thin, light, regular, bold, etc.) optionally followed by 'Italic'" "(thin, light, regular, bold, etc.) optionally followed by 'Italic'"
#: src/app/main/data/workspace/tokens/errors.cljs:91
msgid "workspace.tokens.invalid-token-value-typography"
msgstr "Invalid value: must reference a composite typography token."
#: src/app/main/data/workspace/tokens/errors.cljs:23 #: src/app/main/data/workspace/tokens/errors.cljs:23
msgid "workspace.tokens.invalid-json" msgid "workspace.tokens.invalid-json"
msgstr "Import Error: Invalid token data in JSON." msgstr "Import Error: Invalid token data in JSON."

View File

@@ -7483,6 +7483,10 @@ msgstr ""
"estándar (thin, light, regular, bold, etc.) opcionalmente seguidos de " "estándar (thin, light, regular, bold, etc.) opcionalmente seguidos de "
"'Italic'" "'Italic'"
#: src/app/main/data/workspace/tokens/errors.cljs:91
msgid "workspace.tokens.invalid-token-value-typography"
msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto."
#: src/app/main/data/workspace/tokens/errors.cljs:23 #: src/app/main/data/workspace/tokens/errors.cljs:23
msgid "workspace.tokens.invalid-json" msgid "workspace.tokens.invalid-json"
msgstr "Error al importar: Datos de token no válidos en JSON." msgstr "Error al importar: Datos de token no válidos en JSON."