diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index df9e6f7b0a..ea3bed841c 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -487,6 +487,7 @@ :vertical-margin #{:spacing :dimensions} :sided-margins #{:spacing :dimensions} :line-height #{:line-height :number} + :opacity #{:opacity} :font-size #{:font-size} :letter-spacing #{:letter-spacing} :fill #{:color} diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 9c2b2fac1a..4e8130e4eb 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -101,6 +101,70 @@ test.describe("Tokens: Apply token", () => { 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 }) => { const { workspacePage, tokensSidebar, tokenContextMenuForToken } = await setupTypographyTokensFile(page); @@ -129,189 +193,6 @@ test.describe("Tokens: Apply token", () => { 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 ({ page, }) => { diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index a6c7153e72..0e01d81b4b 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -1008,6 +1008,41 @@ test.describe("Tokens - creation", () => { ).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 }) => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = @@ -1256,6 +1291,58 @@ test.describe("Tokens - creation", () => { ).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 creates grouped color token", async ({ page }) => { const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = await setupEmptyTokensFile(page); @@ -1340,6 +1427,91 @@ test.describe("Tokens - creation", () => { }); }); + + + test("User creates grouped color token", async ({ page }) => { + const { workspacePage, tokensUpdateCreateModal, tokensSidebar } = + await setupEmptyTokensFile(page); + + await tokensSidebar + .getByRole("button", { name: "Add Token: Color" }) + .click(); + + // Create grouped color token with mouse + + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + + await nameField.click(); + await nameField.fill("dark.primary"); + + await valueField.click(); + await valueField.fill("red"); + + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); + + 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", + }); + + // Initially submit button should be disabled + await expect(submitButton).toBeDisabled(); + + // Fill in name but leave value empty + await nameField.click(); + await nameField.fill("primary"); + + // Submit button should remain disabled when value is empty + await expect(submitButton).toBeDisabled(); + }); + + test("User duplicate color token", async ({ page }) => { + const { tokensSidebar, tokenContextMenuForToken } = + await setupTokensFile(page); + + await expect(tokensSidebar).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(); + }); + test.describe("Tokens tab - edition", () => { test("User edits typography token and all fields are valid", async ({ page, diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 6b65ec63c7..d7dbf94de3 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -216,7 +216,7 @@ is-selected-on-focus nillable tokens applied-token empty-to-end on-change on-blur on-focus on-detach - property align ref] + property align ref name] :rest props}] (let [;; NOTE: we use mfu/bean here for transparently handle @@ -662,7 +662,10 @@ label (get token :name) token-value (or (get token :resolved-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 {:id id :label label diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss index ea5fef97ce..b7c3d2e40a 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss @@ -7,6 +7,7 @@ @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/typography.scss" as *; +@use "ds/_utils.scss" as *; .option-list { --options-dropdown-icon-fg-color: var(--color-foreground-secondary); @@ -15,32 +16,32 @@ --options-dropdown-border-color: var(--color-background-quaternary); position: absolute; - top: $sz-36; - width: var(--dropdown-width, 100%); + inset-block-start: $sz-36; + inline-size: var(--dropdown-width, 100%); transform: translateX(var(--dropdown-translate-distance, 0)); background-color: var(--options-dropdown-bg-color); border-radius: $br-8; border: $b-1 solid var(--options-dropdown-border-color); padding-block: var(--sp-xs); margin-block-end: 0; - max-height: $sz-400; + max-block-size: $sz-400; overflow-y: auto; overflow-x: hidden; z-index: var(--z-index-dropdown); } .left-align { - left: var(--dropdown-offset, 0); + inset-inline-start: var(--dropdown-offset, 0); } .right-align { - right: var(--dropdown-offset, 0); + inset-inline-end: var(--dropdown-offset, 0); } .option-separator { border: $b-1 solid var(--options-dropdown-border-color); - margin-top: var(--sp-xs); - margin-bottom: var(--sp-xs); + margin-block-start: var(--sp-xs); + margin-block-end: var(--sp-xs); } .group-option, @@ -51,11 +52,11 @@ gap: var(--sp-xs); color: var(--color-foreground-secondary); padding-inline: var(--sp-s); - height: var(--sp-xxxl); + block-size: var(--sp-xxxl); } .option-empty { justify-content: center; text-align: center; - padding: 0 40px; + padding: 0 px2rem(40); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs index 1bafa4a95a..47b27f7f6e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs @@ -55,7 +55,6 @@ (-> (deref tokens) (select-keys (get tk/tokens-by-input name)) (not-empty)))) - on-detach-attr (mf/use-fn (mf/deps on-detach name) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 5060b8cc83..fc765f3cc6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -9,13 +9,17 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.types.token :as tk] [app.main.data.workspace :as dw] [app.main.data.workspace.shapes :as dwsh] + [app.main.data.workspace.tokens.application :as dwta] [app.main.features :as features] [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.context :as muc] [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.render-wasm.api :as wasm.api] [app.util.i18n :as i18n :refer [tr]] @@ -39,11 +43,16 @@ (defn- check-layer-menu-props [old-props new-props] (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") (unchecked-get new-props "class")) (identical? (unchecked-get old-props "ids") (unchecked-get new-props "ids")) + (identical? old-applied-tokens + new-applied-tokens) (identical? (get old-values :opacity) (get new-values :opacity)) (identical? (get old-values :blend-mode) @@ -53,12 +62,53 @@ (identical? (get old-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/wrap [#(mf/memo' % check-layer-menu-props)]} - [{:keys [ids values]}] - (let [hidden? (get values :hidden) + [{:keys [ids values applied-tokens]}] + (let [token-numeric-inputs + (features/use-feature "tokens/numeric-input") + + hidden? (get values :hidden) 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-opacity (opacity->string (:opacity values)) @@ -118,6 +168,17 @@ (let [value (/ value 100)] (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 (mf/use-fn (mf/deps ids) @@ -176,8 +237,9 @@ preview-complete?)) (swap! state* assoc :selected-blend-mode current-blend-mode))) - [:div {:class (stl/css-case :element-set-content true - :hidden hidden?)} + [:section {:class (stl/css-case :element-set-content true + :hidden hidden?) + :aria-label "layer-menu-section"} [:div {:class (stl/css :select)} [:& select {:default-value selected-blend-mode @@ -187,16 +249,34 @@ :class (stl/css-case :hidden-select hidden?) :on-pointer-enter-option handle-blend-mode-enter :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* - {:value current-opacity - :placeholder "--" - :on-change handle-opacity-change - :min 0 - :max 100 - :className (stl/css :numeric-input)}]] + + + + (if token-numeric-inputs + + [:> numeric-input-wrapper* + {:on-change on-opacity-change + :on-detach on-detach-token + :icon i/percentage + :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)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss index 45591f7f59..173c482e9f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss @@ -6,6 +6,7 @@ @use "refactor/common-refactor.scss" as deprecated; @use "../../../sidebar/common/sidebar.scss" as sidebar; +@use "ds/_utils.scss" as *; .element-set-content { @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)}; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index cef7a66c57..ce4e318153 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -85,6 +85,7 @@ [:* [:> layer-menu* {:ids ids :type type + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index 6641fa5c3e..ca4b85ba0e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -84,6 +84,7 @@ [:* [:> layer-menu* {:ids ids :type type + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 149179a337..a40f017873 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -100,6 +100,7 @@ [:* [:> layer-menu* {:ids ids :type shape-type + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:ids ids :applied-tokens applied-tokens diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 4efd8f7ac1..716a836a30 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -111,6 +111,7 @@ [:div {:class (stl/css :options)} [:> layer-menu* {:type type :ids layer-ids + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:type type :ids measure-ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 98626bb4ff..60148a4776 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -382,7 +382,7 @@ objects objects))) - [layer-ids layer-values] + [layer-ids layer-values layer-tokens] (get-attrs shapes objects :layer) [text-ids text-values] @@ -406,7 +406,7 @@ [exports-ids exports-values] (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) [layout-item-ids layout-item-values {}] @@ -442,6 +442,7 @@ (when-not (empty? layer-ids) [:> layer-menu* {:type type :ids layer-ids + :applied-tokens layer-tokens :values layer-values}]) (when-not (empty? measure-ids) @@ -459,7 +460,7 @@ {:type type :ids layout-container-ids :values layout-container-values - :applied-tokens layout-contianer-tokens + :applied-tokens layout-container-tokens :multiple true}] (when (or is-layout-child? has-flex-layout-container?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index 2c666ed1d9..95d7f4a83f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -84,6 +84,7 @@ [:* [:> layer-menu* {:ids ids + :applied-tokens applied-tokens :type type :values layer-values}] [:> measures-menu* {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index 980185ab85..a6c47380e7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -85,6 +85,7 @@ [:* [:> layer-menu* {:ids ids :type type + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:ids ids :type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 4d5b180409..a89a6e673e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -125,6 +125,7 @@ [:* [:> layer-menu* {:ids ids :type type + :applied-tokens applied-tokens :values layer-values}] [:> measures-menu* {:ids ids