From dd2d03e6a0577d60f6a0de6bbd2112d7d4ccd7b3 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 14 Jan 2026 12:45:40 +0100 Subject: [PATCH] :recycle: Replace border radius inputs (#7953) * :sparkles: Replace border radius numeric input * :sparkles: Add border radius token inputs on multiple selection --- frontend/playwright/ui/specs/tokens.spec.js | 92 ++++- .../data/workspace/tokens/application.cljs | 37 ++ .../main/ui/ds/controls/numeric_input.cljs | 8 +- .../main/ui/ds/controls/numeric_input.scss | 7 +- .../ds/controls/shared/options_dropdown.cljs | 2 +- .../ds/controls/shared/options_dropdown.scss | 4 +- .../ui/ds/controls/shared/token_option.cljs | 2 +- .../ui/ds/controls/utilities/input_field.cljs | 9 +- .../ui/ds/controls/utilities/token_field.cljs | 16 +- .../ui/ds/controls/utilities/token_field.scss | 30 +- .../src/app/main/ui/ds/tooltip/tooltip.scss | 2 + .../sidebar/options/menus/border_radius.cljs | 346 ++++++++++++++---- .../sidebar/options/menus/border_radius.scss | 26 +- .../options/menus/layout_container.cljs | 2 +- .../options/menus/layout_container.scss | 2 +- .../sidebar/options/menus/measures.cljs | 5 +- .../sidebar/options/menus/measures.scss | 2 +- .../sidebar/options/shapes/multiple.cljs | 7 +- 18 files changed, 475 insertions(+), 124 deletions(-) diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index aba514549e..c2bc7399be 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -50,7 +50,7 @@ const setupTokensFile = async (page, options = {}) => { const { file = "workspace/get-file-tokens.json", fileFragment = "workspace/get-file-fragment-tokens.json", - flags = [], + flags = ["enable-feature-token-input"], } = options; const workspacePage = new WorkspacePage(page); @@ -2242,6 +2242,56 @@ test.describe("Tokens: Apply token", () => { ).toBeVisible(); }); + test("User applies border-radius 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 border radius tokens + await page.getByRole("button", { name: "Border Radius 3" }).click(); + await expect( + tokensSidebar.getByRole("button", { name: "borderRadius" }), + ).toBeVisible(); + await tokensSidebar.getByRole("button", { name: "borderRadius" }).click(); + await expect( + tokensSidebar.getByRole("button", { name: "borderRadius.sm" }), + ).toBeVisible(); + + // Apply border radius token from token panels + await tokensSidebar.getByRole("button", { name: "borderRadius.sm" }).click(); + + // Check if border radius sections is visible on right sidebar + const borderRadiusSection = page.getByRole("region", {name: "border-radius-section"}); + await expect(borderRadiusSection).toBeVisible(); + + // Check if token pill is visible on design tab on right sidebar + const brTokenPillSM = borderRadiusSection.getByRole('button', { name: 'borderRadius.sm' }); + await expect(brTokenPillSM).toBeVisible(); + await brTokenPillSM.click(); + + // Change token from dropdown + const brTokenOptionXl = borderRadiusSection.getByLabel('borderRadius.xl') + await expect(brTokenOptionXl).toBeVisible(); + await brTokenOptionXl.click(); + + await expect(brTokenPillSM).not.toBeVisible(); + const brTokenPillXL = borderRadiusSection.getByRole('button', { name: 'borderRadius.xl' }); + await expect(brTokenPillXL).toBeVisible(); + + // Detach token from design tab on right sidebar + const detachButton = borderRadiusSection.getByRole('button', { name: 'Detach token' }); + await detachButton.click(); + await expect(brTokenPillXL).not.toBeVisible(); + }); + test("User applies typography token to a text shape", async ({ page }) => { const { workspacePage, tokensSidebar, tokenContextMenuForToken } = await setupTypographyTokensFile(page); @@ -2417,12 +2467,13 @@ test.describe("Tokens: Apply token", () => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.fill(newTokenTitle); - const referenceTabButton = - tokensUpdateCreateModal.getByRole('button', { name: 'Use a reference' }); + const referenceTabButton = tokensUpdateCreateModal.getByRole("button", { + name: "Use a reference", + }); referenceTabButton.click(); - const referenceField = tokensUpdateCreateModal.getByRole('textbox', { - name: 'Reference' + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", }); await referenceField.fill("{Full}"); @@ -2782,14 +2833,18 @@ test.describe("Tokens: Remapping Feature", () => { .click(); await expect(tokensUpdateCreateModal).toBeVisible(); - nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); + nameField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Name", + }); await nameField.fill("derived-shadow"); const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceToggle.click(); - const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); await referenceField.fill("{base-shadow}"); submitButton = tokensUpdateCreateModal.getByRole("button", { @@ -2878,7 +2933,9 @@ test.describe("Tokens: Remapping Feature", () => { tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceToggle.click(); - const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); await referenceField.fill("{primary-shadow}"); submitButton = tokensUpdateCreateModal.getByRole("button", { @@ -2950,7 +3007,8 @@ test.describe("Tokens: Remapping Feature", () => { // Verify the shape still has the shadow applied with the UPDATED color value // Expand the shadow section to access the color field - const shadowSection = workspacePage.rightSidebar.getByTestId("shadow-section"); + const shadowSection = + workspacePage.rightSidebar.getByTestId("shadow-section"); await expect(shadowSection).toBeVisible(); // Click to expand the shadow options (the menu button) @@ -3008,14 +3066,18 @@ test.describe("Tokens: Remapping Feature", () => { .click(); await expect(tokensUpdateCreateModal).toBeVisible(); - nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); + nameField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Name", + }); await nameField.fill("body-text"); const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceToggle.click(); - const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}) + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); await referenceField.fill("{base-text}"); submitButton = tokensUpdateCreateModal.getByRole("button", { @@ -3096,14 +3158,18 @@ test.describe("Tokens: Remapping Feature", () => { .click(); await expect(tokensUpdateCreateModal).toBeVisible(); - nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); + nameField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Name", + }); await nameField.fill("paragraph-style"); const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceToggle.click(); - const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); await referenceField.fill("{body-style}"); submitButton = tokensUpdateCreateModal.getByRole("button", { diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index c58b8c8135..55f7c9a661 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -633,6 +633,43 @@ :shape-ids shape-ids :on-update-shape on-update-shape})))))))) +(defn toggle-border-radius-token + [{:keys [token attrs shape-ids expand-with-children]}] + (ptk/reify ::on-toggle-border-radius-token + ptk/WatchEvent + (watch [_ state _] + (let [objects (dsh/lookup-page-objects state) + shapes (into [] (keep (d/getf objects)) shape-ids) + + shapes + (if expand-with-children + (into [] + (mapcat (fn [shape] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape]))) + shapes) + shapes) + + {:keys [attributes all-attributes]} + (get token-properties (:type token)) + + unapply-tokens? + (cft/shapes-token-applied? token shapes (or attrs all-attributes attributes)) + + shape-ids (map :id shapes)] + + (if unapply-tokens? + (rx/of + (unapply-token {:attributes (or attrs all-attributes attributes) + :token token + :shape-ids shape-ids})) + (rx/of + (apply-token {:attributes attrs + :token token + :shape-ids shape-ids + :on-update-shape update-shape-radius-for-corners}))))))) + (defn apply-token-on-selected [color-operations token] 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 518d98a80c..6b65ec63c7 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -183,6 +183,7 @@ [:map [:id {:optional true} :string] [:class {:optional true} :string] + [:inner-class {:optional true} :string] [:value {:optional true} [:maybe [:or :int :float @@ -209,7 +210,8 @@ (mf/defc numeric-input* {::mf/schema schema:numeric-input} - [{:keys [id class value default placeholder icon disabled + [{:keys [id class value default placeholder + icon disabled inner-class min max max-length step is-selected-on-focus nillable tokens applied-token empty-to-end @@ -624,6 +626,7 @@ (mf/spread-props props {:ref ref :type "text" :id id + :class inner-class :placeholder (if is-multiple? (tr "labels.mixed-values") placeholder) @@ -644,7 +647,7 @@ :class (stl/css :icon)}]])) :slot-end (when-not disabled (when (some? tokens) - (mf/html [:> icon-button* {:variant "action" + (mf/html [:> icon-button* {:variant "ghost" :icon i/tokens :class (stl/css :invisible-button) :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") @@ -669,6 +672,7 @@ :on-token-key-down on-token-key-down :disabled disabled :on-blur on-blur + :class inner-class :slot-start (when icon (mf/html [:> tooltip* {:content property diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index ec9b2aec69..45a440b62d 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -33,12 +33,17 @@ } .invisible-button { + position: absolute; + inset-inline-end: 0; + inset-block-start: 0; opacity: var(--opacity-button); - + background-color: var(--color-background-quaternary); &:hover { + background-color: var(--color-background-quaternary); --opacity-button: 1; } &:focus { + background-color: var(--color-background-quaternary); --opacity-button: 1; } } diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index d604b60319..3633b0145b 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -26,7 +26,7 @@ [:map [:id {:optional true} :string] [:resolved-value {:optional true} - [:or :int :string]] + [:or :int :string :float]] [:name {:optional true} :string] [:icon {:optional true} schema:icon-list] [:label {:optional true} :string] 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 59f72657b4..ea5fef97ce 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 @@ -30,11 +30,11 @@ } .left-align { - left: 0; + left: var(--dropdown-offset, 0); } .right-align { - right: 0; + right: var(--dropdown-offset, 0); } .option-separator { diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs index f60e5350ae..3c13988cd3 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs @@ -18,7 +18,7 @@ [:map [:id {:optiona true} :string] [:ref some?] - [:resolved {:optional true} [:or :int :string]] + [:resolved {:optional true} [:or :int :string :float]] [:name {:optional true} :string] [:on-click {:optional true} fn?] [:selected {:optional true} :boolean] diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs index 1b3c9514c7..ece0fdb5d2 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs @@ -17,7 +17,7 @@ (def ^:private schema:input-field [:map - [:class {:optional true} :string] + [:class {:optional true} [:maybe :string]] [:aria-label {:optional true} [:maybe :string]] [:id :string] [:icon {:optional true} @@ -44,9 +44,10 @@ tooltip-id (mf/use-id) props (mf/spread-props props - {:class (stl/css-case - :input true - :input-with-icon (some? icon)) + {:class [class + (stl/css-case + :input true + :input-with-icon (some? icon))] :ref (or ref input-ref) :aria-invalid (when (and has-hint (= hint-type "error")) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs index b644899ea9..2e61cebdd8 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs @@ -19,6 +19,7 @@ (def ^:private schema:token-field [:map + [:class {:optional true} [:maybe :string]] [:id {:optional true} [:maybe :string]] [:label {:optional true} [:maybe :string]] [:value :any] @@ -32,7 +33,7 @@ (mf/defc token-field* {::mf/schema schema:token-field} - [{:keys [id label value slot-start disabled + [{:keys [id label value slot-start disabled class on-click on-token-key-down on-blur detach-token token-wrapper-ref token-detach-btn-ref on-focus]}] (let [set-active? (some? id) @@ -48,14 +49,11 @@ (fn [event] (when-not ^boolean disabled (dom/prevent-default event) - (dom/focus! (mf/ref-val token-wrapper-ref))))) + (dom/focus! (mf/ref-val token-wrapper-ref)))))] - class - (stl/css-case :token-field true - :with-icon (some? slot-start) - :token-field-disabled disabled)] - - [:div {:class class + [:div {:class [class (stl/css-case :token-field true + :with-icon (some? slot-start) + :token-field-disabled disabled)] :on-click focus-wrapper :disabled disabled :on-key-down on-token-key-down @@ -80,7 +78,7 @@ [:div {:class (stl/css :pill-dot)}])]] (when-not ^boolean disabled - [:> icon-button* {:variant "action" + [:> icon-button* {:variant "ghost" :class (stl/css :invisible-button) :icon i/broken-link :ref token-detach-btn-ref diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss index 37498bdf7e..a4ff7b8acc 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss @@ -8,6 +8,7 @@ @use "ds/_sizes.scss" as *; @use "ds/typography.scss" as t; @use "ds/colors.scss" as *; +@use "ds/mixins.scss" as *; .token-field { --token-field-bg-color: var(--color-background-tertiary); @@ -16,9 +17,7 @@ --token-field-outline-color: none; --token-field-height: var(--sp-xxxl); --token-field-margin: unset; - display: grid; - grid-template-columns: 1fr auto; column-gap: var(--sp-xs); align-items: center; position: relative; @@ -27,6 +26,7 @@ border-radius: $br-8; padding: var(--sp-xs); outline: $b-1 solid var(--token-field-outline-color); + position: relative; &:hover { --token-field-bg-color: var(--color-background-quaternary); @@ -39,7 +39,7 @@ } .with-icon { - grid-template-columns: auto 1fr auto; + grid-template-columns: auto 1fr; } .token-field-disabled { @@ -57,14 +57,17 @@ --pill-bg-color: var(--color-background-tertiary); --pill-fg-color: var(--color-token-foreground); @include t.use-typography("code-font"); - height: var(--sp-xxl); - width: fit-content; + @include textEllipsis; + display: block; + block-size: var(--sp-xxl); + inline-size: fit-content; background: var(--pill-bg-color); cursor: pointer; border: $b-1 solid var(--pill-border-color); color: var(--pill-fg-color); border-radius: $br-6; padding-inline: $sz-6; + max-inline-size: 100%; &:hover { --pill-bg-color: var(--color-token-background); --pill-fg-color: var(--color-foreground-primary); @@ -103,24 +106,29 @@ } .pill-dot { - width: $sz-6; - height: $sz-6; + inline-size: $sz-6; + block-size: $sz-6; outline: var(--sp-xxs) solid var(--color-background-primary); border-radius: 50%; background-color: var(--color-foreground-error); - margin-left: var(--sp-xs); + margin-inline-start: var(--sp-xs); position: absolute; - right: 0; - top: 0; + inset-inline-end: 0; + inset-block-start: 0; } .invisible-button { + position: absolute; + inset-inline-end: 0; + inset-block-start: 0; opacity: var(--opacity-button); - + background-color: var(--color-background-quaternary); &:hover { + background-color: var(--color-background-quaternary); --opacity-button: 1; } &:focus { + background-color: var(--color-background-quaternary); --opacity-button: 1; } } diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss index df9047d2dd..11df707d51 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss @@ -159,4 +159,6 @@ $arrow-side: 12px; block-size: fit-content; inline-size: fit-content; line-height: 0; + display: grid; + max-width: 100%; } 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 e425539f6d..1bafa4a95a 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 @@ -3,10 +3,15 @@ (:require [app.common.data.macros :as dm] [app.common.types.shape.radius :as ctsr] + [app.common.types.token :as tk] [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 :as deprecated-input] + [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 :refer [icon*] :as i] [app.main.ui.hooks :as hooks] [app.util.i18n :as i18n :refer [tr]] @@ -21,11 +26,17 @@ (defn- check-border-radius-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? (unchecked-get old-props "shapes") + (unchecked-get new-props "shapes")) + (identical? old-applied-tokens + new-applied-tokens) (identical? (get old-values :r1) (get new-values :r1)) (identical? (get old-values :r2) @@ -35,13 +46,114 @@ (identical? (get old-values :r4) (get new-values :r4))))) +(mf/defc numeric-input-wrapper* + {::mf/private true} + [{:keys [values name applied-tokens align on-detach radius] :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)) + + r1-value (get applied-tokens :r1) + all-token-equal? (and (seq applied-tokens) (all-equal? applied-tokens)) + all-values-equal? (all-equal? values) + + applied-token (cond + (not (seq applied-tokens)) + nil + + (and (= radius :all) (or (not all-values-equal?) (not all-token-equal?))) + :multiple + + (and all-token-equal? all-values-equal? (= radius :all)) + r1-value + + :else + (get applied-tokens radius)) + + + placeholder (if (= radius :all) + (cond + (or (not all-values-equal?) + (not all-token-equal?)) + (tr "settings.multiple") + :else + "--") + + (cond + (or (= :multiple (:applied-tokens values)) + (= :multiple (get values name))) + (tr "settings.multiple") + :else + "--")) + + + props (mf/spread-props props + {:placeholder placeholder + :applied-token applied-token + :tokens (if (delay? tokens) @tokens tokens) + :align align + :on-detach on-detach-attr + :value values})] + [:> numeric-input* props])) + (mf/defc border-radius-menu* {::mf/wrap [#(mf/memo' % check-border-radius-menu-props)]} - [{:keys [class ids values]}] - (let [all-equal? (all-equal? values) + [{:keys [class ids values applied-tokens]}] + (let [token-numeric-inputs + (features/use-feature "tokens/numeric-input") + + all-values-equal? (all-equal? values) + radius-expanded* (mf/use-state false) radius-expanded (deref radius-expanded*) + ;; DETACH + 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})))) + + on-detach-all + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (run! #(on-detach-token token %) [:r1 :r2 :r3 :r4]))) + + on-detach-r1 + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (on-detach-token token :r1))) + + on-detach-r2 + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (on-detach-token token :r2))) + + on-detach-r3 + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (on-detach-token token :r3))) + + on-detach-r4 + (mf/use-fn + (mf/deps on-detach-token) + (fn [token] + (on-detach-token token :r4))) + change-radius (mf/use-fn (mf/deps ids) @@ -54,31 +166,54 @@ {:reg-objects? true :attrs [:r1 :r2 :r3 :r4]}))) + change-one-radius + (mf/use-fn + (mf/deps ids) + (fn [update-fn attr] + (dwsh/update-shapes ids + (fn [shape] + (if (ctsr/has-radius? shape) + (update-fn shape) + shape)) + {:reg-objects? true + :attrs [attr]}))) + toggle-radius-mode (mf/use-fn (mf/deps radius-expanded) (fn [] (swap! radius-expanded* not))) + + on-all-radius-change + (mf/use-fn + (mf/deps change-radius ids) + (fn [value] + (if (or (string? value) (number? value)) + (st/emit! + (change-radius (fn [shape] + (ctsr/set-radius-to-all-corners shape value)))) + (doseq [attr [:r1 :r2 :r3 :r4]] + (st/emit! + (dwta/toggle-token {:token (first value) + :attrs #{attr} + :shape-ids ids})))))) + + on-single-radius-change (mf/use-fn - (mf/deps ids change-radius) - (fn [value] - (st/emit! - (change-radius (fn [shape] - (ctsr/set-radius-to-all-corners shape value)))))) - - - on-radius-4-change - (mf/use-fn - (mf/deps ids change-radius) + (mf/deps change-one-radius ids) (fn [value attr] - (st/emit! (change-radius #(ctsr/set-radius-to-single-corner % attr value))))) + (if (or (string? value) (number? value)) + (st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr)) + (st/emit! (dwta/toggle-border-radius-token {:token (first value) + :attrs #{attr} + :shape-ids ids}))))) - on-radius-r1-change #(on-radius-4-change % :r1) - on-radius-r2-change #(on-radius-4-change % :r2) - on-radius-r3-change #(on-radius-4-change % :r3) - on-radius-r4-change #(on-radius-4-change % :r4) + on-radius-r1-change #(on-single-radius-change % :r1) + on-radius-r2-change #(on-single-radius-change % :r2) + on-radius-r3-change #(on-single-radius-change % :r3) + on-radius-r4-change #(on-single-radius-change % :r4) expand-stream (mf/with-memo [] @@ -92,58 +227,139 @@ (mf/with-effect [ids] (reset! radius-expanded* false)) - [:div {:class (dm/str class " " (stl/css :radius))} + [:section {:class (dm/str class " " (stl/css :radius)) + :aria-label "border-radius-section"} (if (not radius-expanded) - [:div {:class (stl/css :radius-1) - :title (tr "workspace.options.radius")} - [:> icon* {:icon-id i/corner-radius - :size "s" - :class (stl/css :icon)}] - [:> deprecated-input/numeric-input* - {:placeholder (cond - (not all-equal?) - (tr "settings.multiple") - (= :multiple (:r1 values)) - (tr "settings.multiple") - :else - "--") - :min 0 - :nillable true - :on-change on-single-radius-change - :value (if all-equal? (:r1 values) nil)}]] - - [:div {:class (stl/css :radius-4)} - [:div {:class (stl/css :small-input)} - [:> deprecated-input/numeric-input* - {:placeholder "--" - :title (tr "workspace.options.radius-top-left") + (if token-numeric-inputs + [:> numeric-input-wrapper* + {:on-change on-all-radius-change + :on-detach on-detach-all + :icon i/corner-radius :min 0 - :on-change on-radius-r1-change - :value (:r1 values)}]] + :name :border-radius + :nillable true + :property (tr "workspace.options.radius") + :class (stl/css :radius-wrapper) + :applied-tokens applied-tokens + :radius :all + :align :right + :values (if all-values-equal? + (if (nil? (:r1 values)) + 0 + (:r1 values)) + nil)}] - [:div {:class (stl/css :small-input)} - [:> deprecated-input/numeric-input* - {:placeholder "--" - :title (tr "workspace.options.radius-top-right") - :min 0 - :on-change on-radius-r2-change - :value (:r2 values)}]] + [:div {:class (stl/css :radius-1) + :title (tr "workspace.options.radius")} + [:> icon* {:icon-id i/corner-radius + :size "s" + :class (stl/css :icon)}] + [:> deprecated-input/numeric-input* + {:placeholder (cond + (not all-values-equal?) + (tr "settings.multiple") + (= :multiple (:r1 values)) + (tr "settings.multiple") + :else + "--") + :min 0 + :nillable true + :on-change on-all-radius-change + :value (if all-values-equal? + (if (nil? (:r1 values)) + 0 + (:r1 values)) + nil)}]]) - [:div {:class (stl/css :small-input)} - [:> deprecated-input/numeric-input* - {:placeholder "--" - :title (tr "workspace.options.radius-bottom-left") - :min 0 - :on-change on-radius-r4-change - :value (:r4 values)}]] + (if token-numeric-inputs + [:div {:class (stl/css :radius-4)} + [:> numeric-input-wrapper* + {:on-change on-radius-r1-change + :on-detach on-detach-r1 + :min 0 + :name :border-radius + :property (tr "workspace.options.radius-top-left") + :applied-tokens applied-tokens + :radius :r1 + :align :right + :class (stl/css :radius-wrapper :dropdown-offset) + :inner-class (stl/css :no-icon-input) + :values (:r1 values)}] - [:div {:class (stl/css :small-input)} - [:> deprecated-input/numeric-input* - {:placeholder "--" - :title (tr "workspace.options.radius-bottom-right") - :min 0 - :on-change on-radius-r3-change - :value (:r3 values)}]]]) + [:> numeric-input-wrapper* + {:on-change on-radius-r2-change + :on-detach on-detach-r2 + :min 0 + :name :border-radius + :nillable true + :property (tr "workspace.options.radius-top-right") + :applied-tokens applied-tokens + :align :right + :class (stl/css :radius-wrapper) + :inner-class (stl/css :no-icon-input) + :radius :r2 + :values (:r2 values)}] + + [:> numeric-input-wrapper* + {:on-change on-radius-r4-change + :on-detach on-detach-r4 + :min 0 + :name :border-radius + :nillable true + :property (tr "workspace.options.radius-bottom-left") + :applied-tokens applied-tokens + :class (stl/css :radius-wrapper :dropdown-offset) + :inner-class (stl/css :no-icon-input) + :radius :r4 + :align :right + :values (:r4 values)}] + + [:> numeric-input-wrapper* + {:on-change on-radius-r3-change + :on-detach on-detach-r3 + :min 0 + :name :border-radius + :nillable true + :property (tr "workspace.options.radius-bottom-right") + :applied-tokens applied-tokens + :radius :r3 + :align :right + :class (stl/css :radius-wrapper) + :inner-class (stl/css :no-icon-input) + :values (:r3 values)}]] + + [:div {:class (stl/css :radius-4)} + [:div {:class (stl/css :small-input)} + [:> deprecated-input/numeric-input* + {:placeholder "--" + :title (tr "workspace.options.radius-top-left") + :min 0 + :on-change on-radius-r1-change + :value (:r1 values)}]] + + [:div {:class (stl/css :small-input)} + [:> deprecated-input/numeric-input* + {:placeholder "--" + :title (tr "workspace.options.radius-top-right") + :min 0 + :on-change on-radius-r2-change + :value (:r2 values)}]] + + [:div {:class (stl/css :small-input)} + [:> deprecated-input/numeric-input* + {:placeholder "--" + :title (tr "workspace.options.radius-bottom-left") + :min 0 + :on-change on-radius-r4-change + :value (:r4 values)}]] + + [:div {:class (stl/css :small-input)} + [:> deprecated-input/numeric-input* + {:placeholder "--" + :title (tr "workspace.options.radius-bottom-right") + :min 0 + :on-change on-radius-r3-change + :value (:r3 values)}]]])) [:> icon-button* {:variant "ghost" :on-click toggle-radius-mode diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss index 1c230b4108..152b39b1d7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss @@ -5,6 +5,8 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography" as t; +@use "ds/_utils.scss" as *; .radius { display: grid; @@ -14,7 +16,7 @@ .radius-1 { @extend .input-element; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); } .radius-4 { @@ -25,9 +27,27 @@ .small-input { @extend .input-element; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); +} + +.selected { + border-color: var(--button-icon-border-color-selected); + background-color: var(--button-icon-background-color-selected); + color: var(--color-accent-primary); } .icon { - margin-inline: deprecated.$s-4; + margin-inline: var(--sp-xs); +} + +.radius-wrapper { + --dropdown-width: var(--7-columns-dropdown-width); +} + +.no-icon-input { + padding-inline-start: px2rem(6); +} + +.dropdown-offset { + --dropdown-offset: #{px2rem(-65)}; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 9987306617..3333dfd5cb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -78,7 +78,7 @@ (nil? (get values name))) (tr "settings.multiple") "--") - :class (stl/css :numeric-input-measures) + :class (stl/css :numeric-input-layout) :applied-token (get applied-tokens name) :tokens tokens :align align diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss index 18a2644d63..b89ea6197f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss @@ -358,6 +358,6 @@ align-items: center; } -.numeric-input-measures { +.numeric-input-layout { --dropdown-width: var(--7-columns-dropdown-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 3bc39653a0..8c705b47e1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -600,10 +600,7 @@ [:> border-radius-menu* {:class (stl/css :border-radius) :ids ids :values values - :applied-tokens applied-tokens - :shapes shapes - :shape shape}])]) - + :applied-tokens applied-tokens}])]) (when (or (options :clip-content) (options :show-in-viewer)) [:div {:class (stl/css :clip-show)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index 719a46a2d9..eb01d6e2dd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.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 { display: grid; @@ -156,7 +157,6 @@ gap: deprecated.$s-4; } -// TODO: Add a proper variable to this sizing .numeric-input-measures { --dropdown-width: var(--7-columns-dropdown-width); } 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 a11926743e..98626bb4ff 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 @@ -223,7 +223,6 @@ (cond (= existing ::not-found) (assoc acc t-attr new-val) (= existing new-val) acc - (nil? new-val) acc :else (assoc acc t-attr :multiple)))) merge-shape-attr @@ -237,10 +236,8 @@ (fn [acc shape-attrs applied-tokens] "Merges token values across all shape attributes. For each shape attribute, its corresponding token attributes are merged - into the accumulator. If applied tokens are empty, the accumulator is returned unchanged." - (if (seq applied-tokens) - (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs) - acc)) + into the accumulator." + (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)) extract-attrs (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}]