Add ability to remap tokens when renamed ones are referenced by other child tokens (#8035)

* 🎉 Add ability to remap tokens when renamed ones are referenced by other child tokens

Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>

* 🐛 Fix remap skipping tokens with same name in different sets

* 📚 Update CHANGES.md

* 🔧 Fix css styles

---------

Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
Co-authored-by: Akshay Gupta <gravity.akshay@gmail.com>
This commit is contained in:
Andrés Moya 2026-01-08 13:42:06 +01:00 committed by GitHub
parent 795f65632a
commit 2ad42cfd9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1687 additions and 119 deletions

View File

@ -10,6 +10,8 @@
### :sparkles: New features & Enhancements
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
### :bug: Bugs fixed

View File

@ -47,6 +47,18 @@
self-reference? (get token-references token-name)]
self-reference?))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
[value token-name]
(cond
(string? value)
(boolean (some #(= % token-name) (find-token-value-references value)))
(map? value)
(some true? (map #(references-token? % token-name) (vals value)))
(sequential? value)
(some true? (map #(references-token? % token-name) value))
:else false))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -558,3 +570,18 @@
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))

View File

@ -909,7 +909,8 @@ Will return a value that matches this schema:
`:all` All of the nested sets are active
`:partial` Mixed active state of nested sets")
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
(get-all-tokens [_] "all tokens in the lib")
(get-all-tokens [_] "all tokens in the lib, as a sequence")
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
(declare parse-multi-set-dtcg-json)
@ -1306,6 +1307,10 @@ Will return a value that matches this schema:
tokens))
(get-all-tokens [this]
(mapcat #(vals (get-tokens- %))
(get-sets this)))
(get-all-tokens-map [this]
(reduce
(fn [tokens' set]
(into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set)))))

View File

@ -2740,3 +2740,639 @@ test.describe("Tokens: Apply token", () => {
});
});
});
test.describe("Tokens: Remapping Feature", () => {
test.describe("Box Shadow Token Remapping", () => {
test("User renames box shadow token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-shadow");
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base-shadow
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("derived-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{base-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base-shadow token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-shadow",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("foundation-shadow");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await expect(remappingModal).toContainText("1");
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "foundation-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
test("User renames and updates shadow token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-shadow");
let colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{primary-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const cardShadowToken = tokensSidebar.getByRole("button", {
name: "card-shadow",
});
await cardShadowToken.click();
// Rename and update value of base token
const primaryToken = tokensSidebar.getByRole("button", {
name: "primary-shadow",
});
await primaryToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("main-shadow");
// Update the color value
colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#FF0000");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "main-shadow" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "card-shadow" }),
).toBeVisible();
// Verify the shape still has the token applied with the NEW name
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
// Verify the shape still has the shadow applied with the UPDATED color value
// Expand the shadow section to access the color field
const shadowSection = workspacePage.rightSidebar.getByText("Drop shadow");
await expect(shadowSection).toBeVisible();
// Click to expand the shadow options (the menu button)
const shadowMenuButton = workspacePage.rightSidebar
.getByRole("button", { name: "open more options" })
.first();
await shadowMenuButton.click();
// Wait for the advanced options to appear
await page.waitForTimeout(500);
// Verify the color value has updated from #000000 to #FF0000
// Find the color input - it should be a textbox with a 6-character hex value
// We look for all textboxes and find the one with a hex color pattern
const allInputs = await workspacePage.rightSidebar
.locator('input[type="text"]')
.all();
let colorInput = null;
for (const input of allInputs) {
const value = await input.inputValue().catch(() => '');
if (/^[A-Fa-f0-9]{6}$/.test(value)) {
colorInput = input;
break;
}
}
expect(colorInput).not.toBeNull();
const colorValue = await colorInput.inputValue();
expect(colorValue.toUpperCase()).toBe("FF0000");
});
});
test.describe("Typography Token Remapping", () => {
test("User renames typography token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-text");
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("body-text");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{base-text}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-text",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("default-text");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "default-text" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "body-text" }),
).toBeVisible();
});
test("User renames and updates typography token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("body-style");
let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("paragraph-style");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{body-style}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a text shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const paragraphToken = tokensSidebar.getByRole("button", {
name: "paragraph-style",
});
await paragraphToken.click();
// Rename and update value of base token
const bodyToken = tokensSidebar.getByRole("button", {
name: "body-style",
});
await bodyToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("text-base");
// Update the font size value
fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("18");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "text-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "paragraph-style" }),
).toBeVisible();
// Verify the text shape still has the token applied with NEW name and value
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
// Verify the shape shows the updated font size value (18)
// This proves the remapping worked and the value update propagated through the reference
const fontSizeInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
await expect(fontSizeInput).toBeVisible();
await expect(fontSizeInput).toHaveValue("18");
});
});
test.describe("Border Radius Token Remapping", () => {
test("User renames border radius token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-radius");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{base-radius}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-radius",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-radius");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "primary-radius" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "card-radius" }),
).toBeVisible();
});
test("User renames and updates border radius token - referenced token updates", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-sm");
let valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("button-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{radius-sm}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename and update value of base token
const radiusToken = tokensSidebar.getByRole("button", {
name: "radius-sm",
});
await radiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-base");
// Update the value
valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("8");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "radius-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "button-radius" }),
).toBeVisible();
// Verify the referenced token now points to the renamed token
// by opening it and checking the reference
const buttonRadiusToken = tokensSidebar.getByRole("button", {
name: "button-radius",
});
await buttonRadiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const currentValue = tokensUpdateCreateModal.getByLabel("Value");
await expect(currentValue).toHaveValue("{radius-base}");
});
});
});

