mirror of
https://github.com/penpot/penpot.git
synced 2025-02-08 08:09:14 -05:00
Merge pull request #161 from tokens-studio/dimensions-context-menu
Dimensions context menu
This commit is contained in:
commit
93c249c77a
5 changed files with 240 additions and 15 deletions
|
@ -37,7 +37,7 @@
|
||||||
:border-radius
|
:border-radius
|
||||||
:stroke-width
|
:stroke-width
|
||||||
:box-shadow
|
:box-shadow
|
||||||
:dimension
|
:dimensions
|
||||||
:numeric
|
:numeric
|
||||||
:opacity
|
:opacity
|
||||||
:other
|
:other
|
||||||
|
@ -98,8 +98,8 @@
|
||||||
[:p2 {:optional true} ::sm/uuid]
|
[:p2 {:optional true} ::sm/uuid]
|
||||||
[:p3 {:optional true} ::sm/uuid]
|
[:p3 {:optional true} ::sm/uuid]
|
||||||
[:p4 {:optional true} ::sm/uuid]
|
[:p4 {:optional true} ::sm/uuid]
|
||||||
[:position-x {:optional true} ::sm/uuid]
|
[:x {:optional true} ::sm/uuid]
|
||||||
[:position-y {:optional true} ::sm/uuid]])
|
[:y {:optional true} ::sm/uuid]])
|
||||||
|
|
||||||
(def spacing-keys (schema-keys ::spacing))
|
(def spacing-keys (schema-keys ::spacing))
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,22 @@
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.types.shape.radius :as ctsr]
|
[app.common.types.shape.radius :as ctsr]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.data.shortcuts :as scd]
|
||||||
[app.main.data.tokens :as dt]
|
[app.main.data.tokens :as dt]
|
||||||
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.changes :as dch]
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.shape-layout :as dwsl]
|
[app.main.data.workspace.shape-layout :as dwsl]
|
||||||
[app.main.data.workspace.transforms :as dwt]
|
[app.main.data.workspace.transforms :as dwt]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
[app.main.ui.workspace.context-menu :refer [menu-entry prevent-default]]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.workspace.tokens.core :as wtc]
|
[app.main.ui.workspace.tokens.core :as wtc]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
|
[app.util.timers :as timers]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
@ -27,6 +31,92 @@
|
||||||
(def tokens-menu-ref
|
(def tokens-menu-ref
|
||||||
(l/derived :token-context-menu refs/workspace-local))
|
(l/derived :token-context-menu refs/workspace-local))
|
||||||
|
|
||||||
|
(defn- prevent-default
|
||||||
|
[event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event))
|
||||||
|
|
||||||
|
(mf/defc menu-entry
|
||||||
|
{::mf/props :obj}
|
||||||
|
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave
|
||||||
|
on-unmount children selected? icon disabled value]}]
|
||||||
|
(let [submenu-ref (mf/use-ref nil)
|
||||||
|
hovering? (mf/use-ref false)
|
||||||
|
on-pointer-enter
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(mf/set-ref-val! hovering? true)
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (some? submenu-node)
|
||||||
|
(dom/set-css-property! submenu-node "display" "block")))
|
||||||
|
(when on-pointer-enter (on-pointer-enter))))
|
||||||
|
|
||||||
|
on-pointer-leave
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(mf/set-ref-val! hovering? false)
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (some? submenu-node)
|
||||||
|
(timers/schedule
|
||||||
|
50
|
||||||
|
#(when-not (mf/ref-val hovering?)
|
||||||
|
(dom/set-css-property! submenu-node "display" "none")))))
|
||||||
|
(when on-pointer-leave (on-pointer-leave))))
|
||||||
|
|
||||||
|
set-dom-node
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [dom]
|
||||||
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
|
(when (and (some? dom) (some? submenu-node))
|
||||||
|
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps on-unmount)
|
||||||
|
(constantly on-unmount))
|
||||||
|
|
||||||
|
(if icon
|
||||||
|
[:li {:class (stl/css :icon-menu-item)
|
||||||
|
:disabled disabled
|
||||||
|
:data-value value
|
||||||
|
:ref set-dom-node
|
||||||
|
:on-click on-click
|
||||||
|
:on-pointer-enter on-pointer-enter
|
||||||
|
:on-pointer-leave on-pointer-leave}
|
||||||
|
[:span
|
||||||
|
{:class (stl/css :icon-wrapper)}
|
||||||
|
(if selected? [:span {:class (stl/css :selected-icon)}
|
||||||
|
i/tick]
|
||||||
|
[:span {:class (stl/css :selected-icon)}])
|
||||||
|
[:span {:class (stl/css :shape-icon)} icon]]
|
||||||
|
[:span {:class (stl/css :title)} title]]
|
||||||
|
[:li {:class (stl/css :context-menu-item)
|
||||||
|
:disabled disabled
|
||||||
|
:ref set-dom-node
|
||||||
|
:data-value value
|
||||||
|
:on-click on-click
|
||||||
|
:on-pointer-enter on-pointer-enter
|
||||||
|
:on-pointer-leave on-pointer-leave}
|
||||||
|
[:span {:class (stl/css :title)} title]
|
||||||
|
(when shortcut
|
||||||
|
[:span {:class (stl/css :shortcut)}
|
||||||
|
(for [[idx sc] (d/enumerate (scd/split-sc shortcut))]
|
||||||
|
[:span {:key (dm/str shortcut "-" idx)
|
||||||
|
:class (stl/css :shortcut-key)} sc])])
|
||||||
|
|
||||||
|
(when (> (count children) 1)
|
||||||
|
[:span {:class (stl/css :submenu-icon)} i/arrow])
|
||||||
|
|
||||||
|
(when (> (count children) 1)
|
||||||
|
[:ul {:class (stl/css :token-context-submenu)
|
||||||
|
:ref submenu-ref
|
||||||
|
:style {:display "none" :left 235}
|
||||||
|
:on-context-menu prevent-default}
|
||||||
|
children])])))
|
||||||
|
|
||||||
|
(mf/defc menu-separator
|
||||||
|
[]
|
||||||
|
[:li {:class (stl/css :separator)}])
|
||||||
|
|
||||||
(defn update-shape-radius-single-corner [value shape-ids attribute]
|
(defn update-shape-radius-single-corner [value shape-ids attribute]
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(dch/update-shapes shape-ids
|
(dch/update-shapes shape-ids
|
||||||
|
@ -65,6 +155,22 @@
|
||||||
:token-type-props updated-token-type-props
|
:token-type-props updated-token-type-props
|
||||||
:selected-shapes selected-shapes})))
|
:selected-shapes selected-shapes})))
|
||||||
|
|
||||||
|
(defn update-shape-position [value shape-ids attributes]
|
||||||
|
(doseq [shape-id shape-ids]
|
||||||
|
(st/emit! (dw/update-position shape-id {(first attributes) value}))))
|
||||||
|
|
||||||
|
(defn apply-dimensions-token [{:keys [token-id token-type-props selected-shapes]} attributes]
|
||||||
|
(let [token (dt/get-token-data-from-token-id token-id)
|
||||||
|
attributes (set attributes)
|
||||||
|
updated-token-type-props (if (set/superset? #{:x :y} attributes)
|
||||||
|
(assoc token-type-props
|
||||||
|
:on-update-shape update-shape-position
|
||||||
|
:attributes attributes)
|
||||||
|
token-type-props)]
|
||||||
|
(wtc/on-apply-token {:token token
|
||||||
|
:token-type-props updated-token-type-props
|
||||||
|
:selected-shapes selected-shapes})))
|
||||||
|
|
||||||
(defn update-shape-dimensions [value shape-ids attributes]
|
(defn update-shape-dimensions [value shape-ids attributes]
|
||||||
(st/emit! (dwt/update-dimensions shape-ids (first attributes) value)))
|
(st/emit! (dwt/update-dimensions shape-ids (first attributes) value)))
|
||||||
|
|
||||||
|
@ -125,6 +231,16 @@
|
||||||
{:title "Min height" :attributes #{:layout-item-min-h}}
|
{:title "Min height" :attributes #{:layout-item-min-h}}
|
||||||
{:title "Max height" :attributes #{:layout-item-max-h}}])
|
{:title "Max height" :attributes #{:layout-item-max-h}}])
|
||||||
|
|
||||||
|
:dimensions (attributes->actions
|
||||||
|
apply-dimensions-token
|
||||||
|
[{:title "Spacing" :submenu :spacing}
|
||||||
|
{:title "Sizing" :submenu :sizing}
|
||||||
|
{:title "Border Radius" :submenu :border-radius}
|
||||||
|
;; TODO: BORDER_WIDTH {:title "Border Width" :attributes #{:width} :children true}
|
||||||
|
{:title "x" :attributes #{:x}}
|
||||||
|
{:title "y" :attributes #{:y}}])
|
||||||
|
;;TODO: Background blur {:title "Background blur" :attributes #{:width}}])
|
||||||
|
|
||||||
[])))
|
[])))
|
||||||
|
|
||||||
(defn generate-menu-entries [{:keys [token-id token-type-props token-type selected-shapes] :as context-data}]
|
(defn generate-menu-entries [{:keys [token-id token-type-props token-type selected-shapes] :as context-data}]
|
||||||
|
@ -148,14 +264,23 @@
|
||||||
(mf/defc token-pill-context-menu
|
(mf/defc token-pill-context-menu
|
||||||
[context-data]
|
[context-data]
|
||||||
(let [menu-entries (generate-menu-entries context-data)]
|
(let [menu-entries (generate-menu-entries context-data)]
|
||||||
(for [[index {:keys [title action selected?]}] (d/enumerate menu-entries)]
|
(for [[index {:keys [title action selected? children submenu]}] (d/enumerate menu-entries)]
|
||||||
[:& menu-entry {:key index
|
[:& menu-entry (cond-> {:key index
|
||||||
:title title
|
:title title}
|
||||||
:on-click action
|
(not submenu) (assoc :on-click action
|
||||||
;; TODO: Allow selected items wihtout an icon for the context menu
|
;; TODO: Allow selected items wihtout an icon for the context menu
|
||||||
:icon (mf/html [:div {:class (stl/css-case :empty-icon true
|
:icon (mf/html [:div {:class (stl/css-case :empty-icon true
|
||||||
:hidden-icon (not selected?))}])
|
:hidden-icon (not selected?))}])
|
||||||
:selected? selected?}])))
|
:selected? selected?))
|
||||||
|
(when submenu
|
||||||
|
(let [submenu-entries (additional-actions (assoc context-data :token-type submenu))]
|
||||||
|
(for [[index {:keys [title action selected?]}] (d/enumerate submenu-entries)]
|
||||||
|
[:& menu-entry {:key index
|
||||||
|
:title title
|
||||||
|
:on-click action
|
||||||
|
:icon (mf/html [:div {:class (stl/css-case :empty-icon true
|
||||||
|
:hidden-icon (not selected?))}])
|
||||||
|
:selected? selected?}])))])))
|
||||||
|
|
||||||
(mf/defc token-context-menu
|
(mf/defc token-context-menu
|
||||||
[]
|
[]
|
||||||
|
@ -179,7 +304,6 @@
|
||||||
"left: " (- left delta-x) "px;")]
|
"left: " (- left delta-x) "px;")]
|
||||||
(when (or (> delta-x 0) (> delta-y 0))
|
(when (or (> delta-x 0) (> delta-y 0))
|
||||||
(.setAttribute ^js dropdown "style" new-style))))))
|
(.setAttribute ^js dropdown "style" new-style))))))
|
||||||
|
|
||||||
[:& dropdown {:show (boolean mdata)
|
[:& dropdown {:show (boolean mdata)
|
||||||
:on-close #(st/emit! dt/hide-token-context-menu)}
|
:on-close #(st/emit! dt/hide-token-context-menu)}
|
||||||
[:div {:class (stl/css :token-context-menu)
|
[:div {:class (stl/css :token-context-menu)
|
||||||
|
|
|
@ -25,13 +25,114 @@
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
// TODO: Fixes missing styles from parent context menu
|
|
||||||
li {
|
li {
|
||||||
@include bodySmallTypography;
|
@include bodySmallTypography;
|
||||||
color: var(--menu-foreground-color);
|
color: var(--menu-foreground-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.token-context-submenu {
|
||||||
|
position: absolute;
|
||||||
|
padding: $s-4;
|
||||||
|
margin-left: $s-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
height: $s-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: $s-28;
|
||||||
|
width: 100%;
|
||||||
|
padding: $s-6;
|
||||||
|
border-radius: $br-8;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include bodySmallTypography;
|
||||||
|
color: var(--menu-foreground-color);
|
||||||
|
margin-left: calc(($s-32 + $s-28) / 2);
|
||||||
|
}
|
||||||
|
.shortcut {
|
||||||
|
@include flexCenter;
|
||||||
|
gap: $s-2;
|
||||||
|
color: var(--menu-shortcut-foreground-color);
|
||||||
|
.shortcut-key {
|
||||||
|
@include bodySmallTypography;
|
||||||
|
@include flexCenter;
|
||||||
|
height: $s-20;
|
||||||
|
padding: $s-2 $s-6;
|
||||||
|
border-radius: $br-6;
|
||||||
|
background-color: var(--menu-shortcut-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-icon svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-background-color-hover);
|
||||||
|
.title {
|
||||||
|
color: var(--menu-foreground-color-hover);
|
||||||
|
}
|
||||||
|
.shortcut {
|
||||||
|
color: var(--menu-shortcut-foreground-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid var(--menu-border-color-focus);
|
||||||
|
background-color: var(--menu-background-color-focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
height: $s-28;
|
||||||
|
padding: $s-6;
|
||||||
|
border-radius: $br-8;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menu-background-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.title {
|
||||||
|
margin-left: $s-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-icon {
|
||||||
|
svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-icon {
|
||||||
|
margin-left: $s-2;
|
||||||
|
svg {
|
||||||
|
@extend .button-icon-small;
|
||||||
|
stroke: var(--menu-foreground-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-menu-item[disabled],
|
||||||
|
.context-menu-item[disabled] {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Allow selected items wihtout an icon for the context menu
|
// TODO: Allow selected items wihtout an icon for the context menu
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -159,7 +159,7 @@
|
||||||
:modal {:key :tokens/sizing
|
:modal {:key :tokens/sizing
|
||||||
:fields [{:label "Sizing"
|
:fields [{:label "Sizing"
|
||||||
:key :sizing}]}}]
|
:key :sizing}]}}]
|
||||||
[:dimension
|
[:dimensions
|
||||||
{:title "Dimensions"
|
{:title "Dimensions"
|
||||||
:attributes ctt/dimensions-keys
|
:attributes ctt/dimensions-keys
|
||||||
:on-update-shape update-shape-dimensions
|
:on-update-shape update-shape-dimensions
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
:stroke-width i/stroke-size
|
:stroke-width i/stroke-size
|
||||||
:typography i/text
|
:typography i/text
|
||||||
;; TODO: Add diagonal icon here when it's available
|
;; TODO: Add diagonal icon here when it's available
|
||||||
:dimension [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
:dimensions [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
||||||
:sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
:sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
||||||
i/add))
|
i/add))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue