mirror of
https://github.com/penpot/penpot.git
synced 2026-06-16 04:12:03 +00:00
🎉 Add flyout and semantic improvements to main toolbar
This commit is contained in:
parent
b03537fa68
commit
c52e130f84
@ -0,0 +1,296 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tool-toolbar.tool-toolbar
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :as file-uploader]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as ts]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private toolbar-hidden-ref
|
||||
(l/derived (fn [state]
|
||||
(let [visibility (get state :hide-toolbar)
|
||||
path-edit-state (get state :edit-path)
|
||||
|
||||
selected (get state :selected)
|
||||
edition (get state :edition)
|
||||
single? (= (count selected) 1)
|
||||
|
||||
path-editing? (and single? (some? (get path-edit-state edition)))]
|
||||
(if path-editing? true visibility)))
|
||||
refs/workspace-local))
|
||||
|
||||
(defn- tool-label
|
||||
[tool]
|
||||
(case tool
|
||||
:move (tr "workspace.toolbar.move" (sc/get-tooltip :move))
|
||||
:frame (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
|
||||
:rect (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
|
||||
:circle (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
|
||||
:text (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
|
||||
:path (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
|
||||
:curve (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
|
||||
:plugins (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins))
|
||||
:debug "Debugging tool"
|
||||
(name tool)))
|
||||
|
||||
(defn- active-group-tool
|
||||
[group drawtool]
|
||||
(if (contains? (:tools group) drawtool)
|
||||
drawtool
|
||||
(:default-tool group)))
|
||||
|
||||
(defn- selected-group?
|
||||
[group drawtool]
|
||||
(contains? (:tools group) drawtool))
|
||||
|
||||
(defn- group-menu-label
|
||||
[group drawtool]
|
||||
(let [tool-id (active-group-tool group drawtool)]
|
||||
(str (tr "labels.options") ": " (tool-label tool-id))))
|
||||
|
||||
(mf/defc tool-button*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [selected title icon on-click aria-haspopup aria-expanded role data-tool]}]
|
||||
[:button {:type "button"
|
||||
:title title
|
||||
:aria-label title
|
||||
:aria-haspopup aria-haspopup
|
||||
:aria-expanded aria-expanded
|
||||
:aria-pressed selected
|
||||
:role role
|
||||
:class (stl/css :main-toolbar-options-button)
|
||||
:on-click on-click
|
||||
:data-tool data-tool}
|
||||
[:> icon* {:icon-id icon
|
||||
:aria-hidden true
|
||||
:class (stl/css :main-toolbar-icon)}]])
|
||||
|
||||
(def grouped-tools
|
||||
{:shapes {:default-tool :rect
|
||||
:tools {:rect {:icon i/rectangle}
|
||||
:circle {:icon i/elipse}}}
|
||||
:free-draw {:default-tool :path
|
||||
:tools {:path {:icon i/path}
|
||||
:curve {:icon i/curve}}}})
|
||||
|
||||
(defn- cancel-timer!
|
||||
[timer-ref*]
|
||||
(when-let [timer (mf/ref-val timer-ref*)]
|
||||
(ts/dispose! timer)
|
||||
(mf/set-ref-val! timer-ref* nil)))
|
||||
|
||||
(mf/defc grouped-tool-flyout*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [group drawtool on-select-tool]}]
|
||||
(let [default-tool (active-group-tool group drawtool)
|
||||
default-icon (:icon (get-in group [:tools default-tool]))
|
||||
subtools (:tools group)
|
||||
open* (mf/use-state false)
|
||||
open-timer* (mf/use-ref nil)
|
||||
close-timer* (mf/use-ref nil)
|
||||
open (deref open*)
|
||||
menu-label (group-menu-label group drawtool)
|
||||
selected (boolean (selected-group? group drawtool))
|
||||
|
||||
on-display-menu (mf/use-fn
|
||||
(fn []
|
||||
(cancel-timer! close-timer*)
|
||||
(cancel-timer! open-timer*)
|
||||
(mf/set-ref-val!
|
||||
open-timer*
|
||||
(ts/schedule 350
|
||||
#(do
|
||||
(reset! open* true)
|
||||
(mf/set-ref-val! open-timer* nil))))))
|
||||
on-hide-menu (mf/use-fn
|
||||
(fn []
|
||||
(cancel-timer! open-timer*)
|
||||
(cancel-timer! close-timer*)
|
||||
(mf/set-ref-val!
|
||||
close-timer*
|
||||
(ts/schedule 350
|
||||
#(do
|
||||
(reset! open* false)
|
||||
(mf/set-ref-val! close-timer* nil))))))]
|
||||
(mf/with-effect []
|
||||
(fn []
|
||||
(cancel-timer! open-timer*)
|
||||
(cancel-timer! close-timer*)))
|
||||
|
||||
[:li {:on-pointer-enter on-display-menu
|
||||
:on-pointer-leave on-hide-menu
|
||||
:class (stl/css :main-toolbar-group)}
|
||||
[:div {:role "group"
|
||||
:aria-label menu-label}
|
||||
[:> tool-button* {:title (tool-label default-tool)
|
||||
:selected selected
|
||||
:icon default-icon
|
||||
:on-click on-select-tool
|
||||
:data-tool (name default-tool)
|
||||
:aria-haspopup true
|
||||
:aria-expanded open}]
|
||||
|
||||
(when open
|
||||
[:ul {:role "menu"
|
||||
:class (stl/css :main-toolbar-flyout)
|
||||
:aria-label menu-label}
|
||||
(for [[id {:keys [icon]}] subtools]
|
||||
[:li {:key (name id)
|
||||
:role "none"}
|
||||
[:> tool-button* {:title (tool-label id)
|
||||
:selected (= drawtool id)
|
||||
:icon icon
|
||||
:on-click on-select-tool
|
||||
:data-tool (name id)
|
||||
:role "menuitemradio"
|
||||
:aria-checked (= drawtool id)}]])])]]))
|
||||
|
||||
(mf/defc image-upload-tool
|
||||
{::mf/wrap [mf/memo]}
|
||||
[]
|
||||
(let [ref (mf/use-ref nil)
|
||||
file-id (mf/use-ctx ctx/current-file-id)
|
||||
|
||||
display-uploader
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! :interrupt (dw/clear-edition-mode))
|
||||
(dom/click (mf/ref-val ref))))
|
||||
|
||||
on-selected
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [blobs]
|
||||
;; We don't want to add a ref because that redraws the component
|
||||
;; for everychange. Better direct access on the callback.
|
||||
(let [vbox (deref refs/vbox)
|
||||
x (+ (:x vbox) (/ (:width vbox) 2))
|
||||
y (+ (:y vbox) (/ (:height vbox) 2))
|
||||
params {:file-id file-id
|
||||
:blobs (seq blobs)
|
||||
:position (gpt/point x y)}]
|
||||
(st/emit! (dwm/upload-media-workspace params)))))]
|
||||
[:li
|
||||
[:> tool-button* {:title (tool-label :image)
|
||||
:selected nil
|
||||
:icon i/img
|
||||
:on-click display-uploader}]
|
||||
[:& file-uploader/file-uploader
|
||||
{:input-id "image-upload"
|
||||
:accept dwm/accept-image-types
|
||||
:multi true
|
||||
:ref ref
|
||||
:on-selected on-selected}]]))
|
||||
|
||||
(mf/defc tool-toolbar*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [layout]}]
|
||||
(let [selected-drawing-tool (mf/deref refs/selected-drawing-tool)
|
||||
selected-edition (mf/deref refs/selected-edition)
|
||||
plugins-enabled (features/active-feature? @st/state "plugins/runtime")
|
||||
rulers-enabled (mf/deref refs/rulers?)
|
||||
toolbar-hidden (mf/deref toolbar-hidden-ref)
|
||||
display-plugins-manager (mf/use-fn
|
||||
(fn []
|
||||
(st/emit!
|
||||
(ptk/data-event ::ev/event {::ev/name "open-plugins-manager"
|
||||
::ev/origin "workspace:toolbar"})
|
||||
(modal/show :plugin-management {}))))
|
||||
toggle-debug-panel (mf/use-fn
|
||||
(mf/deps layout)
|
||||
(fn []
|
||||
(let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)]
|
||||
(when is-sidebar-closed?
|
||||
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
|
||||
(st/emit!
|
||||
(dw/remove-layout-flag :shortcuts)
|
||||
(-> (dw/toggle-layout-flag :debug-panel)
|
||||
(vary-meta assoc ::ev/origin "workspace-left-toolbar"))))))
|
||||
|
||||
on-interrupt (mf/use-fn #(st/emit! :interrupt (dw/clear-edition-mode)))
|
||||
|
||||
on-select-tool (mf/use-fn
|
||||
(fn [event]
|
||||
(let [tool (-> (dom/get-current-target event)
|
||||
(dom/get-data "tool")
|
||||
(keyword))]
|
||||
(on-interrupt)
|
||||
|
||||
;; Delay so anything that launched :interrupt can finish
|
||||
(ts/schedule 100
|
||||
#(st/emit! (dw/select-for-drawing tool))))))]
|
||||
|
||||
[:div {:role "toolbar"
|
||||
:aria-label (tr "workspace.toolbar.label")
|
||||
:tabindex "0"
|
||||
:class (stl/css-case :main-toolbar true
|
||||
:main-toolbar-no-rulers (not rulers-enabled)
|
||||
:main-toolbar-hidden toolbar-hidden)}
|
||||
[:ul {:class (stl/css :main-toolbar-options)}
|
||||
[:li {:class (stl/css :main-toolbar-option)}
|
||||
[:> tool-button* {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
|
||||
:selected (and (nil? selected-drawing-tool)
|
||||
(not selected-edition))
|
||||
:icon i/move
|
||||
:on-click on-interrupt}]]
|
||||
|
||||
[:li {:class (stl/css :main-toolbar-option)}
|
||||
[:> tool-button* {:title (tool-label :frame)
|
||||
:selected (= selected-drawing-tool :frame)
|
||||
:icon i/board
|
||||
:on-click on-select-tool
|
||||
:data-tool "frame"}]]
|
||||
|
||||
|
||||
[:> grouped-tool-flyout* {:key :shapes
|
||||
:group (get grouped-tools :shapes)
|
||||
:drawtool selected-drawing-tool
|
||||
:on-select-tool on-select-tool}]
|
||||
|
||||
[:li {:class (stl/css :main-toolbar-option)}
|
||||
[:> tool-button* {:title (tool-label :text)
|
||||
:selected (= selected-drawing-tool :text)
|
||||
:icon i/text
|
||||
:on-click on-select-tool
|
||||
:data-tool "text"}]]
|
||||
|
||||
[:> image-upload-tool]
|
||||
|
||||
[:> grouped-tool-flyout* {:key :free-draw
|
||||
:group (get grouped-tools :free-draw)
|
||||
:drawtool selected-drawing-tool
|
||||
:on-select-tool on-select-tool}]
|
||||
|
||||
(when plugins-enabled
|
||||
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-plugins)}
|
||||
[:> tool-button* {:title (tool-label :plugins)
|
||||
:icon i/puzzle
|
||||
:on-click display-plugins-manager
|
||||
:data-tool "plugins"}]])
|
||||
|
||||
(when *assert*
|
||||
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-debug)}
|
||||
[:> tool-button* {:title (tool-label :debug)
|
||||
:selected (contains? layout :debug-panel)
|
||||
:icon i/bug
|
||||
:on-click toggle-debug-panel}]])]]))
|
||||
@ -0,0 +1,139 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/borders.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
|
||||
.main-toolbar {
|
||||
--toolbar-position-y: 28px;
|
||||
--toolbar-offset-y: 0px;
|
||||
--menu-border-color: var(--color-background-quaternary);
|
||||
--menu-background-color: var(--color-background-primary);
|
||||
|
||||
cursor: initial;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
// height: 56px;
|
||||
padding: var(--sp-s) var(--sp-l);
|
||||
border-radius: $br-8;
|
||||
border: 2px solid var(--menu-border-color);
|
||||
z-index: var(--z-index-panels);
|
||||
background-color: var(--menu-background-color);
|
||||
transition:
|
||||
top 0.3s,
|
||||
height 0.3s,
|
||||
opacity 0.3s;
|
||||
top: calc(var(--toolbar-position-y) + var(--toolbar-offset-y));
|
||||
}
|
||||
|
||||
.main-toolbar-no-rulers {
|
||||
--toolbar-position-y: 0px;
|
||||
--toolbar-offset-y: 8px;
|
||||
}
|
||||
|
||||
.main-toolbar-hidden {
|
||||
--toolbar-offset-y: -4px;
|
||||
|
||||
height: 16px;
|
||||
z-index: 1;
|
||||
border-radius: 0 0 $br-8 $br-8;
|
||||
border-block-start: 0;
|
||||
|
||||
.main-toolbar-options {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.main-toolbar-options {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.main-toolbar-option {
|
||||
position: relative;
|
||||
|
||||
&.main-toolbar-option-plugins {
|
||||
--separator-color: var(--color-background-quaternary);
|
||||
|
||||
margin-inline-start: var(--sp-l);
|
||||
padding-inline-start: var(--sp-l);
|
||||
border-inline-start: 1px solid var(--separator-color);
|
||||
}
|
||||
}
|
||||
|
||||
.main-toolbar-options-button {
|
||||
--toolbar-option-button-background: transparent;
|
||||
|
||||
appearance: none;
|
||||
border: none;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: $br-8;
|
||||
margin: 0 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--toolbar-option-button-background);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[aria-pressed="true"],
|
||||
&[aria-pressed="true"],
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
--toolbar-option-button-background: var(--color-background-quaternary);
|
||||
|
||||
& .main-toolbar-icon {
|
||||
--stroke-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-toolbar-icon {
|
||||
--stroke-color: var(--color-foreground-secondary);
|
||||
|
||||
stroke: var(--stroke-color);
|
||||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
}
|
||||
|
||||
.main-toolbar-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-toolbar-flyout {
|
||||
position: absolute;
|
||||
top: 130%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: var(--sp-xxs);
|
||||
padding: var(--sp-xxs) 0;
|
||||
border-radius: $br-8;
|
||||
border: 1px solid var(--menu-border-color);
|
||||
background-color: var(--menu-background-color);
|
||||
z-index: var(--z-index-panels);
|
||||
display: flex;
|
||||
}
|
||||
@ -33,7 +33,7 @@
|
||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline*]]
|
||||
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
||||
[app.main.ui.workspace.shapes.text.viewport-texts-html :as stvh]
|
||||
[app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
|
||||
[app.main.ui.workspace.tool-toolbar.tool-toolbar :refer [tool-toolbar*]]
|
||||
[app.main.ui.workspace.viewport-wasm :as viewport.wasm]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.comments :as comments]
|
||||
@ -53,7 +53,9 @@
|
||||
[app.main.ui.workspace.viewport.selection :as selection]
|
||||
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
|
||||
[app.main.ui.workspace.viewport.snap-points :as snap-points]
|
||||
[app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar* path-edition-bar* view-only-bar*]]
|
||||
[app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar*
|
||||
path-edition-bar*
|
||||
view-only-bar*]]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]]
|
||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||
@ -326,7 +328,7 @@
|
||||
:else
|
||||
[:*
|
||||
(when-not hide-ui?
|
||||
[:> top-toolbar* {:layout layout}])
|
||||
[:> tool-toolbar* {:layout layout}])
|
||||
|
||||
(when (and ^boolean path-editing?
|
||||
^boolean single-select?)
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline*]]
|
||||
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
||||
[app.main.ui.workspace.shapes.text.v3-editor :as editor-v3]
|
||||
[app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
|
||||
[app.main.ui.workspace.tool-toolbar.tool-toolbar :refer [tool-toolbar*]]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.comments :as comments]
|
||||
[app.main.ui.workspace.viewport.debug :as wvd]
|
||||
@ -51,7 +51,9 @@
|
||||
[app.main.ui.workspace.viewport.selection :as selection]
|
||||
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
|
||||
[app.main.ui.workspace.viewport.snap-points :as snap-points]
|
||||
[app.main.ui.workspace.viewport.top-bar :refer [path-edition-bar* grid-edition-bar* view-only-bar*]]
|
||||
[app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar*
|
||||
path-edition-bar*
|
||||
view-only-bar*]]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.ui.workspace.viewport.viewport-ref :as vp-ref :refer [create-viewport-ref]]
|
||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||
@ -559,7 +561,7 @@
|
||||
:else
|
||||
[:*
|
||||
(when-not hide-ui?
|
||||
[:> top-toolbar* {:layout layout}])
|
||||
[:> tool-toolbar* {:layout layout}])
|
||||
|
||||
(when (and ^boolean path-editing?
|
||||
^boolean single-select?)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user