View File

@ -74,7 +74,7 @@
(when unknown-tokens
(st/emit! (show-unknown-types-warning unknown-tokens)))
(try
(->> (ctob/get-all-tokens tokens-lib)
(->> (ctob/get-all-tokens-map tokens-lib)
(sd/resolve-tokens-with-verbose-errors)
(rx/map (fn [_]
tokens-lib))

View File

@ -0,0 +1,177 @@
;; 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 INC
(ns app.main.data.workspace.tokens.remapping
"Core logic for token remapping functionality"
(:require
[app.common.files.changes-builder :as pcb]
[app.common.files.tokens :as cft]
[app.common.logging :as log]
[app.common.types.container :refer [shapes-seq]]
[app.common.types.file :refer [object-containers-seq]]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dh]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;; Token Reference Scanning
;; ========================
(defn scan-shape-applied-tokens
"Scan a shape for applied token references to a specific token name"
[shape token-name container]
(when-let [applied-tokens (:applied-tokens shape)]
(for [[attribute applied-token-name] applied-tokens
:when (= applied-token-name token-name)]
{:type :applied-token
:shape-id (:id shape)
:attribute attribute
:token-name applied-token-name
:container container})))
(defn scan-token-value-references
"Scan a token value for references to a specific token name (alias), supporting complex token values."
[token token-name]
(letfn [(find-all-token-value-references [token-value]
(cond
(string? token-value)
(filter #(= % token-name) (cto/find-token-value-references token-value))
(map? token-value)
(mapcat find-all-token-value-references (vals token-value))
(sequential? token-value)
(mapcat find-all-token-value-references token-value)
:else
[]))]
(when-let [value (:value token)]
(for [referenced-token-name (find-all-token-value-references value)]
{:type :token-alias
:source-token-name (:name token)
:referenced-token-name referenced-token-name}))))
(defn scan-workspace-token-references
"Scan entire workspace for all token references to a specific token"
[file-data old-token-name]
(let [tokens-lib (:tokens-lib file-data)
containers (object-containers-seq file-data)
;; Scan all shapes for applied token references to the specific token
matching-applied (mapcat (fn [container]
(let [shapes (shapes-seq container)]
(mapcat #(scan-shape-applied-tokens % old-token-name container) shapes)))
containers)
;; Scan tokens library for alias references to the specific token
matching-aliases (if tokens-lib
(let [all-tokens (ctob/get-all-tokens tokens-lib)]
(mapcat #(scan-token-value-references % old-token-name) all-tokens))
[])]
(log/info :hint "token-scan-details"
:token-name old-token-name
:containers-count (count containers)
:total-applied-refs (count matching-applied)
:matching-applied (count matching-applied)
:total-alias-refs (count matching-aliases)
:matching-aliases (count matching-aliases))
{:applied-tokens matching-applied
:token-aliases matching-aliases
:total-references (+ (count matching-applied) (count matching-aliases))}))
;; Token Remapping Core Logic
;; ==========================
(defn remap-tokens
"Main function to remap all token references when a token name changes"
[old-token-name new-token-name]
(ptk/reify ::remap-tokens
ptk/WatchEvent
(watch [_ state _]
(let [file-data (dh/lookup-file-data state)
scan-results (scan-workspace-token-references file-data old-token-name)
tokens-lib (:tokens-lib file-data)
sets (ctob/get-sets tokens-lib)
tokens-with-sets (mapcat (fn [set]
(map (fn [token]
{:token token :set set})
(vals (ctob/get-tokens tokens-lib (ctob/get-id set)))))
sets)
;; Group applied token references by container
refs-by-container (group-by :container (:applied-tokens scan-results))
;; Use apply-token logic to update shapes for both direct and alias references
shape-changes (reduce-kv
(fn [changes container refs]
(let [shape-ids (map :shape-id refs)
;; Find the correct token to apply (new or alias)
token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets)
(some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets))
attributes (set (map :attribute refs))]
(if token
(-> (pcb/with-container changes container)
(pcb/update-shapes shape-ids
(fn [shape]
(update shape :applied-tokens
#(merge % (cft/attributes-map attributes (:token token)))))))
changes)))
(-> (pcb/empty-changes)
(pcb/with-file-data file-data)
(pcb/with-library-data file-data))
refs-by-container)
;; Create changes for updating token alias references
token-changes (reduce
(fn [changes ref]
(let [source-token-name (:source-token-name ref)]
(when-let [{:keys [token set]} (some #(when (= (:name (:token %)) source-token-name) %) tokens-with-sets)]
(let [old-value (:value token)
new-value (cto/update-token-value-references old-value old-token-name new-token-name)]
(pcb/set-token changes (ctob/get-id set) (:id token)
(assoc token :value new-value))))))
shape-changes
(:token-aliases scan-results))]
(log/info :hint "token-remapping"
:old-name old-token-name
:new-name new-token-name
:references-count (:total-references scan-results))
(rx/of (dch/commit-changes token-changes))))))
(defn validate-token-remapping
"Validate that a token remapping operation is safe to perform"
[old-name new-name]
(cond
(str/blank? new-name)
{:valid? false
:error :invalid-name
:message "Token name cannot be empty"}
(= old-name new-name)
{:valid? false
:error :no-change
:message "New name is the same as current name"}
:else
{:valid? true}))
(defn count-token-references
"Count the number of references to a token in the workspace"
[file-data token-name]
(let [scan-results (scan-workspace-token-references file-data token-name)]
(log/info :hint "token-reference-scan"
:token-name token-name
:applied-refs (count (:applied-tokens scan-results))
:alias-refs (count (:token-aliases scan-results))
:total (:total-references scan-results))
(:total-references scan-results)))

View File

@ -36,6 +36,7 @@
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.forms.modals]
[app.main.ui.workspace.tokens.remapping-modal]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]
[app.main.ui.workspace.viewport :refer [viewport*]]

View File

@ -37,6 +37,7 @@
props
(mf/spread-props props {:token-type token-type
:tokens-tree-in-selected-set tokens-tree-in-selected-set
:tokens-in-selected-set tokens-in-selected-set
:token token})
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})

View File

@ -12,19 +12,21 @@
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.helpers :as dh]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.data.workspace.tokens.remapping :as remap]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
[app.main.ui.workspace.tokens.remapping-modal :as remapping-modal]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@ -92,6 +94,7 @@
initial
type
value-subfield
tokens-in-selected-set
input-value-placeholder] :as props}]
(let [make-schema (or make-schema default-make-schema)
@ -121,11 +124,11 @@
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [tokens token]
(mf/with-memo [tokens tokens-in-selected-set token]
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(cond-> tokens
(cond-> (merge tokens tokens-in-selected-set)
(and (:name token) (:value token))
(assoc (:name token) token)))
@ -144,10 +147,6 @@
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-cancel
(mf/use-fn
(fn [e]
@ -191,19 +190,38 @@
:tokens tokens})
(rx/subs!
(fn [valid-token]
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide))))))))]
(let [state @st/state
file-data (dh/lookup-file-data state)
old-name (:name token)
is-rename (and (= action "edit") (not= name old-name))
references-count (remap/count-token-references file-data old-name)]
(if (and is-rename (> references-count 0))
(remapping-modal/show-remapping-modal
{:old-token-name old-name
:new-token-name name
:references-count references-count
:on-confirm (fn []
(st/emit!
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description})
(remap/remap-tokens old-name name)
(dwtp/propagate-workspace-tokens)
(modal/hide!)))
:on-cancel #(modal/hide!)})
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name
:type token-type
:value (:value valid-token)
:description description}))
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description}))
(dwtp/propagate-workspace-tokens)
(modal/hide!))))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
@ -222,12 +240,7 @@
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
:max-length max-input-length
:variant "comfortable"
:auto-focus true}]
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
[:> context-notification*
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
:auto-focus true}]]
[:div {:class (stl/css :input-row)}
(case type

View File

@ -50,10 +50,6 @@
grid-template-columns: 1fr auto auto;
}
.warning-name-change-notification-wrapper {
margin-block-start: var(--sp-l);
}
.delete-btn {
justify-self: start;
}

View File

@ -0,0 +1,106 @@
;; 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 INC
(ns app.main.ui.workspace.tokens.remapping-modal
"Token remapping confirmation modal"
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(defn show-remapping-modal
"Show the token remapping confirmation modal"
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
(let [props {:old-token-name old-token-name
:new-token-name new-token-name
:references-count references-count
:on-confirm on-confirm
:on-cancel on-cancel}]
(st/emit! (modal/show :tokens/remapping-confirmation props))))
(defn hide-remapping-modal
"Hide the token remapping confirmation modal"
[]
(st/emit! (modal/hide)))
;; Remapping Modal Component
(mf/defc token-remapping-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :tokens/remapping-confirmation}
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
(let [remapping-in-progress* (mf/use-state false)
remapping-in-progress? (deref remapping-in-progress*)
;; Remap logic on confirm
on-confirm-remap
(mf/use-fn
(mf/deps on-confirm remapping-in-progress*)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(reset! remapping-in-progress* true)
;; Call shared remapping logic
(let [state @st/state
remap-modal (:remap-modal state)
old-token-name (:old-token-name remap-modal)
new-token-name (:new-token-name remap-modal)]
(st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
(when (fn? on-confirm)
(on-confirm))))
on-cancel-remap
(mf/use-fn
(mf/deps on-cancel)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(modal/hide!)
(when (fn? on-cancel)
(on-cancel))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)
:data-testid "token-remapping-modal"}
[:div {:class (stl/css :modal-header)}
[:> heading* {:level 2
:typography "headline-medium"
:class (stl/css :modal-title)}
(tr "workspace.tokens.remap-token-references")]]
[:div {:class (stl/css :modal-content)}
[:> heading* {:level 3
:typography "title-medium"
:class (stl/css :modal-msg)}
(tr "workspace.tokens.renaming-token-from-to" old-token-name new-token-name)]
[:div {:class (stl/css :modal-scd-msg)}
(if (> references-count 0)
(tr "workspace.tokens.references-found" references-count)
(tr "workspace.tokens.no-references-found"))]
(when remapping-in-progress?
[:> context-notification*
{:level :info
:appearance :ghost}
(tr "workspace.tokens.remapping-in-progress")])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:on-click on-cancel-remap
:type "button"
:variant "secondary"
:disabled remapping-in-progress?}
(tr "labels.cancel")]
[:> button* {:on-click on-confirm-remap
:type "button"
:variant "primary"
:disabled remapping-in-progress?}
(if (> references-count 0)
(tr "workspace.tokens.remap-and-rename")
(tr "workspace.tokens.rename-only"))]]]]]))

