mirror of
https://github.com/penpot/penpot.git
synced 2026-01-15 09:50:23 -05:00
Compare commits
2 Commits
develop
...
eva-replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71818c4b2b | ||
|
|
df1c8b6388 |
@@ -487,6 +487,7 @@
|
|||||||
:vertical-margin #{:spacing :dimensions}
|
:vertical-margin #{:spacing :dimensions}
|
||||||
:sided-margins #{:spacing :dimensions}
|
:sided-margins #{:spacing :dimensions}
|
||||||
:line-height #{:line-height :number}
|
:line-height #{:line-height :number}
|
||||||
|
:opacity #{:opacity}
|
||||||
:font-size #{:font-size}
|
:font-size #{:font-size}
|
||||||
:letter-spacing #{:letter-spacing}
|
:letter-spacing #{:letter-spacing}
|
||||||
:fill #{:color}
|
:fill #{:color}
|
||||||
|
|||||||
@@ -101,6 +101,70 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(brTokenPillXL).not.toBeVisible();
|
await expect(brTokenPillXL).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User applies opacity token to a shape from sidebar", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||||
|
await setupTokensFile(page);
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Layers" }).click();
|
||||||
|
|
||||||
|
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||||
|
|
||||||
|
// Open tokens sections on left sidebar
|
||||||
|
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||||
|
await tokensTabButton.click();
|
||||||
|
|
||||||
|
// Unfold opacity tokens
|
||||||
|
await page.getByRole("button", { name: "Opacity 3" }).click();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "opacity", exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await tokensSidebar
|
||||||
|
.getByRole("button", { name: "opacity", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "opacity.high" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Apply opacity token from token panels
|
||||||
|
await tokensSidebar.getByRole("button", { name: "opacity.high" }).click();
|
||||||
|
|
||||||
|
// Check if opacity sections is visible on right sidebar
|
||||||
|
const layerMenuSection = page.getByRole("region", {
|
||||||
|
name: "layer-menu-section",
|
||||||
|
});
|
||||||
|
await expect(layerMenuSection).toBeVisible();
|
||||||
|
|
||||||
|
// Check if token pill is visible on design tab on right sidebar
|
||||||
|
const opacityHighPill = layerMenuSection.getByRole("button", {
|
||||||
|
name: "opacity.high",
|
||||||
|
});
|
||||||
|
await expect(opacityHighPill).toBeVisible();
|
||||||
|
|
||||||
|
// Detach token from design tab on right sidebar
|
||||||
|
const detachButton = layerMenuSection.getByRole("button", {
|
||||||
|
name: "Detach token",
|
||||||
|
});
|
||||||
|
await detachButton.click();
|
||||||
|
|
||||||
|
// Open dropdown from input
|
||||||
|
const dropdownBtn = layerMenuSection.getByLabel('Open token list');
|
||||||
|
await expect(dropdownBtn).toBeVisible();
|
||||||
|
await dropdownBtn.click();
|
||||||
|
|
||||||
|
// Change token from dropdown
|
||||||
|
const opacityLowOption = layerMenuSection.getByRole('option', { name: 'opacity.low' });
|
||||||
|
await expect(opacityLowOption).toBeVisible();
|
||||||
|
await opacityLowOption.click();
|
||||||
|
|
||||||
|
await expect(opacityHighPill).not.toBeVisible();
|
||||||
|
const opacityLowPill = layerMenuSection.getByRole("button", {
|
||||||
|
name: "opacity.low",
|
||||||
|
});
|
||||||
|
await expect(opacityLowPill).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("User applies typography token to a text shape", async ({ page }) => {
|
test("User applies typography token to a text shape", async ({ page }) => {
|
||||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||||
await setupTypographyTokensFile(page);
|
await setupTypographyTokensFile(page);
|
||||||
@@ -129,189 +193,6 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(fontSizeInput).toHaveValue("100");
|
await expect(fontSizeInput).toHaveValue("100");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User edits typography token and all fields are valid", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
await tokensSidebar
|
|
||||||
.getByRole("button")
|
|
||||||
.filter({ hasText: "Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Open edit modal for "Full" typography token
|
|
||||||
const token = tokensSidebar.getByRole("button", { name: "Full" });
|
|
||||||
await token.click({ button: "right" });
|
|
||||||
await page.getByText("Edit token").click();
|
|
||||||
|
|
||||||
// Modal opens
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const saveButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: /save/i,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
await fontFamilyField.fill("OneWord");
|
|
||||||
|
|
||||||
// Invalidate incorrect values for font size
|
|
||||||
const fontSizeField = tokensUpdateCreateModal.getByLabel(/Font Size/i);
|
|
||||||
await fontSizeField.fill("invalid");
|
|
||||||
await expect(
|
|
||||||
tokensUpdateCreateModal.getByText(/Invalid token value:/),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Show error with line-height depending on invalid font-size
|
|
||||||
await fontSizeField.fill("");
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Fill in values for all fields and verify they persist when switching tabs
|
|
||||||
await fontSizeField.fill("16");
|
|
||||||
await expect(saveButton).toBeEnabled();
|
|
||||||
|
|
||||||
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
|
|
||||||
const letterSpacingField =
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Capture all values before switching tabs
|
|
||||||
const originalValues = {
|
|
||||||
fontSize: await fontSizeField.inputValue(),
|
|
||||||
fontFamily: await fontFamilyField.inputValue(),
|
|
||||||
fontWeight: await fontWeightField.inputValue(),
|
|
||||||
letterSpacing: await letterSpacingField.inputValue(),
|
|
||||||
lineHeight: await lineHeightField.inputValue(),
|
|
||||||
textCase: await textCaseField.inputValue(),
|
|
||||||
textDecoration: await textDecorationField.inputValue(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Switch to reference tab and back to composite tab
|
|
||||||
const referenceTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceTabButton.click();
|
|
||||||
|
|
||||||
// Empty reference tab should be disabled
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
const compositeTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("composite-opt");
|
|
||||||
await compositeTabButton.click();
|
|
||||||
|
|
||||||
// Filled composite tab should be enabled
|
|
||||||
await expect(saveButton).toBeEnabled();
|
|
||||||
|
|
||||||
// Verify all values are preserved after switching tabs
|
|
||||||
await expect(fontSizeField).toHaveValue(originalValues.fontSize);
|
|
||||||
await expect(fontFamilyField).toHaveValue(originalValues.fontFamily);
|
|
||||||
await expect(fontWeightField).toHaveValue(originalValues.fontWeight);
|
|
||||||
await expect(letterSpacingField).toHaveValue(originalValues.letterSpacing);
|
|
||||||
await expect(lineHeightField).toHaveValue(originalValues.lineHeight);
|
|
||||||
await expect(textCaseField).toHaveValue(originalValues.textCase);
|
|
||||||
await expect(textDecorationField).toHaveValue(
|
|
||||||
originalValues.textDecoration,
|
|
||||||
);
|
|
||||||
|
|
||||||
await saveButton.click();
|
|
||||||
|
|
||||||
// Modal should close, token should be visible (with new name) in sidebar
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User cant submit empty typography token or reference", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("typography.empty");
|
|
||||||
|
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
|
|
||||||
|
|
||||||
// Insert a value and then delete it
|
|
||||||
await valueField.fill("1");
|
|
||||||
await valueField.fill("");
|
|
||||||
|
|
||||||
// Submit button should be disabled when field is empty
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Switch to reference tab, should not be submittable either
|
|
||||||
const referenceTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceTabButton.click();
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User adds typography token with reference", async ({ page }) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
const newTokenTitle = "NewReference";
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill(newTokenTitle);
|
|
||||||
|
|
||||||
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Use a reference",
|
|
||||||
});
|
|
||||||
referenceTabButton.click();
|
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Reference",
|
|
||||||
});
|
|
||||||
await referenceField.fill("{Full}");
|
|
||||||
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
|
|
||||||
const resolvedValue =
|
|
||||||
await tokensUpdateCreateModal.getByText("Resolved value:");
|
|
||||||
await expect(resolvedValue).toBeVisible();
|
|
||||||
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
|
|
||||||
await expect(resolvedValue).toContainText("Font Size: 100");
|
|
||||||
await expect(resolvedValue).toContainText("Font Weight: 300");
|
|
||||||
await expect(resolvedValue).toContainText("Letter Spacing: 2");
|
|
||||||
await expect(resolvedValue).toContainText("Text Case: uppercase");
|
|
||||||
await expect(resolvedValue).toContainText("Text Decoration: underline");
|
|
||||||
|
|
||||||
await expect(submitButton).toBeEnabled();
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
const newToken = tokensSidebar.getByRole("button", {
|
|
||||||
name: newTokenTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(newToken).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User adds shadow token with multiple shadows and applies it to shape", async ({
|
test("User adds shadow token with multiple shadows and applies it to shape", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1008,6 +1008,41 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User cant submit empty typography token or reference", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill("typography.empty");
|
||||||
|
|
||||||
|
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
|
||||||
|
|
||||||
|
// Insert a value and then delete it
|
||||||
|
await valueField.fill("1");
|
||||||
|
await valueField.fill("");
|
||||||
|
|
||||||
|
// Submit button should be disabled when field is empty
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
|
||||||
|
// Switch to reference tab, should not be submittable either
|
||||||
|
const referenceTabButton =
|
||||||
|
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||||
|
await referenceTabButton.click();
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
test("User creates typography token", async ({ page }) => {
|
test("User creates typography token", async ({ page }) => {
|
||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||||
@@ -1256,6 +1291,58 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User adds typography token with reference", async ({ page }) => {
|
||||||
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
const newTokenTitle = "NewReference";
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill(newTokenTitle);
|
||||||
|
|
||||||
|
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Use a reference",
|
||||||
|
});
|
||||||
|
referenceTabButton.click();
|
||||||
|
|
||||||
|
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: "Reference",
|
||||||
|
});
|
||||||
|
await referenceField.fill("{Full}");
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedValue =
|
||||||
|
await tokensUpdateCreateModal.getByText("Resolved value:");
|
||||||
|
await expect(resolvedValue).toBeVisible();
|
||||||
|
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
|
||||||
|
await expect(resolvedValue).toContainText("Font Size: 100");
|
||||||
|
await expect(resolvedValue).toContainText("Font Weight: 300");
|
||||||
|
await expect(resolvedValue).toContainText("Letter Spacing: 2");
|
||||||
|
await expect(resolvedValue).toContainText("Text Case: uppercase");
|
||||||
|
await expect(resolvedValue).toContainText("Text Decoration: underline");
|
||||||
|
|
||||||
|
await expect(submitButton).toBeEnabled();
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
const newToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: newTokenTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(newToken).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("User edits typography token and all fields are valid", async ({
|
test("User edits typography token and all fields are valid", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
is-selected-on-focus nillable
|
is-selected-on-focus nillable
|
||||||
tokens applied-token empty-to-end
|
tokens applied-token empty-to-end
|
||||||
on-change on-blur on-focus on-detach
|
on-change on-blur on-focus on-detach
|
||||||
property align ref]
|
property align ref name]
|
||||||
:rest props}]
|
:rest props}]
|
||||||
|
|
||||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||||
@@ -662,7 +662,10 @@
|
|||||||
label (get token :name)
|
label (get token :name)
|
||||||
token-value (or (get token :resolved-value)
|
token-value (or (get token :resolved-value)
|
||||||
(or (mf/ref-val last-value*)
|
(or (mf/ref-val last-value*)
|
||||||
(fmt/format-number value)))]
|
(fmt/format-number value)))
|
||||||
|
token-value (if (= name :opacity)
|
||||||
|
(* 100 token-value)
|
||||||
|
token-value)]
|
||||||
(mf/spread-props props
|
(mf/spread-props props
|
||||||
{:id id
|
{:id id
|
||||||
:label label
|
:label label
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@use "ds/_borders.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/typography.scss" as *;
|
@use "ds/typography.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
.option-list {
|
.option-list {
|
||||||
--options-dropdown-icon-fg-color: var(--color-foreground-secondary);
|
--options-dropdown-icon-fg-color: var(--color-foreground-secondary);
|
||||||
@@ -15,32 +16,32 @@
|
|||||||
--options-dropdown-border-color: var(--color-background-quaternary);
|
--options-dropdown-border-color: var(--color-background-quaternary);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $sz-36;
|
inset-block-start: $sz-36;
|
||||||
width: var(--dropdown-width, 100%);
|
inline-size: var(--dropdown-width, 100%);
|
||||||
transform: translateX(var(--dropdown-translate-distance, 0));
|
transform: translateX(var(--dropdown-translate-distance, 0));
|
||||||
background-color: var(--options-dropdown-bg-color);
|
background-color: var(--options-dropdown-bg-color);
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
border: $b-1 solid var(--options-dropdown-border-color);
|
border: $b-1 solid var(--options-dropdown-border-color);
|
||||||
padding-block: var(--sp-xs);
|
padding-block: var(--sp-xs);
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
max-height: $sz-400;
|
max-block-size: $sz-400;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
z-index: var(--z-index-dropdown);
|
z-index: var(--z-index-dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-align {
|
.left-align {
|
||||||
left: var(--dropdown-offset, 0);
|
inset-inline-start: var(--dropdown-offset, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-align {
|
.right-align {
|
||||||
right: var(--dropdown-offset, 0);
|
inset-inline-end: var(--dropdown-offset, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-separator {
|
.option-separator {
|
||||||
border: $b-1 solid var(--options-dropdown-border-color);
|
border: $b-1 solid var(--options-dropdown-border-color);
|
||||||
margin-top: var(--sp-xs);
|
margin-block-start: var(--sp-xs);
|
||||||
margin-bottom: var(--sp-xs);
|
margin-block-end: var(--sp-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-option,
|
.group-option,
|
||||||
@@ -51,11 +52,11 @@
|
|||||||
gap: var(--sp-xs);
|
gap: var(--sp-xs);
|
||||||
color: var(--color-foreground-secondary);
|
color: var(--color-foreground-secondary);
|
||||||
padding-inline: var(--sp-s);
|
padding-inline: var(--sp-s);
|
||||||
height: var(--sp-xxxl);
|
block-size: var(--sp-xxxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-empty {
|
.option-empty {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 40px;
|
padding: 0 px2rem(40);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
(-> (deref tokens)
|
(-> (deref tokens)
|
||||||
(select-keys (get tk/tokens-by-input name))
|
(select-keys (get tk/tokens-by-input name))
|
||||||
(not-empty))))
|
(not-empty))))
|
||||||
|
|
||||||
on-detach-attr
|
on-detach-attr
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-detach name)
|
(mf/deps on-detach name)
|
||||||
|
|||||||
@@ -9,13 +9,17 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.types.token :as tk]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||||
[app.main.ui.components.select :refer [select]]
|
[app.main.ui.components.select :refer [select]]
|
||||||
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
@@ -39,11 +43,16 @@
|
|||||||
(defn- check-layer-menu-props
|
(defn- check-layer-menu-props
|
||||||
[old-props new-props]
|
[old-props new-props]
|
||||||
(let [old-values (unchecked-get old-props "values")
|
(let [old-values (unchecked-get old-props "values")
|
||||||
new-values (unchecked-get new-props "values")]
|
new-values (unchecked-get new-props "values")
|
||||||
|
|
||||||
|
old-applied-tokens (unchecked-get old-props "appliedTokens")
|
||||||
|
new-applied-tokens (unchecked-get new-props "appliedTokens")]
|
||||||
(and (identical? (unchecked-get old-props "class")
|
(and (identical? (unchecked-get old-props "class")
|
||||||
(unchecked-get new-props "class"))
|
(unchecked-get new-props "class"))
|
||||||
(identical? (unchecked-get old-props "ids")
|
(identical? (unchecked-get old-props "ids")
|
||||||
(unchecked-get new-props "ids"))
|
(unchecked-get new-props "ids"))
|
||||||
|
(identical? old-applied-tokens
|
||||||
|
new-applied-tokens)
|
||||||
(identical? (get old-values :opacity)
|
(identical? (get old-values :opacity)
|
||||||
(get new-values :opacity))
|
(get new-values :opacity))
|
||||||
(identical? (get old-values :blend-mode)
|
(identical? (get old-values :blend-mode)
|
||||||
@@ -53,12 +62,54 @@
|
|||||||
(identical? (get old-values :hidden)
|
(identical? (get old-values :hidden)
|
||||||
(get new-values :hidden)))))
|
(get new-values :hidden)))))
|
||||||
|
|
||||||
|
(mf/defc numeric-input-wrapper*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||||
|
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||||
|
tokens (mf/with-memo [tokens name]
|
||||||
|
(delay
|
||||||
|
(-> (deref tokens)
|
||||||
|
(select-keys (get tk/tokens-by-input name))
|
||||||
|
(not-empty))))
|
||||||
|
|
||||||
|
on-detach-attr
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-detach name)
|
||||||
|
#(on-detach % name))
|
||||||
|
|
||||||
|
applied-token (get applied-tokens name)
|
||||||
|
opacity-value (or (get values name) 1)
|
||||||
|
|
||||||
|
props (mf/spread-props props
|
||||||
|
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||||
|
(= :multiple opacity-value))
|
||||||
|
(tr "settings.multiple")
|
||||||
|
"--")
|
||||||
|
:applied-token applied-token
|
||||||
|
:tokens (if (delay? tokens) @tokens tokens)
|
||||||
|
:align align
|
||||||
|
:on-detach on-detach-attr
|
||||||
|
:name name
|
||||||
|
:value (* 100 opacity-value)})]
|
||||||
|
[:> numeric-input* props]))
|
||||||
|
|
||||||
(mf/defc layer-menu*
|
(mf/defc layer-menu*
|
||||||
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
||||||
[{:keys [ids values]}]
|
[{:keys [ids values applied-tokens]}]
|
||||||
(let [hidden? (get values :hidden)
|
(let [token-numeric-inputs
|
||||||
|
(features/use-feature "tokens/numeric-input")
|
||||||
|
|
||||||
|
hidden? (get values :hidden)
|
||||||
blocked? (get values :blocked)
|
blocked? (get values :blocked)
|
||||||
|
|
||||||
|
on-detach-token
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps ids)
|
||||||
|
(fn [token attr]
|
||||||
|
(st/emit! (dwta/unapply-token {:token (first token)
|
||||||
|
:attributes #{attr}
|
||||||
|
:shape-ids ids}))))
|
||||||
|
|
||||||
current-blend-mode (or (get values :blend-mode) :normal)
|
current-blend-mode (or (get values :blend-mode) :normal)
|
||||||
current-opacity (opacity->string (:opacity values))
|
current-opacity (opacity->string (:opacity values))
|
||||||
|
|
||||||
@@ -118,6 +169,17 @@
|
|||||||
(let [value (/ value 100)]
|
(let [value (/ value 100)]
|
||||||
(on-change ids :opacity value))))
|
(on-change ids :opacity value))))
|
||||||
|
|
||||||
|
on-opacity-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-change handle-opacity-change)
|
||||||
|
(fn [value]
|
||||||
|
(if (or (string? value) (int? value))
|
||||||
|
(handle-opacity-change value)
|
||||||
|
(do
|
||||||
|
(st/emit! (dwta/toggle-token {:token (first value)
|
||||||
|
:attrs #{:opacity}
|
||||||
|
:shape-ids ids}))))))
|
||||||
|
|
||||||
handle-set-hidden
|
handle-set-hidden
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps ids)
|
(mf/deps ids)
|
||||||
@@ -176,8 +238,9 @@
|
|||||||
preview-complete?))
|
preview-complete?))
|
||||||
(swap! state* assoc :selected-blend-mode current-blend-mode)))
|
(swap! state* assoc :selected-blend-mode current-blend-mode)))
|
||||||
|
|
||||||
[:div {:class (stl/css-case :element-set-content true
|
[:section {:class (stl/css-case :element-set-content true
|
||||||
:hidden hidden?)}
|
:hidden hidden?)
|
||||||
|
:aria-label "layer-menu-section"}
|
||||||
[:div {:class (stl/css :select)}
|
[:div {:class (stl/css :select)}
|
||||||
[:& select
|
[:& select
|
||||||
{:default-value selected-blend-mode
|
{:default-value selected-blend-mode
|
||||||
@@ -187,16 +250,34 @@
|
|||||||
:class (stl/css-case :hidden-select hidden?)
|
:class (stl/css-case :hidden-select hidden?)
|
||||||
:on-pointer-enter-option handle-blend-mode-enter
|
:on-pointer-enter-option handle-blend-mode-enter
|
||||||
:on-pointer-leave-option handle-blend-mode-leave}]]
|
:on-pointer-leave-option handle-blend-mode-leave}]]
|
||||||
[:div {:class (stl/css :input)
|
|
||||||
:title (tr "workspace.options.opacity")}
|
|
||||||
[:span {:class (stl/css :icon)} "%"]
|
|
||||||
[:> numeric-input*
|
(if token-numeric-inputs
|
||||||
{:value current-opacity
|
|
||||||
:placeholder "--"
|
[:> numeric-input-wrapper*
|
||||||
:on-change handle-opacity-change
|
{:on-change on-opacity-change
|
||||||
:min 0
|
:on-detach on-detach-token
|
||||||
:max 100
|
:icon i/percentage
|
||||||
:className (stl/css :numeric-input)}]]
|
:min 0
|
||||||
|
:max 100
|
||||||
|
:name :opacity
|
||||||
|
:property (tr "workspace.options.opacity")
|
||||||
|
:applied-tokens applied-tokens
|
||||||
|
:align :right
|
||||||
|
:class (stl/css :numeric-input-wrapper)
|
||||||
|
:values values}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input)
|
||||||
|
:title (tr "workspace.options.opacity")}
|
||||||
|
[:span {:class (stl/css :icon)} "%"]
|
||||||
|
[:> deprecated-input/numeric-input*
|
||||||
|
{:value current-opacity
|
||||||
|
:placeholder "--"
|
||||||
|
:on-change handle-opacity-change
|
||||||
|
:min 0
|
||||||
|
:max 100
|
||||||
|
:className (stl/css :numeric-input)}]])
|
||||||
|
|
||||||
|
|
||||||
[:div {:class (stl/css :actions)}
|
[:div {:class (stl/css :actions)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
.element-set-content {
|
.element-set-content {
|
||||||
@include sidebar.option-grid-structure;
|
@include sidebar.option-grid-structure;
|
||||||
@@ -43,3 +44,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.numeric-input-wrapper {
|
||||||
|
grid-column: span 2;
|
||||||
|
--dropdown-width: var(--7-columns-dropdown-width);
|
||||||
|
--dropdown-offset: #{px2rem(-35)};
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
|
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
|
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type shape-type
|
:type shape-type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
:applied-tokens applied-tokens
|
:applied-tokens applied-tokens
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
[:div {:class (stl/css :options)}
|
[:div {:class (stl/css :options)}
|
||||||
[:> layer-menu* {:type type
|
[:> layer-menu* {:type type
|
||||||
:ids layer-ids
|
:ids layer-ids
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:type type
|
[:> measures-menu* {:type type
|
||||||
:ids measure-ids
|
:ids measure-ids
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
objects
|
objects
|
||||||
objects)))
|
objects)))
|
||||||
|
|
||||||
[layer-ids layer-values]
|
[layer-ids layer-values layer-tokens]
|
||||||
(get-attrs shapes objects :layer)
|
(get-attrs shapes objects :layer)
|
||||||
|
|
||||||
[text-ids text-values]
|
[text-ids text-values]
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
[exports-ids exports-values]
|
[exports-ids exports-values]
|
||||||
(get-attrs shapes objects :exports)
|
(get-attrs shapes objects :exports)
|
||||||
|
|
||||||
[layout-container-ids layout-container-values layout-contianer-tokens]
|
[layout-container-ids layout-container-values layout-container-tokens]
|
||||||
(get-attrs shapes objects :layout-container)
|
(get-attrs shapes objects :layout-container)
|
||||||
|
|
||||||
[layout-item-ids layout-item-values {}]
|
[layout-item-ids layout-item-values {}]
|
||||||
@@ -442,6 +442,7 @@
|
|||||||
(when-not (empty? layer-ids)
|
(when-not (empty? layer-ids)
|
||||||
[:> layer-menu* {:type type
|
[:> layer-menu* {:type type
|
||||||
:ids layer-ids
|
:ids layer-ids
|
||||||
|
:applied-tokens layer-tokens
|
||||||
:values layer-values}])
|
:values layer-values}])
|
||||||
|
|
||||||
(when-not (empty? measure-ids)
|
(when-not (empty? measure-ids)
|
||||||
@@ -459,7 +460,7 @@
|
|||||||
{:type type
|
{:type type
|
||||||
:ids layout-container-ids
|
:ids layout-container-ids
|
||||||
:values layout-container-values
|
:values layout-container-values
|
||||||
:applied-tokens layout-contianer-tokens
|
:applied-tokens layout-container-tokens
|
||||||
:multiple true}]
|
:multiple true}]
|
||||||
|
|
||||||
(when (or is-layout-child? has-flex-layout-container?)
|
(when (or is-layout-child? has-flex-layout-container?)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:type type
|
:type type
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu*
|
[:> measures-menu*
|
||||||
{:ids ids
|
{:ids ids
|
||||||
|
|||||||
Reference in New Issue
Block a user