Compare commits

...

2 Commits

Author SHA1 Message Date
Eva Marco
0bf9008089 Add border radius token inputs on multiple selection 2026-01-12 13:00:22 +01:00
Eva Marco
52ed5fb242 Replace border radius numeric input 2026-01-08 13:07:15 +01:00
17 changed files with 398 additions and 97 deletions

View File

@@ -633,6 +633,43 @@
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
(defn toggle-br-token
[{:keys [token attrs shape-ids expand-with-children]}]
(ptk/reify ::on-toggle-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]

View File

@@ -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
@@ -205,11 +206,12 @@
[:on-focus {:optional true} fn?]
[:on-detach {:optional true} fn?]
[:property {:optional true} :string]
[:align {:optional true} [:maybe [:enum :left :right]]]])
[:align {:optional true} [:maybe [:enum :left :right :right-adjust]]]])
(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)
@@ -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

View File

@@ -32,13 +32,19 @@
min-width: var(--sp-l);
}
// TODO: Review if we need other type of button, so we don't need important here
.invisible-button {
position: absolute;
right: 4px;
top: 4px;
opacity: var(--opacity-button);
background-color: var(--color-background-quaternary) !important;
&:hover {
background-color: var(--color-background-quaternary);
--opacity-button: 1;
}
&:focus {
background-color: var(--color-background-quaternary);
--opacity-button: 1;
}
}

View File

@@ -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]
@@ -40,7 +40,7 @@
[:selected {:optional true} :any]
[:focused {:optional true} :any]
[:empty-to-end {:optional true} [:maybe :boolean]]
[:align {:optional true} [:maybe [:enum :left :right]]]])
[:align {:optional true} [:maybe [:enum :left :right :right-adjust]]]])
(def ^:private
xf:filter-blank-id
@@ -109,7 +109,8 @@
(mf/spread-props props
{:class (stl/css-case :option-list true
:left-align (= align :left)
:right-align (= align :right))
:right-align (= align :right)
:adjust-align (= align :right-adjust))
:tab-index "-1"
:role "listbox"})

View File

@@ -37,6 +37,10 @@
right: 0;
}
.adjust-align {
right: var(--dropdown-adjustment);
}
.option-separator {
border: $b-1 solid var(--options-dropdown-border-color);
margin-top: var(--sp-xs);

View File

@@ -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]

View File

@@ -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"))

View File

@@ -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

View File

@@ -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,8 @@
--token-field-outline-color: none;
--token-field-height: var(--sp-xxxl);
--token-field-margin: unset;
display: grid;
grid-template-columns: 1fr auto;
width: inherit;
column-gap: var(--sp-xs);
align-items: center;
position: relative;
@@ -27,6 +27,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 +40,7 @@
}
.with-icon {
grid-template-columns: auto 1fr auto;
grid-template-columns: auto 1fr;
}
.token-field-disabled {
@@ -57,6 +58,8 @@
--pill-bg-color: var(--color-background-tertiary);
--pill-fg-color: var(--color-token-foreground);
@include t.use-typography("code-font");
@include textEllipsis;
display: block;
height: var(--sp-xxl);
width: fit-content;
background: var(--pill-bg-color);
@@ -65,6 +68,7 @@
color: var(--pill-fg-color);
border-radius: $br-6;
padding-inline: $sz-6;
max-width: 100%;
&:hover {
--pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary);
@@ -115,6 +119,9 @@
}
.invisible-button {
position: absolute;
right: 0;
top: 0;
opacity: var(--opacity-button);
&:hover {

View File

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

View File

@@ -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 "applied-tokens")
new-applied-tokens (unchecked-get new-props "applied-tokens")]
(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 (not (empty? 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)
@@ -53,6 +165,18 @@
shape))
{: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
@@ -60,25 +184,52 @@
(fn []
(swap! radius-expanded* not)))
on-single-radius-change
do-all-radius-change
(mf/use-fn
(mf/deps ids change-radius)
(mf/deps change-radius)
(fn [value]
(st/emit!
(change-radius (fn [shape]
(ctsr/set-radius-to-all-corners shape value))))))
on-radius-4-change
on-all-radius-change
(mf/use-fn
(mf/deps ids change-radius)
(fn [value attr]
(st/emit! (change-radius #(ctsr/set-radius-to-single-corner % attr value)))))
(mf/deps do-all-radius-change ids)
(fn [value]
(if (or (string? value) (number? value))
(do-all-radius-change value)
(do
(let [resolved-value (:resolved-value (first value))]
(doseq [attr [:r1 :r2 :r3 :r4]]
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids})))
(do-all-radius-change resolved-value))))))
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)
do-single-radius-change
(mf/use-fn
(mf/deps change-radius)
(fn [value attr]
(st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr))))
on-single-radius-change
(mf/use-fn
(mf/deps do-all-radius-change ids)
(fn [value attr]
(if (or (string? value) (number? value))
(do-single-radius-change value attr)
(do
(let [resolved-value (:resolved-value (first value))]
(st/emit! (dwta/toggle-br-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))
(do-single-radius-change resolved-value attr))))))
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 []
@@ -94,56 +245,136 @@
[:div {:class (dm/str class " " (stl/css :radius))}
(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 :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-top-right")
:min 0
:on-change on-radius-r2-change
:value (:r2 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-adjust
:class (stl/css :radius-wrapper :dropdown-adjustment)
:inner-class (stl/css :no-icon-input)
:values (:r1 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-adjustment)
:inner-class (stl/css :no-icon-input)
:radius :r4
:align :right-adjust
: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 :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 :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-bottom-right")
:min 0
:on-change on-radius-r3-change
:value (:r3 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* {:class (stl/css-case :selected radius-expanded)
:variant "ghost"

View File

@@ -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,15 +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(--button-icon-foreground-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-adjustment {
--dropdown-adjustment: #{px2rem(-65)};
}

View File

@@ -77,7 +77,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

View File

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

View File

@@ -605,9 +605,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)}
(when (options :clip-content)

View File

@@ -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;
@@ -188,7 +189,6 @@
@extend .button-icon;
}
// TODO: Add a proper variable to this sizing
.numeric-input-measures {
--dropdown-width: var(--7-columns-dropdown-width);
}

View File

@@ -236,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}]