mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
156 lines
6.9 KiB
Clojure
156 lines
6.9 KiB
Clojure
;; 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.tokens.sidebar
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.tokens :as dt]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.icons :as i]
|
|
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
|
[app.main.ui.workspace.tokens.common :refer [workspace-shapes]]
|
|
[app.main.ui.workspace.tokens.core :as wtc]
|
|
[app.util.dom :as dom]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(mf/defc token-pill
|
|
{::mf/wrap-props false}
|
|
[{:keys [on-click token highlighted? on-context-menu]}]
|
|
(let [{:keys [name value]} token
|
|
resolved-value (try
|
|
(wtc/resolve-token-value token)
|
|
(catch js/Error _ nil))]
|
|
[:div {:class (stl/css-case :token-pill true
|
|
:token-pill-highlighted highlighted?
|
|
:token-pill-invalid (not resolved-value))
|
|
:title (str (if resolved-value "Token value: " "Invalid token value: ") value)
|
|
:on-click on-click
|
|
:on-context-menu on-context-menu}
|
|
name]))
|
|
|
|
(mf/defc token-section-icon
|
|
{::mf/wrap-props false}
|
|
[{:keys [type]}]
|
|
(case type
|
|
:border-radius i/corner-radius
|
|
:numeric [:span {:class (stl/css :section-text-icon)} "123"]
|
|
:boolean i/boolean-difference
|
|
:opacity [:span {:class (stl/css :section-text-icon)} "%"]
|
|
:rotation i/rotation
|
|
:spacing i/padding-extended
|
|
:string i/text-mixed
|
|
:stroke-width i/stroke-size
|
|
:typography i/text
|
|
;; TODO: Add diagonal icon here when it's available
|
|
:dimension [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
|
:sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
|
|
i/add))
|
|
|
|
(mf/defc token-component
|
|
[{:keys [type file tokens selected-shapes token-type-props]}]
|
|
(let [open? (mf/use-state false)
|
|
{:keys [modal attributes title]} token-type-props
|
|
|
|
on-context-menu (mf/use-fn
|
|
(fn [event token]
|
|
(dom/prevent-default event)
|
|
(dom/stop-propagation event)
|
|
(st/emit! (dt/show-token-context-menu {:type :token
|
|
:position (dom/get-client-position event)
|
|
:token-id (:id token)}))))
|
|
|
|
on-toggle-open-click (mf/use-fn
|
|
(mf/deps open? tokens)
|
|
#(when (seq tokens)
|
|
(swap! open? not)))
|
|
on-popover-open-click (mf/use-fn
|
|
(fn [event]
|
|
(let [{:keys [key fields]} modal]
|
|
(dom/stop-propagation event)
|
|
(modal/show! key {:x (.-clientX ^js event)
|
|
:y (.-clientY ^js event)
|
|
:position :right
|
|
:fields fields
|
|
:token-type type}))))
|
|
|
|
on-token-pill-click (mf/use-fn
|
|
(mf/deps selected-shapes token-type-props)
|
|
(fn [event token]
|
|
(dom/stop-propagation event)
|
|
(wtc/on-apply-token {:token token
|
|
:token-type-props token-type-props
|
|
:selected-shapes selected-shapes})))
|
|
tokens-count (count tokens)]
|
|
[:div {:on-click on-toggle-open-click}
|
|
[:& cmm/asset-section {:file-id (:id file)
|
|
:icon (mf/fnc icon-wrapper [_]
|
|
[:div {:class (stl/css :section-icon)}
|
|
[:& token-section-icon {:type type}]])
|
|
|
|
:title title
|
|
:assets-count tokens-count
|
|
:open? @open?}
|
|
[:& cmm/asset-section-block {:role :title-button}
|
|
[:button {:class (stl/css :action-button)
|
|
:on-click on-popover-open-click}
|
|
i/add]]
|
|
(when open?
|
|
[:& cmm/asset-section-block {:role :content}
|
|
[:div {:class (stl/css :token-pills-wrapper)}
|
|
(for [token tokens]
|
|
[:& token-pill
|
|
{:key (:id token)
|
|
:token token
|
|
:highlighted? (wtc/tokens-applied? token selected-shapes attributes)
|
|
:on-click #(on-token-pill-click % token)
|
|
:on-context-menu #(on-context-menu % token)}])]])]]))
|
|
|
|
(defn sorted-token-groups
|
|
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
|
|
Sort each group alphabetically (by their `:token-key`)."
|
|
[tokens]
|
|
(let [tokens-by-group (->> (vals tokens)
|
|
(group-by :type))
|
|
{:keys [empty filled]} (->> wtc/token-types
|
|
(map (fn [[token-key token-type-props]]
|
|
{:token-key token-key
|
|
:token-type-props token-type-props
|
|
:tokens (get tokens-by-group token-key [])}))
|
|
(group-by (fn [{:keys [tokens]}]
|
|
(if (empty? tokens) :empty :filled))))]
|
|
{:empty (sort-by :token-key empty)
|
|
:filled (sort-by :token-key filled)}))
|
|
|
|
(mf/defc tokens-explorer
|
|
[_props]
|
|
(let [file (mf/deref refs/workspace-file)
|
|
current-page-id (:current-page-id @st/state)
|
|
workspace-data (mf/deref refs/workspace-data)
|
|
tokens (get workspace-data :tokens)
|
|
token-groups (mf/with-memo [tokens]
|
|
(sorted-token-groups tokens))
|
|
selected-shape-ids (mf/deref refs/selected-shapes)
|
|
selected-shapes (workspace-shapes workspace-data current-page-id selected-shape-ids)]
|
|
[:article
|
|
[:div.assets-bar
|
|
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
|
|
(:empty token-groups))]
|
|
[:& token-component {:key token-key
|
|
:type token-key
|
|
:file file
|
|
:selected-shapes selected-shapes
|
|
:tokens tokens
|
|
:token-type-props token-type-props}])]]))
|
|
|
|
(mf/defc tokens-sidebar-tab
|
|
{::mf/wrap [mf/memo]
|
|
::mf/wrap-props false}
|
|
[_props]
|
|
[:div {:class (stl/css :sidebar-tab-wrapper)}
|
|
[:& tokens-explorer]])
|