mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
✨ Allow references to other typography tokens (#7251)
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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")}})
|
||||||
|
|||||||
32
frontend/src/app/main/data/workspace/tokens/format.cljs
Normal file
32
frontend/src/app/main/data/workspace/tokens/format.cljs
Normal 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)))
|
||||||
@@ -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*
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -31,3 +31,7 @@
|
|||||||
@include textEllipsis;
|
@include textEllipsis;
|
||||||
color: var(--label-color);
|
color: var(--label-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resolved-value {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"))))]
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user