View File

@ -0,0 +1,113 @@
// 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 INC
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend .modal-overlay-base;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
height: 100%;
width: 100%;
background-color: var(--overlay-color);
}
.modal-dialog {
@extend .modal-container-base;
width: 100%;
max-width: 32rem;
max-height: unset;
user-select: none;
position: relative;
}
.modal-header {
margin-block-end: var(--sp-xxl);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
@include t.use-typography("headline-medium");
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
@extend .modal-close-btn-base;
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: var(--sp-xs);
}
.modal-content {
@include t.use-typography("body-large");
margin-block-end: var(--sp-xxl);
padding: var(--sp-xxl) 0;
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.modal-footer {
margin-block-start: var(--sp-xxl);
gap: var(--sp-s);
}
.action-buttons {
@extend .modal-action-btns;
gap: var(--sp-s);
}
.cancel-button {
@extend .modal-cancel-btn;
}
.accept-btn {
@extend .modal-accept-btn;
}
.modal-scd-msg,
.modal-msg {
@include t.use-typography("body-large");
color: var(--modal-text-foreground-color);
}
.remap-explanation {
margin: var(--spacing-sm) 0 0 0;
color: var(--color-foreground-secondary);
font-size: var(--fs-12);
line-height: var(--lh-1-4);
}
.no-references-info {
margin-block-end: var(--spacing-md);
}
.no-remap-explanation {
margin: var(--spacing-sm) 0 0 0;
color: var(--color-foreground-secondary);
font-size: var(--fs-12);
line-height: var(--lh-1-4);
}
.progress-info {
margin-block-end: var(--spacing-md);
padding: var(--spacing-sm);
background: var(--color-background-secondary);
border-radius: var(--radius-sm);
}
.modal-actions {
@extend .modal-action-btns;
}

View File

@ -16,6 +16,7 @@
[frontend-tests.tokens.logic.token-actions-test]
[frontend-tests.tokens.logic.token-data-test]
[frontend-tests.tokens.style-dictionary-test]
[frontend-tests.tokens.workspace-tokens-remap-test]
[frontend-tests.util-object-test]
[frontend-tests.util-range-tree-test]
[frontend-tests.util-simple-math-test]
@ -49,4 +50,5 @@
'frontend-tests.util-object-test
'frontend-tests.util-range-tree-test
'frontend-tests.util-simple-math-test
'frontend-tests.tokens.workspace-tokens-remap-test
'frontend-tests.worker-snap-test))

View File

@ -4,13 +4,13 @@
[app.common.geom.point :as gpt]
[app.common.types.color :as clr]
[app.main.data.workspace.libraries :as dwl]
[app.test-helpers.events :as the]
[app.test-helpers.libraries :as thl]
[app.test-helpers.pages :as thp]
[beicon.v2.core :as rx]
[cljs.pprint :refer [pprint]]
[cljs.test :as t :include-macros true]
[clojure.stacktrace :as stk]
[frontend-tests.helpers.events :as the]
[frontend-tests.helpers.libraries :as thl]
[frontend-tests.helpers.pages :as thp]
[linked.core :as lks]
[potok.v2.core :as ptk]))

