diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc index b8fcef2f5..cb4ea47eb 100644 --- a/common/app/common/pages_helpers.cljc +++ b/common/app/common/pages_helpers.cljc @@ -214,3 +214,36 @@ (concat new-children new-child-objects) (concat updated-children updated-child-objects)))))))) + +(defn indexed-shapes + "Retrieves a list with the indexes for each element in the layer tree. + This will be used for shift+selection." + [objects] + (let [rec-index + (fn rec-index [cur-idx id] + (let [object (get objects id) + red-fn + (fn [cur-idx id] + (let [[prev-idx _] (first cur-idx) + prev-idx (or prev-idx 0) + cur-idx (conj cur-idx [(inc prev-idx) id])] + (rec-index cur-idx id)))] + (reduce red-fn cur-idx (reverse (:shapes object)))))] + (into {} (rec-index '() uuid/zero)))) + + +(defn expand-region-selection + "Given a selection selects all the shapes between the first and last in + an indexed manner (shift selection)" + [objects selection] + (let [indexed-shapes (indexed-shapes objects) + filter-indexes (->> indexed-shapes + (filter (comp selection second)) + (map first)) + + from (apply min filter-indexes) + to (apply max filter-indexes)] + (->> indexed-shapes + (filter (fn [[idx _]] (and (>= idx from) (<= idx to)))) + (map second) + (into #{})))) diff --git a/frontend/resources/images/icons/interaction.svg b/frontend/resources/images/icons/interaction.svg index 217110b57..afc359d84 100644 --- a/frontend/resources/images/icons/interaction.svg +++ b/frontend/resources/images/icons/interaction.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/frontend/resources/images/icons/size-horiz.svg b/frontend/resources/images/icons/size-horiz.svg index bda1b64e9..ab9456580 100644 --- a/frontend/resources/images/icons/size-horiz.svg +++ b/frontend/resources/images/icons/size-horiz.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/size-vert.svg b/frontend/resources/images/icons/size-vert.svg index a927a3fd7..f2e807c40 100644 --- a/frontend/resources/images/icons/size-vert.svg +++ b/frontend/resources/images/icons/size-vert.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss index eb5796ea1..adac8714e 100644 --- a/frontend/resources/styles/main/partials/handoff.scss +++ b/frontend/resources/styles/main/partials/handoff.scss @@ -96,6 +96,11 @@ position: relative; align-items: center; + .color-text { + width: 3rem; + text-transform: uppercase; + } + .attributes-color-display { display: flex; } diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 07ae045db..11275e5c2 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -571,14 +571,13 @@ svg { cursor: pointer; height: 20px; - stroke: $color-gray-40; - stroke-width: 30px; + fill: $color-gray-40; width: 20px; } &:hover { svg { - stroke: $color-gray-10; + fill: $color-gray-10; } } } diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index ef51b5068..75ea52d0c 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -20,7 +20,8 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.util.router :as rt] - [app.common.uuid :as uuid])) + [app.common.uuid :as uuid] + [app.common.pages-helpers :as cph])) ;; --- Specs @@ -242,20 +243,17 @@ frames (get-in state [:viewer-data :frames]) share-token (get-in state [:viewer-data :share-token]) index (d/index-of-pred frames #(= (:id %) frame-id))] - (rx/of (rt/nav :viewer {:page-id page-id :file-id file-id} {:token share-token - :index index})))))) - -;; --- Shortcuts - -(def shortcuts - {"+" #(st/emit! increase-zoom) - "-" #(st/emit! decrease-zoom) - "shift+0" #(st/emit! zoom-to-50) - "shift+1" #(st/emit! reset-zoom) - "shift+2" #(st/emit! zoom-to-200) - "left" #(st/emit! select-prev-frame) - "right" #(st/emit! select-next-frame)}) + (rx/of (rt/nav :viewer + {:page-id page-id + :file-id file-id} + {:token share-token + :index index})))))) +(defn set-current-frame [frame-id] + (ptk/reify ::current-frame + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-data :current-frame-id] frame-id)))) (defn deselect-all [] (ptk/reify ::deselect-all @@ -264,17 +262,49 @@ (assoc-in state [:viewer-local :selected] #{})))) (defn select-shape - ([id] (select-shape id false)) - ([id toggle?] + ([id] (ptk/reify ::select-shape ptk/UpdateEvent (update [_ state] (-> state (assoc-in [:viewer-local :selected] #{id})))))) -;; TODO -(defn collapse-all [] - (ptk/reify ::collapse-all)) +(defn toggle-selection + [id] + (ptk/reify ::toggle-selection + ptk/UpdateEvent + (update [_ state] + (let [selected (get-in state [:viewer-local :selected])] + (cond-> state + (not (selected id)) (update-in [:viewer-local :selected] conj id) + (selected id) (update-in [:viewer-local :selected] disj id)))))) + +(defn shift-select-to + [id] + (ptk/reify ::shift-select-to + ptk/UpdateEvent + (update [_ state] + (let [objects (get-in state [:viewer-data :objects]) + selection (-> state + (get-in [:viewer-local :selected] #{}) + (conj id))] + (-> state + (assoc-in [:viewer-local :selected] + (cph/expand-region-selection objects selection))))))) + +(defn select-all + [] + (ptk/reify ::shift-select-to + ptk/UpdateEvent + (update [_ state] + (let [objects (get-in state [:viewer-data :objects]) + frame-id (get-in state [:viewer-data :current-frame-id]) + selection (->> objects + (filter #(= (:frame-id (second %)) frame-id)) + (map first) + (into #{frame-id}))] + (-> state + (assoc-in [:viewer-local :selected] selection)))))) (defn toggle-collapse [id] (ptk/reify ::toggle-collapse @@ -288,3 +318,17 @@ ptk/UpdateEvent (update [_ state] (update-in state [:viewer-local :hover] (if hover? conj disj) id)))) + + +;; --- Shortcuts + +(def shortcuts + {"+" #(st/emit! increase-zoom) + "-" #(st/emit! decrease-zoom) + "ctrl+a" #(st/emit! (select-all)) + "shift+0" #(st/emit! zoom-to-50) + "shift+1" #(st/emit! reset-zoom) + "shift+2" #(st/emit! zoom-to-200) + "left" #(st/emit! select-prev-frame) + "right" #(st/emit! select-next-frame)}) + diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 5814c4a5d..f4f2377cb 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -74,8 +74,12 @@ (logjs "state" @state)) (defn ^:export dump-objects [] - (let [page-id (get @state :current-page-id)] - (logjs "state" (get-in @state [:workspace-data :pages-index page-id :objects])))) + (let [page-id (get @state :current-page-id) + objects (get-in @state [:workspace-data :pages-index page-id :objects])] + (logjs "index" (->> (cph/indexed-shapes objects) + (d/mapm (fn [k id] (get objects id))) + (map (fn [[idx obj]] (str idx " - " (:name obj)))))) + (logjs "state" objects))) (defn ^:export dump-object [name] (let [page-id (get @state :current-page-id)] diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 6410c0478..636e3226b 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -150,10 +150,9 @@ (mf/defc debug-icons-preview {::mf/wrap-props false} [props] - [:section.debug-icons-preview {:style {:background-color "black"}} + [:section.debug-icons-preview (for [[key val] (sort-by first (ns-publics 'app.main.ui.icons))] (when (not= key 'debug-icons-preview) - [:div.icon-item {:key key - :style {:fill "white"}} + [:div.icon-item {:key key} (deref val) [:span (pr-str key)]]))]) diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs index 8875df8c9..cd1aa34cd 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -26,10 +26,9 @@ [app.main.ui.keyboard :as kbd] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] - [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] - [app.main.ui.viewer.handoff.layers-sidebar :refer [layers-sidebar]] - [app.main.ui.viewer.handoff.attributes-sidebar :refer [attributes-sidebar]]) + [app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] + [app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]]) (:import goog.events.EventType)) (defn handle-select-frame [frame] @@ -47,7 +46,8 @@ (mf/use-effect (mf/deps index) (fn [] - (st/emit! (dv/select-shape (:id frame))))) + (st/emit! (dv/set-current-frame (:id frame)) + (dv/select-shape (:id frame))))) [:section.viewer-preview (cond @@ -61,12 +61,12 @@ :else [:* - [:& layers-sidebar {:frame frame}] + [:& left-sidebar {:frame frame}] [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} [:& render-frame-svg {:frame-id (:id frame) :zoom (:zoom local) :objects objects}]] - [:& attributes-sidebar {:frame frame}]])])) + [:& right-sidebar {:frame frame}]])])) (mf/defc handoff-content [{:keys [data local index] :as props}] diff --git a/frontend/src/app/main/ui/viewer/handoff/attrib_panel.cljs b/frontend/src/app/main/ui/viewer/handoff/attrib_panel.cljs deleted file mode 100644 index b8888e0d4..000000000 --- a/frontend/src/app/main/ui/viewer/handoff/attrib_panel.cljs +++ /dev/null @@ -1,440 +0,0 @@ -;; 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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL - -(ns app.main.ui.viewer.handoff.attrib-panel - (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.config :as cfg] - [app.util.data :as d] - [app.util.dom :as dom] - [app.util.i18n :refer [locale t]] - [app.util.color :as uc] - [app.util.text :as ut] - [app.common.math :as mth] - [app.common.geom.shapes :as gsh] - [app.main.fonts :as fonts] - [app.main.ui.icons :as i] - [app.util.webapi :as wapi] - [app.main.ui.components.color-bullet :refer [color-bullet color-name]])) - -(defn copy-cb [values properties & {:keys [to-prop format] :or {to-prop {}}}] - (fn [event] - (let [ - ;; We allow the :format and :to-prop to be a map for different properties - ;; or just a value for a single property. This code transform a single - ;; property to a uniform one - properties (if-not (coll? properties) [properties] properties) - - format (if (not (map? format)) - (into {} (map #(vector % format) properties)) - format) - - to-prop (if (not (map? to-prop)) - (into {} (map #(vector % to-prop) properties)) - to-prop) - - default-format (fn [value] (str (mth/precision value 2) "px")) - format-property (fn [prop] - (let [css-prop (or (prop to-prop) (name prop))] - (str/fmt " %s: %s;" css-prop ((or (prop format) default-format) (prop values) values)))) - - text-props (->> properties - (remove #(let [value (get values %)] - (or (nil? value) (= value 0)))) - (map format-property) - (str/join "\n")) - - result (str/fmt "{\n%s\n}" text-props)] - - (wapi/write-to-clipboard result)))) - - -(mf/defc color-row [{:keys [color format on-copy on-change-format]}] - (let [locale (mf/deref locale)] - [:div.attributes-color-row - [:& color-bullet {:color color}] - - (if (:gradient color) - [:& color-name {:color color}] - (case format - :rgba (let [[r g b a] (->> (uc/hex->rgba (:color color) (:opacity color)) (map #(mth/precision % 2)))] - [:div (str/fmt "%s, %s, %s, %s" r g b a)]) - :hsla (let [[h s l a] (->> (uc/hex->hsla (:color color) (:opacity color)) (map #(mth/precision % 2)))] - [:div (str/fmt "%s, %s, %s, %s" h s l a)]) - [:* - [:& color-name {:color color}] - (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) - - (when-not (and on-change-format (:gradient color)) - [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} - [:option {:value "hex"} - (t locale "handoff.attributes.color.hex")] - - [:option {:value "rgba"} - (t locale "handoff.attributes.color.rgba")] - - [:option {:value "hsla"} - (t locale "handoff.attributes.color.hsla")]]) - - (when on-copy - [:button.attributes-copy-button {:on-click on-copy} i/copy])])) - -(mf/defc layout-panel - [{:keys [shape locale]}] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.layout")] - [:button.attributes-copy-button - {:on-click (copy-cb shape - [:width :height :x :y :rotation] - :to-prop {:x "left" :y "top" :rotation "transform"} - :format {:rotation #(str/fmt "rotate(%sdeg)" %)})} - i/copy]] - - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.layout.width")] - [:div.attributes-value (mth/precision (:width shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :width)} - i/copy]] - - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.layout.height")] - [:div.attributes-value (mth/precision (:height shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :height)} - i/copy]] - - (when (not= (:x shape) 0) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.layout.left")] - [:div.attributes-value (mth/precision (:x shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :x :to-prop "left")} - i/copy]]) - - (when (not= (:y shape) 0) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.layout.top")] - [:div.attributes-value (mth/precision (:y shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :y :to-prop "top")} - i/copy]]) - - (when (not= (:rotation shape 0) 0) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.layout.rotation")] - [:div.attributes-value (mth/precision (:rotation shape) 2) "deg"] - [:button.attributes-copy-button - {:on-click (copy-cb shape - :rotation - :to-prop "transform" - :format #(str/fmt "rotate(%sdeg)" %))} - i/copy]])]) - -(mf/defc fill-panel - [{:keys [shape locale]}] - (let [color-format (mf/use-state :hex) - color {:color (:fill-color shape) - :opacity (:fill-opacity shape) - :gradient (:fill-color-gradient shape) - :id (:fill-ref-id shape) - :file-id (:fill-ref-file-id shape)} - - handle-copy (copy-cb shape - [:fill-color :fill-color-gradient] - :to-prop "background" - :format #(uc/color->background color))] - - (when (or (:color color) (:gradient color)) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.fill")] - [:button.attributes-copy-button - {:on-click handle-copy} - i/copy]] - - [:& color-row {:color color - :format @color-format - :on-change-format #(reset! color-format %) - :on-copy handle-copy}]]))) - -(mf/defc stroke-panel - [{:keys [shape locale]}] - (let [color-format (mf/use-state :hex) - color {:color (:stroke-color shape) - :opacity (:stroke-opacity shape) - :gradient (:stroke-color-gradient shape) - :id (:stroke-color-ref-id shape) - :file-id (:stroke-color-file-id shape)} - - handle-copy-stroke (copy-cb shape - :stroke-style - :to-prop "border" - :format #(let [width (:stroke-width %2) - style (name (:stroke-style %2)) - color (uc/color->background color)] - (str/format "%spx %s %s" width style color)))] - - (when (and (:stroke-style shape) (not= (:stroke-style shape) :none)) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.stroke")] - [:button.attributes-copy-button - {:on-click handle-copy-stroke} i/copy]] - - [:& color-row {:color color - :format @color-format - :on-change-format #(reset! color-format %) - :on-copy (copy-cb shape - :stroke-color - :to-prop "border-color" - :format #(uc/color->background color))}] - - [:div.attributes-stroke-row - [:div.attributes-label (t locale "handoff.attributes.stroke.width")] - [:div.attributes-value (:stroke-width shape) "px"] - [:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))] - [:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))] - [:button.attributes-copy-button - {:on-click handle-copy-stroke} i/copy]]]))) - -(defn shadow->css [shadow] - (let [{:keys [style offset-x offset-y blur spread]} shadow - css-color (uc/color->background (:color shadow))] - (str - (if (= style :inner-shadow) "inset " "") - (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) - -(mf/defc shadow-block [{:keys [shape locale shadow]}] - (let [color-format (mf/use-state :hex)] - [:div.attributes-shadow-block - [:div.attributes-shadow-row - [:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))] - [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")] - [:div.attributes-value (str (:offset-x shadow))]] - - [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-y")] - [:div.attributes-value (str (:offset-y shadow))]] - - [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.blur")] - [:div.attributes-value (str (:blur shadow))]] - - [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.spread")] - [:div.attributes-value (str (:spread shadow))]] - - [:button.attributes-copy-button - {:on-click (copy-cb shadow - :style - :to-prop "box-shadow" - :format #(shadow->css shadow))} - i/copy]] - [:& color-row {:color (:color shadow) - :format @color-format - :on-change-format #(reset! color-format %)}]])) - -(mf/defc shadow-panel [{:keys [shape locale]}] - (when (seq (:shadow shape)) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.shadow")] - [:button.attributes-copy-button - {:on-click (copy-cb shape - :shadow - :to-prop "box-shadow" - :format #(str/join ", " (map shadow->css (:shadow shape))))} - i/copy]] - - (for [shadow (:shadow shape)] - [:& shadow-block {:shape shape - :locale locale - :shadow shadow}])])) - -(mf/defc blur-panel [{:keys [shape locale]}] - (let [handle-copy - (copy-cb shape - :blur - :to-prop "filter" - :format #(str/fmt "blur(%spx)" %))] - (when (:blur shape) - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.blur")] - [:button.attributes-copy-button {:on-click handle-copy} i/copy]] - - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.blur.value")] - [:div.attributes-value (-> shape :blur :value) "px"] - [:button.attributes-copy-button {:on-click handle-copy} i/copy]]]))) - -(mf/defc image-panel [{:keys [shape locale]}] - [:div.attributes-block - [:div.attributes-image-row - [:div.attributes-image - [:img {:src (cfg/resolve-media-path (-> shape :metadata :path))}]]] - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.image.width")] - [:div.attributes-value (-> shape :metadata :width) "px"] - [:button.attributes-copy-button {:on-click (copy-cb shape :width)} i/copy]] - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.image.height")] - [:div.attributes-value (-> shape :metadata :height) "px"] - [:button.attributes-copy-button {:on-click (copy-cb shape :height)} i/copy]] - (let [filename (last (str/split (-> shape :metadata :path) "/"))] - [:a.download-button {:target "_blank" - :download filename - :href (cfg/resolve-media-path (-> shape :metadata :path))} - (t locale "handoff.attributes.image.download")])]) - - -(mf/defc text-block [{:keys [shape locale text style full-style]}] - (let [color-format (mf/use-state :hex) - color {:color (:fill-color style) - :opacity (:fill-opacity style) - :gradient (:fill-color-gradient style) - :id (:fill-color-ref-id style) - :file-id (:fill-color-ref-file-id style)} - properties [:fill-color - :fill-color-gradient - :font-family - :font-style - :font-size - :line-height - :letter-spacing - :text-decoration - :text-transform] - format {:font-family identity - :font-style identity - :font-size #(str % "px") - :line-height #(str % "px") - :letter-spacing #(str % "px") - :text-decoration name - :text-transform name - :fill-color #(uc/color->background color) - :fill-color-gradient #(uc/color->background color)} - to-prop {:fill-color "color" - :fill-color-gradient "color"}] - [:div.attributes-text-block - [:div.attributes-typography-row - [:div.typography-sample - {:style {:font-family (:font-family full-style) - :font-weight (:font-weight full-style) - :font-style (:font-style full-style)}} - (t locale "workspace.assets.typography.sample")] - [:button.attributes-copy-button - {:on-click (copy-cb style properties :to-prop to-prop :format format)} i/copy]] - - [:div.attributes-content-row - [:pre.attributes-content (str/trim text)] - [:button.attributes-copy-button - {:on-click #(wapi/write-to-clipboard (str/trim text))} - i/copy]] - - (when (or (:fill-color style) (:fill-color-gradient style)) - (let [color {:color (:fill-color style) - :opacity (:fill-opacity style) - :gradient (:fill-color-gradient style) - :id (:fill-ref-id style) - :file-id (:fill-ref-file-id style)}] - [:& color-row {:format @color-format - :on-change-format #(reset! color-format %) - :color color - :on-copy (copy-cb style [:fill-color :fill-color-gradient] :to-prop to-prop :format format)}])) - - (when (:font-id style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-family")] - [:div.attributes-value (-> style :font-id fonts/get-font-data :name)] - [:button.attributes-copy-button {:on-click (copy-cb style :font-family :format identity)} i/copy]]) - - (when (:font-style style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-style")] - [:div.attributes-value (str (:font-style style))] - [:button.attributes-copy-button {:on-click (copy-cb style :font-style :format identity)} i/copy]]) - - (when (:font-size style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.font-size")] - [:div.attributes-value (str (:font-size style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :font-size :format #(str % "px"))} i/copy]]) - - (when (:line-height style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.line-height")] - [:div.attributes-value (str (:line-height style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :line-height :format #(str % "px"))} i/copy]]) - - (when (:letter-spacing style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")] - [:div.attributes-value (str (:letter-spacing style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :letter-spacing :format #(str % "px"))} i/copy]]) - - (when (:text-decoration style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] - [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] - [:button.attributes-copy-button {:on-click (copy-cb style :text-decoration :format name)} i/copy]]) - - (when (:text-transform style) - [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] - [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] - [:button.attributes-copy-button {:on-click (copy-cb style :text-transform :format name)} i/copy]])])) - -(mf/defc typography-panel [{:keys [shape locale]}] - (let [font (ut/search-text-attrs (:content shape) - (keys ut/default-text-attrs)) - - style-text-blocks (->> (keys ut/default-text-attrs) - (ut/parse-style-text-blocks (:content shape)) - (remove (fn [[style text]] (str/empty? (str/trim text)))) - (mapv (fn [[style text]] (vector (merge ut/default-text-attrs style) text)))) - - font (merge ut/default-text-attrs font)] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.typography")]] - - (for [[idx [full-style text]] (map-indexed vector style-text-blocks)] - (let [previus-style (first (nth style-text-blocks (dec idx) nil)) - style (d/remove-equal-values full-style previus-style) - - ;; If the color is set we need to add opacity otherwise the display will not work - style (cond-> style - (:fill-color style) - (assoc :fill-opacity (:fill-opacity full-style)))] - [:& text-block {:shape shape - :locale locale - :full-style full-style - :style style - :text text}]))])) - -(mf/defc attrib-panel [{:keys [shape frame options]}] - (let [locale (mf/deref locale)] - [:div.element-options - (for [option options] - [:> - (case option - :layout layout-panel - :fill fill-panel - :stroke stroke-panel - :shadow shadow-panel - :blur blur-panel - :image image-panel - :typography typography-panel) - {:shape (gsh/translate-to-frame shape frame) - :frame frame - :locale locale}])])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs new file mode 100644 index 000000000..2863ced87 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs @@ -0,0 +1,43 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes + (:require + [rumext.alpha :as mf] + [app.util.i18n :as i18n] + [app.common.geom.shapes :as gsh] + [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] + [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] + [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] + [app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]] + [app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]] + [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] + [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]])) + +(mf/defc attributes + [{:keys [shapes frame options]}] + (let [locale (mf/deref i18n/locale) + shapes (->> shapes + (map #(gsh/translate-to-frame % frame))) + + shape (first shapes)] + [:div.element-options + (for [option options] + [:> (case option + :layout layout-panel + :fill fill-panel + :stroke stroke-panel + :shadow shadow-panel + :blur blur-panel + :image image-panel + :text text-panel) + {:shapes shapes + :frame frame + :locale locale}])])) + diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs new file mode 100644 index 000000000..a84a91b9c --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs @@ -0,0 +1,41 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.blur + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.i18n :refer [t]] + [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) + +(defn has-blur? [shape] + (:blur shape)) + +(defn copy-blur [shape] + (copy-cb shape + :blur + :to-prop "filter" + :format #(str/fmt "blur(%spx)" (:value %)))) + +(mf/defc blur-panel [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-blur?)) + handle-copy (when (= (count shapes) 1) (copy-blur (first shapes)))] + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.blur")] + (when handle-copy + [:button.attributes-copy-button {:on-click handle-copy} i/copy])] + + (for [shape shapes] + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.blur.value")] + [:div.attributes-value (-> shape :blur :value) "px"] + [:button.attributes-copy-button {:on-click (copy-blur shape)} i/copy]])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs new file mode 100644 index 000000000..b7ccd21fb --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs @@ -0,0 +1,81 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.common + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.dom :as dom] + [app.util.i18n :refer [t] :as i18n] + [app.util.color :as uc] + [app.common.math :as mth] + [app.main.ui.icons :as i] + [app.util.webapi :as wapi] + [app.main.ui.components.color-bullet :refer [color-bullet color-name]])) + +(defn copy-cb [values properties & {:keys [to-prop format] :or {to-prop {}}}] + (fn [event] + (let [ + ;; We allow the :format and :to-prop to be a map for different properties + ;; or just a value for a single property. This code transform a single + ;; property to a uniform one + properties (if-not (coll? properties) [properties] properties) + + format (if (not (map? format)) + (into {} (map #(vector % format) properties)) + format) + + to-prop (if (not (map? to-prop)) + (into {} (map #(vector % to-prop) properties)) + to-prop) + + default-format (fn [value] (str (mth/precision value 2) "px")) + format-property (fn [prop] + (let [css-prop (or (prop to-prop) (name prop))] + (str/fmt " %s: %s;" css-prop ((or (prop format) default-format) (prop values) values)))) + + text-props (->> properties + (remove #(let [value (get values %)] + (or (nil? value) (= value 0)))) + (map format-property) + (str/join "\n")) + + result (str/fmt "{\n%s\n}" text-props)] + + (wapi/write-to-clipboard result)))) + +(mf/defc color-row [{:keys [color format on-copy on-change-format]}] + (let [locale (mf/deref i18n/locale)] + [:div.attributes-color-row + [:& color-bullet {:color color}] + + (if (:gradient color) + [:& color-name {:color color}] + (case format + :rgba (let [[r g b a] (->> (uc/hex->rgba (:color color) (:opacity color)) (map #(mth/precision % 2)))] + [:div (str/fmt "%s, %s, %s, %s" r g b a)]) + :hsla (let [[h s l a] (->> (uc/hex->hsla (:color color) (:opacity color)) (map #(mth/precision % 2)))] + [:div (str/fmt "%s, %s, %s, %s" h s l a)]) + [:* + [:& color-name {:color color}] + (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) + + (when-not (and on-change-format (:gradient color)) + [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} + [:option {:value "hex"} + (t locale "handoff.attributes.color.hex")] + + [:option {:value "rgba"} + (t locale "handoff.attributes.color.rgba")] + + [:option {:value "hsla"} + (t locale "handoff.attributes.color.hsla")]]) + + (when on-copy + [:button.attributes-copy-button {:on-click on-copy} i/copy])])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs new file mode 100644 index 000000000..c4c9d6e27 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs @@ -0,0 +1,67 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.fill + (:require + [rumext.alpha :as mf] + [app.util.i18n :refer [t]] + [app.util.color :as uc] + [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + +(def fill-attributes [:fill-color :fill-color-gradient]) + +(defn shape->color [shape] + {:color (:fill-color shape) + :opacity (:fill-opacity shape) + :gradient (:fill-color-gradient shape) + :id (:fill-ref-id shape) + :file-id (:fill-ref-file-id shape)}) + +(defn has-color? [shape] + (and + (not (contains? #{:image :text :group} (:type shape))) + (or (:fill-color shape) + (:fill-color-gradient shape)))) + +(mf/defc fill-block [{:keys [shape locale]}] + (let [color-format (mf/use-state :hex) + color (shape->color shape) + handle-copy (copy-cb shape + fill-attributes + :to-prop "background" + :format #(uc/color->background color))] + + [:& color-row {:color color + :format @color-format + :on-change-format #(reset! color-format %) + :on-copy handle-copy}])) + +(mf/defc fill-panel + [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-color?)) + handle-copy (when (= (count shapes) 1) + (copy-cb (first shapes) + fill-attributes + :to-prop "background" + :format #(-> shapes first shape->color uc/color->background)))] + + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.fill")] + (when handle-copy + [:button.attributes-copy-button + {:on-click handle-copy} + i/copy])] + + (for [shape shapes] + [:& fill-block {:key (str "fill-block-" (:id shape)) + :shape shape + :locale locale}])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs new file mode 100644 index 000000000..057c02369 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs @@ -0,0 +1,44 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.image + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.config :as cfg] + [app.util.i18n :refer [t]] + [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) + +(defn has-image? [shape] + (and (= (:type shape) :image))) + +(mf/defc image-panel [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-image?))] + (for [shape shapes] + [:div.attributes-block {:key (str "image-" (:id shape))} + [:div.attributes-image-row + [:div.attributes-image + [:img {:src (cfg/resolve-media-path (-> shape :metadata :path))}]]] + + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.image.width")] + [:div.attributes-value (-> shape :metadata :width) "px"] + [:button.attributes-copy-button {:on-click (copy-cb shape :width)} i/copy]] + + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.image.height")] + [:div.attributes-value (-> shape :metadata :height) "px"] + [:button.attributes-copy-button {:on-click (copy-cb shape :height)} i/copy]] + + (let [filename (last (str/split (-> shape :metadata :path) "/"))] + [:a.download-button {:target "_blank" + :download filename + :href (cfg/resolve-media-path (-> shape :metadata :path))} + (t locale "handoff.attributes.image.download")])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs index 3f20d212b..748905449 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -10,100 +10,75 @@ (ns app.main.ui.viewer.handoff.attributes.layout (:require [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.i18n :refer [t]] + [app.common.math :as mth] [app.main.ui.icons :as i] - [app.main.ui.components.color-bullet :refer [color-bullet color-name]])) + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) +(defn copy-layout [shape] + (copy-cb shape + [:width :height :x :y :rotation] + :to-prop {:x "left" :y "top" :rotation "transform"} + :format {:rotation #(str/fmt "rotate(%sdeg)" %)})) -(mf/defc layout-panel [{:keys [shapes]}] +(mf/defc layout-block + [{:keys [shape locale]}] [:* - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text "Layout"] - [:button.attributes-copy-button i/copy]] + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.width")] + [:div.attributes-value (mth/precision (:width shape) 2) "px"] + [:button.attributes-copy-button + {:on-click (copy-cb shape :width)} + i/copy]] - [:div.attributes-unit-row - [:div.attributes-label "Width"] - [:div.attributes-value "100px"] - [:button.attributes-copy-button i/copy]] + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.height")] + [:div.attributes-value (mth/precision (:height shape) 2) "px"] + [:button.attributes-copy-button + {:on-click (copy-cb shape :height)} + i/copy]] - [:div.attributes-unit-row - [:div.attributes-label "Height"] - [:div.attributes-value "100px"] - [:button.attributes-copy-button i/copy]] + (when (not= (:x shape) 0) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.left")] + [:div.attributes-value (mth/precision (:x shape) 2) "px"] + [:button.attributes-copy-button + {:on-click (copy-cb shape :x :to-prop "left")} + i/copy]]) + + (when (not= (:y shape) 0) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.top")] + [:div.attributes-value (mth/precision (:y shape) 2) "px"] + [:button.attributes-copy-button + {:on-click (copy-cb shape :y :to-prop "top")} + i/copy]]) - [:div.attributes-unit-row - [:div.attributes-label "Top"] - [:div.attributes-value "100px"] - [:button.attributes-copy-button i/copy]] + (when (not= (:rotation shape 0) 0) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.rotation")] + [:div.attributes-value (mth/precision (:rotation shape) 2) "deg"] + [:button.attributes-copy-button + {:on-click (copy-cb shape + :rotation + :to-prop "transform" + :format #(str/fmt "rotate(%sdeg)" %))} + i/copy]])]) - [:div.attributes-unit-row - [:div.attributes-label "Left"] - [:div.attributes-value "100px"] - [:button.attributes-copy-button i/copy]]] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text "Fill"] - [:button.attributes-copy-button i/copy]] +(mf/defc layout-panel + [{:keys [shapes locale]}] + (let [handle-copy (when (= (count shapes) 1) + (copy-layout (first shapes)))] + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.layout")] + (when handle-copy + [:button.attributes-copy-button + {:on-click handle-copy} + i/copy])] - [:div.attributes-shadow-row - [:div.attributes-label "Drop"] - [:div.attributes-shadow - [:div.attributes-label "X"] - [:div.attributes-value "4"]] - - [:div.attributes-shadow - [:div.attributes-label "Y"] - [:div.attributes-value "4"]] - - [:div.attributes-shadow - [:div.attributes-label "B"] - [:div.attributes-value "0"]] - - [:div.attributes-shadow - [:div.attributes-label "B"] - [:div.attributes-value "0"]] - - [:button.attributes-copy-button i/copy]] - - [:div.attributes-color-row - [:& color-bullet {:color {:color "#000000" :opacity 0.5}}] - - [:* - [:div "#000000"] - [:div "100%"]] - - [:select - [:option "Hex"] - [:option "RGBA"] - [:option "HSLA"]] - - [:button.attributes-copy-button i/copy]] - - [:div.attributes-stroke-row - [:div.attributes-label "Width"] - [:div.attributes-value "1px"] - [:div.attributes-value "Solid"] - [:div.attributes-label "Center"] - [:button.attributes-copy-button i/copy]]] - - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text "Content"] - [:button.attributes-copy-button i/copy]] - - [:div.attributes-content-row - [:div.attributes-content - "Hi, how are you"] - [:button.attributes-copy-button i/copy]]] - - [:div.attributes-block - [:div.attributes-image-row - [:div.attributes-image - #_[:img {:src "https://www.publico.es/tremending/wp-content/uploads/2019/05/Cxagv.jpg"}] - #_[:img {:src "https://i.blogs.es/3861b2/grumpy-cat/1366_2000.png"}] - [:img {:src "https://abs.twimg.com/favicons/twitter.ico"}] - ]] - [:button.download-button "Dowload source image"]] - - ]) + (for [shape shapes] + [:& layout-block {:shape shape + :locale locale}])])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs new file mode 100644 index 000000000..05ab8d2dc --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs @@ -0,0 +1,79 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.shadow + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.i18n :refer [t]] + [app.util.color :as uc] + [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + +(defn has-shadow? [shape] + (:shadow shape)) + +(defn shadow->css [shadow] + (let [{:keys [style offset-x offset-y blur spread]} shadow + css-color (uc/color->background (:color shadow))] + (str + (if (= style :inner-shadow) "inset " "") + (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) + + +(mf/defc shadow-block [{:keys [shape locale shadow]}] + (let [color-format (mf/use-state :hex)] + [:div.attributes-shadow-block + [:div.attributes-shadow-row + [:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))] + [:div.attributes-shadow + [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")] + [:div.attributes-value (str (:offset-x shadow))]] + + [:div.attributes-shadow + [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-y")] + [:div.attributes-value (str (:offset-y shadow))]] + + [:div.attributes-shadow + [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.blur")] + [:div.attributes-value (str (:blur shadow))]] + + [:div.attributes-shadow + [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.spread")] + [:div.attributes-value (str (:spread shadow))]] + + [:button.attributes-copy-button + {:on-click (copy-cb shadow + :style + :to-prop "box-shadow" + :format #(shadow->css shadow))} + i/copy]] + [:& color-row {:color (:color shadow) + :format @color-format + :on-change-format #(reset! color-format %)}]])) + +(mf/defc shadow-panel [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-shadow?)) + handle-copy-shadow (when (= (count shapes) 1) + (copy-cb (first shapes) + :shadow + :to-prop "box-shadow" + :format #(str/join ", " (map shadow->css (:shadow (first shapes))))))] + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.shadow")] + (when handle-copy-shadow + [:button.attributes-copy-button {:on-click handle-copy-shadow} i/copy])] + + (for [shape shapes] + (for [shadow (:shadow shape)] + [:& shadow-block {:shape shape + :locale locale + :shadow shadow}]))]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs new file mode 100644 index 000000000..6dc73f410 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs @@ -0,0 +1,83 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.stroke + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.i18n :refer [t]] + [app.util.color :as uc] + [app.main.ui.icons :as i] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + +(defn shape->color [shape] + {:color (:stroke-color shape) + :opacity (:stroke-opacity shape) + :gradient (:stroke-color-gradient shape) + :id (:stroke-ref-id shape) + :file-id (:stroke-ref-file-id shape)}) + +(defn format-stroke [shape] + (let [width (:stroke-width shape) + style (name (:stroke-style shape)) + color (-> shape shape->color uc/color->background)] + (str/format "%spx %s %s" width style color))) + +(defn has-stroke? [shape] + (and (:stroke-style shape) + (not= (:stroke-style shape) :none))) + +(mf/defc stroke-block + [{:keys [shape locale]}] + (let [color-format (mf/use-state :hex) + color (shape->color shape) + handle-copy-stroke (copy-cb shape + :stroke-style + :to-prop "border" + :format #(format-stroke shape)) + + handle-copy-color (copy-cb shape + :stroke-color + :to-prop "border-color" + :format #(uc/color->background color))] + + [:* + [:& color-row {:color color + :format @color-format + :on-change-format #(reset! color-format %) + :on-copy handle-copy-color}] + + [:div.attributes-stroke-row + [:div.attributes-label (t locale "handoff.attributes.stroke.width")] + [:div.attributes-value (:stroke-width shape) "px"] + [:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))] + [:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))] + [:button.attributes-copy-button {:on-click handle-copy-stroke} i/copy]]])) + +(mf/defc stroke-panel + [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-stroke?)) + handle-copy (when (= (count shapes) 1) + (copy-cb (first shapes) + :stroke-style + :to-prop "border" + :format #(format-stroke (first shapes))))] + + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.stroke")] + (when handle-copy + [:button.attributes-copy-button + {:on-click handle-copy} i/copy])] + + (for [shape shapes] + [:& stroke-block {:key (str "stroke-color-" (:id shape)) + :shape shape + :locale locale}])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs new file mode 100644 index 000000000..b5847cc14 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs @@ -0,0 +1,164 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes.text + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [app.util.data :as d] + [app.util.i18n :refer [t]] + [app.util.color :as uc] + [app.util.text :as ut] + [app.main.fonts :as fonts] + [app.main.ui.icons :as i] + [app.util.webapi :as wapi] + [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + +(defn has-text? [shape] + (:content shape)) + +(def properties [:fill-color + :fill-color-gradient + :font-family + :font-style + :font-size + :line-height + :letter-spacing + :text-decoration + :text-transform]) + +(defn shape->color [shape] + {:color (:fill-color shape) + :opacity (:fill-opacity shape) + :gradient (:fill-color-gradient shape) + :id (:fill-ref-id shape) + :file-id (:fill-ref-file-id shape)}) + +(defn format-style [color] + {:font-family #(str "'" % "'") + :font-style #(str "'" % "'") + :font-size #(str % "px") + :line-height #(str % "px") + :letter-spacing #(str % "px") + :text-decoration name + :text-transform name + :fill-color #(uc/color->background color) + :fill-color-gradient #(uc/color->background color)}) + +(mf/defc typography-block [{:keys [shape locale text style full-style]}] + (let [color-format (mf/use-state :hex) + color (shape->color style) + to-prop {:fill-color "color" + :fill-color-gradient "color"}] + [:div.attributes-text-block + [:div.attributes-typography-row + [:div.typography-sample + {:style {:font-family (:font-family full-style) + :font-weight (:font-weight full-style) + :font-style (:font-style full-style)}} + (t locale "workspace.assets.typography.sample")] + [:button.attributes-copy-button + {:on-click (copy-cb style properties + :to-prop to-prop + :format (format-style color))} + i/copy]] + + [:div.attributes-content-row + [:pre.attributes-content (str/trim text)] + [:button.attributes-copy-button + {:on-click #(wapi/write-to-clipboard (str/trim text))} + i/copy]] + + (when (or (:fill-color style) (:fill-color-gradient style)) + [:& color-row {:format @color-format + :on-change-format #(reset! color-format %) + :color (shape->color style) + :on-copy (copy-cb style + [:fill-color :fill-color-gradient] + :to-prop to-prop + :format (format-style color))}]) + + (when (:font-id style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.font-family")] + [:div.attributes-value (-> style :font-id fonts/get-font-data :name)] + [:button.attributes-copy-button {:on-click (copy-cb style :font-family :format identity)} i/copy]]) + + (when (:font-style style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.font-style")] + [:div.attributes-value (str (:font-style style))] + [:button.attributes-copy-button {:on-click (copy-cb style :font-style :format identity)} i/copy]]) + + (when (:font-size style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.font-size")] + [:div.attributes-value (str (:font-size style)) "px"] + [:button.attributes-copy-button {:on-click (copy-cb style :font-size :format #(str % "px"))} i/copy]]) + + (when (:line-height style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.line-height")] + [:div.attributes-value (str (:line-height style)) "px"] + [:button.attributes-copy-button {:on-click (copy-cb style :line-height :format #(str % "px"))} i/copy]]) + + (when (:letter-spacing style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")] + [:div.attributes-value (str (:letter-spacing style)) "px"] + [:button.attributes-copy-button {:on-click (copy-cb style :letter-spacing :format #(str % "px"))} i/copy]]) + + (when (:text-decoration style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] + [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] + [:button.attributes-copy-button {:on-click (copy-cb style :text-decoration :format name)} i/copy]]) + + (when (:text-transform style) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] + [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] + [:button.attributes-copy-button {:on-click (copy-cb style :text-transform :format name)} i/copy]])])) + + +(mf/defc text-block [{:keys [shape locale]}] + (let [font (ut/search-text-attrs (:content shape) + (keys ut/default-text-attrs)) + + style-text-blocks (->> (keys ut/default-text-attrs) + (ut/parse-style-text-blocks (:content shape)) + (remove (fn [[style text]] (str/empty? (str/trim text)))) + (mapv (fn [[style text]] (vector (merge ut/default-text-attrs style) text)))) + + font (merge ut/default-text-attrs font)] + (for [[idx [full-style text]] (map-indexed vector style-text-blocks)] + (let [previus-style (first (nth style-text-blocks (dec idx) nil)) + style (d/remove-equal-values full-style previus-style) + + ;; If the color is set we need to add opacity otherwise the display will not work + style (cond-> style + (:fill-color style) + (assoc :fill-opacity (:fill-opacity full-style)))] + [:& typography-block {:shape shape + :locale locale + :full-style full-style + :style style + :text text}])))) + +(mf/defc text-panel [{:keys [shapes locale]}] + (let [shapes (->> shapes (filter has-text?))] + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.typography")]] + + (for [shape shapes] + [:& text-block {:shape shape + :locale locale}])]))) + diff --git a/frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs similarity index 83% rename from frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs rename to frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs index 7e7d269e4..0c2a4ef82 100644 --- a/frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.layers-sidebar +(ns app.main.ui.viewer.handoff.left-sidebar (:require [rumext.alpha :as mf] [okulary.core :as l] @@ -45,29 +45,22 @@ toggle-collapse (fn [event] (dom/stop-propagation event) - (if (and expanded? (kbd/shift? event)) - (st/emit! (dv/collapse-all)) - (st/emit! (dv/toggle-collapse id)))) + (st/emit! (dv/toggle-collapse id))) select-shape (fn [event] (dom/prevent-default event) (let [id (:id item)] - (st/emit! (dv/select-shape id)) - #_(cond - (or (:blocked item) - (:hidden item)) - nil + (cond + (.-ctrlKey event) + (st/emit! (dv/toggle-selection id)) (.-shiftKey event) - (st/emit! (dv/select-shape id true)) + (st/emit! (dv/shift-select-to id)) - (> (count selected) 1) - (st/emit! (dv/deselect-all) - (dv/select-shape id)) :else - (st/emit! (dv/deselect-all) - (dv/select-shape id))))) + (st/emit! (dv/select-shape id))) + )) ] (mf/use-effect @@ -105,7 +98,7 @@ :objects objects :key (:id item)}]))])])) -(mf/defc layers-sidebar [{:keys [frame]}] +(mf/defc left-sidebar [{:keys [frame]}] (let [page (mf/deref page-ref) selected (mf/deref selected-shapes) objects (:objects page)] diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 5d55f462c..ab760080f 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -44,10 +44,17 @@ (st/emit! (dv/hover-shape id hover?))))) (defn select-shape [{:keys [type id]}] - #(when-not (#{:group :frame} type) - (dom/prevent-default %) - (dom/stop-propagation %) - (st/emit! (dv/select-shape id)))) + (fn [event] + (when-not (#{:group :frame} type) + (do + (dom/stop-propagation event) + (dom/prevent-default event) + (cond + (.-shiftKey event) + (st/emit! (dv/toggle-selection id)) + + :else + (st/emit! (dv/select-shape id))))))) (defn shape-wrapper-factory [component] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs similarity index 66% rename from frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs rename to frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs index 195628073..3a97ff6b2 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes-sidebar +(ns app.main.ui.viewer.handoff.right-sidebar (:require [rumext.alpha :as mf] [okulary.core :as l] @@ -15,7 +15,7 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.components.tab-container :refer [tab-container tab-element]] - [app.main.ui.viewer.handoff.attrib-panel :refer [attrib-panel]] + [app.main.ui.viewer.handoff.attributes :refer [attributes]] [app.main.ui.workspace.sidebar.layers :refer [element-icon]])) (defn make-selected-shapes-iref @@ -28,30 +28,28 @@ (mapv resolve-shape selected)))] #(l/derived selected->shapes st/state))) -(mf/defc info-panel [{:keys [frame shapes]}] - (if (> (count shapes) 1) - ;; TODO:Multiple selection - nil - ;; Single shape - (when-let [shape (first shapes)] - (let [options - (case (:type shape) - :frame [:layout :fill] - :group [:layout] - :rect [:layout :fill :stroke :shadow :blur] - :circle [:layout :fill :stroke :shadow :blur] - :path [:layout :fill :stroke :shadow :blur] - :curve [:layout :fill :stroke :shadow :blur] - :image [:image :layout :shadow :blur] - :text [:layout :typography :shadow :blur])] - [:& attrib-panel {:frame frame - :shape shape - :options options}])))) +(mf/defc attributes-panel [{:keys [frame shapes]}] + (let [type (if (= (count shapes) 1) + (-> shapes first :type) + :multiple)] + (let [options (case type + :multiple [:fill :stroke :image :text :shadow :blur] + :frame [:layout :fill] + :group [:layout] + :rect [:layout :fill :stroke :shadow :blur] + :circle [:layout :fill :stroke :shadow :blur] + :path [:layout :fill :stroke :shadow :blur] + :curve [:layout :fill :stroke :shadow :blur] + :image [:image :layout :shadow :blur] + :text [:layout :text :shadow :blur])] + [:& attributes {:frame frame + :shapes shapes + :options options}]))) (mf/defc code-panel [] [:div.element-options]) -(mf/defc attributes-sidebar [{:keys [frame]}] +(mf/defc right-sidebar [{:keys [frame]}] (let [locale (mf/deref i18n/locale) section (mf/use-state :info #_:code) selected-ref (mf/use-memo (make-selected-shapes-iref)) @@ -74,8 +72,8 @@ [:& tab-container {:on-change-tab #(reset! section %) :selected @section} [:& tab-element {:id :info :title (t locale "handoff.tabs.info")} - [:& info-panel {:frame frame - :shapes shapes}]] + [:& attributes-panel {:frame frame + :shapes shapes}]] [:& tab-element {:id :code :title (t locale "handoff.tabs.code")} [:& code-panel]]]]])]]))