0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-23 07:16:07 -05:00

💄 New UI for Swap Component

This commit is contained in:
Pablo Alba 2023-11-09 19:19:21 +01:00
parent de3605356c
commit 5cf93e7a3d
4 changed files with 449 additions and 263 deletions

View file

@ -187,10 +187,14 @@
--assets-item-background-color-hover: var(--color-background-quaternary);
--assets-item-name-foreground-color: var(--color-foreground-secondary);
--assets-item-name-foreground-color-hover: var(--color-foreground-primary);
--assets-item-name-foreground-color-disabled: var(--color-foreground-disabled);
--assets-item-border-color: var(--color-accent-primary);
--assets-item-background-color-drag: var(--color-accent-primary-muted);
--assets-item-border-color-drag: var(--color-select);
--assets-component-background-color: var(--white); // We don't want this color to change with palette
--assets-component-background-color-disabled: var(
--off-white
); // We don't want this color to change with palette
--radio-btns-background-color: var(--color-background-tertiary);
--radio-btn-background-color-selected: var(--color-background-quaternary);

View file

@ -2591,128 +2591,3 @@
cursor: pointer;
}
}
.component-swap {
.search-block {
margin: 0.7rem 0.5rem 0.2rem 0.2rem;
height: 2.1rem;
width: 100%;
}
svg {
fill: $color-gray-20;
height: 0.7rem;
width: 0.7rem;
cursor: pointer;
}
.search-block {
border: 1px solid $color-gray-30;
margin: 0.6rem 0.5rem 0.2rem 0.2rem;
padding: $size-1 $size-2;
display: flex;
align-items: center;
&:hover {
border-color: $color-gray-20;
}
&:focus-within {
border-color: $color-primary;
}
& .search-input {
background-color: $color-gray-50;
border: none;
color: $color-gray-10;
font-size: $fs12;
margin: 0;
padding: 0;
flex-grow: 1;
&:focus {
color: lighten($color-gray-10, 8%);
outline: none;
}
}
& .search-icon {
display: flex;
align-items: center;
svg {
fill: $color-gray-30;
height: 16px;
width: 16px;
}
&.close {
transform: rotate(45deg);
cursor: pointer;
}
}
}
.component-path {
display: flex;
margin: 0.4rem 0 0 0.4rem;
cursor: pointer;
svg {
height: 8px;
width: 8px;
margin-right: 0.5rem;
transform: rotate(180deg);
}
}
.component-list {
margin: 0.7rem 0.5rem 0.5rem 0.5rem;
}
.component-item {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
font-size: 12px;
cursor: pointer;
svg,
img {
background-color: $color-canvas;
border-radius: $br4;
border: 2px solid transparent;
height: 24px;
width: 24px;
margin-right: $size-2;
}
.selected {
color: $color-primary;
}
&:hover {
color: $color-primary;
}
&.disabled {
cursor: auto;
color: $color-gray-30;
}
}
.component-group {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
justify-content: space-between;
font-size: 12px;
cursor: pointer;
height: 24px;
svg {
height: 8px;
width: 8px;
}
&:hover {
color: $color-primary;
}
}
}

View file

