♻️ Replace border radius inputs (#7953)

*  Replace border radius numeric input

*  Add border radius token inputs on multiple selection
This commit is contained in:
Eva Marco 2026-01-14 12:45:40 +01:00 committed by GitHub
parent c98373658e
commit dd2d03e6a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 475 additions and 124 deletions

View File

@ -50,7 +50,7 @@ const setupTokensFile = async (page, options = {}) => {
const { const {
file = "workspace/get-file-tokens.json", file = "workspace/get-file-tokens.json",
fileFragment = "workspace/get-file-fragment-tokens.json", fileFragment = "workspace/get-file-fragment-tokens.json",
flags = [], flags = ["enable-feature-token-input"],
} = options; } = options;
const workspacePage = new WorkspacePage(page); const workspacePage = new WorkspacePage(page);
@ -2242,6 +2242,56 @@ test.describe("Tokens: Apply token", () => {
).toBeVisible(); ).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 }) => { 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);
@ -2417,12 +2467,13 @@ test.describe("Tokens: Apply token", () => {
const nameField = tokensUpdateCreateModal.getByLabel("Name"); const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newTokenTitle); await nameField.fill(newTokenTitle);
const referenceTabButton = const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
tokensUpdateCreateModal.getByRole('button', { name: 'Use a reference' }); name: "Use a reference",
});
referenceTabButton.click(); referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByRole('textbox', { const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: 'Reference' name: "Reference",
}); });
await referenceField.fill("{Full}"); await referenceField.fill("{Full}");
@ -2782,14 +2833,18 @@ test.describe("Tokens: Remapping Feature", () => {
.click(); .click();
await expect(tokensUpdateCreateModal).toBeVisible(); await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("derived-shadow"); await nameField.fill("derived-shadow");
const referenceToggle = const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt"); tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click(); await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{base-shadow}"); await referenceField.fill("{base-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", { submitButton = tokensUpdateCreateModal.getByRole("button", {
@ -2878,7 +2933,9 @@ test.describe("Tokens: Remapping Feature", () => {
tokensUpdateCreateModal.getByTestId("reference-opt"); tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click(); await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{primary-shadow}"); await referenceField.fill("{primary-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", { 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 // Verify the shape still has the shadow applied with the UPDATED color value
// Expand the shadow section to access the color field // 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(); await expect(shadowSection).toBeVisible();
// Click to expand the shadow options (the menu button) // Click to expand the shadow options (the menu button)
@ -3008,14 +3066,18 @@ test.describe("Tokens: Remapping Feature", () => {
.click(); .click();
await expect(tokensUpdateCreateModal).toBeVisible(); await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("body-text"); await nameField.fill("body-text");
const referenceToggle = const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt"); tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click(); await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}) const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{base-text}"); await referenceField.fill("{base-text}");
submitButton = tokensUpdateCreateModal.getByRole("button", { submitButton = tokensUpdateCreateModal.getByRole("button", {
@ -3096,14 +3158,18 @@ test.describe("Tokens: Remapping Feature", () => {
.click(); .click();
await expect(tokensUpdateCreateModal).toBeVisible(); await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"}); nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("paragraph-style"); await nameField.fill("paragraph-style");
const referenceToggle = const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt"); tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click(); await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"}); const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{body-style}"); await referenceField.fill("{body-style}");
submitButton = tokensUpdateCreateModal.getByRole("button", { submitButton = tokensUpdateCreateModal.getByRole("button", {

View File

@ -633,6 +633,43 @@
:shape-ids shape-ids :shape-ids shape-ids
:on-update-shape on-update-shape})))))))) :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 (defn apply-token-on-selected
[color-operations token] [color-operations token]

View File

@ -183,6 +183,7 @@
[:map [:map
[:id {:optional true} :string] [:id {:optional true} :string]
[:class {:optional true} :string] [:class {:optional true} :string]
[:inner-class {:optional true} :string]
[:value {:optional true} [:maybe [:or [:value {:optional true} [:maybe [:or
:int :int
:float :float
@ -209,7 +210,8 @@
(mf/defc numeric-input* (mf/defc numeric-input*
{::mf/schema schema: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 min max max-length step
is-selected-on-focus nillable is-selected-on-focus nillable
tokens applied-token empty-to-end tokens applied-token empty-to-end
@ -624,6 +626,7 @@
(mf/spread-props props {:ref ref (mf/spread-props props {:ref ref
:type "text" :type "text"
:id id :id id
:class inner-class
:placeholder (if is-multiple? :placeholder (if is-multiple?
(tr "labels.mixed-values") (tr "labels.mixed-values")
placeholder) placeholder)
@ -644,7 +647,7 @@
:class (stl/css :icon)}]])) :class (stl/css :icon)}]]))
:slot-end (when-not disabled :slot-end (when-not disabled
(when (some? tokens) (when (some? tokens)
(mf/html [:> icon-button* {:variant "action" (mf/html [:> icon-button* {:variant "ghost"
:icon i/tokens :icon i/tokens
:class (stl/css :invisible-button) :class (stl/css :invisible-button)
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
@ -669,6 +672,7 @@
:on-token-key-down on-token-key-down :on-token-key-down on-token-key-down
:disabled disabled :disabled disabled
:on-blur on-blur :on-blur on-blur
:class inner-class
:slot-start (when icon :slot-start (when icon
(mf/html [:> tooltip* (mf/html [:> tooltip*
{:content property {:content property

View File

@ -33,12 +33,17 @@
} }
.invisible-button { .invisible-button {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
opacity: var(--opacity-button); opacity: var(--opacity-button);
background-color: var(--color-background-quaternary);
&:hover { &:hover {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
&:focus { &:focus {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
} }

View File

@ -26,7 +26,7 @@
[:map [:map
[:id {:optional true} :string] [:id {:optional true} :string]
[:resolved-value {:optional true} [:resolved-value {:optional true}
[:or :int :string]] [:or :int :string :float]]
[:name {:optional true} :string] [:name {:optional true} :string]
[:icon {:optional true} schema:icon-list] [:icon {:optional true} schema:icon-list]
[:label {:optional true} :string] [:label {:optional true} :string]

View File

@ -30,11 +30,11 @@
} }
.left-align { .left-align {
left: 0; left: var(--dropdown-offset, 0);
} }
.right-align { .right-align {
right: 0; right: var(--dropdown-offset, 0);
} }
.option-separator { .option-separator {

View File

@ -18,7 +18,7 @@
[:map [:map
[:id {:optiona true} :string] [:id {:optiona true} :string]
[:ref some?] [:ref some?]
[:resolved {:optional true} [:or :int :string]] [:resolved {:optional true} [:or :int :string :float]]
[:name {:optional true} :string] [:name {:optional true} :string]
[:on-click {:optional true} fn?] [:on-click {:optional true} fn?]
[:selected {:optional true} :boolean] [:selected {:optional true} :boolean]

View File

@ -17,7 +17,7 @@
(def ^:private schema:input-field (def ^:private schema:input-field
[:map [:map
[:class {:optional true} :string] [:class {:optional true} [:maybe :string]]
[:aria-label {:optional true} [:maybe :string]] [:aria-label {:optional true} [:maybe :string]]
[:id :string] [:id :string]
[:icon {:optional true} [:icon {:optional true}
@ -44,9 +44,10 @@
tooltip-id (mf/use-id) tooltip-id (mf/use-id)
props (mf/spread-props props props (mf/spread-props props
{:class (stl/css-case {:class [class
:input true (stl/css-case
:input-with-icon (some? icon)) :input true
:input-with-icon (some? icon))]
:ref (or ref input-ref) :ref (or ref input-ref)
:aria-invalid (when (and has-hint :aria-invalid (when (and has-hint
(= hint-type "error")) (= hint-type "error"))

View File

@ -19,6 +19,7 @@
(def ^:private schema:token-field (def ^:private schema:token-field
[:map [:map
[:class {:optional true} [:maybe :string]]
[:id {:optional true} [:maybe :string]] [:id {:optional true} [:maybe :string]]
[:label {:optional true} [:maybe :string]] [:label {:optional true} [:maybe :string]]
[:value :any] [:value :any]
@ -32,7 +33,7 @@
(mf/defc token-field* (mf/defc token-field*
{::mf/schema schema: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 on-click on-token-key-down on-blur detach-token
token-wrapper-ref token-detach-btn-ref on-focus]}] token-wrapper-ref token-detach-btn-ref on-focus]}]
(let [set-active? (some? id) (let [set-active? (some? id)
@ -48,14 +49,11 @@
(fn [event] (fn [event]
(when-not ^boolean disabled (when-not ^boolean disabled
(dom/prevent-default event) (dom/prevent-default event)
(dom/focus! (mf/ref-val token-wrapper-ref))))) (dom/focus! (mf/ref-val token-wrapper-ref)))))]
class [:div {:class [class (stl/css-case :token-field true
(stl/css-case :token-field true :with-icon (some? slot-start)
:with-icon (some? slot-start) :token-field-disabled disabled)]
:token-field-disabled disabled)]
[:div {:class class
:on-click focus-wrapper :on-click focus-wrapper
:disabled disabled :disabled disabled
:on-key-down on-token-key-down :on-key-down on-token-key-down
@ -80,7 +78,7 @@
[:div {:class (stl/css :pill-dot)}])]] [:div {:class (stl/css :pill-dot)}])]]
(when-not ^boolean disabled (when-not ^boolean disabled
[:> icon-button* {:variant "action" [:> icon-button* {:variant "ghost"
:class (stl/css :invisible-button) :class (stl/css :invisible-button)
:icon i/broken-link :icon i/broken-link
:ref token-detach-btn-ref :ref token-detach-btn-ref

View File

@ -8,6 +8,7 @@
@use "ds/_sizes.scss" as *; @use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t; @use "ds/typography.scss" as t;
@use "ds/colors.scss" as *; @use "ds/colors.scss" as *;
@use "ds/mixins.scss" as *;
.token-field { .token-field {
--token-field-bg-color: var(--color-background-tertiary); --token-field-bg-color: var(--color-background-tertiary);
@ -16,9 +17,7 @@
--token-field-outline-color: none; --token-field-outline-color: none;
--token-field-height: var(--sp-xxxl); --token-field-height: var(--sp-xxxl);
--token-field-margin: unset; --token-field-margin: unset;
display: grid; display: grid;
grid-template-columns: 1fr auto;
column-gap: var(--sp-xs); column-gap: var(--sp-xs);
align-items: center; align-items: center;
position: relative; position: relative;
@ -27,6 +26,7 @@
border-radius: $br-8; border-radius: $br-8;
padding: var(--sp-xs); padding: var(--sp-xs);
outline: $b-1 solid var(--token-field-outline-color); outline: $b-1 solid var(--token-field-outline-color);
position: relative;
&:hover { &:hover {
--token-field-bg-color: var(--color-background-quaternary); --token-field-bg-color: var(--color-background-quaternary);
@ -39,7 +39,7 @@
} }
.with-icon { .with-icon {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr;
} }
.token-field-disabled { .token-field-disabled {
@ -57,14 +57,17 @@
--pill-bg-color: var(--color-background-tertiary); --pill-bg-color: var(--color-background-tertiary);
--pill-fg-color: var(--color-token-foreground); --pill-fg-color: var(--color-token-foreground);
@include t.use-typography("code-font"); @include t.use-typography("code-font");
height: var(--sp-xxl); @include textEllipsis;
width: fit-content; display: block;
block-size: var(--sp-xxl);
inline-size: fit-content;
background: var(--pill-bg-color); background: var(--pill-bg-color);
cursor: pointer; cursor: pointer;
border: $b-1 solid var(--pill-border-color); border: $b-1 solid var(--pill-border-color);
color: var(--pill-fg-color); color: var(--pill-fg-color);
border-radius: $br-6; border-radius: $br-6;
padding-inline: $sz-6; padding-inline: $sz-6;
max-inline-size: 100%;
&:hover { &:hover {
--pill-bg-color: var(--color-token-background); --pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary); --pill-fg-color: var(--color-foreground-primary);
@ -103,24 +106,29 @@
} }
.pill-dot { .pill-dot {
width: $sz-6; inline-size: $sz-6;
height: $sz-6; block-size: $sz-6;
outline: var(--sp-xxs) solid var(--color-background-primary); outline: var(--sp-xxs) solid var(--color-background-primary);
border-radius: 50%; border-radius: 50%;
background-color: var(--color-foreground-error); background-color: var(--color-foreground-error);
margin-left: var(--sp-xs); margin-inline-start: var(--sp-xs);
position: absolute; position: absolute;
right: 0; inset-inline-end: 0;
top: 0; inset-block-start: 0;
} }
.invisible-button { .invisible-button {
position: absolute;
inset-inline-end: 0;
inset-block-start: 0;
opacity: var(--opacity-button); opacity: var(--opacity-button);
background-color: var(--color-background-quaternary);
&:hover { &:hover {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
&:focus { &:focus {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
} }

View File

@ -159,4 +159,6 @@ $arrow-side: 12px;
block-size: fit-content; block-size: fit-content;
inline-size: fit-content; inline-size: fit-content;
line-height: 0; line-height: 0;
display: grid;
max-width: 100%;
} }

View File

@ -3,10 +3,15 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.shape.radius :as ctsr] [app.common.types.shape.radius :as ctsr]
[app.common.types.token :as tk]
[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.store :as st] [app.main.store :as st]
[app.main.ui.components.numeric-input :as deprecated-input] [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.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.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -21,11 +26,17 @@
(defn- check-border-radius-menu-props (defn- check-border-radius-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? (unchecked-get old-props "shapes")
(unchecked-get new-props "shapes"))
(identical? old-applied-tokens
new-applied-tokens)
(identical? (get old-values :r1) (identical? (get old-values :r1)
(get new-values :r1)) (get new-values :r1))
(identical? (get old-values :r2) (identical? (get old-values :r2)
@ -35,13 +46,114 @@
(identical? (get old-values :r4) (identical? (get old-values :r4)
(get new-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/defc border-radius-menu*
{::mf/wrap [#(mf/memo' % check-border-radius-menu-props)]} {::mf/wrap [#(mf/memo' % check-border-radius-menu-props)]}
[{:keys [class ids values]}] [{:keys [class ids values applied-tokens]}]
(let [all-equal? (all-equal? values) (let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
all-values-equal? (all-equal? values)
radius-expanded* (mf/use-state false) radius-expanded* (mf/use-state false)
radius-expanded (deref radius-expanded*) 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 change-radius
(mf/use-fn (mf/use-fn
(mf/deps ids) (mf/deps ids)
@ -54,31 +166,54 @@
{:reg-objects? true {:reg-objects? true
:attrs [:r1 :r2 :r3 :r4]}))) :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 toggle-radius-mode
(mf/use-fn (mf/use-fn
(mf/deps radius-expanded) (mf/deps radius-expanded)
(fn [] (fn []
(swap! radius-expanded* not))) (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 on-single-radius-change
(mf/use-fn (mf/use-fn
(mf/deps ids change-radius) (mf/deps change-one-radius ids)
(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)
(fn [value attr] (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-r1-change #(on-single-radius-change % :r1)
on-radius-r2-change #(on-radius-4-change % :r2) on-radius-r2-change #(on-single-radius-change % :r2)
on-radius-r3-change #(on-radius-4-change % :r3) on-radius-r3-change #(on-single-radius-change % :r3)
on-radius-r4-change #(on-radius-4-change % :r4) on-radius-r4-change #(on-single-radius-change % :r4)
expand-stream expand-stream
(mf/with-memo [] (mf/with-memo []
@ -92,58 +227,139 @@
(mf/with-effect [ids] (mf/with-effect [ids]
(reset! radius-expanded* false)) (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) (if (not radius-expanded)
[:div {:class (stl/css :radius-1) (if token-numeric-inputs
:title (tr "workspace.options.radius")} [:> numeric-input-wrapper*
[:> icon* {:icon-id i/corner-radius {:on-change on-all-radius-change
:size "s" :on-detach on-detach-all
:class (stl/css :icon)}] :icon i/corner-radius
[:> 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")
:min 0 :min 0
:on-change on-radius-r1-change :name :border-radius
:value (:r1 values)}]] :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)} [:div {:class (stl/css :radius-1)
[:> deprecated-input/numeric-input* :title (tr "workspace.options.radius")}
{:placeholder "--" [:> icon* {:icon-id i/corner-radius
:title (tr "workspace.options.radius-top-right") :size "s"
:min 0 :class (stl/css :icon)}]
:on-change on-radius-r2-change [:> deprecated-input/numeric-input*
:value (:r2 values)}]] {: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)} (if token-numeric-inputs
[:> deprecated-input/numeric-input* [:div {:class (stl/css :radius-4)}
{:placeholder "--" [:> numeric-input-wrapper*
:title (tr "workspace.options.radius-bottom-left") {:on-change on-radius-r1-change
:min 0 :on-detach on-detach-r1
:on-change on-radius-r4-change :min 0
:value (:r4 values)}]] :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)} [:> numeric-input-wrapper*
[:> deprecated-input/numeric-input* {:on-change on-radius-r2-change
{:placeholder "--" :on-detach on-detach-r2
:title (tr "workspace.options.radius-bottom-right") :min 0
:min 0 :name :border-radius
:on-change on-radius-r3-change :nillable true
:value (:r3 values)}]]]) :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" [:> icon-button* {:variant "ghost"
:on-click toggle-radius-mode :on-click toggle-radius-mode

View File

@ -5,6 +5,8 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography" as t;
@use "ds/_utils.scss" as *;
.radius { .radius {
display: grid; display: grid;
@ -14,7 +16,7 @@
.radius-1 { .radius-1 {
@extend .input-element; @extend .input-element;
@include deprecated.bodySmallTypography; @include t.use-typography("body-small");
} }
.radius-4 { .radius-4 {
@ -25,9 +27,27 @@
.small-input { .small-input {
@extend .input-element; @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 { .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)};
} }

View File

@ -78,7 +78,7 @@
(nil? (get values name))) (nil? (get values name)))
(tr "settings.multiple") (tr "settings.multiple")
"--") "--")
:class (stl/css :numeric-input-measures) :class (stl/css :numeric-input-layout)
:applied-token (get applied-tokens name) :applied-token (get applied-tokens name)
:tokens tokens :tokens tokens
:align align :align align

View File

@ -358,6 +358,6 @@
align-items: center; align-items: center;
} }
.numeric-input-measures { .numeric-input-layout {
--dropdown-width: var(--7-columns-dropdown-width); --dropdown-width: var(--7-columns-dropdown-width);
} }

View File

@ -600,10 +600,7 @@
[:> border-radius-menu* {:class (stl/css :border-radius) [:> border-radius-menu* {:class (stl/css :border-radius)
:ids ids :ids ids
:values values :values values
:applied-tokens applied-tokens :applied-tokens applied-tokens}])])
:shapes shapes
:shape shape}])])
(when (or (options :clip-content) (when (or (options :clip-content)
(options :show-in-viewer)) (options :show-in-viewer))
[:div {:class (stl/css :clip-show)} [:div {:class (stl/css :clip-show)}

View File

@ -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 { .element-set {
display: grid; display: grid;
@ -156,7 +157,6 @@
gap: deprecated.$s-4; gap: deprecated.$s-4;
} }
// TODO: Add a proper variable to this sizing
.numeric-input-measures { .numeric-input-measures {
--dropdown-width: var(--7-columns-dropdown-width); --dropdown-width: var(--7-columns-dropdown-width);
} }

View File

@ -223,7 +223,6 @@
(cond (cond
(= existing ::not-found) (assoc acc t-attr new-val) (= existing ::not-found) (assoc acc t-attr new-val)
(= existing new-val) acc (= existing new-val) acc
(nil? new-val) acc
:else (assoc acc t-attr :multiple)))) :else (assoc acc t-attr :multiple))))
merge-shape-attr merge-shape-attr
@ -237,10 +236,8 @@
(fn [acc shape-attrs applied-tokens] (fn [acc shape-attrs applied-tokens]
"Merges token values across all shape attributes. "Merges token values across all shape attributes.
For each shape attribute, its corresponding token attributes are merged For each shape attribute, its corresponding token attributes are merged
into the accumulator. If applied tokens are empty, the accumulator is returned unchanged." into the accumulator."
(if (seq applied-tokens) (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs))
(reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)
acc))
extract-attrs extract-attrs
(fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}] (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}]