From b06a43404bd4469ac0c507f9e41c189e9af4b2d3 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 5 Oct 2026 11:32:29 +0200 Subject: [PATCH] :tada: Add detach to typography and shadow tokens (#11969) * :tada: Add detach to typography tokens * :tada: Add detach to shadow tokens * :tada: Add tests --- .../ui/specs/tokens/detach-reference.spec.js | 194 ++++++++++++++++++ .../management/forms/controls/utils.cljs | 27 +++ .../tokens/management/forms/shadow.cljs | 109 +++++++--- .../tokens/management/forms/shadow.scss | 9 + .../tokens/management/forms/typography.cljs | 144 +++++++++---- .../tokens/management/forms/typography.scss | 9 + frontend/test/frontend_tests/runner.cljs | 2 + .../tokens/referenced_token_value_test.cljs | 55 +++++ frontend/translations/en.po | 18 +- frontend/translations/es.po | 18 +- 10 files changed, 500 insertions(+), 85 deletions(-) create mode 100644 frontend/playwright/ui/specs/tokens/detach-reference.spec.js create mode 100644 frontend/test/frontend_tests/tokens/referenced_token_value_test.cljs diff --git a/frontend/playwright/ui/specs/tokens/detach-reference.spec.js b/frontend/playwright/ui/specs/tokens/detach-reference.spec.js new file mode 100644 index 0000000000..b56f14f655 --- /dev/null +++ b/frontend/playwright/ui/specs/tokens/detach-reference.spec.js @@ -0,0 +1,194 @@ +import { test, expect } from "@playwright/test"; +import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; +import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage"; +import { + setupEmptyTokensFileRender, + setupTypographyTokensFileRender, + unfoldTokenType, +} from "./helpers"; + +test.beforeEach(async ({ page }) => { + await WasmWorkspacePage.init(page); + await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json"); +}); + +const openEditModal = async (page, tokensTabPanel, name) => { + await tokensTabPanel + .getByRole("button", { name, exact: true }) + .click({ button: "right" }); + await page.getByText("Edit token").click(); +}; + +// Hovers the reference input and clicks the detach button, which is only +// visible while the reference row is hovered or focused. +const detachReference = async (modal) => { + const referenceField = modal.getByRole("textbox", { name: "Reference" }); + const detachButton = modal.getByRole("button", { name: "Detach reference" }); + + await expect(referenceField).toBeVisible(); + await expect(detachButton).toBeHidden(); + + await referenceField.hover(); + await expect(detachButton).toBeVisible(); + await detachButton.click(); +}; + +test.describe("Tokens - detach reference", () => { + test("User detaches a typography reference and edits the copied values", async ({ + page, + }) => { + const { tokensUpdateCreateModal, tokensSidebar } = + await setupTypographyTokensFileRender(page); + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + const saveButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // Create a typography token that references "Full" from the fixture. + await tokensTabPanel + .getByRole("button", { name: "Add Token: Typography" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + await tokensUpdateCreateModal.getByLabel("Name").fill("detached"); + await tokensUpdateCreateModal.getByTestId("reference-opt").click(); + await tokensUpdateCreateModal + .getByRole("textbox", { name: "Reference" }) + .fill("{Full}"); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + await unfoldTokenType(tokensSidebar, "typography"); + await openEditModal(page, tokensTabPanel, "detached"); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await detachReference(tokensUpdateCreateModal); + + // The values of "Full" are now in the composite tab. + const fontFamilyField = tokensUpdateCreateModal + .getByRole("textbox", { name: "Font family" }) + .first(); + const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font Size", + }); + + await expect(fontFamilyField).toHaveValue("42dot Sans"); + await expect(fontSizeField).toHaveValue("100"); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Font Weight" }), + ).toHaveValue("300"); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Line Height" }), + ).toHaveValue("2"); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Letter Spacing" }), + ).toHaveValue("2"); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Text Case" }), + ).toHaveValue("uppercase"); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Text Decoration" }), + ).toHaveValue("underline"); + + await fontSizeField.fill("24"); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // The detached token keeps its own values. + await openEditModal(page, tokensTabPanel, "detached"); + await expect(fontSizeField).toHaveValue("24"); + await tokensUpdateCreateModal + .getByRole("button", { name: "Cancel" }) + .click(); + + // The referenced token is not modified. + await openEditModal(page, tokensTabPanel, "Full"); + await expect(fontSizeField).toHaveValue("100"); + }); + + test("User detaches a shadow reference and edits the copied values", async ({ + page, + }) => { + const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page, { + flags: ["enable-token-shadow"], + }); + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + const addShadowButton = tokensTabPanel.getByRole("button", { + name: "Add Token: Shadow", + }); + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + const offsetXField = tokensUpdateCreateModal.getByRole("textbox", { + name: "X", + }); + const offsetYField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Y", + }); + const blurField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Blur", + }); + const spreadField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Spread", + }); + const saveButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // Base shadow token. + await addShadowButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + await nameField.fill("base-shadow"); + await colorField.fill("red"); + await offsetXField.fill("2"); + await offsetYField.fill("3"); + await blurField.fill("5"); + await spreadField.fill("1"); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // Shadow token that references the base one. + await addShadowButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + await nameField.fill("alias-shadow"); + await tokensUpdateCreateModal.getByTestId("reference-opt").click(); + await tokensUpdateCreateModal + .getByRole("textbox", { name: "Reference" }) + .fill("{base-shadow}"); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + await unfoldTokenType(tokensTabPanel, "shadow"); + await openEditModal(page, tokensTabPanel, "alias-shadow"); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await detachReference(tokensUpdateCreateModal); + + // The values of "base-shadow" are now in the shadow tab. + await expect(colorField).toHaveValue("red"); + await expect(offsetXField).toHaveValue("2"); + await expect(offsetYField).toHaveValue("3"); + await expect(blurField).toHaveValue("5"); + await expect(spreadField).toHaveValue("1"); + + await offsetXField.fill("10"); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); + + // The detached token keeps its own values. + await openEditModal(page, tokensTabPanel, "alias-shadow"); + await expect(offsetXField).toHaveValue("10"); + await tokensUpdateCreateModal + .getByRole("button", { name: "Cancel" }) + .click(); + + // The referenced token is not modified. + await openEditModal(page, tokensTabPanel, "base-shadow"); + await expect(offsetXField).toHaveValue("2"); + }); +}); diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index 65bd7787c6..75e2317814 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -125,3 +125,30 @@ [error token-name] (let [translated-string (tr "errors.tokens.name-collision" token-name)] (= error translated-string))) + +(defn- referenced-token-name + "Name of the token `reference` points to, when `reference` is exactly + one `{name}` reference." + [reference] + (when (string? reference) + (let [reference (str/trim reference) + names (cto/find-token-value-references reference) + name (first names)] + (when (and (= 1 (count names)) + (= reference (str "{" name "}"))) + name)))) + +(defn referenced-token-value + "Composite value of the `token-type` token that `reference` points to, + following chained references. Nil when the chain does not end in a + `token-type` token with a composite value." + [tokens reference token-type] + (loop [reference reference + visited #{}] + (when-let [name (referenced-token-name reference)] + (let [{:keys [type value]} (get tokens name)] + (when (and (= token-type type) + (not (contains? visited name))) + (if (string? value) + (recur value (conj visited name)) + value)))))) 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 90fef836e7..2d3807b57a 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 @@ -18,6 +18,7 @@ [app.main.ui.forms :as forms] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] [app.main.ui.workspace.tokens.management.forms.validators :refer [check-self-reference default-validate-token]] [app.util.dom :as dom] @@ -75,6 +76,25 @@ :blur "4" :spread "0"}) +(defn- make-default-value + [value] + (cond + (string? value) + {:reference value + :shadow []} + + (vector? value) + {:reference nil + :shadow (mapv (fn [shadow] + (-> shadow + (update :blur #(if (str/blank? %) "0" %)) + (update :spread #(if (str/blank? %) "0" %)))) + value)} + + :else + {:reference nil + :shadow [default-token-shadow]})) + (defn get-subtoken [token index prop value-subfield] (let [value (get-in token [:value value-subfield index prop])] @@ -202,19 +222,66 @@ :show-button (> length 1)}]))) (mf/defc reference-form* - [{:keys [token tokens] :as props}] - [:div {:class (stl/css :input-row-reference)} - [:> token.controls/input-composite* - {:placeholder (tr "workspace.tokens.reference-composite-shadow") - :aria-label (tr "labels.reference") - :icon i/drop-shadow - :name :reference - :token token - :tokens tokens}]]) + [{:keys [token tokens on-detach] :as props}] + (let [form (mf/use-ctx forms/context) + reference (get-in @form [:data :value :reference]) + + detached-value + (mf/with-memo [tokens reference] + (csu/referenced-token-value tokens reference :shadow)) + + on-detach-click + (mf/use-fn + (mf/deps detached-value on-detach) + (fn [event] + (dom/prevent-default event) + (on-detach detached-value)))] + + [:div {:class (stl/css :input-row-reference :reference-row)} + [:> token.controls/input-composite* + {:placeholder (tr "workspace.tokens.reference-composite-shadow") + :aria-label (tr "labels.reference") + :icon i/drop-shadow + :name :reference + :token token + :tokens tokens + :slot-end (when (some? detached-value) + (mf/html + [:> icon-button* {:variant "action" + :type "button" + :class (stl/css :detach-button) + :aria-label (tr "workspace.tokens.detach-reference") + :on-click on-detach-click + :icon i/detach}]))}]])) (mf/defc tabs-wrapper* [{:keys [token tokens tab handle-toggle value-subfield] :rest props}] (let [form (mf/use-ctx forms/context) + + ;; Form value of the detached reference, used so the shadow + ;; inputs show the resolved value of the copied fields. + detached* (mf/use-state nil) + detached (deref detached*) + + composite-token + (mf/with-memo [token detached] + (if (some? detached) + (assoc token :value detached) + token)) + + on-detach + (mf/use-fn + (mf/deps form handle-toggle) + (fn [value] + (let [value (make-default-value value)] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value] value) + (update :errors dissoc :value) + (update :extra-errors dissoc :value)))) + (reset! detached* value) + (handle-toggle "composite")))) + on-add-shadow-block (mf/use-fn (mf/deps value-subfield) @@ -250,13 +317,14 @@ :id "reference-opt"}]]] (if (= tab :composite) - [:> composite-form* {:token token + [:> composite-form* {:token composite-token :tokens tokens :remove-shadow-block remove-shadow-block :value-subfield value-subfield}] [:> reference-form* {:token token - :tokens tokens}])])) + :tokens tokens + :on-detach on-detach}])])) ;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields (defn- make-schema @@ -329,25 +397,6 @@ (or ref-valid? valid-composite-shadow?)))]])) -(defn- make-default-value - [value] - (cond - (string? value) - {:reference value - :shadow []} - - (vector? value) - {:reference nil - :shadow (mapv (fn [shadow] - (-> shadow - (update :blur #(if (str/blank? %) "0" %)) - (update :spread #(if (str/blank? %) "0" %)))) - value)} - - :else - {:reference nil - :shadow [default-token-shadow]})) - (mf/defc form* [{:keys [token token-type diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss index ead3c104fe..f3a7047749 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss @@ -32,6 +32,15 @@ padding-inline-start: var(--sp-m); } +.detach-button { + visibility: hidden; +} + +.reference-row:hover .detach-button, +.reference-row:focus-within .detach-button { + visibility: visible; +} + .title-bar { display: grid; grid-template-columns: 1fr auto auto; 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 d23a9a1543..a34dced97c 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 @@ -13,10 +13,14 @@ [app.common.types.token :as cto] [app.main.data.workspace.tokens.errors :as wte] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.forms :as forms] [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] [app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference check-self-reference default-validate-token]] + [app.util.dom :as dom] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -60,6 +64,25 @@ check-typography-token-self-reference]) (default-validate-token)))) +;; HELPERS + +(def ^:private composite-keys + [:font-family + :font-size + :font-weight + :line-height + :letter-spacing + :text-case + :text-decoration]) + +(defn- composite-value->form-value + "Form data for the composite tab from a typography token map `value`." + [value] + (-> (cond-> value + (:font-family value) + (update :font-family cto/join-font-family)) + (select-keys composite-keys))) + ;; COMPONENTS (mf/defc composite-form* @@ -172,40 +195,88 @@ :tokens tokens}]]])) (mf/defc reference-form* - [{:keys [token tokens] :as props}] - [:div {:class (stl/css :input-row)} - [:> token.controls/input-composite* - {:placeholder (tr "workspace.tokens.reference-composite") - :aria-label (tr "labels.reference") - :icon i/text-typography - :name :reference - :token token - :tokens tokens}]]) + [{:keys [token tokens on-detach] :as props}] + (let [form (mf/use-ctx forms/context) + reference (get-in @form [:data :value :reference]) + + detached-value + (mf/with-memo [tokens reference] + (csu/referenced-token-value tokens reference :typography)) + + on-detach-click + (mf/use-fn + (mf/deps detached-value on-detach) + (fn [event] + (dom/prevent-default event) + (on-detach detached-value)))] + + [:div {:class (stl/css :input-row :reference-row)} + [:> token.controls/input-composite* + {:placeholder (tr "workspace.tokens.reference-composite") + :aria-label (tr "labels.reference") + :icon i/text-typography + :name :reference + :token token + :tokens tokens + :slot-end (when (some? detached-value) + (mf/html + [:> icon-button* {:variant "action" + :type "button" + :class (stl/css :detach-button) + :aria-label (tr "workspace.tokens.detach-reference") + :on-click on-detach-click + :icon i/detach}]))}]])) (mf/defc tabs-wrapper* [{:keys [token tokens tab handle-toggle] :rest props}] - [:* - [:div {:class (stl/css :title-bar)} - [:div {:class (stl/css :title)} (tr "labels.typography")] - [:& radio-buttons {:class (stl/css :listing-options) - :selected (d/name tab) - :on-change handle-toggle - :name "reference-composite-tab"} - [:& radio-button {:icon i/layers - :value "composite" - :title (tr "workspace.tokens.individual-tokens") - :id "composite-opt"}] - [:& radio-button {:icon i/tokens - :value "reference" - :title (tr "workspace.tokens.use-reference") - :id "reference-opt"}]]] - [:div {:class (stl/css :inputs-wrapper)} - (if (= tab :composite) - [:> composite-form* {:token token - :tokens tokens}] + (let [form (mf/use-ctx forms/context) - [:> reference-form* {:token token - :tokens tokens}])]]) + ;; Raw value of the detached reference, used so the composite + ;; inputs show the resolved value of the copied fields. + detached* (mf/use-state nil) + detached (deref detached*) + + composite-token + (mf/with-memo [token detached] + (if (some? detached) + (assoc token :value detached) + token)) + + on-detach + (mf/use-fn + (mf/deps form handle-toggle) + (fn [value] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value] (composite-value->form-value value)) + (update :errors dissoc :value) + (update :extra-errors dissoc :value)))) + (reset! detached* value) + (handle-toggle "composite")))] + + [:* + [:div {:class (stl/css :title-bar)} + [:div {:class (stl/css :title)} (tr "labels.typography")] + [:& radio-buttons {:class (stl/css :listing-options) + :selected (d/name tab) + :on-change handle-toggle + :name "reference-composite-tab"} + [:& radio-button {:icon i/layers + :value "composite" + :title (tr "workspace.tokens.individual-tokens") + :id "composite-opt"}] + [:& radio-button {:icon i/tokens + :value "reference" + :title (tr "workspace.tokens.use-reference") + :id "reference-opt"}]]] + [:div {:class (stl/css :inputs-wrapper)} + (if (= tab :composite) + [:> composite-form* {:token composite-token + :tokens tokens}] + + [:> reference-form* {:token token + :tokens tokens + :on-detach on-detach}])]])) ;; SCHEMA @@ -274,17 +345,8 @@ {:reference value} (map? value) - (let [value (cond-> value - (:font-family value) - (update :font-family cto/join-font-family))] - (select-keys value - [:font-family - :font-size - :font-weight - :line-height - :letter-spacing - :text-case - :text-decoration])) + (composite-value->form-value value) + :else {})] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss index 2aa3e9707c..6fc73253bc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss @@ -23,6 +23,15 @@ gap: var(--sp-xs); } +.detach-button { + visibility: hidden; +} + +.reference-row:hover .detach-button, +.reference-row:focus-within .detach-button { + visibility: visible; +} + .title-bar { display: grid; grid-template-columns: 1fr auto; diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 6c0c436df0..413f40cc08 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -97,6 +97,7 @@ [frontend-tests.tokens.logic.token-data-test] [frontend-tests.tokens.logic.token-remapping-test] [frontend-tests.tokens.logic.tokens-status-test] + [frontend-tests.tokens.referenced-token-value-test] [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-errors-test] [frontend-tests.tokens.workspace-tokens-remap-test] @@ -234,6 +235,7 @@ 'frontend-tests.tokens.logic.token-actions-test 'frontend-tests.tokens.logic.token-data-test 'frontend-tests.tokens.logic.token-remapping-test + 'frontend-tests.tokens.referenced-token-value-test 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-errors-test 'frontend-tests.tokens.logic.tokens-status-test diff --git a/frontend/test/frontend_tests/tokens/referenced_token_value_test.cljs b/frontend/test/frontend_tests/tokens/referenced_token_value_test.cljs new file mode 100644 index 0000000000..d31928b01c --- /dev/null +++ b/frontend/test/frontend_tests/tokens/referenced_token_value_test.cljs @@ -0,0 +1,55 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns frontend-tests.tokens.referenced-token-value-test + (:require + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] + [cljs.test :as t :include-macros true])) + +(def ^:private typography-value + {:font-family ["Inter"] + :font-size "16" + :font-weight "600"}) + +(def ^:private shadow-value + [{:offset-x "4" :offset-y "4" :blur "8" :spread "0" :color "#000000" :inset false} + {:offset-x "0" :offset-y "1" :blur "2" :spread "0" :color "#00000033" :inset true}]) + +(def ^:private tokens + {"base" {:name "base" :type :typography :value typography-value} + "alias" {:name "alias" :type :typography :value "{base}"} + "loop-a" {:name "loop-a" :type :typography :value "{loop-b}"} + "loop-b" {:name "loop-b" :type :typography :value "{loop-a}"} + "font-size" {:name "font-size" :type :font-size :value "16"} + "shadow-base" {:name "shadow-base" :type :shadow :value shadow-value} + "shadow-alias" {:name "shadow-alias" :type :shadow :value "{shadow-base}"}}) + +(t/deftest referenced-typography-value + (t/testing "returns the value of the referenced typography token" + (t/is (= typography-value (csu/referenced-token-value tokens "{base}" :typography))) + (t/is (= typography-value (csu/referenced-token-value tokens " {base} " :typography)))) + + (t/testing "follows chained references" + (t/is (= typography-value (csu/referenced-token-value tokens "{alias}" :typography)))) + + (t/testing "returns nil when the reference cannot be detached" + (t/is (nil? (csu/referenced-token-value tokens "{missing}" :typography))) + (t/is (nil? (csu/referenced-token-value tokens "{font-size}" :typography))) + (t/is (nil? (csu/referenced-token-value tokens "{loop-a}" :typography))) + (t/is (nil? (csu/referenced-token-value tokens "{base} {alias}" :typography))) + (t/is (nil? (csu/referenced-token-value tokens "base" :typography))) + (t/is (nil? (csu/referenced-token-value tokens nil :typography))))) + +(t/deftest referenced-shadow-value + (t/testing "returns the value of the referenced shadow token" + (t/is (= shadow-value (csu/referenced-token-value tokens "{shadow-base}" :shadow)))) + + (t/testing "follows chained references" + (t/is (= shadow-value (csu/referenced-token-value tokens "{shadow-alias}" :shadow)))) + + (t/testing "returns nil for a token of another type" + (t/is (nil? (csu/referenced-token-value tokens "{base}" :shadow))) + (t/is (nil? (csu/referenced-token-value tokens "{shadow-base}" :typography))))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f51ef64459..c38fce16ae 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3403,7 +3403,7 @@ msgstr "Themes" msgid "labels.tutorials" msgstr "Tutorials" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:189 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "labels.typography" msgstr "Typography" @@ -8711,6 +8711,10 @@ msgstr "Delete token" msgid "workspace.tokens.delete-theme-title" msgstr "Delete theme" +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 +msgid "workspace.tokens.detach-reference" +msgstr "Detach reference" + #: src/app/main/ui/workspace/tokens/management/context_menu.cljs:430 msgid "workspace.tokens.duplicate" msgstr "Duplicate token" @@ -8771,7 +8775,7 @@ msgstr "Preview:" msgid "workspace.tokens.export.single-file" msgstr "Single file" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.font-size-value-enter" msgstr "Font size or {alias}" @@ -8855,11 +8859,11 @@ msgstr "Theme" msgid "workspace.tokens.label.theme-placeholder" msgstr "Add a theme (i.e. Light)" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.letter-spacing-value-enter-composite" msgstr "Letter spacing or {alias}" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:145 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.line-height-value-enter" msgstr "Line height (multiplier, px, %) or {alias}" @@ -8951,7 +8955,7 @@ msgstr "Radius" msgid "workspace.tokens.ref-not-valid" msgstr "Reference in {%s} is not valid or is not in any active set." -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.reference-composite" msgstr "Enter a token typography alias" @@ -9105,11 +9109,11 @@ msgstr "Description" msgid "workspace.tokens.token-font-family-select" msgstr "Select font family" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:121 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.token-font-family-value" msgstr "Font family" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:120 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.token-font-family-value-enter" msgstr "Font family or list of fonts separated by comma (,)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a45e2512dd..00ea23e0b5 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3440,7 +3440,7 @@ msgstr "Temas" msgid "labels.tutorials" msgstr "Tutoriales" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:189 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "labels.typography" msgstr "Tipografía" @@ -8783,6 +8783,10 @@ msgstr "Eliminar token" msgid "workspace.tokens.delete-theme-title" msgstr "Borrar theme" +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 +msgid "workspace.tokens.detach-reference" +msgstr "Desvincular referencia" + #: src/app/main/ui/workspace/tokens/management/context_menu.cljs:430 msgid "workspace.tokens.duplicate" msgstr "Duplicar token" @@ -8845,7 +8849,7 @@ msgstr "Previsualizar:" msgid "workspace.tokens.export.single-file" msgstr "fichero único" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.font-size-value-enter" msgstr "Font size o {alias}" @@ -8933,11 +8937,11 @@ msgstr "Tema" msgid "workspace.tokens.label.theme-placeholder" msgstr "Añade un Tema (p. ej. Claro)" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.letter-spacing-value-enter-composite" msgstr "Letter spacing o {alias}" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:145 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.line-height-value-enter" msgstr "Line height (multiplicador, px o %) o {alias}" @@ -9031,7 +9035,7 @@ msgstr "Radio" msgid "workspace.tokens.ref-not-valid" msgstr "La referencia en {%s} no es válida o no se encuentra en ningún set activo." -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.reference-composite" msgstr "Introduce un alias de tipografía de token" @@ -9187,11 +9191,11 @@ msgstr "Descripción" msgid "workspace.tokens.token-font-family-select" msgstr "Seleccionar familia tipográfica" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:121 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.token-font-family-value" msgstr "Familia tipográfica" -#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:120 +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:253, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:226 msgid "workspace.tokens.token-font-family-value-enter" msgstr "Familia tipográfica o lista de fuentes separadas por coma (,)"