@ -19,6 +19,9 @@
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.context :as ctx]
[app.main.ui.hooks :as h]
@ -26,7 +29,6 @@
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -149,7 +151,7 @@
(mf/defc component-swap-item
{::mf/wrap-props false}
[{:keys [item loop shapes file-id root-shape container component-id is-search] :as props}]
[{:keys [item loop shapes file-id root-shape container component-id is-search listing-thumbs] :as props}]
(let [on-select-component
(mf/use-fn
(mf/deps shapes file-id item)
@ -157,9 +159,12 @@
(st/emit! (dwl/component-multi-swap shapes file-id (:id item)))))
item-ref (mf/use-ref)
visible? (h/use-visible item-ref :once? true)]
[:div.component-item
[:div
{:ref item-ref
:class (stl/css-case :disabled loop)
:class (stl/css-case :component-item (not listing-thumbs)
:grid-cell listing-thumbs
:selected (= (:id item) component-id)
:disabled loop)
:key (str "swap-item-" (:id item))
:on-click on-select-component}
(when visible?
@ -167,16 +172,28 @@
:root-shape root-shape
:component item
:container container}])
[:span.component-name
{:class (stl/css-case :selected (= (:id item) component-id))}
[:span
{:class (stl/css-case :component-name true :selected (= (:id item) component-id))}
(if is-search (:full-name item) (:name item))]]))
(mf/defc component-group-item
[{:keys [item on-enter-group] :as props}]
(let [group-name (:name item)
path (cph/butlast-path group-name)
on-group-click #(on-enter-group group-name)]
[:div {:class (stl/css :component-group)
:key (uuid/next) :on-click on-group-click}
[:div
(when-not (str/blank? path)
[:span {:class (stl/css :component-group-path)} (str "\u00A0/\u00A0" path)])
[:span {:class (stl/css :component-group-name)} (cph/last-path group-name)]]
[:span i/arrow-slide]]))
(mf/defc component-swap
[{:keys [shapes] :as props}]
(let [single? (= 1 (count shapes))
shape (first shapes)
new-css-system false ;;(mf/use-ctx ctx/new-css-system) ;;TODO do not show new-css-system until we have the new design
current-file-id (mf/use-ctx ctx/current-file-id)
workspace-file (deref refs/workspace-file)
workspace-data (deref refs/workspace-data)
@ -211,7 +228,8 @@
filters* (mf/use-state
{:term ""
:file-id file-id
:path path})
:path path
:listing-thumbs? false})
filters (deref filters*)
is-search? (not (str/blank? (:term filters)))
@ -236,13 +254,14 @@
(filter #(= (cph/butlast-path %) (:path filters))))
groups (when-not is-search?
(sort (sequence xform components)))
(->> (sort (sequence xform components))
(map #(assoc {} :name %))))
components (if is-search?
(filter #(str/includes? (str/lower (:full-name %)) (str/lower (:term filters))) components)
(filter #(= (:path %) (:path filters)) components))
items (if is-search?
items (if (or is-search? (:listing-thumbs? filters))
(sort-by :full-name components)
(->> (concat groups components)
(sort-by :name)))
@ -262,27 +281,22 @@
(mapcat #(get-comps-ids % []))
set)
libraries-options (map (fn [library] {:value (:id library) :label (:name library)}) (vals libraries))
current-library-id (:file-id filters)
current-library-name (if (= current-library-id current-file-id)
(str/upper (tr "workspace.assets.local-library"))
(get-in libraries [current-library-id :name]))
on-library-change
(mf/use-fn
(fn [event]
(let [value (or (-> (dom/get-target event)
(dom/get-value))
(as-> (dom/get-current-target event) $
(dom/get-attribute $ "data-test")))
value (uuid/uuid value)]
(swap! filters* assoc :file-id value :term "" :path ""))))
(fn [id]
(swap! filters* assoc :file-id id :term "" :path "")))
on-search-term-change
(mf/use-fn
(mf/deps new-css-system)
(fn [event]
;; NOTE: When old-css-system is removed this function will recibe value and event
;; Let won't be necessary any more
(let [value (if ^boolean new-css-system
event
(dom/get-target-val event))]
(swap! filters* assoc :term value))))
(fn [term]
(swap! filters* assoc :term term)))
on-search-clear-click
@ -296,47 +310,58 @@
on-enter-group
(mf/use-fn #(swap! filters* assoc :path %))
handle-key-down
toggle-list-style
(mf/use-fn
(fn [event]
(let [enter? (kbd/enter? event)
esc? (kbd/esc? event)
node (dom/event->target event)]
(fn [style]
(swap! filters* assoc :listing-thumbs? (= style "grid"))))]
(when ^boolean enter? (dom/blur! node))
(when ^boolean esc? (dom/blur! node)))))]
[:div.component-swap
[:div.element-set-title
[:div {:class (stl/css :component-swap)}
[:div {:class (stl/css :element-set-title)}
[:span (tr "workspace.options.component.swap")]]
[:div.component-swap-content
[:div.search-block
[:input.search-input
{:placeholder (str (tr "labels.search") " " (get-in libraries [(:file-id filters) :name]))
:type "text"
[:div {:class (stl/css :component-swap-content)}
[:div {:class (stl/css :search-field)}
[:& search-bar {:on-change on-search-term-change
:clear-action on-search-clear-click
:value (:term filters)
:on-change on-search-term-change
:on-key-down handle-key-down}]
:placeholder (str (tr "labels.search") " " (get-in libraries [(:file-id filters) :name]))
:icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]]
(if is-search?
[:div.search-icon.close
{:on-click on-search-clear-click}
i/close]
[:div.search-icon
i/search])]
[:div {:class (stl/css :select-field)}
[:& select
{:class (stl/css :select-library)
:default-value (:file-id filters)
:options libraries-options
:on-change on-library-change}]]
[:select.input-select {:value (:file-id filters)
:data-mousetrap-dont-stop true
:on-change on-library-change}
(for [library (vals libraries)]
[:option {:key (:id library) :value (:id library)} (:name library)])]
[:div {:class (stl/css :library-name)} current-library-name]
(when-not (or is-search? (str/empty? (:path filters)))
[:div.component-path {:on-click on-go-back}
[:div {:class (stl/css :listing-options-wrapper)}
[:& radio-buttons {:class (stl/css :listing-options)
:selected (if (:listing-thumbs? filters) "grid" "list")
:on-change toggle-list-style
:name "swap-listing-style"}
[:& radio-button {:icon i/view-as-list-refactor
:value "list"
:id "swap-opt-list"}]
[:& radio-button {:icon i/flex-grid-refactor
:value "grid"
:id "swap-opt-grid"}]]]
(if (or is-search? (str/empty? (:path filters)))
[:div {:class (stl/css :component-path-empty)}]
[:button {:class (stl/css :component-path)
:on-click on-go-back}
[:span i/arrow-slide]
[:span (-> (cph/split-path (:path filters))
last)]])
[:div.component-list
[:span (:path filters)]])
(when (:listing-thumbs? filters)
[:div {:class (stl/css :component-list)}
(for [item groups]
[:& component-group-item {:item item :on-enter-group on-enter-group}])])
[:div {:class (stl/css-case :component-grid (:listing-thumbs? filters)
:component-list (not (:listing-thumbs? filters)))}
(for [item items]
(if (:id item)
(let [data (get-in libraries [(:file-id filters) :data])
@ -351,11 +376,9 @@
:root-shape root-shape
:container container
:component-id current-comp-id
:is-search is-search?}])
(let [on-group-click #(on-enter-group item)]
[:div.component-group {:key (uuid/next) :on-click on-group-click}
[:span (cph/last-path item)]
[:span i/arrow-slide]])))]]]))
:is-search is-search?
:listing-thumbs (:listing-thumbs? filters)}])
[:& component-group-item {:item item :on-enter-group on-enter-group}]))]]]))
(mf/defc component-ctx-menu
[{:keys [menu-entries on-close show type] :as props}]
@ -379,8 +402,7 @@
(mf/defc component-menu
[{:keys [shapes swap-opened?] :as props}]
(let [new-css-system false ;;(mf/use-ctx ctx/new-css-system) ;;TODO do not show new-css-system until we have the new design
current-file-id (mf/use-ctx ctx/current-file-id)
(let [current-file-id (mf/use-ctx ctx/current-file-id)
components-v2 (mf/use-ctx ctx/components-v2)
workspace-data (deref refs/workspace-data)
workspace-libraries (deref refs/workspace-libraries)
@ -421,30 +443,42 @@
(mf/use-fn
#(st/emit! :interrupt))
open-component-panel
(mf/use-fn
(mf/deps can-swap? shapes)
#(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap shapes))))
menu-entries (cmm/generate-components-menu-entries shapes components-v2)
show-menu? (seq menu-entries)]
(when (seq shapes)
(if new-css-system
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
(if swap-opened?
[:button {:class (stl/css :title-back)
:on-click on-component-back}
[:span i/arrow-slide]
[:span (tr "workspace.options.component")]]
[:& title-bar {:collapsable? true
:collapsed? (not open?)
:on-collapsed toggle-content
:title (tr "workspace.options.component")
:class (stl/css :title-spacing-component)}]]
:class (stl/css :title-spacing-component)}])]
(when open?
[:div {:class (stl/css :element-content)}
[:div {:class (stl/css :component-wrapper)}
[:div {:class (stl/css :component-name-wrapper)}
[:div {:class (stl/css-case :component-name-wrapper true :with-main (and can-swap? (not multi)) :swappeable (and can-swap? (not swap-opened?)))
:on-click open-component-panel}
[:span {:class (stl/css :component-icon)}
(if main-instance?
i/component-refactor
i/copy-refactor)]
[:div {:class (stl/css :component-name)} shape-name]]
[:div {:class (stl/css :component-name)} (if multi
(tr "settings.multiple")
shape-name)]
(when show-menu?
[:div {:class (stl/css :component-actions)}
[:button {:class (stl/css :menu-btn)
:on-click on-menu-click}
@ -453,57 +487,12 @@
[:& component-ctx-menu {:show menu-open?
:on-close on-menu-close
:menu-entries menu-entries
:type :dropdown}]]]
(when (and (not multi) components-v2)
[:& component-annotation {:id id :shape shape :component component}])])]
[:div.element-set
(if swap-opened?
[:button.element-set-title.component-block-title {:class (stl/css-case :back swap-opened?)
:on-click on-component-back}
[:div
[:span i/arrow-slide]
[:span (tr "workspace.options.component")]]
(when-not multi
[:span (if main-instance?
(tr "workspace.options.component.main")
(tr "workspace.options.component.copy"))])]
[:div.element-set-title
[:span (tr "workspace.options.component")]
(when-not multi
[:span (if main-instance?
(tr "workspace.options.component.main")
(tr "workspace.options.component.copy"))])])
[:div.element-set-content
[:div.row-flex.component-row
{:class (stl/css-case :copy can-swap?)
:on-click #(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap shapes)))}
(if multi
i/component-copy
(if main-instance?
i/component
i/component-copy))
[:div.component-name (if multi
(tr "settings.multiple")
shape-name)]
(when show-menu?
[:div.row-actions
{:on-click on-menu-click}
i/actions
[:& component-ctx-menu {:on-close on-menu-close
:show menu-open?
:menu-entries menu-entries
:type :context-menu}]])
:type :dropdown}]])
(when (and can-swap? (not multi))
[:div.component-parent-name
(cph/merge-path-item (:path component) (:name component))])]
[:div {:class (stl/css :component-parent-name)}
(cph/merge-path-item (:path component) (:name component))])]]
(when swap-opened?
[:& component-swap {:shapes shapes}])
(when (and (not swap-opened?) (not multi) components-v2)
[:& component-annotation {:id id :shape shape :component component}])]]))))
[:& component-annotation {:id id :shape shape :component component}])])])))

