mirror of
https://github.com/penpot/penpot.git
synced 2026-03-22 10:23:43 +00:00
@@ -5,6 +5,7 @@
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@@ -15,11 +16,10 @@
|
||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
||||
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
||||
- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
|
||||
|
||||
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
## 2.15.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -147,6 +147,27 @@
|
||||
#(and (some? tokens-tree)
|
||||
(not (ctob/token-name-path-exists? % tokens-tree)))]])
|
||||
|
||||
(defn make-node-token-name-schema
|
||||
"Dynamically generates a schema to check a token node name, adding translated error messages
|
||||
and two additional validations:
|
||||
- Min and max length.
|
||||
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
|
||||
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
|
||||
[active-tokens tokens-tree node]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(-> cto/schema:token-node-name
|
||||
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
(fn [name]
|
||||
(let [current-path (:path node)
|
||||
current-name (:name node)
|
||||
new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)]
|
||||
(and (some? new-tokens)
|
||||
(some (fn [[token-name _]]
|
||||
(not (ctob/token-name-path-exists? token-name tokens-tree)))
|
||||
new-tokens))))]])
|
||||
|
||||
(def schema:token-description
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
|
||||
|
||||
@@ -165,6 +186,11 @@
|
||||
(when (and name value)
|
||||
(not (cto/token-value-self-reference? name value))))]])
|
||||
|
||||
(defn make-node-token-schema
|
||||
[active-tokens tokens-tree node]
|
||||
[:map
|
||||
[:name (make-node-token-name-schema active-tokens tokens-tree node)]])
|
||||
|
||||
(defn convert-dtcg-token
|
||||
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
|
||||
Eg. From this:
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
(def token-name-validation-regex
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||
|
||||
(def token-node-name-validation-regex
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||
|
||||
(def schema:token-name
|
||||
"A token name can contains letters, numbers, underscores the character $ and dots, but
|
||||
not start with $ or end with a dot. The $ character does not have any special meaning,
|
||||
@@ -153,6 +156,14 @@
|
||||
:gen/gen sg/text}
|
||||
token-ref-validation-regex])
|
||||
|
||||
(def schema:token-node-name
|
||||
"A token node name can contains letters, numbers, underscores and the character $, but
|
||||
not start with $ or a dot, or end with a dot. The $ character does not have any special meaning,
|
||||
but dots separate token groups (e.g. color.primary.background)."
|
||||
[:re {:title "TokenNodeName"
|
||||
:gen/gen sg/text}
|
||||
token-node-name-validation-regex])
|
||||
|
||||
(def schema:token-type
|
||||
[::sm/one-of {:decode/json (fn [type]
|
||||
(if (string? type)
|
||||
|
||||
@@ -153,6 +153,18 @@
|
||||
tokens)]
|
||||
(group-by :type tokens')))
|
||||
|
||||
(defn rename-path
|
||||
"Renames a node or token path segment with a new name.
|
||||
If token is provided, it renames a token path, otherwise it renames a node path."
|
||||
([node new-name]
|
||||
(rename-path node nil new-name))
|
||||
([node token new-name]
|
||||
(let [element (if token (:name token) (:path node))
|
||||
split-path (cpn/split-path element :separator ".")
|
||||
updated-split-element-name (assoc split-path (:depth node) new-name)
|
||||
new-element-path (cpn/join-path updated-split-element-name :separator "." :with-spaces? false)]
|
||||
new-element-path)))
|
||||
|
||||
;; === Token Set
|
||||
|
||||
(defprotocol ITokenSet
|
||||
@@ -1490,6 +1502,30 @@ Will return a value that matches this schema:
|
||||
(seq)
|
||||
(boolean)))))
|
||||
|
||||
(defn update-tokens-group
|
||||
"Updates the active tokens path when renaming a group node.
|
||||
- Filters tokens whose path matches the current path prefix
|
||||
- Replaces the token name with the new name
|
||||
- Updates the :path value in the token object
|
||||
|
||||
active-tokens: map of token-name to token-object for all active tokens in the set
|
||||
current-path: the path of the group being renamed, e.g. \"foo.bar\"
|
||||
current-name: the current name of the group being renamed, e.g. \"bar\"
|
||||
new-name: the new name for the group being renamed, e.g. \"baz\""
|
||||
|
||||
[active-tokens current-path current-name new-name]
|
||||
(let [path-prefix (str/replace current-path current-name "")]
|
||||
(mapv (fn [[token-path token-obj]]
|
||||
(if (str/starts-with? token-path path-prefix)
|
||||
(let [new-token-path (str/replace token-path current-name new-name)
|
||||
new-token-obj (-> token-obj
|
||||
(assoc :name new-token-path)
|
||||
(cond-> (:path token-obj)
|
||||
(assoc :path (str/replace (:path token-obj) current-name new-name))))]
|
||||
[new-token-path new-token-obj])
|
||||
[token-path token-obj]))
|
||||
active-tokens)))
|
||||
|
||||
;; === Import / Export from JSON format
|
||||
|
||||
;; Supported formats:
|
||||
|
||||
@@ -108,7 +108,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async waitForIdle() {
|
||||
await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
|
||||
await this.page.evaluate(
|
||||
() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,6 +192,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
this.tokensUpdateCreateModal = page.getByTestId(
|
||||
"token-update-create-modal",
|
||||
);
|
||||
this.tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||
this.tokenThemeUpdateCreateModal = page.getByTestId(
|
||||
"token-theme-update-create-modal",
|
||||
);
|
||||
@@ -224,7 +227,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
|
||||
async #waitForWebSocketReadiness() {
|
||||
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
|
||||
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 })
|
||||
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 });
|
||||
}
|
||||
|
||||
async sendPresenceMessage(fixture) {
|
||||
@@ -309,7 +312,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
await this.page.waitForTimeout(100);
|
||||
const box = await this.viewport.boundingBox();
|
||||
if (!box) throw new Error('Viewport not visible');
|
||||
if (!box) throw new Error("Viewport not visible");
|
||||
|
||||
const startX = box.x + x;
|
||||
const startY = box.y + y;
|
||||
@@ -362,7 +365,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
|
||||
const layersCountBefore = await this.layers.getByTestId("layer-row").count();
|
||||
const layersCountBefore = await this.layers
|
||||
.getByTestId("layer-row")
|
||||
.count();
|
||||
await this.clickAndMove(x1, y1, x2, y2);
|
||||
|
||||
if (initialText) {
|
||||
@@ -385,10 +390,13 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.keyboard.press("ControlOrMeta+C");
|
||||
}
|
||||
// wait for the clipboard to be updated
|
||||
await this.page.waitForFunction(async () => {
|
||||
const content = await navigator.clipboard.readText()
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
await this.page.waitForFunction(
|
||||
async () => {
|
||||
const content = await navigator.clipboard.readText();
|
||||
return content !== "";
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
}
|
||||
|
||||
async cut(kind = "keyboard", locator = undefined) {
|
||||
@@ -399,13 +407,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.keyboard.press("ControlOrMeta+X");
|
||||
}
|
||||
// wait for the clipboard to be updated
|
||||
await this.page.waitForFunction(async () => {
|
||||
const content = await navigator.clipboard.readText()
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
await this.page.waitForFunction(
|
||||
async () => {
|
||||
const content = await navigator.clipboard.readText();
|
||||
return content !== "";
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -914,7 +914,9 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFileRender(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1130,7 +1132,9 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFileRender(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1576,7 +1580,8 @@ test.describe("Tokens - creation", () => {
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill(newTokenTitle);
|
||||
|
||||
const referenceTabButton = tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
const referenceTabButton =
|
||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
await referenceTabButton.click();
|
||||
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
|
||||
@@ -161,6 +161,7 @@ const setupTokensFileRender = async (page, options = {}) => {
|
||||
workspacePage,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokensRenameNodeModal: workspacePage.tokensRenameNodeModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
|
||||
@@ -123,7 +123,7 @@ const createCompositeDerivedToken = async (page, type, name, reference) => {
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
};
|
||||
|
||||
test.describe("Remapping Tokens", () => {
|
||||
test.describe("Remapping a single token", () => {
|
||||
test.describe("Box Shadow Token Remapping", () => {
|
||||
test("User renames box shadow token with alias references", async ({
|
||||
page,
|
||||
@@ -634,3 +634,148 @@ test.describe("Remapping Tokens", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Remapping group of tokens", () => {
|
||||
test("User renames a group - no remap", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
|
||||
// Create multiple tokens in a group
|
||||
await createToken(page, "Color", "dark.primary", "Value", "#000000");
|
||||
await createToken(page, "Color", "dark.secondary", "Value", "#111111");
|
||||
|
||||
// Verify that the node and child token are visible before deletion
|
||||
const darkNode = tokensSidebar.getByRole("button", {
|
||||
name: "dark",
|
||||
exact: true,
|
||||
});
|
||||
const darkNodeToken = tokensSidebar.getByRole("button", {
|
||||
name: "primary",
|
||||
});
|
||||
|
||||
// Select a node and right click on it to open context menu
|
||||
await expect(darkNode).toBeVisible();
|
||||
await expect(darkNodeToken).toBeVisible();
|
||||
await darkNode.click({ button: "right" });
|
||||
|
||||
// select "Rename" from the context menu
|
||||
const renameNodeButton = page.getByRole("button", {
|
||||
name: "Rename",
|
||||
exact: true,
|
||||
});
|
||||
await expect(renameNodeButton).toBeVisible();
|
||||
await renameNodeButton.click();
|
||||
|
||||
// Expect the rename modal to be visible, fill in the new name and submit
|
||||
const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||
await expect(tokenRenameNodeModal).toBeVisible();
|
||||
|
||||
const nameField = tokenRenameNodeModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill("darker");
|
||||
|
||||
const submitButton = tokenRenameNodeModal.getByRole("button", {
|
||||
name: "Rename",
|
||||
});
|
||||
await submitButton.click();
|
||||
|
||||
// Ensure that the remapping modal does not appear
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).not.toBeVisible();
|
||||
|
||||
// Verify that the node has been renamed and tokens are still visible
|
||||
const darkerNode = tokensSidebar.getByRole("button", {
|
||||
name: "darker",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(darkerNode).toBeVisible();
|
||||
});
|
||||
|
||||
test("User renames a group - and remaps", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
const rightSidebar = workspacePage.rightSidebar;
|
||||
|
||||
// Create multiple tokens in a group
|
||||
await createToken(page, "Color", "light.primary", "Value", "#FFFFFF");
|
||||
await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE");
|
||||
|
||||
// Verify that the node and child token are visible before deletion
|
||||
const lightNode = tokensSidebar.getByRole("button", {
|
||||
name: "light",
|
||||
exact: true,
|
||||
});
|
||||
const lightNodeToken = tokensSidebar.getByRole("button", {
|
||||
name: "primary",
|
||||
});
|
||||
|
||||
// Select a node and right click on it to open context menu
|
||||
await expect(lightNode).toBeVisible();
|
||||
await expect(lightNodeToken).toBeVisible();
|
||||
|
||||
// Apply token to a shape to ensure remapping modal appears with applied token reference
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
await page
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Rectangle" })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.getByRole("tab", { name: "Tokens" }).click();
|
||||
const lightPrimaryToken = tokensSidebar.getByRole("button", {
|
||||
name: "primary",
|
||||
});
|
||||
await lightPrimaryToken.click();
|
||||
|
||||
// Right click on the node to rename
|
||||
|
||||
await lightNode.click({ button: "right" });
|
||||
const renameNodeButton = page.getByRole("button", {
|
||||
name: "Rename",
|
||||
exact: true,
|
||||
});
|
||||
await expect(renameNodeButton).toBeVisible();
|
||||
await renameNodeButton.click();
|
||||
|
||||
// Expect the rename modal to be visible, fill in the new name and submit
|
||||
const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
||||
await expect(tokenRenameNodeModal).toBeVisible();
|
||||
|
||||
const nameField = tokenRenameNodeModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill("lighter");
|
||||
|
||||
const submitButton = tokenRenameNodeModal.getByRole("button", {
|
||||
name: "Rename",
|
||||
});
|
||||
await submitButton.click();
|
||||
|
||||
// Ensure that the remapping modal appears and confirm remap
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
// Verify that the node has been renamed and tokens are still visible
|
||||
const lighterNode = tokensSidebar.getByRole("button", {
|
||||
name: "lighter",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(lighterNode).toBeVisible();
|
||||
|
||||
// Verify that the applied token reference has been updated in the right sidebar for the selected shape
|
||||
const fillSection = rightSidebar.getByTestId("fill-section");
|
||||
await expect(fillSection).toBeVisible();
|
||||
|
||||
const tokenReference = fillSection.getByLabel("lighter.primary", {
|
||||
exact: true,
|
||||
});
|
||||
await expect(tokenReference).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -456,6 +456,34 @@
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
|
||||
|
||||
(defn bulk-update-tokens
|
||||
[set-id token-ids type old-path new-path]
|
||||
(dm/assert! (uuid? set-id))
|
||||
(dm/assert! (every? uuid? token-ids))
|
||||
(ptk/reify ::bulk-update-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))
|
||||
data (dsh/lookup-file-data state)
|
||||
changes (reduce (fn [changes token-id]
|
||||
(let [token (-> (get-tokens-lib state)
|
||||
(ctob/get-token (ctob/get-id token-set) token-id))
|
||||
new-name (str/replace (:name token) old-path new-path)
|
||||
token' (->> (merge token {:name new-name})
|
||||
(into {})
|
||||
(ctob/make-token))]
|
||||
(pcb/set-token changes (ctob/get-id token-set) token-id token')))
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data))
|
||||
|
||||
token-ids)]
|
||||
(toggle-token-path (str (name type) "." old-path))
|
||||
(toggle-token-path (str (name type) "." new-path))
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "bulk-update-tokens" :type type}))))))
|
||||
|
||||
(defn delete-token
|
||||
[set-id token-id]
|
||||
(dm/assert! (uuid? set-id))
|
||||
|
||||
@@ -150,6 +150,18 @@
|
||||
|
||||
(rx/of (dch/commit-changes token-changes))))))
|
||||
|
||||
(defn bulk-remap-tokens
|
||||
"Helper function to remap a batch of tokens, used for node renaming"
|
||||
[tokens-in-path new-tokens]
|
||||
(ptk/reify ::bulk-remap-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(map (fn [old-token new-token]
|
||||
(remap-tokens (:name old-token) (:name new-token)))
|
||||
tokens-in-path
|
||||
new-tokens)))))
|
||||
|
||||
(defn validate-token-remapping
|
||||
"Validate that a token remapping operation is safe to perform"
|
||||
[old-name new-name]
|
||||
|
||||
@@ -67,24 +67,38 @@
|
||||
|
||||
(mf/defc form-submit*
|
||||
[{:keys [disabled on-submit] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx context)
|
||||
disabled? (or (and (some? form)
|
||||
(or (not (:valid @form))
|
||||
(seq (:async-errors @form))
|
||||
(seq (:extra-errors @form))))
|
||||
(true? disabled))
|
||||
form-state (when form @form)
|
||||
|
||||
disabled? (mf/use-memo
|
||||
(mf/deps form form-state disabled)
|
||||
(fn []
|
||||
(boolean
|
||||
(or (nil? form)
|
||||
(true? disabled)
|
||||
(not (:valid form-state))
|
||||
(seq (:async-errors form-state))
|
||||
(seq (:extra-errors form-state))))))
|
||||
|
||||
handle-key-down-save
|
||||
(mf/use-fn
|
||||
(mf/deps on-submit form)
|
||||
(mf/deps on-submit form disabled?)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(when (and (or (k/enter? e) (k/space? e)) (not disabled?))
|
||||
(dom/prevent-default e)
|
||||
(on-submit form e))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:disabled disabled?
|
||||
:on-key-down handle-key-down-save
|
||||
:type "submit"})]
|
||||
(mf/spread-props props {:on-key-down handle-key-down-save
|
||||
:type "submit"})
|
||||
|
||||
props
|
||||
(if disabled?
|
||||
(mf/spread-props props {:disabled true
|
||||
:on-key-down handle-key-down-save
|
||||
:type "submit"})
|
||||
props)]
|
||||
|
||||
[:> button* props]))
|
||||
|
||||
|
||||
@@ -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.management.forms.rename-node-modal]
|
||||
[app.main.ui.workspace.tokens.remapping-modal]
|
||||
[app.main.ui.workspace.tokens.settings]
|
||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
(dom/set-attribute! checkbox "indeterminate" true)
|
||||
(dom/remove-attribute! checkbox "indeterminate"))))
|
||||
|
||||
[:div {:class (stl/css :fill-section)}
|
||||
[:div {:class (stl/css :fill-section) :data-testid "fill-section"}
|
||||
[:div {:class (stl/css :fill-title)}
|
||||
[:> title-bar* {:collapsable has-fills?
|
||||
:collapsed (not open?)
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.types.shape.layout :as ctsl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.helpers :as dh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[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.foundations.assets.icon :refer [icon*] :as i]
|
||||
@@ -17,6 +22,7 @@
|
||||
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
|
||||
[app.util.array :as array]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cljs.pprint :as pp]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -124,6 +130,17 @@
|
||||
(mf/with-memo [tokens-by-type]
|
||||
(get-sorted-token-groups tokens-by-type))
|
||||
|
||||
;; Filter tokens by their path and return the tokens
|
||||
filter-tokens-by-path
|
||||
(mf/use-fn
|
||||
(fn [tokens-filtered-by-type node]
|
||||
(->> tokens-filtered-by-type
|
||||
(filter (fn [token]
|
||||
(let [token-path (cpn/split-path (:name token) :separator ".")
|
||||
_ (pp/pprint {:token-path token-path :count (count token-path)})]
|
||||
(and (> (count token-path) 0)
|
||||
(str/starts-with? (:name token) (str (:path node) ".")))))))))
|
||||
|
||||
;; Filter tokens by their path and return their ids
|
||||
filter-tokens-by-path-ids
|
||||
(mf/use-fn
|
||||
@@ -132,7 +149,7 @@
|
||||
(->> selected-token-set-tokens
|
||||
(filter (fn [token]
|
||||
(let [[_ token-value] token]
|
||||
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
|
||||
(and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path "."))))))
|
||||
(mapv (fn [token]
|
||||
(let [[_ token-value] token]
|
||||
(:id token-value)))))))
|
||||
@@ -176,7 +193,88 @@
|
||||
;; Remove from unfolded tree path
|
||||
(if remaining-tokens?
|
||||
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
|
||||
(st/emit! (dwtl/toggle-token-path (name type)))))))]
|
||||
(st/emit! (dwtl/toggle-token-path (name type)))))))
|
||||
|
||||
bulk-rename-tokens-in-path
|
||||
;; Rename tokens in bulk affected by a node rename.
|
||||
(mf/use-fn
|
||||
(mf/deps filter-tokens-by-path-ids selected-token-set-id)
|
||||
(fn [node type new-node-name]
|
||||
(let [old-path (:path node)
|
||||
new-path (ctob/rename-path node new-node-name)
|
||||
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path)))))
|
||||
|
||||
bulk-remap-tokens-in-path
|
||||
;; Remap tokens in bulk affected by a node rename.
|
||||
;; It will update the token names and propagate the changes to the workspace.
|
||||
(mf/use-fn
|
||||
(mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id)
|
||||
(fn [node type new-node-name]
|
||||
(let [old-path (:path node)
|
||||
;; Get tokens in path to remap their names after remapping the node
|
||||
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
||||
tokens-filtered-by-type (get tokens-by-type type)
|
||||
tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node)
|
||||
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)
|
||||
new-node-path (ctob/rename-path node new-node-name)
|
||||
new-tokens (map (fn [token]
|
||||
(let [new-token-path (ctob/rename-path node token new-node-name)]
|
||||
(assoc token :name new-token-path)))
|
||||
tokens-in-path)]
|
||||
(st/emit!
|
||||
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path)
|
||||
(remap/bulk-remap-tokens tokens-in-path new-tokens)
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide)))))
|
||||
|
||||
on-remap-node-warning
|
||||
;; If there are tokens that will be affected by the node rename, we show the remap modal
|
||||
(mf/use-fn
|
||||
(mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path)
|
||||
(fn [node type new-node-name]
|
||||
(let [remap-data {:new-name new-node-name
|
||||
:old-name (:name node)
|
||||
:type "node"}
|
||||
remap-handler #(bulk-remap-tokens-in-path node type new-node-name)
|
||||
rename-handler #(bulk-rename-tokens-in-path node type new-node-name)]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(modal/show :tokens/remapping-confirmation {:remap-data remap-data
|
||||
:on-remap remap-handler
|
||||
:on-rename rename-handler})))))
|
||||
|
||||
on-rename-node
|
||||
;; When user renames a node, we need to check if there are tokens that will be affected by this change.
|
||||
;; If there are, we display the remap modal, otherwise, we rename the tokens directly.
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path)
|
||||
(fn [node type new-node-name]
|
||||
(let [state @st/state
|
||||
file-data (dh/lookup-file-data state)
|
||||
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
|
||||
tokens-filtered-by-type (get tokens-by-type type)
|
||||
tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node)
|
||||
_ (pp/pprint {:tokens-in-current-path tokens-in-current-path})
|
||||
token-references-count (reduce (fn [count token]
|
||||
(+ count (remap/count-token-references file-data (:name token))))
|
||||
0
|
||||
tokens-in-current-path)]
|
||||
(if (> token-references-count 0)
|
||||
(on-remap-node-warning node type new-node-name)
|
||||
(bulk-rename-tokens-in-path node type new-node-name)))))
|
||||
|
||||
open-rename-node-modal
|
||||
;; When user renames a node, we display a form modal
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-tokens on-rename-node)
|
||||
(fn [node type]
|
||||
(let [on-rename-node-handler #(on-rename-node node type %)]
|
||||
(st/emit! (modal/show :tokens/rename-node {:node node
|
||||
:tokens-in-active-set selected-token-set-tokens
|
||||
:on-rename on-rename-node-handler})))))]
|
||||
|
||||
(mf/with-effect [tokens-lib selected-token-set-id]
|
||||
(when (and tokens-lib
|
||||
@@ -190,7 +288,8 @@
|
||||
|
||||
[:*
|
||||
[:& token-context-menu {:on-delete-token delete-token}]
|
||||
[:> token-node-context-menu* {:on-delete-node delete-node}]
|
||||
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
|
||||
:on-delete-node delete-node}]
|
||||
|
||||
[:> selected-set-info* {:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]
|
||||
|
||||
@@ -160,13 +160,13 @@
|
||||
on-remap-token
|
||||
(mf/use-fn
|
||||
(mf/deps token)
|
||||
(fn [valid-token name old-name description]
|
||||
(fn [valid-token new-name old-name description]
|
||||
(st/emit!
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
{:name new-name
|
||||
:value (:value valid-token)
|
||||
:description description})
|
||||
(remap/remap-tokens old-name name)
|
||||
(remap/remap-tokens old-name new-name)
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide!))))
|
||||
|
||||
@@ -203,11 +203,12 @@
|
||||
is-rename (and (= action "edit") (not= name old-name))
|
||||
references-count (remap/count-token-references file-data old-name)
|
||||
on-remap #(on-remap-token valid-token name old-name description)
|
||||
on-rename #(on-rename-token valid-token name description)]
|
||||
on-rename #(on-rename-token valid-token name description)
|
||||
remap-data {:new-name name
|
||||
:old-name old-name
|
||||
:type "token"}]
|
||||
(if (and is-rename (> references-count 0))
|
||||
(st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
|
||||
:new-token-name name
|
||||
:references-count references-count
|
||||
(st/emit! (modal/show :tokens/remapping-confirmation {:remap-data remap-data
|
||||
:on-remap on-remap
|
||||
:on-rename on-rename}))
|
||||
(st/emit!
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
(ns app.main.ui.workspace.tokens.management.forms.rename-node-modal
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc rename-node-form*
|
||||
[{:keys [node active-tokens tokens-tree on-close on-submit]}]
|
||||
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
|
||||
|
||||
schema
|
||||
(mf/with-memo [active-tokens]
|
||||
(make-schema))
|
||||
|
||||
initial (mf/with-memo [node]
|
||||
{:name (:name node)})
|
||||
|
||||
form (fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
on-submit (mf/use-fn
|
||||
(mf/deps form on-submit)
|
||||
(fn []
|
||||
(let [name (get-in @form [:clean-data :name])]
|
||||
(when (and (get-in @form [:touched :name]) (not= name (:name node)))
|
||||
(on-submit name)))))
|
||||
|
||||
is-disabled? (or (not (:valid @form))
|
||||
(not (get-in @form [:touched :name]))
|
||||
(= (get-in @form [:clean-data :name]) (:name node)))
|
||||
|
||||
new-path (mf/with-memo [@form node]
|
||||
(let [new-name (get-in @form [:clean-data :name])
|
||||
path (str (:path node))
|
||||
new-path (str/replace path (:name node) new-name)]
|
||||
new-path))]
|
||||
|
||||
[:*
|
||||
[:> heading* {:level 2
|
||||
:typography "headline-medium"
|
||||
:class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.rename-group")]
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:> fc/form-input* {:id "rename-node"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.token-name")
|
||||
:max-length 255
|
||||
:variant "comfortable"
|
||||
:hint-type "hint"
|
||||
:hint-message (tr "workspace.tokens.rename-group-name-hint" new-path)
|
||||
:auto-focus true}]
|
||||
[:div {:class (stl/css :form-actions)}
|
||||
[:> button* {:variant "secondary"
|
||||
:name "cancel"
|
||||
:on-click on-close} (tr "labels.cancel")]
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:disabled is-disabled?
|
||||
:name "rename"} (tr "labels.rename")]]]]))
|
||||
|
||||
(mf/defc rename-node-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/rename-node}
|
||||
[{:keys [node tokens-in-active-set on-rename]}]
|
||||
|
||||
(let [tokens-tree-in-selected-set
|
||||
(mf/with-memo [tokens-in-active-set node]
|
||||
(-> (ctob/tokens-tree tokens-in-active-set)
|
||||
(d/dissoc-in (:name node))))
|
||||
|
||||
close-modal
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
rename
|
||||
(mf/use-fn
|
||||
(mf/deps on-rename)
|
||||
(fn [new-name]
|
||||
(on-rename new-name)))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps [close-modal])
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(close-modal))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)
|
||||
:on-key-down on-key-down
|
||||
:data-testid "token-rename-node-modal"}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:> icon-button* {:class (stl/css :close-btn)
|
||||
:on-click close-modal
|
||||
:aria-label (tr "labels.close")
|
||||
:variant "ghost"
|
||||
:icon i/close}]
|
||||
[:> rename-node-form* {:node node
|
||||
:active-tokens tokens-in-active-set
|
||||
:tokens-tree tokens-tree-in-selected-set
|
||||
:on-close close-modal
|
||||
:on-submit rename}]]]))
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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 {
|
||||
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
@extend .modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: $sz-6;
|
||||
inset-inline-end: $sz-6;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 32rem;
|
||||
max-block-size: unset;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
(def ^:private schema:token-node-context-menu
|
||||
[:map
|
||||
[:on-rename-node fn?]
|
||||
[:on-delete-node fn?]])
|
||||
|
||||
(def ^:private tokens-node-menu-ref
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
(mf/defc token-node-context-menu*
|
||||
{::mf/schema schema:token-node-context-menu}
|
||||
[{:keys [on-delete-node]}]
|
||||
[{:keys [on-rename-node on-delete-node]}]
|
||||
(let [mdata (mf/deref tokens-node-menu-ref)
|
||||
is-open? (boolean mdata)
|
||||
dropdown-ref (mf/use-ref)
|
||||
@@ -35,7 +36,13 @@
|
||||
dropdown-direction-change* (mf/use-ref 0)
|
||||
top (+ (get-in mdata [:position :y]) 5)
|
||||
left (+ (get-in mdata [:position :x]) 5)
|
||||
|
||||
rename-node (mf/use-fn
|
||||
(mf/deps mdata on-rename-node)
|
||||
(fn []
|
||||
(let [node (get mdata :node)
|
||||
type (get mdata :type)]
|
||||
(when node
|
||||
(on-rename-node node type)))))
|
||||
delete-node (mf/use-fn
|
||||
(mf/deps mdata)
|
||||
(fn []
|
||||
@@ -75,6 +82,11 @@
|
||||
:on-context-menu prevent-default}
|
||||
(when mdata
|
||||
[:ul {:class (stl/css :token-node-context-menu-list)}
|
||||
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
||||
[:button {:class (stl/css :token-node-context-menu-action)
|
||||
:type "button"
|
||||
:on-click rename-node}
|
||||
(tr "labels.rename")]]
|
||||
[:li {:class (stl/css :token-node-context-menu-listitem)}
|
||||
[:button {:class (stl/css :token-node-context-menu-action)
|
||||
:type "button"
|
||||
|
||||
@@ -20,27 +20,41 @@
|
||||
[app.util.keyboard :as kbd]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn hide-remapping-modal
|
||||
(defn- hide-remapping-modal
|
||||
"Hide the token remapping confirmation modal"
|
||||
[]
|
||||
(st/emit! (modal/hide)))
|
||||
|
||||
;; TODO: Uncomment when modal components support schema validation
|
||||
|
||||
;; (def ^:private schema:remap-data
|
||||
;; [:map
|
||||
;; [:old-name :string]
|
||||
;; [:new-name :string]
|
||||
;; [:type [:enum "token" "node"]]])
|
||||
|
||||
;; (def ^:private schema:token-remapping-modal
|
||||
;; [:map
|
||||
;; [:remap-data [:maybe schema:remap-data]]
|
||||
;; [:on-remap {:optional true} [:maybe fn?]]
|
||||
;; [:on-rename {:optional true} [:maybe fn?]]])
|
||||
|
||||
;; Remapping Modal Component
|
||||
(mf/defc token-remapping-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/remapping-confirmation}
|
||||
[{:keys [old-token-name new-token-name on-remap on-rename]}]
|
||||
(let [remap-modal (get @st/state :remap-modal)
|
||||
::mf/register-as :tokens/remapping-confirmation
|
||||
;; TODO: Uncomment when modal components support schema validation
|
||||
;; ::mf/schema schema:token-remapping-modal
|
||||
}
|
||||
[{:keys [remap-data on-remap on-rename]}]
|
||||
(let [old-name (:old-name remap-data)
|
||||
new-name (:new-name remap-data)
|
||||
|
||||
;; Remap logic on confirm
|
||||
confirm-remap
|
||||
(mf/use-fn
|
||||
(mf/deps on-remap remap-modal)
|
||||
(mf/deps on-remap old-name new-name)
|
||||
(fn []
|
||||
;; Call shared remapping logic
|
||||
(let [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-remap)
|
||||
(on-remap))))
|
||||
|
||||
@@ -83,9 +97,13 @@
|
||||
:id "modal-title"
|
||||
:typography "headline-large"
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
|
||||
(if (= (:type remap-data) "token")
|
||||
(tr "workspace.tokens.remap-token-references-title" old-name new-name)
|
||||
(tr "workspace.tokens.remap-node-references-title" old-name new-name))]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
|
||||
(if (= (:type remap-data) "token")
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-token-warning-effects")]
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-node-warning-effects")])
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
@@ -4272,6 +4272,26 @@ msgstr "Pàgines"
|
||||
msgid "workspace.sitemap"
|
||||
msgstr "Mapa del lloc"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-node-references-title"
|
||||
msgstr "Canviar el nom de `%s` a `%s` i remapejar tots els tokens d'aquest grup?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-warning-effects"
|
||||
msgstr "Això canviarà totes les capes i referències que utilitzen el token antic."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
||||
msgid "workspace.tokens.remap-warning-time"
|
||||
msgstr "Aquest procés pot trigar una mica"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group"
|
||||
msgstr "Canviar nom del grup de tokens"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group-name-hint"
|
||||
msgstr "Els teus tokens es renomenaran automàticament a %s.(sufix).(token)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Recursos"
|
||||
|
||||
@@ -8301,11 +8301,19 @@ msgstr "Remap tokens"
|
||||
msgid "workspace.tokens.remap-token-references-title"
|
||||
msgstr "Remap all tokens that use `%s` to `%s`?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88
|
||||
msgid "workspace.tokens.remap-warning-effects"
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-node-references-title"
|
||||
msgstr "Rename `%s` to `%s` and remap all tokens in this group?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-warning-effects"
|
||||
msgstr "This will change all layers and references that use the old token name."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-node-warning-effects"
|
||||
msgstr "This will update all tokens and references that use the old tokens name."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-warning-time"
|
||||
msgstr "This action could take a while."
|
||||
|
||||
@@ -8547,6 +8555,14 @@ msgstr "Renaming this token will break any reference to its old name"
|
||||
msgid "workspace.tokens.error-text-edition"
|
||||
msgstr "Tokens can't be applied while editing text. Select the text layer instead."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group"
|
||||
msgstr "Rename Tokens Group"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group-name-hint"
|
||||
msgstr "Your tokens will automatically be renamed to %s.(suffix).(tokenName)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Assets"
|
||||
|
||||
@@ -8175,11 +8175,13 @@ msgstr "Actualizar tokens"
|
||||
msgid "workspace.tokens.remap-token-references-title"
|
||||
msgstr "¿Actualizar todas las referencias de `%s` a `%s`?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-node-references-title"
|
||||
msgstr "¿Renombrar `%s` to `%s` y remapear todos los tokens de este grupo?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88
|
||||
msgid "workspace.tokens.remap-warning-effects"
|
||||
msgstr ""
|
||||
"Esta acción actualizará todas las capas y referencias que usen el token "
|
||||
"antiguo"
|
||||
msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89
|
||||
msgid "workspace.tokens.remap-warning-time"
|
||||
@@ -8404,6 +8406,14 @@ msgstr ""
|
||||
msgid "workspace.tokens.error-text-edition"
|
||||
msgstr "No se pueden aplicar tokens mientras se edita texto. Seleccione la capa de texto en su lugar."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group"
|
||||
msgstr "Renombrar grupo de tokens"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
|
||||
msgid "workspace.tokens.rename-group-name-hint"
|
||||
msgstr "Tus tokens serán automáticamente renombrados a %s.(sufijo).(token)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Recursos"
|
||||
|
||||
Reference in New Issue
Block a user