Add shortcuts for creating variants and properties (#7181)

*  Add shortcuts for creating variants and properties

* 📎 PR changes
This commit is contained in:
Luis de Dios 2025-08-26 09:32:41 +02:00 committed by GitHub
parent d80ef17623
commit f1e7149e88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 178 additions and 219 deletions

View File

@ -50,9 +50,8 @@
(def ref:annotations-state (def ref:annotations-state
(l/derived :workspace-annotations st/state)) (l/derived :workspace-annotations st/state))
(mf/defc component-annotation (mf/defc component-annotation*
{::mf/props :obj {::mf/private true}
::mf/private true}
[{:keys [id shape component rerender-fn]}] [{:keys [id shape component rerender-fn]}]
(let [main-instance? (:main-instance shape) (let [main-instance? (:main-instance shape)
component-id (:component-id shape) component-id (:component-id shape)
@ -500,8 +499,7 @@
(tr "workspace.options.component.variant.duplicated.copy.locate")]]))])) (tr "workspace.options.component.variant.duplicated.copy.locate")]]))]))
(mf/defc component-swap-item (mf/defc component-swap-item*
{::mf/props :obj}
[{:keys [item loop shapes file-id root-shape container component-id is-search listing-thumbs num-variants]}] [{:keys [item loop shapes file-id root-shape container component-id is-search listing-thumbs num-variants]}]
(let [on-select (let [on-select
(mf/use-fn (mf/use-fn
@ -535,8 +533,7 @@
[:span {:class (stl/css-case :variant-mark-cell listing-thumbs :variant-icon true) [:span {:class (stl/css-case :variant-mark-cell listing-thumbs :variant-icon true)
:title (tr "workspace.assets.components.num-variants" num-variants)} i/variant])])) :title (tr "workspace.assets.components.num-variants" num-variants)} i/variant])]))
(mf/defc component-group-item (mf/defc component-group-item*
{::mf/props :obj}
[{:keys [item on-enter-group]}] [{:keys [item on-enter-group]}]
(let [group-name (:name item) (let [group-name (:name item)
on-group-click #(on-enter-group group-name)] on-group-click #(on-enter-group group-name)]
@ -571,8 +568,7 @@
(= (:component-id shape-a) (= (:component-id shape-a)
(:component-id shape-b))) (:component-id shape-b)))
(mf/defc component-swap (mf/defc component-swap*
{::mf/props :obj}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [single? (= 1 (count shapes)) (let [single? (= 1 (count shapes))
shape (first shapes) shape (first shapes)
@ -753,7 +749,7 @@
(when (:listing-thumbs? filters) (when (:listing-thumbs? filters)
[:div {:class (stl/css :component-list)} [:div {:class (stl/css :component-list)}
(for [item groups] (for [item groups]
[:& component-group-item {:item item :on-enter-group on-enter-group}])]) [:> component-group-item* {:item item :on-enter-group on-enter-group}])])
[:div {:class (stl/css-case :component-grid (:listing-thumbs? filters) [:div {:class (stl/css-case :component-grid (:listing-thumbs? filters)
:component-list (not (:listing-thumbs? filters)))} :component-list (not (:listing-thumbs? filters)))}
@ -766,24 +762,23 @@
(keep :component-id) (keep :component-id)
set) set)
loop? (some #(contains? components %) parent-components)] loop? (some #(contains? components %) parent-components)]
[:& component-swap-item {:key (dm/str (:id item)) [:> component-swap-item* {:key (dm/str (:id item))
:item item :item item
:loop loop? :loop loop?
:shapes shapes :shapes shapes
:file-id current-library-id :file-id current-library-id
:root-shape root-shape :root-shape root-shape
:container container :container container
:component-id component-id :component-id component-id
:is-search is-search? :is-search is-search?
:listing-thumbs (:listing-thumbs? filters) :listing-thumbs (:listing-thumbs? filters)
:num-variants (count-variants item)}]) :num-variants (count-variants item)}])
[:& component-group-item {:item item [:> component-group-item* {:item item
:key (:name item) :key (:name item)
:on-enter-group on-enter-group}]))]]]])) :on-enter-group on-enter-group}]))]]]]))
(mf/defc component-ctx-menu (mf/defc component-ctx-menu*
{::mf/props :obj}
[{:keys [menu-entries on-close show main-instance]}] [{:keys [menu-entries on-close show main-instance]}]
(let [do-action (let [do-action
(fn [action event] (fn [action event]
@ -879,8 +874,20 @@
(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap))) (when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap)))
(tm/schedule-on-idle #(dom/focus! (dom/get-element search-id)))))) (tm/schedule-on-idle #(dom/focus! (dom/get-element search-id))))))
create-variant
(mf/use-fn
(mf/deps id)
#(st/emit! (dwv/add-new-variant id)))
add-new-property
(mf/use-fn
(mf/deps shape)
#(st/emit! (dwv/add-new-property (:variant-id shape) {:property-value "Value 1"
:editing? true})))
on-combine-as-variants on-combine-as-variants
#(st/emit! (dwv/combine-as-variants)) (mf/use-fn
#(st/emit! (dwv/combine-as-variants)))
;; NOTE: function needed for force rerender from the bottom ;; NOTE: function needed for force rerender from the bottom
;; components. This is because `component-annotation` ;; components. This is because `component-annotation`
@ -903,7 +910,8 @@
(if swap-opened? (if swap-opened?
[:button {:class (stl/css :title-back) [:button {:class (stl/css :title-back)
:on-click on-component-back} :on-click on-component-back}
[:span {:class (stl/css :icon-back)} i/arrow] [:> icon* {:icon-id "arrow-left"
:size "s"}]
[:span (tr "workspace.options.component")]] [:span (tr "workspace.options.component")]]
[:* [:*
@ -921,60 +929,74 @@
(tr "workspace.options.component.copy"))]] (tr "workspace.options.component.copy"))]]
(when is-variant? (when is-variant?
[:div {:class (stl/css :variants-help-modal-button)} [:> icon-button* {:variant "ghost"
[:> icon-button* {:variant "ghost" :aria-label (tr "workspace.options.component.variants-help-modal.title")
:aria-label (tr "workspace.options.component.variants-help-modal.title") :on-click on-click-variant-title-help
:on-click on-click-variant-title-help :icon "help"}])
:icon "help"}]])])]
(when (and is-variant? main-instance?)
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant")
:on-click create-variant
:icon "variant"}])])]
(when open? (when open?
[:div {:class (stl/css :element-content)} [:div {:class (stl/css :element-content)}
[:div {:class (stl/css-case :component-wrapper true [:div {:class (stl/css :component-line)}
:with-actions show-menu?
:without-actions (not show-menu?))}
[:button {:class (stl/css-case :component-name-wrapper true
:with-main (and can-swap? (not multi))
:swappeable (and can-swap? (not swap-opened?)))
:data-testid "swap-component-btn"
:on-click open-component-panel}
[:span {:class (stl/css :component-icon)} [:div {:class (stl/css :component-wrapper)}
(if main-instance?
(if is-variant?
i/variant
i/component)
i/component-copy)]
[:div {:class (stl/css :name-wrapper)} [:button {:class (stl/css-case :component-name-wrapper true
[:div {:class (stl/css :component-name)} :without-menu (not show-menu?)
[:span {:class (stl/css :component-name-inside)} :swappeable (and can-swap? (not swap-opened?)))
(if (and multi (not same-variant?)) :data-testid "swap-component-btn"
(tr "settings.multiple") :on-click open-component-panel}
(cfh/last-path shape-name))]]
(when (and can-swap? (not multi)) [:div {:class (stl/css :component-icon)}
[:div {:class (stl/css :component-parent-name)} [:> icon* {:size "s"
(if (:deleted component) :icon-id (if main-instance?
(tr "workspace.options.component.unlinked") (if is-variant? "variant" "component")
(cfh/merge-path-item-with-dot path (:name component)))])]] "component-copy")}]]
(when show-menu? [:div {:class (stl/css :component-name-outside)}
[:div {:class (stl/css :component-actions)} [:div {:class (stl/css :component-name)}
[:button {:class (stl/css-case :menu-btn true [:span {:class (stl/css :component-name-inside)}
:selected menu-open?) (if (and multi (not same-variant?))
:on-click on-menu-click} (tr "settings.multiple")
i/menu] (cfh/last-path shape-name))]]
[:& component-ctx-menu {:show menu-open? (when (and can-swap? (not multi))
:on-close on-menu-close [:div {:class (stl/css :component-parent-name)}
:menu-entries menu-entries (if (:deleted component)
:main-instance main-instance?}]])] (tr "workspace.options.component.unlinked")
(cfh/merge-path-item-with-dot path (:name component)))])]]
(when show-menu?
[:div {:class (stl/css :component-actions)}
[:button {:class (stl/css-case :component-menu-btn true
:selected menu-open?)
:on-click on-menu-click}
[:> icon* {:icon-id "menu"}]]
[:> component-ctx-menu* {:show menu-open?
:on-close on-menu-close
:menu-entries menu-entries
:main-instance main-instance?}]])]
(when (and is-variant? main-instance?)
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant-property")
:on-click add-new-property
:icon "add"}])]
(when swap-opened? (when swap-opened?
[:& component-swap {:shapes copies}]) [:> component-swap* {:shapes copies}])
(when (and (not swap-opened?) (not multi)) (when (and (not swap-opened?) (not multi))
[:& component-annotation {:id id :shape shape :component component :rerender-fn rerender-fn}]) [:> component-annotation* {:id id
:shape shape
:component component
:rerender-fn rerender-fn}])
(when (and is-variant? (when (and is-variant?
(not main-instance?) (not main-instance?)
@ -1044,11 +1066,21 @@
menu-open* (mf/use-state false) menu-open* (mf/use-state false)
menu-open? (deref menu-open*) menu-open? (deref menu-open*)
menu-entries [{:title (tr "workspace.shape.menu.add-variant-property") create-variant
:action #(st/emit! (dwv/add-new-property variant-id {:property-value "Value 1" (mf/use-fn
:editing? true}))} (mf/deps shape)
{:title (tr "workspace.shape.menu.add-variant") #(st/emit! (dwv/add-new-variant (:id shape))))
:action #(st/emit! (dwv/add-new-variant (:id shape)))}]
add-new-property
(mf/use-fn
(mf/deps variant-id)
#(st/emit! (dwv/add-new-property variant-id {:property-value "Value 1"
:editing? true})))
menu-entries [{:title (tr "workspace.shape.menu.add-variant-property")
:action add-new-property}
{:title (tr "workspace.shape.menu.add-variant")
:action create-variant}]
toggle-content toggle-content
(mf/use-fn (mf/use-fn
@ -1118,42 +1150,52 @@
[:span {:class (stl/css :copy-text)} [:span {:class (stl/css :copy-text)}
(tr "workspace.options.component.main")]] (tr "workspace.options.component.main")]]
[:div {:class (stl/css :variants-help-modal-button)} [:div {:class (stl/css :title-actions)}
[:> icon-button* {:variant "ghost" [:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.component.variants-help-modal.title") :aria-label (tr "workspace.options.component.variants-help-modal.title")
:on-click on-click-variant-title-help :on-click on-click-variant-title-help
:icon "help"}]]]] :icon "help"}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant")
:on-click create-variant
:icon "variant"}]]]]
(when open? (when open?
[:div {:class (stl/css :element-content)} [:div {:class (stl/css :element-content)}
[:div {:class (stl/css-case :component-wrapper true [:div {:class (stl/css :component-line)}
:with-actions (not multi?)
:without-actions multi?)}
[:button {:class (stl/css-case :component-name-wrapper true
:with-main true
:swappeable false)}
[:span {:class (stl/css :component-icon)} i/component] [:div {:class (stl/css :component-wrapper)}
[:div {:class (stl/css :name-wrapper)} [:button {:class (stl/css :component-name-wrapper)}
[:div {:class (stl/css :component-name)}
[:span {:class (stl/css :component-name-inside)}
(if multi?
(tr "settings.multiple")
(cfh/last-path shape-name))]]]]
[:div {:class (stl/css :component-icon)}
[:> icon* {:size "s"
:icon-id "component"}]]
(when-not multi? [:div {:class (stl/css :component-name-outside)}
[:div {:class (stl/css :component-actions)} [:div {:class (stl/css :component-name)}
[:button {:class (stl/css-case :menu-btn true [:span {:class (stl/css :component-name-inside)}
:selected menu-open?) (if multi?
:on-click on-menu-click} (tr "settings.multiple")
i/menu] (cfh/last-path shape-name))]]]]
[:& component-ctx-menu {:show menu-open? (when-not multi?
:on-close on-menu-close [:div {:class (stl/css :component-actions)}
:menu-entries menu-entries
:main-instance true}]])] [:button {:class (stl/css-case :component-menu-btn true
:selected menu-open?)
:on-click on-menu-click}
[:> icon* {:icon-id "menu"}]]
[:> component-ctx-menu* {:show menu-open?
:on-close on-menu-close
:menu-entries menu-entries
:main-instance true}]])]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant-property")
:on-click add-new-property
:icon "add"}]]
(when-not multi? (when-not multi?
[:div {:class (stl/css :variant-property-list)} [:div {:class (stl/css :variant-property-list)}

View File

@ -9,6 +9,9 @@
.element-set { .element-set {
margin: 0; margin: 0;
display: grid;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs);
} }
.element-content { .element-content {
@ -21,9 +24,9 @@
} }
.element-title { .element-title {
display: grid; grid-column: span 8;
grid-template-columns: repeat(8, var(--sp-xxxl));
column-gap: var(--sp-xs); column-gap: var(--sp-xs);
display: flex;
} }
.title-back { .title-back {
@ -44,50 +47,31 @@
.title-spacing-component { .title-spacing-component {
justify-content: flex-start; justify-content: flex-start;
gap: $s-8; gap: $s-8;
grid-column: span 7; flex-grow: 1;
} }
.title-bar-variant { .title-bar-variant {
flex-grow: 0; flex-grow: 0;
} }
.variants-help-modal-button { .title-actions {
margin-inline-start: auto; display: flex;
grid-column: span 1; gap: var(--sp-xs);
} }
.icon-back { .component-line {
@include flexCenter;
width: $s-12;
height: 100%;
svg {
height: $s-12;
width: $s-12;
stroke: var(--icon-foreground);
transform: rotate(180deg);
}
}
.component-wrapper {
grid-column: span 8; grid-column: span 8;
width: 100%; width: 100%;
min-height: $s-32; min-height: $s-32;
border-radius: $br-8; border-radius: $br-8;
display: flex;
gap: var(--sp-xs);
}
&.with-actions { .component-wrapper {
display: flex; display: flex;
gap: $s-1; flex-grow: 1;
} gap: $s-1;
&.without-actions {
padding-right: 0.5rem;
.component-name-wrapper {
width: 100%;
border-radius: $br-8;
}
}
} }
.component-name-wrapper { .component-name-wrapper {
@ -102,24 +86,27 @@
background-color: var(--assets-item-background-color); background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover); color: var(--assets-item-name-foreground-color-hover);
&:hover { &.without-menu {
background-color: var(--assets-item-background-color-hover); width: 100%;
color: var(--assets-item-name-foreground-color-hover); border-radius: $br-8;
}
&.swappeable {
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
}
} }
} }
.component-icon { .component-icon {
@include flexCenter; display: flex;
height: $s-32; height: $s-32;
width: $s-16; align-items: center;
color: var(--icon-foreground);
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
} }
.name-wrapper { .component-name-outside {
@include flexColumn; @include flexColumn;
min-height: $s-32; min-height: $s-32;
padding: $s-8 0 $s-8 $s-2; padding: $s-8 0 $s-8 $s-2;
@ -156,32 +143,16 @@
width: $s-32; width: $s-32;
} }
.menu-btn { .component-menu-btn {
@extend .button-tertiary; @extend .button-secondary;
cursor: unset;
height: 100%; height: 100%;
width: 100%; width: 100%;
border-radius: 0 $br-8 $br-8 0; border-radius: 0 $br-8 $br-8 0;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
svg { &.selected {
@extend .button-icon; @extend .button-icon-selected;
min-height: $s-16;
min-width: $s-16;
} }
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
&.selected {
@extend .button-icon-selected;
}
}
}
.menu-btn.selected {
@extend .button-icon-selected;
} }
.copy-text { .copy-text {
@ -192,10 +163,6 @@
color: var(--title-foreground-color); color: var(--title-foreground-color);
} }
.swappeable {
cursor: pointer;
}
.custom-select-dropdown { .custom-select-dropdown {
@extend .dropdown-wrapper; @extend .dropdown-wrapper;
right: 0; right: 0;
@ -211,56 +178,6 @@
@extend .dropdown-element-base; @extend .dropdown-element-base;
} }
.icon-wrapper {
display: flex;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.input-text {
@include removeInputStyle;
height: $s-32;
width: 100%;
margin: 0;
padding: $s-4;
border: 0;
font-size: $fs-12;
color: var(--input-foreground-color-active);
&::placeholder {
color: var(--input-foreground-color-disabled);
}
&:focus-visible {
border-color: var(--input-border-outline-color-active);
}
}
.clear-btn {
@include buttonStyle;
@include flexCenter;
height: $s-16;
width: $s-16;
.clear-icon {
@include flexCenter;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
}
.search-icon {
@include flexCenter;
margin-left: $s-8;
flex: 0 0 $s-16;
}
.component-path { .component-path {
display: flex; display: flex;
align-items: center; align-items: center;