View File

@ -0,0 +1,127 @@
;; 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 INC
(ns frontend-tests.tokens.logic.token-remapping-test
(:require
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.remapping :as dwtr]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.files :as thf]
[frontend-tests.helpers.ids :as thi]
[frontend-tests.helpers.objects :as tho]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.tokens :as tht]))
(defn setup-file-with-tokens
"Setup a test file with tokens and shapes that use those tokens"
[]
(let [color-token {:name "color.primary"
:value "#FF0000"
:type :color}
alias-token {:name "color.secondary"
:value "{color.primary}"
:type :color}]
(-> (thf/sample-file :file-1 :page-label :page-1)
(tho/add-rect :rect-1)
(tho/add-rect :rect-2)
(assoc-in [:data :tokens-lib]
(-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"}))
(ctob/set-active-themes #{"/Theme A"})
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a)
:name "Set A"))
(ctob/add-token (thi/id :set-a)
(ctob/make-token color-token))
(ctob/add-token (thi/id :set-a)
(ctob/make-token alias-token))))
;; Apply the token to rect-1
(tht/apply-token-to-shape :fill "color.primary" :rect-1))))
(t/deftest test-scan-workspace-token-references
(t/testing "should find applied token references"
(let [file (setup-file-with-tokens)
file-data (:data file)
scan-results (dwtr/scan-workspace-token-references file-data "color.primary")]
(t/is (= 1 (count (:applied-tokens scan-results))))
(t/is (= 1 (count (:token-aliases scan-results))))
(t/is (= 2 (:total-references scan-results)))
;; Check applied token reference
(let [applied-ref (first (:applied-tokens scan-results))]
(t/is (= :applied-token (:type applied-ref)))
(t/is (= "color.primary" (:token-name applied-ref)))
(t/is (= :fill (:attribute applied-ref))))
;; Check alias reference
(let [alias-ref (first (:token-aliases scan-results))]
(t/is (= :token-alias (:type alias-ref)))
(t/is (= "color.secondary" (:source-token-name alias-ref)))
(t/is (= "color.primary" (:referenced-token-name alias-ref)))))))
(t/deftest test-scan-token-value-references
(t/testing "should extract token references from alias values"
(let [token {:name "color.secondary"
:value "{color.primary}"
:type :color}
references (dwtr/scan-token-value-references token)]
(t/is (= 1 (count references)))
(let [ref (first references)]
(t/is (= :token-alias (:type ref)))
(t/is (= "color.secondary" (:source-token-name ref)))
(t/is (= "color.primary" (:referenced-token-name ref))))))
(t/testing "should handle multiple references in one value"
(let [token {:name "spacing.complex"
:value "calc({spacing.base} + {spacing.small})"
:type :spacing}
references (dwtr/scan-token-value-references token)]
(t/is (= 2 (count references)))
(t/is (some #(= "spacing.base" (:referenced-token-name %)) references))
(t/is (some #(= "spacing.small" (:referenced-token-name %)) references)))))
(t/deftest test-update-token-value-references
(t/testing "should update token references in alias values"
(let [old-value "{color.primary}"
new-value (dwtr/update-token-value-references old-value "color.primary" "brand.primary")]
(t/is (= "{brand.primary}" new-value))))
(t/testing "should update multiple references"
(let [old-value "calc({spacing.base} + {spacing.base})"
new-value (dwtr/update-token-value-references old-value "spacing.base" "spacing.foundation")]
(t/is (= "calc({spacing.foundation} + {spacing.foundation})" new-value))))
(t/testing "should not update partial matches"
(let [old-value "{color.primary.light}"
new-value (dwtr/update-token-value-references old-value "color.primary" "brand.primary")]
(t/is (= "{color.primary.light}" new-value)))))
(t/deftest test-count-token-references
(t/testing "should count total references to a token"
(let [file (setup-file-with-tokens)
file-data (:data file)
count (dwtr/count-token-references file-data "color.primary")]
(t/is (= 2 count)))))
(t/deftest test-validate-token-remapping
(t/testing "should validate remapping parameters"
(let [file-data (:data (setup-file-with-tokens))]
(t/testing "empty name should be invalid"
(let [result (dwtr/validate-token-remapping file-data "color.primary" "")]
(t/is (false? (:valid? result)))
(t/is (= :invalid-name (:error result)))))
(t/testing "same name should be invalid"
(let [result (dwtr/validate-token-remapping file-data "color.primary" "color.primary")]
(t/is (false? (:valid? result)))
(t/is (= :no-change (:error result)))))
(t/testing "valid new name should be valid"
(let [result (dwtr/validate-token-remapping file-data "color.primary" "brand.primary")]
(t/is (true? (:valid? result))))))))

View File

@ -39,7 +39,7 @@
(ctob/make-token {:name "borderRadius.largeFn"
:value "{borderRadius.sm} * 200000000"
:type :border-radius}))
(ctob/get-all-tokens))]
(ctob/get-all-tokens-map))]
(-> (sd/resolve-tokens tokens)
(rx/sub!
(fn [resolved-tokens]

View File

@ -0,0 +1,372 @@
;; 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 INC
(ns frontend-tests.tokens.workspace-tokens-remap-test
(:require
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.common.types.text :as txt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.remapping :as remap]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[frontend-tests.tokens.helpers.state :as tohs]
[frontend-tests.tokens.helpers.tokens :as toht]))
(t/use-fixtures :each
{:before cthi/reset-idmap!})
(def token-set-name "remap-test-set")
(def token-theme-name "remap-test-theme")
(defn- make-base-file []
(-> (cthf/sample-file :file-1 :page-label :page-1)
(ctho/add-rect :rect-shape)
(ctho/add-text :text-shape "Sample text")))
(defn- attach-token-set [file tokens]
(let [set-id (cthi/new-id! :token-set)
tokens-lib (reduce
(fn [lib token]
(ctob/add-token lib set-id (ctob/make-token token)))
(-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-id
:name token-set-name))
(ctob/add-theme (ctob/make-token-theme :name token-theme-name
:sets #{token-set-name}))
(ctob/set-active-themes #{(str "/" token-theme-name)}))
tokens)]
(assoc-in file [:data :tokens-lib] tokens-lib)))
(defn- tokens-lib [file]
(get-in file [:data :tokens-lib]))
(defn- find-token-entry [tokens-lib token-name]
(when tokens-lib
(some (fn [set]
(let [set-id (ctob/get-id set)
tokens (ctob/get-tokens tokens-lib set-id)]
(some (fn [[_ token]]
(when (= (:name token) token-name)
{:set-id set-id
:token token}))
tokens)))
(ctob/get-sets tokens-lib))))
(defn- rename-token-in-file [file old-name new-name]
(let [tokens-lib (tokens-lib file)]
(if-let [{:keys [set-id token]} (find-token-entry tokens-lib old-name)]
(let [tokens-lib' (ctob/update-token tokens-lib set-id (:id token) #(assoc % :name new-name))]
(assoc-in file [:data :tokens-lib] tokens-lib'))
file)))
(defn- alias-reference [token-name]
(str "{" token-name "}"))
(defn- alias-token-for-case [{:keys [token]}]
{:name (str (:name token) "-alias")
:type (:type token)
:value (alias-reference (:name token))})
(defn- file-for-case [{:keys [token attribute shape]}]
(let [file (-> (make-base-file)
(attach-token-set [token]))]
(toht/apply-token-to-shape file shape (:name token) #{attribute})))
(def token-remap-cases
[{:case :boolean
:token {:name "boolean-token" :type :boolean :value true}
:attribute :visible
:shape :rect-shape}
{:case :border-radius
:token {:name "border-radius-token" :type :border-radius :value "12"}
:attribute :r1
:shape :rect-shape}
{:case :shadow
:token {:name "shadow-token"
:type :shadow
:value [{:offset-x 0
:offset-y 1
:blur 2
:spread 0
:color "rgba(0,0,0,0.5)"
:inset false}]}
:attribute :shadow
:shape :rect-shape}
{:case :color
:token {:name "color-token" :type :color :value "#ff0000"}
:attribute :fill
:shape :rect-shape}
{:case :dimensions
:token {:name "dimensions-token" :type :dimensions :value "100px"}
:attribute :width
:shape :rect-shape}
{:case :font-family
:token {:name "font-family-token"
:type :font-family
:value ["Arial" "Helvetica"]}
:attribute :font-family
:shape :text-shape}
{:case :font-size
:token {:name "font-size-token" :type :font-size :value "16px"}
:attribute :font-size
:shape :text-shape}
{:case :letter-spacing
:token {:name "letter-spacing-token" :type :letter-spacing :value "1"}
:attribute :letter-spacing
:shape :text-shape}
{:case :number
:token {:name "number-token" :type :number :value "42"}
:attribute :line-height
:shape :text-shape}
{:case :opacity
:token {:name "opacity-token" :type :opacity :value 0.5}
:attribute :opacity
:shape :rect-shape}
{:case :other
:token {:name "other-token" :type :other :value "misc"}
:attribute :custom-data
:shape :rect-shape}
{:case :rotation
:token {:name "rotation-token" :type :rotation :value 45}
:attribute :rotation
:shape :rect-shape}
{:case :sizing
:token {:name "sizing-token" :type :sizing :value "200px"}
:attribute :height
:shape :rect-shape}
{:case :spacing
:token {:name "spacing-token" :type :spacing :value "8"}
:attribute :spacing
:shape :rect-shape}
{:case :string
:token {:name "string-token" :type :string :value "hello"}
:attribute :string-content
:shape :text-shape}
{:case :stroke-width
:token {:name "stroke-width-token" :type :stroke-width :value "2"}
:attribute :stroke-width
:shape :rect-shape}
{:case :text-case
:token {:name "text-case-token" :type :text-case :value "uppercase"}
:attribute :text-transform
:shape :text-shape}
{:case :text-decoration
:token {:name "text-decoration-token" :type :text-decoration :value "underline"}
:attribute :text-decoration
:shape :text-shape}
{:case :font-weight
:token {:name "font-weight-token" :type :font-weight :value "bold"}
:attribute :font-weight
:shape :text-shape}
{:case :typography
:token {:name "typography-token"
:type :typography
:value {:font-size "18px"
:font-family [(:font-id txt/default-text-attrs) "Arial"]
:font-weight "600"
:line-height "20px"
:letter-spacing "1"
:text-case "uppercase"
:text-decoration "underline"}}
:attribute :typography
:shape :text-shape}])
(def token-case-by-type
(into {} (map (juxt :case identity) token-remap-cases)))
(defn- fetch-token-case [case-key]
(or (get token-case-by-type case-key)
(throw (ex-info "Unknown token case" {:case case-key}))))
(defn- run-token-remap-test! [token-case done]
(let [{:keys [token attribute shape]} token-case
old-name (:name token)
new-name (str old-name "-renamed")
file (-> (file-for-case token-case)
(rename-token-in-file old-name new-name))
store (ths/setup-store file)
events [(remap/remap-tokens old-name new-name)]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
shape' (cths/get-shape file' shape)
applied-name (get-in shape' [:applied-tokens attribute])]
(t/is (= new-name applied-name)
(str "attribute " attribute " now references renamed token"))
(t/is (not-any? #(= old-name %) (vals (or (:applied-tokens shape') {})))
"old token name removed from applied tokens"))))))
(defn- run-alias-remap-test! [token-case done]
(let [{:keys [token]} token-case
alias-token (alias-token-for-case token-case)
alias-name (:name alias-token)
old-name (:name token)
new-name (str old-name "-renamed")
file (-> (make-base-file)
(attach-token-set [token alias-token]))
tokens-lib-before (tokens-lib file)
set-id (some-> (ctob/get-set-by-name tokens-lib-before token-set-name) ctob/get-id)
alias-before (ctob/get-token-by-name tokens-lib-before token-set-name alias-name)
alias-id (:id alias-before)
file' (rename-token-in-file file old-name new-name)
store (ths/setup-store file')]
(tohs/run-store-async
store done [(remap/remap-tokens old-name new-name)]
(fn [new-state]
(let [file'' (ths/get-file-from-state new-state)
tokens-lib' (tokens-lib file'')
updated-alias (ctob/get-token tokens-lib' set-id alias-id)]
(t/is (= (alias-reference new-name) (:value updated-alias))
(str "alias for " alias-name " updated to new token name")))))))
(defn- define-remap-test [case-key test-fn]
(t/async done
(test-fn (fetch-token-case case-key) done)))
;; Direct remap tests
(t/deftest remap-boolean-token
(define-remap-test :boolean run-token-remap-test!))
(t/deftest remap-border-radius-token
(define-remap-test :border-radius run-token-remap-test!))
(t/deftest remap-shadow-token
(define-remap-test :shadow run-token-remap-test!))
(t/deftest remap-color-token
(define-remap-test :color run-token-remap-test!))
(t/deftest remap-dimensions-token
(define-remap-test :dimensions run-token-remap-test!))
(t/deftest remap-font-family-token
(define-remap-test :font-family run-token-remap-test!))
(t/deftest remap-font-size-token
(define-remap-test :font-size run-token-remap-test!))
(t/deftest remap-letter-spacing-token
(define-remap-test :letter-spacing run-token-remap-test!))
(t/deftest remap-number-token
(define-remap-test :number run-token-remap-test!))
(t/deftest remap-opacity-token
(define-remap-test :opacity run-token-remap-test!))
(t/deftest remap-other-token
(define-remap-test :other run-token-remap-test!))
(t/deftest remap-rotation-token
(define-remap-test :rotation run-token-remap-test!))
(t/deftest remap-sizing-token
(define-remap-test :sizing run-token-remap-test!))
(t/deftest remap-spacing-token
(define-remap-test :spacing run-token-remap-test!))
(t/deftest remap-string-token
(define-remap-test :string run-token-remap-test!))
(t/deftest remap-stroke-width-token
(define-remap-test :stroke-width run-token-remap-test!))
(t/deftest remap-text-case-token
(define-remap-test :text-case run-token-remap-test!))
(t/deftest remap-text-decoration-token
(define-remap-test :text-decoration run-token-remap-test!))
(t/deftest remap-font-weight-token
(define-remap-test :font-weight run-token-remap-test!))
(t/deftest remap-typography-token
(define-remap-test :typography run-token-remap-test!))
;; Alias remap tests
(t/deftest remap-boolean-alias
(define-remap-test :boolean run-alias-remap-test!))
(t/deftest remap-border-radius-alias
(define-remap-test :border-radius run-alias-remap-test!))
(t/deftest remap-shadow-alias
(define-remap-test :shadow run-alias-remap-test!))
(t/deftest remap-color-alias
(define-remap-test :color run-alias-remap-test!))
(t/deftest remap-dimensions-alias
(define-remap-test :dimensions run-alias-remap-test!))
(t/deftest remap-font-family-alias
(define-remap-test :font-family run-alias-remap-test!))
(t/deftest remap-font-size-alias
(define-remap-test :font-size run-alias-remap-test!))
(t/deftest remap-letter-spacing-alias
(define-remap-test :letter-spacing run-alias-remap-test!))
(t/deftest remap-number-alias
(define-remap-test :number run-alias-remap-test!))
(t/deftest remap-opacity-alias
(define-remap-test :opacity run-alias-remap-test!))
(t/deftest remap-other-alias
(define-remap-test :other run-alias-remap-test!))
(t/deftest remap-rotation-alias
(define-remap-test :rotation run-alias-remap-test!))
(t/deftest remap-sizing-alias
(define-remap-test :sizing run-alias-remap-test!))
(t/deftest remap-spacing-alias
(define-remap-test :spacing run-alias-remap-test!))
(t/deftest remap-string-alias
(define-remap-test :string run-alias-remap-test!))
(t/deftest remap-stroke-width-alias
(define-remap-test :stroke-width run-alias-remap-test!))
(t/deftest remap-text-case-alias
(define-remap-test :text-case run-alias-remap-test!))
(t/deftest remap-text-decoration-alias
(define-remap-test :text-decoration run-alias-remap-test!))
(t/deftest remap-font-weight-alias
(define-remap-test :font-weight run-alias-remap-test!))
(t/deftest remap-typography-alias
(define-remap-test :typography run-alias-remap-test!))

View File

@ -6456,10 +6456,6 @@ msgstr "Nástroje"
msgid "workspace.tokens.value-not-valid"
msgstr "Hodnota není platná"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Přejmenováním tohoto tokenu se přeruší jakýkoli odkaz na jeho starý název."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Položky"

View File

@ -7175,12 +7175,6 @@ msgstr "Der Wert ist nicht gültig"
msgid "workspace.tokens.value-with-percent"
msgstr "Ungültiger Wert: % ist nicht zulässig."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr ""
"Die Umbenennung dieses Tokens macht jeden Verweis auf seinen alten Namen "
"kaputt."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Assets"

View File

@ -7543,6 +7543,42 @@ msgstr "Color"
msgid "workspace.tokens.composite-line-height-needs-font-size"
msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved value."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-token-references"
msgstr "Remap Token References"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.renaming-token-from-to"
msgstr "Renaming token from '%s' to '%s'"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.references-found"
msgstr "%s references found in your design"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-explanation"
msgstr "All references to this token will be automatically updated to use the new name."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.no-references-found"
msgstr "No references found"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.no-remap-needed"
msgstr "This token is not currently used in your design, so no remapping is needed."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remapping-in-progress"
msgstr "Remapping token references..."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-and-rename"
msgstr "Remap & Rename"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.rename-only"
msgstr "Rename"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:78
msgid "workspace.tokens.create-new-theme"
msgstr "Create your first theme now."
@ -8076,7 +8112,7 @@ msgstr "Type '%s' is not supported (%s)\n"
msgid "workspace.tokens.use-reference"
msgstr "Use a reference"
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:131
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:133
msgid "workspace.tokens.value-not-valid"
msgstr "The value is not valid"
@ -8088,10 +8124,6 @@ msgstr "Invalid value: % is not allowed."
msgid "workspace.tokens.value-with-units"
msgstr "Invalid value: Units are not allowed."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Renaming this token will break any reference to its old name."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Assets"

View File

@ -4420,6 +4420,42 @@ msgstr "Mostrar/ocultar recursos"
msgid "shortcuts.toggle-colorpalette"
msgstr "Mostrar/ocultar paleta de colores"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-token-references"
msgstr "Actualizar referencias de token"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.renaming-token-from-to"
msgstr "Renombrando el token de '%s' a '%s'"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.references-found"
msgstr "%s referencias encontradas en tu diseño"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-explanation"
msgstr "Todas las referencias a este token se actualizarán automáticamente para usar el nuevo nombre."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.no-references-found"
msgstr "No se encontraron referencias"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.no-remap-needed"
msgstr "Este token no se utiliza actualmente en tu diseño, por lo que no es necesario actualizar referencias."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remapping-in-progress"
msgstr "Actualizando referencias de token..."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.remap-and-rename"
msgstr "Actualizar referencias y renombrar"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
msgid "workspace.tokens.rename-only"
msgstr "Renombrar"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
msgid "shortcuts.toggle-focus-mode"
msgstr "Mostrar/ocultar focus mode"
@ -7958,10 +7994,6 @@ msgstr "El valor no es válido"
msgid "workspace.tokens.value-with-units"
msgstr "Valor no válido: No se permiten unidades."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Al renombrar este token se romperán las referencias al nombre anterior"
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Recursos"

View File

@ -7893,10 +7893,6 @@ msgstr "Valeur non valide : % n'est pas autorisé."
msgid "workspace.tokens.value-with-units"
msgstr "Valeur non valide : les unités ne sont pas autorisées."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Si vous renommez ce token, toute référence à son ancien nom sera incorrecte."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Ressources"

View File

@ -7810,10 +7810,6 @@ msgstr "ערך שגוי: אסור %."
msgid "workspace.tokens.value-with-units"
msgstr "ערך שגוי: אסור יחידות."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "שינוי שם האסימון הזה יפגע בכל הפניה לשם הישן שלו."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "משאבים"

View File

@ -7300,10 +7300,6 @@ msgstr "मान मान्य नहीं है"
msgid "workspace.tokens.value-with-units"
msgstr "अमान्य मान: इकाइयाँ अनुमति नहीं हैं।"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "इस टोकन का नाम बदलने से इसके पुराने नाम के किसी भी संदर्भ टूट जाएंगे।"
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "एसेट्स"

View File

@ -6477,10 +6477,6 @@ msgstr "Alati"
msgid "workspace.tokens.value-not-valid"
msgstr "Vrijednost nije važeća"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Preimenovanje ovog tokena prekinut će sve reference na njegov stari naziv."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Stavke"

View File

@ -6855,10 +6855,6 @@ msgstr "Peralatan"
msgid "workspace.tokens.value-not-valid"
msgstr "Nilai tidak valid"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Mengubah nama token ini akan merusak referensi nama lamanya."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Aset"

View File

@ -7940,12 +7940,6 @@ msgstr "Valore non valido: % non è consentito."
msgid "workspace.tokens.value-with-units"
msgstr "Valore non valido: le unità non sono consentite."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr ""
"Rinominare questo token interromperà qualsiasi riferimento al suo vecchio "
"nome."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Risorse"

View File

@ -7582,12 +7582,6 @@ msgstr "Vērtība nav derīga"
msgid "workspace.tokens.value-with-units"
msgstr "Nederīga vērtība: mērvienības nav atļautas."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr ""
"Šīs tekstvienības pārdēvēšana salauzīs visas atsauces uz tās iepriekšējo "
"nosaukumu."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Līdzekļi"

View File

@ -7968,12 +7968,6 @@ msgstr "Ongeldige waarde: % is niet toegestaan."
msgid "workspace.tokens.value-with-units"
msgstr "Ongeldige waarde: Eenheden zijn niet toegestaan."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr ""
"Met het wijzigen van de naam van dit token, worden alle verwijzingen naar "
"de oude naam verbroken."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Assets"

View File

@ -5661,10 +5661,6 @@ msgstr "Ferramentas"
msgid "workspace.tokens.value-not-valid"
msgstr "O valor não é válido"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Renomear este token quebrará quaisquer referência para o nome antigo."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Ativos"

View File

@ -7980,10 +7980,6 @@ msgstr "Valoare invalidă: % nu este permis."
msgid "workspace.tokens.value-with-units"
msgstr "Valoare invalidă: Unitățile nu sunt permise."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Redenumirea acestui token va distruge orice referință la numele său vechi."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Obiecte"

View File

@ -7928,10 +7928,6 @@ msgstr "Ogiltigt värde: % är inte tillåtet."
msgid "workspace.tokens.value-with-units"
msgstr "Ogiltigt värde: Enheter är ej tillåtna."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Om du byter namn på denna token bryts alla referenser till dess gamla namn."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Tillgångar"

View File

@ -7935,12 +7935,6 @@ msgstr "Geçersiz değer: % izin verilmiyor."
msgid "workspace.tokens.value-with-units"
msgstr "Geçersiz değer: Birimlere izin verilmiyor."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr ""
"Bu tokenin adını değiştirmek, eski adına yapılan tüm referansları "
"bozacaktır."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Varlıklar"

View File

@ -7437,10 +7437,6 @@ msgstr "Значення не є дійсним"
msgid "workspace.tokens.value-with-units"
msgstr "Помилкове значення: Одиниці не дозволені."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "Якщо перейменувати токен, посилання на старе імʼя буде розірвано."
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "Ресурси"

View File

@ -6295,10 +6295,6 @@ msgstr "工具"
msgid "workspace.tokens.value-not-valid"
msgstr "該值無效"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:181, src/app/main/ui/workspace/tokens/management/create/form.cljs:602
msgid "workspace.tokens.warning-name-change"
msgstr "重新命名此權杖(token)將會中斷對其舊名稱的任何參照。"
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:146
msgid "workspace.toolbar.assets"
msgstr "資源"