View file

@ -10,11 +10,21 @@
@include flexColumn;
.component-wrapper {
display: flex;
gap: $s-4;
margin: 0 $s-4 0 $s-8;
.component-name-wrapper {
@extend .asset-element;
@include flexRow;
flex-grow: 1;
height: 100%;
width: 100%;
flex-wrap: wrap;
padding: 0 0 0 $s-12;
margin-top: $s-8;
&.with-main {
padding-bottom: $s-12;
}
.component-icon {
@include flexCenter;
height: $s-24;
@ -26,8 +36,20 @@
}
.component-name {
@include titleTipography;
flex-grow: 1;
@include textEllipsis;
width: 70%;
flex-grow: 2;
margin-left: $s-8;
}
.component-parent-name {
@include titleTipography;
@include textEllipsis;
padding-left: $s-36;
color: var(--title-foreground-color);
}
}
.swappeable {
cursor: pointer;
}
.component-actions {
position: relative;
@ -52,4 +74,300 @@
}
}
}
.title-back {
@include tabTitleTipography;
cursor: pointer;
width: 100%;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
text-align: left;
border: 0;
margin-bottom: $s-16;
svg {
height: $s-8;
width: $s-8;
fill: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg);
}
}
.component-swap {
.element-set-title {
@include titleTipography;
text-transform: uppercase;
margin: $s-16 $s-4 0 $s-12;
}
svg {
stroke: var(--icon-foreground);
fill: var(--icon-foreground);
height: $s-12;
width: $s-12;
cursor: pointer;
}
.search-field {
display: flex;
align-items: center;
height: $s-32;
margin: $s-16 $s-4 $s-4 $s-12;
border-radius: $br-8;
font-family: "worksans", sans-serif;
background-color: var(--input-background-color);
.search-box {
align-items: center;
display: flex;
width: 100%;
.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;
width: $s-28;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
}
.select-field {
margin: $s-8 $s-4 0 $s-12;
}
.select-library {
padding-left: $s-20;
}
.library-name {
@include titleTipography;
@include textEllipsis;
margin: $s-20 $s-4 0 $s-12;
}
.listing-options-wrapper {
width: 100%;
}
.listing-options {
margin-left: auto;
margin-right: $s-4;
}
.component-path {
@include titleTipography;
@include textEllipsis;
text-align: left;
cursor: pointer;
width: 100%;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
border: 0;
margin: $s-16 0 $s-12 0;
padding: 0 $s-16 0 $s-24;
svg {
height: $s-8;
width: $s-8;
fill: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg);
}
}
.component-path-empty {
height: $s-16;
}
.component-list {
margin: 0 $s-4 0 $s-8;
.component-item {
display: flex;
align-items: center;
margin-bottom: $s-4;
font-size: $s-12;
cursor: pointer;
width: 100%;
height: $s-36;
border-radius: $br-8;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color);
svg,
img {
background-color: var(--assets-component-background-color);
border-radius: $br-8;
height: $s-32;
width: $s-32;
margin: $s-2 $s-8 $s-2 $s-2;
}
.selected {
color: var(--assets-item-name-foreground-color-hover);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
background-color: var(--assets-item-background-color-hover);
}
&.disabled {
cursor: auto;
color: var(--assets-item-name-foreground-color-disabled);
background-color: var(--assets-item-background-color);
svg {
cursor: auto;
}
}
}
.component-group {
@include titleTipography;
text-align: left;
display: flex;
align-items: center;
margin: 0 $s-16 $s-8 $s-8;
justify-content: space-between;
cursor: pointer;
height: $s-24;
svg {
height: $s-8;
width: $s-8;
}
div {
display: flex;
width: 90%;
}
span {
@include textEllipsis;
}
.component-group-path {
direction: rtl;
}
.component-group-name {
color: var(--assets-item-name-foreground-color);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
.component-group-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
}
}
}
.component-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: calc(10vh + $s-16);
gap: $s-4;
margin: 0 $s-4 0 $s-8;
.grid-cell {
@include flexCenter;
flex-wrap: wrap;
position: relative;
padding: $s-8;
border-radius: $br-8;
background-color: var(--assets-component-background-color);
overflow: hidden;
cursor: pointer;
img {
height: auto;
width: auto;
max-height: 100%;
max-width: 100%;
pointer-events: none;
border: 0;
}
svg {
height: 10vh;
width: 10vh;
stroke: none;
}
.component-name {
@include titleTipography;
@include textEllipsis;
display: none;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
padding: $s-2;
text-align: center;
direction: rtl;
}
&:hover {
background-color: var(--assets-item-background-color-hover);
.component-name {
display: block;
color: var(--assets-item-name-foreground-color-hover);
background: linear-gradient(to top, var(--assets-item-background-color-hover) 0%, transparent 100%);
}
}
&.selected {
border: $s-1 solid var(--assets-item-border-color);
.component-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
&.disabled {
background: var(--assets-component-background-color-disabled);
cursor: auto;
svg {
cursor: auto;
}
.component-name {
background: linear-gradient(
to top,
var(--assets-component-background-color-disabled) 0%,
transparent 100%
);
color: var(--assets-item-name-foreground-color-hover);
}
}
}
}
}