diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index 7d6cbec6bd..8c45c402a9 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -166,18 +166,49 @@ (not (ctob/token-name-path-exists? token-name tokens-tree))) new-tokens))))]]) +(defn token-circular-reference? + "Checks if the given `tokens` map contains a circular reference reachable from + `token-name`. Uses DFS with 3-color marking (:in-progress / :done) to detect + cycles without false positives on diamond dependencies (A->B, A->C, B->C). + Returns the token name that closes the cycle, or nil." + [tokens token-name] + (let [find-refs (fn [v] (when (string? v) (cto/find-token-value-references v))) + state (atom {})] + (letfn [(visit [name] + (let [mark (get @state name)] + (if (= mark :in-progress) + name + (when-not (= mark :done) + (swap! state assoc name :in-progress) + (let [token (get tokens name) + result (when token + (let [refs (find-refs (:value token))] + (some visit refs)))] + (swap! state assoc name :done) + result)))))] + (let [token (get tokens token-name)] + (when token + (let [refs (find-refs (:value token))] + (some visit refs))))))) + (def schema:token-description [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]) (defn make-token-schema - [tokens-tree token-type] + [tokens-tree token-type current-token-path] [:and (sm/merge cto/schema:token-attrs [:map - [:name (make-token-name-schema tokens-tree)] + [:name (make-token-name-schema (-> tokens-tree + (d/dissoc-in current-token-path)))] [:value (make-token-value-schema token-type)] [:description {:optional true} schema:token-description]]) + [:fn {:error/field :value + :error/fn #(tr "errors.tokens.circular-reference")} + (fn [{:keys [name]}] + (when name + (not (token-circular-reference? tokens-tree name))))] [:fn {:error/field :value :error/fn #(tr "errors.tokens.self-reference")} (fn [{:keys [name value]}] diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 5cd9aab425..619bc5e2b8 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -48,32 +48,6 @@ self-reference? (get token-references token-name)] (boolean self-reference?))) -;; TODO: Review this function when tokenscript is implemented. -(defn token-circular-reference? - "Checks if the given `tokens` map contains a circular reference reachable from - `token-name`. Uses DFS with 3-color marking (:in-progress / :done) to detect - cycles without false positives on diamond dependencies (A->B, A->C, B->C). - Returns the token name that closes the cycle, or nil." - [tokens token-name] - (let [find-refs (fn [v] (when (string? v) (find-token-value-references v))) - state (atom {})] - (letfn [(visit [name] - (let [mark (get @state name)] - (if (= mark :in-progress) - name - (when-not (= mark :done) - (swap! state assoc name :in-progress) - (let [token (get tokens name) - result (when token - (let [refs (find-refs (:value token))] - (some visit refs)))] - (swap! state assoc name :done) - result)))))] - (let [token (get tokens token-name)] - (when token - (let [refs (find-refs (:value token))] - (some visit refs))))))) - (defn references-token? "Recursively check if a value references the token name. Handles strings, maps, and sequences." [value token-name] diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 237e179d5d..cb45e23f75 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -2351,6 +2351,8 @@ test.describe("Tokens tab - edition", () => { // Fill in values for all fields and verify they persist when switching tabs await fontSizeField.fill("16"); + + await page.waitForTimeout(500); await expect(saveButton).toBeEnabled(); const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 966de5cca0..e5716f07be 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -44,6 +44,10 @@ {:error/code :error.token/direct-self-reference :error/fn #(tr "errors.tokens.self-reference")} + :error.token/circular-reference + {:error/code :error.token/circular-reference + :error/fn #(tr "errors.tokens.circular-reference")} + :error.token/invalid-color {:error/code :error.token/invalid-color :error/fn #(str (tr "errors.tokens.invalid-color" %))} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs index 1c9d3cb10f..b208852c12 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs @@ -13,7 +13,7 @@ [rumext.v2 :as mf])) (mf/defc form* - [{:keys [token token-type] :as props}] + [{:keys [token token-type current-token-path] :as props}] (let [initial (mf/with-memo [token-type token] {:type token-type @@ -22,7 +22,7 @@ :description (:description token "") :color-result ""}) - props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 token-type) + props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 token-type current-token-path) (sm/dissoc-key :id) (sm/assoc-key :color-result :string)) :initial initial diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index ce328d5be6..4399040099 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -73,20 +73,18 @@ (dissoc (:name prev-token)) (update (:name token) #(ctob/make-token (merge % prev-token token))))] - (if (cto/token-circular-reference? tokens (:name token)) - (rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)}) - (->> (if (contains? cf/flags :tokenscript) - (rx/of (ts/resolve-tokens tokens)) - (sd/resolve-tokens-interactive tokens)) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) - resolved-value (if (contains? cf/flags :tokenscript) - (ts/tokenscript-symbols->penpot-unit resolved-value) - resolved-value)] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (first errors)}))))))))) + (->> (if (contains? cf/flags :tokenscript) + (rx/of (ts/resolve-tokens tokens)) + (sd/resolve-tokens-interactive tokens)) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) + resolved-value (if (contains? cf/flags :tokenscript) + (ts/tokenscript-symbols->penpot-unit resolved-value) + resolved-value)] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (first errors)})))))))) (defn- hex->color-obj [hex] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs index d730e12618..c69d877aaf 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -50,22 +50,20 @@ (dissoc (:name prev-token)) (update (:name token) #(ctob/make-token (merge % prev-token token))))] - (if (cto/token-circular-reference? tokens (:name token)) - (rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)}) - (->> (if (contains? cf/flags :tokenscript) - (rx/of (ts/resolve-tokens tokens)) - (sd/resolve-tokens-interactive tokens)) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) - resolved-value (if (contains? cf/flags :tokenscript) - (ts/tokenscript-symbols->penpot-unit resolved-value) - resolved-value)] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (if errors - (first errors) - (wte/error-with-value :error/unknown value))}))))))))) + (->> (if (contains? cf/flags :tokenscript) + (rx/of (ts/resolve-tokens tokens)) + (sd/resolve-tokens-interactive tokens)) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) + resolved-value (if (contains? cf/flags :tokenscript) + (ts/tokenscript-symbols->penpot-unit resolved-value) + resolved-value)] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (if errors + (first errors) + (wte/error-with-value :error/unknown value))})))))))) (mf/defc value-combobox* [{:keys [name tokens token token-type empty-to-end ref] :rest props}] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index b89fc0f9ec..cd52d1bcd0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -68,22 +68,20 @@ tokens (update tokens (:name token) #(ctob/make-token (merge % prev-token token)))] - (if (cto/token-circular-reference? tokens (:name token)) - (rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)}) - (->> (if (contains? cf/flags :tokenscript) - (rx/of (ts/resolve-tokens tokens)) - (sd/resolve-tokens-interactive tokens)) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) - resolved-value (if (contains? cf/flags :tokenscript) - (ts/tokenscript-symbols->penpot-unit resolved-value) - resolved-value)] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (if errors - (first errors) - (wte/error-with-value :error/unknown value))}))))))))) + (->> (if (contains? cf/flags :tokenscript) + (rx/of (ts/resolve-tokens tokens)) + (sd/resolve-tokens-interactive tokens)) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) + resolved-value (if (contains? cf/flags :tokenscript) + (ts/tokenscript-symbols->penpot-unit resolved-value) + resolved-value)] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (if errors + (first errors) + (wte/error-with-value :error/unknown value))})))))))) (mf/defc fonts-combobox* [{:keys [token tokens name] :rest props}] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index 22c7fa27fd..7bed674d67 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -171,22 +171,19 @@ ;; Remove previous token when renaming a token (dissoc (:name prev-token)) (update (:name token) #(ctob/make-token (merge % prev-token token))))] - - (if (cto/token-circular-reference? tokens (:name token)) - (rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)}) - (->> tokens - (sd/resolve-tokens-interactive) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) - resolved-value (if (contains? cf/flags :tokenscript) - (ts/tokenscript-symbols->penpot-unit resolved-value) - resolved-value)] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (if errors - (first errors) - (wte/error-with-value :error/unknown value))}))))))))) + (->> tokens + (sd/resolve-tokens-interactive) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token)) + resolved-value (if (contains? cf/flags :tokenscript) + (ts/tokenscript-symbols->penpot-unit resolved-value) + resolved-value)] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (if errors + (first errors) + (wte/error-with-value :error/unknown value))})))))))) (mf/defc input* [{:keys [name tokens token] :rest props}] @@ -328,7 +325,7 @@ :hint-message (:message hint) :hint-type (:type hint)}) props - (if (or extra-error (and touched? error)) + (if (or extra-error (and touched? error) (and (= :line-height input-name) error)) (mf/spread-props props {:hint-type "error" :hint-message (:message (or error extra-error))}) props) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs index d30e72964d..0168121331 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs @@ -29,7 +29,7 @@ (default-validate-token))) (mf/defc form* - [{:keys [token token-type] :rest props}] + [{:keys [token token-type current-token-path] :rest props}] (let [token (mf/with-memo [token] (if token @@ -37,7 +37,7 @@ {:type token-type})) props (mf/spread-props props {:token token :token-type token-type - :make-schema #(-> (cfo/make-token-schema %1 token-type) + :make-schema #(-> (cfo/make-token-schema %1 token-type current-token-path) (sm/dissoc-key :id) ;; The value as edited in the form is a simple stirng. ;; It's converted to vector in the validator. diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index a54ed8142a..336f5d7d4c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.tokens.management.forms.form-container (:require - [app.common.data :as d] [app.common.types.tokens-lib :as ctob] [app.config :as cf] [app.main.refs :as refs] @@ -35,9 +34,8 @@ (ctob/get-token-path token)) tokens-tree-in-selected-set - (mf/with-memo [token-path tokens-in-selected-set] - (-> (ctob/tokens-tree tokens-in-selected-set) - (d/dissoc-in token-path))) + (mf/with-memo [tokens-in-selected-set] + (ctob/tokens-tree tokens-in-selected-set)) props (mf/spread-props props {:token-type token-type @@ -48,7 +46,8 @@ props (if (contains? cf/flags :token-combobox) - (mf/spread-props props {:input-component token.controls/value-combobox*}) + (mf/spread-props props {:input-component token.controls/value-combobox* + :current-token-path token-path}) props) text-case-props @@ -68,5 +67,4 @@ :text-case [:> generic/form* text-case-props] :text-decoration [:> generic/form* text-decoration-props] :font-weight [:> generic/form* font-weight-props] - :border-radius [:> generic/form* props] [:> generic/form* props]))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index 9e60baf431..ae5c038f55 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -77,9 +77,10 @@ initial-errors value-type value-subfield - input-value-placeholder] :as props}] + input-value-placeholder + current-token-path] :as props}] - (let [make-schema (or make-schema #(-> (cfo/make-token-schema % token-type) + (let [make-schema (or make-schema #(-> (cfo/make-token-schema % token-type current-token-path) (sm/dissoc-key :id))) input-component (or input-component token.controls/input*) validate-token (or validator default-validate-token) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs index c69ec04a76..41e0c2ce88 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -260,7 +260,7 @@ ;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields (defn- make-schema - [tokens-tree active-tab] + [current-token-path tokens-tree active-tab] (sm/schema [:and [:map @@ -271,7 +271,8 @@ (sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (ctob/token-name-path-exists? % tokens-tree))]]] + #(not (ctob/token-name-path-exists? % (-> tokens-tree + (d/dissoc-in current-token-path))))]]] [:value [:map @@ -349,7 +350,8 @@ (mf/defc form* [{:keys [token - token-type] :as props}] + token-type + current-token-path] :as props}] (let [token (mf/with-memo [token] (or token @@ -372,7 +374,7 @@ props (mf/spread-props props {:token token :token-type token-type :initial initial - :make-schema make-schema + :make-schema (partial make-schema current-token-path) :value-type :indexed :value-subfield :shadow :input-component tabs-wrapper* diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs index 72b43f1685..62b9a63410 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs @@ -8,9 +8,9 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.files.tokens :as cfo] [app.common.schema :as sm] [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] [app.main.data.workspace.tokens.errors :as wte] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.ds.foundations.assets.icon :as i] @@ -209,19 +209,12 @@ ;; TODO: use cfo/make-schema:token-value and extend it with typography and reference fields (defn- make-schema - [tokens-tree active-tab] + [current-token-path tokens-tree active-tab] (sm/schema [:and [:map - [:name - [:and - [:string {:min 1 :max 255 - :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/schema:token-name assoc - :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (ctob/token-name-path-exists? % tokens-tree))]]] - + [:name (cfo/make-token-name-schema (-> tokens-tree + (d/dissoc-in current-token-path)))] [:value [:map [:font-family {:optional true} [:maybe :string]] @@ -269,7 +262,7 @@ result))]])) (mf/defc form* - [{:keys [token] :as props}] + [{:keys [token current-token-path] :as props}] (let [initial (mf/with-memo [token] (let [value (:value token) @@ -297,7 +290,7 @@ :value processed-value :description (:description token "")})) props (mf/spread-props props {:initial initial - :make-schema make-schema + :make-schema (partial make-schema current-token-path) :token token :validator validate-typography-token :value-type :composite diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs index e7c41cb236..4fb9be3e4f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs @@ -39,22 +39,20 @@ :always (update (:name token) #(ctob/make-token (merge % prev-token token))))] - (if (cto/token-circular-reference? tokens' (:name token)) - (rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]}) - (->> (if (contains? cf/flags :tokenscript) - (rx/of (ts/resolve-tokens tokens')) - (sd/resolve-tokens-interactive tokens')) - (rx/mapcat - (fn [resolved-tokens] - (let [resolved-token (cond-> (get resolved-tokens (:name token)) - (contains? cf/flags :tokenscript) - (update :resolved-value ts/tokenscript-symbols->penpot-unit))] - (cond - (:resolved-value resolved-token) - (rx/of resolved-token) + (->> (if (contains? cf/flags :tokenscript) + (rx/of (ts/resolve-tokens tokens')) + (sd/resolve-tokens-interactive tokens')) + (rx/mapcat + (fn [resolved-tokens] + (let [resolved-token (cond-> (get resolved-tokens (:name token)) + (contains? cf/flags :tokenscript) + (update :resolved-value ts/tokenscript-symbols->penpot-unit))] + (cond + (:resolved-value resolved-token) + (rx/of resolved-token) - :else (rx/throw {:errors (or (seq (:errors resolved-token)) - [(wte/get-error-code :error/unknown-error)])}))))))))) + :else (rx/throw {:errors (or (seq (:errors resolved-token)) + [(wte/get-error-code :error/unknown-error)])})))))))) (defn- validate-token-with [token validators] (if-let [error (some (fn [validate] (validate token)) validators)] diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index a895d06e56..13ea39e6d0 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -317,7 +317,8 @@ (ctob/tokens-tree))] [:tuple (-> (cfo/make-token-schema tokens-tree - (cto/dtcg-token-type->token-type (-> args (first) (get "type")))) + (cto/dtcg-token-type->token-type (-> args (first) (get "type"))) + nil) ;; Don't allow plugins to set the id (sm/dissoc-key :id) ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9a44f421f6..4967fc5688 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1896,6 +1896,10 @@ msgstr "Opacity must be between 0 and 100% or 0 and 1 (e.g. 50% or 0.5)." msgid "errors.tokens.self-reference" msgstr "Token has self reference" +#: src/app/common/files/tokens.cljc +msgid "errors.tokens.circular-reference" +msgstr "Token has circular reference" + #: src/app/main/data/workspace/tokens/errors.cljs:124 msgid "errors.tokens.shadow-blur-range" msgstr "Shadow blur must be greater than or equal to 0." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9270d93d58..0e4face4b4 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1854,6 +1854,10 @@ msgstr "La opacidad debe estar entre 0 y 100% o 0 y 1 (p.e. 50% o 0.5)." msgid "errors.tokens.self-reference" msgstr "El token tiene una autoreferencia" +#: src/app/common/files/tokens.cljc +msgid "errors.tokens.circular-reference" +msgstr "El token tiene una referencia circular" + #: src/app/main/data/workspace/tokens/errors.cljs:124 msgid "errors.tokens.shadow-blur-range" msgstr "El desenfoque (blur) de la sombra debe ser mayor o igual a 0."