mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🎉 Add detach to typography and shadow tokens (#11969)
* 🎉 Add detach to typography tokens * 🎉 Add detach to shadow tokens * 🎉 Add tests
This commit is contained in:
parent
7c039231f7
commit
b06a43404b
194
frontend/playwright/ui/specs/tokens/detach-reference.spec.js
Normal file
194
frontend/playwright/ui/specs/tokens/detach-reference.spec.js
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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))))))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
{})]
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)))))
|
||||
@ -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 (,)"
|
||||
|
||||
|
||||
@ -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 (,)"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user