diff --git a/CHANGES.md b/CHANGES.md index fabb3ff34e..fdb2d84bd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,6 +58,9 @@ - Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959) - Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865) - Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835) +- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110) +- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167) +- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171) ## 2.12.1 diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 43361039d9..44d5cd7e67 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -124,8 +124,6 @@ (throw (IllegalArgumentException. "invalid email body provided"))) (doseq [[name content] attachments] - - (prn "attachment" name) (let [attachment-part (MimeBodyPart.)] (.setFileName attachment-part ^String name) (.setContent attachment-part ^String content (str "text/plain; charset=" charset)) diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 0e01d81b4b..35e7326eed 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -158,7 +158,7 @@ test.describe("Tokens - creation", () => { const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); await tokensSidebar .getByRole("button", { name: "Add Token: Color" }) @@ -189,7 +189,7 @@ test.describe("Tokens - creation", () => { // 2. Invalid value → disabled + error message await valueField.fill("1"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -197,7 +197,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -207,7 +207,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{color.primary}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -320,7 +320,7 @@ test.describe("Tokens - creation", () => { const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -356,7 +356,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -366,7 +366,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -459,13 +459,13 @@ test.describe("Tokens - creation", () => { test("User creates font weight token", async ({ page }) => { const invalidValueError = - "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'"; + "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -501,7 +501,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -510,7 +510,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -520,7 +520,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -595,13 +595,13 @@ test.describe("Tokens - creation", () => { test("User creates text case token", async ({ page }) => { const invalidValueError = - "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted"; + "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -637,7 +637,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -646,7 +646,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -656,7 +656,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -711,13 +711,13 @@ test.describe("Tokens - creation", () => { test("User creates text decoration token", async ({ page }) => { const invalidValueError = - "Invalid token value: only none, underline and strike-through are accepted"; + "Invalid token value: only none, underline and strike-through are accepted"; const emptyNameError = "Name should be at least 1 character"; const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -755,7 +755,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("red"); const invalidValueErrorNode = - tokensUpdateCreateModal.getByText(invalidValueError); + tokensUpdateCreateModal.getByText(invalidValueError); await expect(invalidValueErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -764,7 +764,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -774,7 +774,7 @@ test.describe("Tokens - creation", () => { await valueField.fill("{my-token}"); const selfRefErrorNode = - tokensUpdateCreateModal.getByText(selfReferenceError); + tokensUpdateCreateModal.getByText(selfReferenceError); await expect(selfRefErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -831,7 +831,7 @@ test.describe("Tokens - creation", () => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] }); + await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -900,7 +900,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -977,9 +977,9 @@ test.describe("Tokens - creation", () => { await nameField.fill("my-token-2"); const referenceToggle = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); const compositeToggle = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await referenceToggle.click(); const referenceInput = tokensUpdateCreateModal.getByPlaceholder( @@ -1012,7 +1012,7 @@ test.describe("Tokens - creation", () => { page, }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); await tokensTabPanel @@ -1038,15 +1038,201 @@ test.describe("Tokens - creation", () => { // Switch to reference tab, should not be submittable either const referenceTabButton = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceTabButton.click(); await expect(submitButton).toBeDisabled(); }); + test("User creates shadow token with negative spread", async ({ page }) => { + const emptyNameError = "Name should be at least 1 character"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]}); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Shadow`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter a value or alias with {alias}", + ), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + const offsetXField = tokensUpdateCreateModal.getByRole("textbox", { + name: "X", + }); + const offsetYField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Y", + }); + const blurField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Blur", + }); + const spreadField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Spread", + }); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Check default values + await expect(offsetXField).toHaveValue("4"); + await expect(offsetYField).toHaveValue("4"); + await expect(blurField).toHaveValue("4"); + await expect(spreadField).toHaveValue("0"); + + // 2. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 3. Invalid color → disabled + error message + await colorField.fill("1"); + + await expect( + tokensUpdateCreateModal.getByText("Invalid color value: 1"), + ).toBeVisible(); + + await expect(submitButton).toBeDisabled(); + + await colorField.fill("{missing-reference}"); + + await expect( + tokensUpdateCreateModal.getByText( + "Missing token references: missing-reference", + ), + ).toBeVisible(); + + // 4. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL FIELDS ------- + // + + // 5. Valid color → resolved + + await colorField.fill("red"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: #ff0000"), + ).toBeVisible(); + const colorSwatch = tokensUpdateCreateModal.getByTestId( + "token-form-color-bullet", + ); + await colorSwatch.click(); + const rampSelector = tokensUpdateCreateModal.getByTestId( + "value-saturation-selector", + ); + await expect(rampSelector).toBeVisible(); + await rampSelector.click({ position: { x: 50, y: 50 } }); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value:"), + ).toBeVisible(); + + const sliderOpacity = tokensUpdateCreateModal.getByTestId("slider-opacity"); + await sliderOpacity.click({ position: { x: 50, y: 0 } }); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Color" }), + ).toHaveValue(/rgba\s*\([^)]*\)/); + + // 6. Valid offset → resolved + await offsetXField.fill("3 + 3"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 6"), + ).toBeVisible(); + + await offsetYField.fill("3 + 7"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 10"), + ).toBeVisible(); + + // 7. Valid blur → resolved + + await blurField.fill("3 + 1"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 4"), + ).toBeVisible(); + + // 8. Valid spread → resolved + + await spreadField.fill("3 - 3"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 0"), + ).toBeVisible(); + + await spreadField.fill("1 - 3"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: -2"), + ).toBeVisible(); + + await nameField.fill("my-token"); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + const compositeToggle = + tokensUpdateCreateModal.getByTestId("composite-opt"); + await referenceToggle.click(); + + const referenceInput = tokensUpdateCreateModal.getByPlaceholder( + "Enter a token shadow alias", + ); + await expect(referenceInput).toBeVisible(); + + await compositeToggle.click(); + await expect(colorField).toBeVisible(); + + await referenceToggle.click(); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); + await referenceField.fill("{my-token}"); + await expect( + tokensUpdateCreateModal.getByText( + "Resolved value: - X: 6 - Y: 10 - Blur: 4 - Spread: -2", + ), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + }); + test("User creates typography token", async ({ page }) => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1101,7 +1287,7 @@ test.describe("Tokens - creation", () => { await nameField.fill(""); const emptyNameErrorNode = - tokensUpdateCreateModal.getByText(emptyNameError); + tokensUpdateCreateModal.getByText(emptyNameError); await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -1257,9 +1443,9 @@ test.describe("Tokens - creation", () => { await nameField.fill("my-token-2"); const referenceToggle = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); const compositeToggle = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await referenceToggle.click(); @@ -1293,7 +1479,7 @@ test.describe("Tokens - creation", () => { test("User adds typography token with reference", async ({ page }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); const newTokenTitle = "NewReference"; @@ -1322,7 +1508,7 @@ test.describe("Tokens - creation", () => { }); const resolvedValue = - await tokensUpdateCreateModal.getByText("Resolved value:"); + await tokensUpdateCreateModal.getByText("Resolved value:"); await expect(resolvedValue).toBeVisible(); await expect(resolvedValue).toContainText("Font Family: 42dot Sans"); await expect(resolvedValue).toContainText("Font Size: 100"); @@ -1345,7 +1531,7 @@ test.describe("Tokens - creation", () => { test("User creates grouped color token", async ({ page }) => { const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); await tokensSidebar .getByRole("button", { name: "Add Token: Color" }) @@ -1405,7 +1591,7 @@ test.describe("Tokens - creation", () => { test("User duplicate color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1429,95 +1615,95 @@ test.describe("Tokens - creation", () => { - test("User creates grouped color token", async ({ page }) => { - const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = - await setupEmptyTokensFile(page); +test("User creates grouped color token", async ({ page }) => { + const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = + await setupEmptyTokensFile(page); - await tokensSidebar - .getByRole("button", { name: "Add Token: Color" }) - .click(); + await tokensSidebar + .getByRole("button", { name: "Add Token: Color" }) + .click(); - // Create grouped color token with mouse + // Create grouped color token with mouse - await expect(tokensUpdateCreateModal).toBeVisible(); + await expect(tokensUpdateCreateModal).toBeVisible(); - const nameField = tokensUpdateCreateModal.getByLabel("Name"); - const valueField = tokensUpdateCreateModal.getByLabel("Value"); + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); - await nameField.click(); - await nameField.fill("dark.primary"); + await nameField.click(); + await nameField.fill("dark.primary"); - await valueField.click(); - await valueField.fill("red"); + await valueField.click(); + await valueField.fill("red"); - const submitButton = tokensUpdateCreateModal.getByRole("button", { - name: "Save", - }); - await expect(submitButton).toBeEnabled(); - await submitButton.click(); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); - await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); + await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); - await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); + await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); +}); + +test("User cant create regular token with value missing", async ({ + page, +}) => { + const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page); + + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + await tokensTabPanel + .getByRole("button", { name: "Add Token: Color" }) + .click(); + + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", }); - test("User cant create regular token with value missing", async ({ - page, - }) => { - const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page); + // Initially submit button should be disabled + await expect(submitButton).toBeDisabled(); - const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); - await tokensTabPanel - .getByRole("button", { name: "Add Token: Color" }) - .click(); + // Fill in name but leave value empty + await nameField.click(); + await nameField.fill("primary"); - await expect(tokensUpdateCreateModal).toBeVisible(); + // Submit button should remain disabled when value is empty + await expect(submitButton).toBeDisabled(); +}); - const nameField = tokensUpdateCreateModal.getByLabel("Name"); - const submitButton = tokensUpdateCreateModal.getByRole("button", { - name: "Save", - }); +test("User duplicate color token", async ({ page }) => { + const { tokensSidebar, tokenContextMenuForToken } = + await setupTokensFile(page); - // Initially submit button should be disabled - await expect(submitButton).toBeDisabled(); + await expect(tokensSidebar).toBeVisible(); - // Fill in name but leave value empty - await nameField.click(); - await nameField.fill("primary"); + unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); - // Submit button should remain disabled when value is empty - await expect(submitButton).toBeDisabled(); + const colorToken = tokensSidebar.getByRole("button", { + name: "100", }); - test("User duplicate color token", async ({ page }) => { - const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await colorToken.click({ button: "right" }); + await expect(tokenContextMenuForToken).toBeVisible(); - await expect(tokensSidebar).toBeVisible(); + await tokenContextMenuForToken.getByText("Duplicate token").click(); + await expect(tokenContextMenuForToken).not.toBeVisible(); - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); - - const colorToken = tokensSidebar.getByRole("button", { - name: "100", - }); - - await colorToken.click({ button: "right" }); - await expect(tokenContextMenuForToken).toBeVisible(); - - await tokenContextMenuForToken.getByText("Duplicate token").click(); - await expect(tokenContextMenuForToken).not.toBeVisible(); - - await expect( - tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }), - ).toBeVisible(); - }); + await expect( + tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }), + ).toBeVisible(); +}); test.describe("Tokens tab - edition", () => { test("User edits typography token and all fields are valid", async ({ page, }) => { const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } = - await setupTypographyTokensFile(page); + await setupTypographyTokensFile(page); await tokensSidebar .getByRole("button") @@ -1538,8 +1724,8 @@ test.describe("Tokens tab - edition", () => { // Fill font-family to verify to verify that input value doesn't get split into list of characters const fontFamilyField = tokensUpdateCreateModal - .getByLabel("Font family") - .first(); + .getByLabel("Font family") + .first(); await fontFamilyField.fill("OneWord"); // Invalidate incorrect values for font size @@ -1560,11 +1746,11 @@ test.describe("Tokens tab - edition", () => { const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i); const letterSpacingField = - tokensUpdateCreateModal.getByLabel(/Letter Spacing/i); + tokensUpdateCreateModal.getByLabel(/Letter Spacing/i); const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i); const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i); const textDecorationField = - tokensUpdateCreateModal.getByLabel(/Text Decoration/i); + tokensUpdateCreateModal.getByLabel(/Text Decoration/i); // Capture all values before switching tabs const originalValues = { @@ -1579,14 +1765,14 @@ test.describe("Tokens tab - edition", () => { // Switch to reference tab and back to composite tab const referenceTabButton = - tokensUpdateCreateModal.getByTestId("reference-opt"); + tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceTabButton.click(); // Empty reference tab should be disabled await expect(saveButton).toBeDisabled(); const compositeTabButton = - tokensUpdateCreateModal.getByTestId("composite-opt"); + tokensUpdateCreateModal.getByTestId("composite-opt"); await compositeTabButton.click(); // Filled composite tab should be enabled @@ -1613,7 +1799,7 @@ test.describe("Tokens tab - edition", () => { page, }) => { const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); @@ -1649,7 +1835,7 @@ test.describe("Tokens tab - edition", () => { page, }) => { const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFile(page); + await setupEmptyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); await tokensTabPanel @@ -1704,7 +1890,7 @@ test.describe("Tokens tab - edition", () => { test.describe("Tokens tab - delete", () => { test("User delete color token", async ({ page }) => { const { tokensSidebar, tokenContextMenuForToken } = - await setupTokensFile(page); + await setupTokensFile(page); await expect(tokensSidebar).toBeVisible(); diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index 58d3586995..dd2e23c348 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -28,7 +28,7 @@ export function startWorker() { } export const IS_DEBUG = process.env.NODE_ENV !== "production"; -export const BUILD_DATE = process.env.BUILD_DATE || (new Date().toString()) ; +export const BUILD_DATE = process.env.BUILD_DATE || new Date().toString(); export const BUILD_TS = process.env.BUILD_TS || Date.now(); export const VERSION = process.env.VERSION || "develop"; export const VERSION_TAG = process.env.VERSION_TAG || VERSION; @@ -51,7 +51,8 @@ async function findFiles(basePath, predicate, options = {}) { function syncDirs(originPath, destPath) { const command = `rsync -ar --delete ${originPath} ${destPath}`; - return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => { + return new Promise((resolve, reject) => { + proc.exec(command, (cause, stdout) => { if (cause) { reject(cause); } else { @@ -174,7 +175,7 @@ export async function watch(baseDir, predicate, callback) { const watcher = new Watcher(baseDir, { persistent: true, recursive: true, - debounce: 500 + debounce: 500, }); watcher.on("change", (path) => { @@ -183,7 +184,6 @@ export async function watch(baseDir, predicate, callback) { } }); - watcher.on("error", (cause) => { console.log("WATCHER ERROR", cause); }); @@ -194,7 +194,6 @@ export async function ensureDirectories() { await fs.mkdir("./resources/public/css/", { recursive: true }); } - async function readManifestFile(resource) { const manifestPath = "resources/public/" + resource; let content = await fs.readFile(manifestPath, { encoding: "utf8" }); @@ -214,20 +213,23 @@ async function generateManifest() { default_translations: "./js/translation.en.js?version=" + VERSION_TAG, importmap: JSON.stringify({ - "imports": { + imports: { "./js/shared.js": "./js/shared.js?version=" + VERSION_TAG, "./js/main.js": "./js/main.js?version=" + VERSION_TAG, "./js/render.js": "./js/render.js?version=" + VERSION_TAG, "./js/render-wasm.js": "./js/render-wasm.js?version=" + VERSION_TAG, "./js/rasterizer.js": "./js/rasterizer.js?version=" + VERSION_TAG, - "./js/main-dashboard.js": "./js/main-dashboard.js?version=" + VERSION_TAG, + "./js/main-dashboard.js": + "./js/main-dashboard.js?version=" + VERSION_TAG, "./js/main-auth.js": "./js/main-auth.js?version=" + VERSION_TAG, "./js/main-viewer.js": "./js/main-viewer.js?version=" + VERSION_TAG, "./js/main-settings.js": "./js/main-settings.js?version=" + VERSION_TAG, - "./js/main-workspace.js": "./js/main-workspace.js?version=" + VERSION_TAG, - "./js/util-highlight.js": "./js/util-highlight.js?version=" + VERSION_TAG - } - }) + "./js/main-workspace.js": + "./js/main-workspace.js?version=" + VERSION_TAG, + "./js/util-highlight.js": + "./js/util-highlight.js?version=" + VERSION_TAG, + }, + }), }; return index; @@ -431,7 +433,7 @@ async function generateTemplates() { }; const context = { - manifest: manifest + manifest: manifest, }; content = await renderTemplate( @@ -463,11 +465,17 @@ async function generateTemplates() { ); await fs.writeFile("./.storybook/preview-head.html", content); - content = await renderTemplate("resources/templates/render.mustache", context); + content = await renderTemplate( + "resources/templates/render.mustache", + context, + ); await fs.writeFile("./resources/public/render.html", content); - content = await renderTemplate("resources/templates/rasterizer.mustache", context); + content = await renderTemplate( + "resources/templates/rasterizer.mustache", + context, + ); await fs.writeFile("./resources/public/rasterizer.html", content); } diff --git a/frontend/scripts/build-storybook-assets.js b/frontend/scripts/build-storybook-assets.js index 092762ceb6..6861165b87 100644 --- a/frontend/scripts/build-storybook-assets.js +++ b/frontend/scripts/build-storybook-assets.js @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import * as h from "./_helpers.js"; -await fs.mkdir("resources/public/js", {recursive: true}); +await fs.mkdir("resources/public/js", { recursive: true }); await h.compileStorybookStyles(); await h.copyAssets(); diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 1bf2de0d0a..0ed5fd68d9 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -126,7 +126,7 @@ If the `value` is not parseable and/or has missing references returns a map with `:errors`. If the `value` is parseable but is out of range returns a map with `warnings`." [value] - (let [missing-references? (seq (seq (cto/find-token-value-references value))) + (let [missing-references? (seq (cto/find-token-value-references value)) parsed-value (cft/parse-token-value value) out-of-scope (not (<= 0 (:value parsed-value) 1)) references (seq (cto/find-token-value-references value))] @@ -152,15 +152,14 @@ [value] (let [missing-references? (seq (cto/find-token-value-references value)) parsed-value (cft/parse-token-value value) - out-of-scope (< (:value parsed-value) 0) - references (seq (cto/find-token-value-references value))] + out-of-scope (< (:value parsed-value) 0)] (cond (and parsed-value (not out-of-scope)) parsed-value - references - {:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)] - :references references} + missing-references? + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references?)] + :references missing-references?} (and (not missing-references?) out-of-scope) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)]} @@ -365,7 +364,7 @@ "Parses shadow spread value (non-negative number)." [value] (let [parsed (parse-sd-token-general-value value) - valid? (and (:value parsed) (>= (:value parsed) 0))] + valid? (:value parsed)] (cond valid? parsed diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index c2e825c2a5..dfecbc779b 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -202,7 +202,6 @@ on-restore-immediately (fn [] - (prn files) (st/emit! (modal/show {:type :confirm :title (tr "dashboard-restore-file-confirmation.title") diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx index b7aca4194f..7133a1b961 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx @@ -10,15 +10,25 @@ import Components from "@target/components"; const { RadioButtons } = Components; const options = [ - {id: "left", label: "Left", value: "left" }, - {id: "center", label: "Center", value: "center" }, - {id: "right", label: "Right", value: "right" }, + { id: "left", label: "Left", value: "left" }, + { id: "center", label: "Center", value: "center" }, + { id: "right", label: "Right", value: "right" }, ]; const optionsIcon = [ - {id: "left", label: "Left align", value: "left", icon: "text-align-left" }, - {id: "center", label: "Center align", value: "center", icon: "text-align-center" }, - {id: "right", label: "Right align", value: "right", icon: "text-align-right" }, + { id: "left", label: "Left align", value: "left", icon: "text-align-left" }, + { + id: "center", + label: "Center align", + value: "center", + icon: "text-align-center", + }, + { + id: "right", + label: "Right align", + value: "right", + icon: "text-align-right", + }, ]; export default { @@ -69,4 +79,4 @@ export const WithIcons = { args: { options: optionsIcon, }, -}; \ No newline at end of file +}; diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index 410498e13f..ac44b9720b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -90,7 +90,8 @@ instance (dwt/create-editor editor-node canvas-node options) - update-name? (nil? content) + ;; Store original content to compare name later + original-content content on-key-up (fn [event] @@ -101,10 +102,22 @@ on-blur (fn [] (when-let [content (content/dom->cljs (dwt/get-editor-root instance))] - (st/emit! (dwt/v2-update-text-shape-content shape-id content - :update-name? update-name? - :name (gen-name instance) - :finalize? true))) + (let [state @st/state + objects (dsh/lookup-page-objects state) + shape (get objects shape-id) + current-name (:name shape) + generated-name (gen-name instance) + ;; Update name if: (1) it's a new shape (nil original content), or + ;; (2) the current name matches the generated name from original content + ;; (meaning it was never manually renamed) + update-name? (or (nil? original-content) + (and (some? current-name) + (some? original-content) + (= current-name (txt/generate-shape-name (txt/content->text original-content)))))] + (st/emit! (dwt/v2-update-text-shape-content shape-id content + :update-name? update-name? + :name generated-name + :finalize? true)))) (let [container-node (mf/ref-val container-ref)] (dom/set-style! container-node "opacity" 0))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index d578ccfbc5..3701bb505d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -61,7 +61,7 @@ (mf/defc page-item {::mf/wrap-props false} - [{:keys [page index deletable? selected? editing? hovering?]}] + [{:keys [page index deletable? selected? editing? hovering? current-page-id]}] (let [input-ref (mf/use-ref) id (:id page) delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) @@ -72,8 +72,10 @@ (mf/use-fn (mf/deps id) (fn [] - ;; when using the wasm renderer, apply a blur effect to the viewport canvas - (if (features/active-feature? @st/state "render-wasm/v1") + ;; For the wasm renderer, apply a blur effect to the viewport canvas + ;; when we navigate to a different page. + (if (and (features/active-feature? @st/state "render-wasm/v1") + (not= id current-page-id)) (do (wasm.api/capture-canvas-pixels) (wasm.api/apply-canvas-blur) @@ -203,12 +205,13 @@ (mf/defc page-item-wrapper {::mf/wrap-props false} - [{:keys [page-id index deletable? selected? editing?]}] + [{:keys [page-id index deletable? selected? editing? current-page-id]}] (let [page-ref (mf/with-memo [page-id] (make-page-ref page-id)) page (mf/deref page-ref)] [:& page-item {:page page :index index + :current-page-id current-page-id :deletable? deletable? :selected? selected? :editing? editing?}])) @@ -231,6 +234,7 @@ :deletable? deletable? :editing? (= page-id editing-page-id) :selected? (= page-id current-page-id) + :current-page-id current-page-id :key page-id}])]])) ;; --- Sitemap Toolbox diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index a4bfbf1b0c..9f9d395013 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -53,10 +53,12 @@ (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens @@ -131,6 +133,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? @@ -260,10 +263,10 @@ :else props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -309,7 +312,7 @@ (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value value-subfield index input-name]) @@ -422,10 +425,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index ba6a8348c2..80f2d91133 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -49,10 +49,12 @@ ;; validate data within the form state. (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value (cto/split-font-family value) - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens @@ -73,6 +75,7 @@ [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? (and (contains? (:data @form) input-name) @@ -152,10 +155,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name touched?] + (mf/with-effect [resolve-stream tokens token input-name touched? token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -200,7 +203,7 @@ [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value input-name]) @@ -276,10 +279,10 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index 2e57f197be..0f1b2a79b1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -139,10 +139,12 @@ (defn- resolve-value - [tokens prev-token value] + [tokens prev-token token-name value] (let [token {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + :name (if (str/blank? token-name) + "__PENPOT__TOKEN__NAME__PLACEHOLDER__" + token-name)} tokens (-> tokens ;; Remove previous token when renaming a token @@ -163,6 +165,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) touched? (and (contains? (:data @form) input-name) @@ -206,11 +209,11 @@ :hint-message (:message error)}) props)] - (mf/with-effect [resolve-stream tokens token input-name] + (mf/with-effect [resolve-stream tokens token input-name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -252,6 +255,7 @@ (let [form (mf/use-ctx fc/context) input-name name + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value input-name]) @@ -298,10 +302,10 @@ (mf/spread-props props {:hint-formated true}) props)] - (mf/with-effect [resolve-stream tokens token input-name name] + (mf/with-effect [resolve-stream tokens token input-name name token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] @@ -365,7 +369,7 @@ (let [form (mf/use-ctx fc/context) input-name name - + token-name (get-in @form [:data :name] nil) error (get-in @form [:errors :value value-subfield index input-name]) @@ -410,10 +414,10 @@ (mf/spread-props props {:hint-formated true}) props)] - (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (mf/with-effect [resolve-stream tokens token input-name index value-subfield token-name] (let [subs (->> resolve-stream (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) + (rx/mapcat (partial resolve-value tokens token token-name)) (rx/map (fn [result] (d/update-when result :error (fn [error] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index af394eadee..70797979c6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -23,19 +23,20 @@ (let [token-type (or (:type token) token-type) + tokens-in-selected-set + (mf/deref refs/workspace-all-tokens-in-selected-set) + token-path (mf/with-memo [token] (cft/token-name->path (:name token))) - all-tokens (mf/deref refs/workspace-all-tokens-map) - - all-tokens - (mf/with-memo [token-path all-tokens] - (-> (ctob/tokens-tree all-tokens) + tokens-tree-in-selected-set + (mf/with-memo [token-path tokens-in-selected-set] + (-> (ctob/tokens-tree tokens-in-selected-set) (d/dissoc-in token-path))) props (mf/spread-props props {:token-type token-type - :all-token-tree all-tokens + :tokens-tree-in-selected-set tokens-tree-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")}) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index 53a7f1cd2d..b1299f4bdb 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -89,7 +89,7 @@ action is-create selected-token-set-id - all-token-tree + tokens-tree-in-selected-set token-type make-schema input-component @@ -114,8 +114,7 @@ token-title (str/lower (:title token-properties)) - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) + tokens (mf/deref refs/workspace-all-tokens-map) tokens-in-selected-set (mf/deref refs/workspace-all-tokens-in-selected-set) @@ -130,8 +129,8 @@ (assoc (:name token) token))) schema - (mf/with-memo [all-token-tree active-tab] - (make-schema all-token-tree active-tab)) + (mf/with-memo [tokens-tree-in-selected-set active-tab] + (make-schema tokens-tree-in-selected-set active-tab)) initial (mf/with-memo [token] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs index 1d8f6e9dff..995d61fec7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -282,12 +282,7 @@ (let [n (d/parse-double blur)] (or (nil? n) (not (< n 0)))))]]] [:spread {:optional true} - [:and - [:maybe :string] - [:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")} - (fn [spread] - (let [n (d/parse-double spread)] - (or (nil? n) (not (< n 0)))))]]] + [:maybe :string]] [:color {:optional true} [:maybe :string]] [:color-result {:optional true} ::sm/any] [:inset {:optional true} [:maybe :boolean]]]]] diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index bbc7931d2d..a5ba8aba4f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -144,7 +144,7 @@ modifiers (hooks/use-equal-memo modifiers) shapes (hooks/use-equal-memo shapes)] - [:g.outlines + [:g.outlines.blurrable [:& shape-outlines-render {:shapes shapes :zoom zoom :modifiers modifiers}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index c329b483a8..17da584434 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -252,7 +252,7 @@ edition (mf/deref refs/selected-edition) grid-edition? (ctl/grid-layout? objects edition)] - [:g.frame-titles + [:g.frame-titles.blurrable (for [{:keys [id parent-id] :as shape} shapes] (when (and (not= id uuid/zero) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 0530b21a06..3e516e923f 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -424,6 +424,7 @@ :xmlnsXlink "http://www.w3.org/1999/xlink" :preserveAspectRatio "xMidYMid meet" :key (str "viewport" page-id) + :id "viewport-controls" :view-box (utils/format-viewbox vbox) :ref on-viewport-ref :class (dm/str @cursor (when drawing-tool " drawing") " " (stl/css :viewport-controls)) @@ -473,7 +474,7 @@ :zoom zoom}] (when (ctl/any-layout? outlined-frame) - [:g.ghost-outline + [:g.ghost-outline.blurrable [:& outline/shape-outlines {:objects base-objects :selected selected diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 24e054cfc0..2aa0dd4ff1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1429,8 +1429,9 @@ (defn apply-canvas-blur [] - (when wasm/canvas - (dom/set-style! wasm/canvas "filter" "blur(4px)"))) + (when wasm/canvas (dom/set-style! wasm/canvas "filter" "blur(4px)")) + (let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")] + (run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur))) (defn init-wasm-module diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index 764da6dfa4..d2da5df87a 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -151,6 +151,8 @@ void main() { (.clear ^js context (.-DEPTH_BUFFER_BIT ^js context)) (.clear ^js context (.-STENCIL_BUFFER_BIT ^js context))) (dom/set-style! wasm/canvas "filter" "none") + (let [controls-to-unblur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")] + (run! #(dom/set-style! % "filter" "none") controls-to-unblur)) (set! wasm/canvas-pixels nil))) (defn capture-canvas-pixels diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 6475fb9e2f..7809d5ca0f 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -227,7 +227,7 @@ :svg-attrs (do (api/set-shape-svg-attrs v) - ;; Always update fills/blur/shadow to clear previous state if filters disappear + ;; Always update fills/blur/shadow to clear previous state if filters disappear (api/set-shape-fills id (:fills shape) false) (api/set-shape-blur (:blur shape)) (api/set-shape-shadows (:shadow shape))) @@ -397,12 +397,18 @@ (next es)) (throw (js/Error. "conj on a map takes map entries or seqables of map entries")))))))) +(def ^:private xf:without-id-and-type + (remove (fn [kvpair] + (let [k (key kvpair)] + (or (= k :id) + (= k :type)))))) + (defn create-shape "Instanciate a shape from a map" [attrs] (ShapeProxy. (:id attrs) (:type attrs) - (dissoc attrs :id :type))) + (into {} xf:without-id-and-type attrs))) (t/add-handlers! ;; We only add a write handler, read handler uses the dynamic dispatch diff --git a/frontend/src/app/worker/index.cljs b/frontend/src/app/worker/index.cljs index 0de440f7c1..c40f0b6fd8 100644 --- a/frontend/src/app/worker/index.cljs +++ b/frontend/src/app/worker/index.cljs @@ -58,6 +58,8 @@ (swap! state update ::snap snap/update-page old-page new-page) (swap! state update ::selection selection/update-page old-page new-page)) + (catch :default cause + (log/error :hint "error updating page index" :id page-id :cause cause)) (finally (let [elapsed (tpoint)] (log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true)))) diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2894156837..b111c942e6 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7804,6 +7804,10 @@ msgstr "Error al importar: No se pudo procesar el JSON." msgid "workspace.tokens.export" msgstr "Exportar" +#: src/app/main/ui/workspace/tokens/export/modal.cljs:125 +msgid "workspace.tokens.export-tokens" +msgstr "Exportar tokens" + #: src/app/main/ui/workspace/tokens/export/modal.cljs:118 msgid "workspace.tokens.export.multiple-files" msgstr "Múltiples ficheros" @@ -7848,10 +7852,26 @@ msgstr "Nombre del grupo" msgid "workspace.tokens.grouping-set-alert" msgstr "La agrupación de sets aun no está soportada." +#: src/app/main/ui/workspace/tokens/import/modal.cljs:233 +msgid "workspace.tokens.import-button-prefix" +msgstr "Importar %s" + #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 msgid "workspace.tokens.import-error" msgstr "Error al importar:" +#: src/app/main/ui/workspace/tokens/import/modal.cljs:273 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "Carpeta" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:271 +msgid "workspace.tokens.import-menu-json-option" +msgstr "Archivo JSON único" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:272 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "Archivo ZIP" + #: src/app/main/ui/workspace/tokens/import/modal.cljs:241 msgid "workspace.tokens.import-multiple-files" msgstr "" @@ -7866,7 +7886,7 @@ msgstr "" #: src/app/main/ui/workspace/tokens/import/modal.cljs:237 msgid "workspace.tokens.import-tokens" -msgstr "Import tokens" +msgstr "Importar tokens" #: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415 #, unused diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index f8e0c6428a..d0db679cf7 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -188,10 +188,20 @@ fn propagate_transform( || !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()) { if let Type::Text(text_content) = &mut shape.shape_type.clone() { + let resized_selrect = math::Rect::from_xywh( + shape.selrect.left(), + shape.selrect.top(), + shape_bounds_after.width(), + shape_bounds_after.height(), + ); match text_content.grow_type() { GrowType::AutoHeight => { - if text_content.needs_update_layout() { - text_content.update_layout(shape.selrect); + // For auto-height, always update layout when width changes + // because the new width affects how text wraps + let width_changed = + !is_close_to(shape_bounds_before.width(), shape_bounds_after.width()); + if width_changed || text_content.needs_update_layout() { + text_content.update_layout(resized_selrect); } let height = text_content.size.height; let resize_transform = math::resize_matrix( @@ -204,8 +214,12 @@ fn propagate_transform( transform.post_concat(&resize_transform); } GrowType::AutoWidth => { - if text_content.needs_update_layout() { - text_content.update_layout(shape.selrect); + // For auto-width, always update layout when height changes + // because the new height affects how text flows + let height_changed = + !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()); + if height_changed || text_content.needs_update_layout() { + text_content.update_layout(resized_selrect); } let width = text_content.width(); let height = text_content.size.height;