diff --git a/common/src/app/common/geom/shapes/layout.cljc b/common/src/app/common/geom/shapes/layout.cljc index 9d7300aca..dc27fb429 100644 --- a/common/src/app/common/geom/shapes/layout.cljc +++ b/common/src/app/common/geom/shapes/layout.cljc @@ -11,7 +11,7 @@ [app.common.geom.shapes.rect :as gre])) ;; :layout ;; true if active, false if not -;; :layout-dir ;; :right, :left, :top, :bottom +;; :layout-flex-dir ;; :row, :column, :reverse-row, :reverse-column ;; :layout-gap ;; number could be negative ;; :layout-type ;; :packed, :space-between, :space-around ;; :layout-wrap-type ;; :wrap, :no-wrap @@ -21,12 +21,12 @@ ;; :layout-v-orientation ;; :left, :center, :right (defn col? - [{:keys [layout-dir]}] - (or (= :right layout-dir) (= :left layout-dir))) + [{:keys [layout-flex-dir]}] + (or (= :column layout-flex-dir) (= :reverse-column layout-flex-dir))) (defn row? - [{:keys [layout-dir]}] - (or (= :top layout-dir) (= :bottom layout-dir))) + [{:keys [layout-flex-dir]}] + (or (= :row layout-flex-dir) (= :reverse-row layout-flex-dir))) (defn h-start? [{:keys [layout-h-orientation]}] @@ -247,9 +247,9 @@ (defn calc-layout-data "Digest the layout data to pass it to the constrains" - [{:keys [layout-dir] :as shape} children layout-bounds] + [{:keys [layout-flex-dir] :as shape} children layout-bounds] - (let [reverse? (or (= :left layout-dir) (= :bottom layout-dir)) + (let [reverse? (or (= :reverse-row layout-flex-dir) (= :reverse-column layout-flex-dir)) layout-bounds (-> layout-bounds (add-padding shape)) children (cond->> children reverse? reverse) layout-lines diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss index a8e229397..b5f2fdfb0 100644 --- a/frontend/resources/styles/main/partials/handoff.scss +++ b/frontend/resources/styles/main/partials/handoff.scss @@ -87,7 +87,7 @@ position: relative; display: flex; flex-direction: row; - padding: 1rem 1.6rem 1rem 0.5rem; + padding: 0.6rem 1.6rem 0.6rem 0.5rem; .attributes-label, .attributes-value { @@ -96,9 +96,12 @@ text-overflow: ellipsis; white-space: nowrap; width: 50%; + .items { + margin-right: 5px; + } } .copy-button { - padding: 1rem 0.5rem; + padding: 0.6rem 0.5rem; margin-top: 0.25rem; } } diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 0b96b54b5..710995c9b 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -16,34 +16,29 @@ (def layout-keys [:layout - :layout-dir + :layout-flex-dir + :layout-gap-type :layout-gap - :layout-type + :layout-align-items + :layout-justify-content + :layout-align-content :layout-wrap-type :layout-padding-type :layout-padding - :layout-h-orientation - :layout-v-orientation - - :layout-align-content - :layout-flex-dir - :layout-align-items - :layout-justify-content - :layout-gap-type ]) (def initial-flex-layout - {:layout :flex - :layout-flex-dir :row - :layout-gap-type :simple - :layout-gap {:row-gap 0 :column-gap 0} - :layout-align-items :start + {:layout :flex + :layout-flex-dir :row + :layout-gap-type :simple + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start :layout-justify-content :start - :layout-align-content :strech - :layout-wrap-type :no-wrap - :layout-padding-type :simple - :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) + :layout-align-content :strech + :layout-wrap-type :no-wrap + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) (def initial-grid-layout ;; TODO {:layout :grid}) diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index fab39b72c..c2f68bd62 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -28,6 +28,10 @@ ([state page-id] (dm/get-in state [:workspace-data :pages-index page-id :objects]))) +(defn lookup-viewer-objects + ([state page-id] + (dm/get-in state [:viewer :pages page-id :objects]))) + (defn lookup-page-options ([state] (lookup-page-options state (:current-page-id state))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4b0c22d8c..5717f7c86 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -363,6 +363,10 @@ ;; ---- Viewer refs +(defn lookup-viewer-objects-by-id + [page-id] + (l/derived #(wsh/lookup-viewer-objects % page-id) st/state =)) + (def viewer-data (l/derived :viewer st/state)) @@ -427,5 +431,13 @@ (some #(-> (cph/get-parent objects %) :layout)))) workspace-page-objects)) +(defn get-flex-child-viewer? + [ids page-id] + (l/derived + (fn [state] + (let [objects (wsh/lookup-viewer-objects state page-id)] + (filterv #(= :flex (:layout (cph/get-parent objects %))) ids))) + st/state =)) + (def colorpicker (l/derived :colorpicker st/state)) diff --git a/frontend/src/app/main/ui/formats.cljs b/frontend/src/app/main/ui/formats.cljs index f09367736..c90ec87ab 100644 --- a/frontend/src/app/main/ui/formats.cljs +++ b/frontend/src/app/main/ui/formats.cljs @@ -41,3 +41,23 @@ (when (d/num? value) (let [value (mth/precision value 0)] (dm/str value)))) + +(defn format-padding-margin-shorthand + [values] + ;; Values come in [p1 p2 p3 p4] + (let [[p1 p2 p3 p4] values] + (cond + (apply = values) + {:p1 p1} + + (= 4 (count (set values))) + {:p1 p1 :p2 p2 :p3 p3} + + (and (= p1 p3) (= p2 p4)) + {:p1 p1 :p3 p3} + + (and (not= p1 p3) (= p2 p4)) + {:p1 p1 :p2 p2 :p3 p3} + + :else + {:p1 p1 :p2 p2 :p3 p3}))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs index c175378e5..91e803da1 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs @@ -12,6 +12,8 @@ [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] + [app.main.ui.viewer.handoff.attributes.layout-flex :refer [layout-flex-panel]] + [app.main.ui.viewer.handoff.attributes.layout-flex-element :refer [layout-flex-element-panel]] [app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]] [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] [app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]] @@ -20,14 +22,14 @@ [rumext.v2 :as mf])) (def type->options - {:multiple [:fill :stroke :image :text :shadow :blur] - :frame [:layout :fill :stroke :shadow :blur] - :group [:layout :svg] - :rect [:layout :fill :stroke :shadow :blur :svg] - :circle [:layout :fill :stroke :shadow :blur :svg] + {:multiple [:fill :stroke :image :text :shadow :blur :layout-flex-item] + :frame [:layout :fill :stroke :shadow :blur :layout-flex :layout-flex-item] + :group [:layout :svg :layout-flex-item] + :rect [:layout :fill :stroke :shadow :blur :svg :layout-flex-item] + :circle [:layout :fill :stroke :shadow :blur :svg :layout-flex-item] :path [:layout :fill :stroke :shadow :blur :svg] - :image [:image :layout :fill :stroke :shadow :blur :svg] - :text [:layout :text :shadow :blur :stroke]}) + :image [:image :layout :fill :stroke :shadow :blur :svg :layout-flex-item] + :text [:layout :text :shadow :blur :stroke :layout-flex-item]}) (mf/defc attributes [{:keys [page-id file-id shapes frame]}] @@ -39,14 +41,16 @@ [: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 - :svg svg-panel) + :layout layout-panel + :layout-flex layout-flex-panel + :layout-flex-item layout-flex-element-panel + :fill fill-panel + :stroke stroke-panel + :shadow shadow-panel + :blur blur-panel + :image image-panel + :text text-panel + :svg svg-panel) {:shapes shapes :frame frame}]) [:& exports 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 7f9e70aad..a3068085c 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -91,4 +91,5 @@ [:& copy-button {:data (copy-data (first shapes))}])] (for [shape shapes] - [:& layout-block {:shape shape}])]) + [:& layout-block {:shape shape + :key (:id shape)}])]) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex.cljs new file mode 100644 index 000000000..2c7e61b73 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex.cljs @@ -0,0 +1,152 @@ +;; 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.viewer.handoff.attributes.layout-flex + (:require + [app.common.data :as d] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.formats :as fm] + [app.util.code-gen :as cg] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn format-gap + [gap-values] + (let [row-gap (:row-gap gap-values) + column-gap (:column-gap gap-values)] + (if (= row-gap column-gap) + (str/fmt "%spx" row-gap) + (str/fmt "%spx %spx" row-gap column-gap)))) + +(defn format-padding + [padding-values] + (let [short-hand (fm/format-padding-margin-shorthand (vals padding-values)) + parsed-values (map #(str/fmt "%spx" %) (vals short-hand))] + (str/join " " parsed-values))) + +(def properties [:layout + :layout-flex-dir + :layout-align-items + :layout-justify-content + :layout-gap + :layout-padding + :layout-wrap-type]) + +(def align-contet-prop [:layout-align-content]) + +(def layout-flex-params + {:props [:layout + :layout-align-items + :layout-flex-dir + :layout-justify-content + :layout-gap + :layout-padding + :layout-wrap-type] + :to-prop {:layout "display" + :layout-flex-dir "flex-direction" + :layout-align-items "align-items" + :layout-justify-content "justify-content" + :layout-wrap-type "wrap" + :layout-gap "gap" + :layout-padding "padding"} + :format {:layout name + :layout-flex-dir name + :layout-align-items name + :layout-justify-content name + :layout-wrap-type name + :layout-gap format-gap + :layout-padding format-padding}}) + +(def layout-align-content-params + {:props [:layout-align-content] + :to-prop {:layout-align-content "align-content"} + :format {:layout-align-content name}}) + +(defn copy-data + ([shape] + (let [properties-for-copy (if (:layout-align-content shape) + (into [] (concat properties align-contet-prop)) + properties)] + (apply copy-data shape properties-for-copy))) + + ([shape & properties] + (let [params (if (:layout-align-content shape) + (d/deep-merge layout-align-content-params layout-flex-params ) + layout-flex-params)] + (cg/generate-css-props shape properties params)))) + +(mf/defc manage-padding + [{:keys [padding type]}] + (let [values (fm/format-padding-margin-shorthand (vals padding))] + [:div.attributes-value + (for [[k v] values] + [:span.items {:key (str type "-" k "-" v)} v "px"])])) + +(mf/defc layout-block + [{:keys [shape]}] + [:* + [:div.attributes-unit-row + [:div.attributes-label "Display"] + [:div.attributes-value "Flex"] + [:& copy-button {:data (copy-data shape)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Direction"] + [:div.attributes-value (str/capital (d/name (:layout-flex-dir shape)))] + [:& copy-button {:data (copy-data shape :layout-flex-dir)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Align-items"] + [:div.attributes-value (str/capital (d/name (:layout-align-items shape)))] + [:& copy-button {:data (copy-data shape :layout-align-items)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Justify-content"] + [:div.attributes-value (str/capital (d/name (:layout-justify-content shape)))] + [:& copy-button {:data (copy-data shape :layout-justify-content)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Wrap"] + [:div.attributes-value (str/capital (d/name (:layout-wrap-type shape)))] + [:& copy-button {:data (copy-data shape :layout-wrap-type)}]] + + (when (= :wrap (:layout-wrap-type shape)) + [:div.attributes-unit-row + [:div.attributes-label "Align-content"] + [:div.attributes-value (str/capital (d/name (:layout-align-content shape)))] + [:& copy-button {:data (copy-data shape :layout-align-content)}]]) + + [:div.attributes-unit-row + [:div.attributes-label "Gap"] + (if (= (:row-gap (:layout-gap shape)) (:column-gap (:layout-gap shape))) + [:div.attributes-value + [:span (str/capital (d/name (:row-gap (:layout-gap shape)))) "px"]] + [:div.attributes-value + [:span.items (:row-gap (:layout-gap shape)) "px"] + [:span (:column-gap (:layout-gap shape)) "px"]]) + [:& copy-button {:data (copy-data shape :layout-gap)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Padding"] + [:& manage-padding {:padding (:layout-padding shape) :type "padding"}] + [:& copy-button {:data (copy-data shape :layout-padding)}]]]) + +(defn has-flex? [shape] + (= :flex (:layout shape))) + +(mf/defc layout-flex-panel + [{:keys [shapes]}] + (let [shapes (->> shapes (filter has-flex?))] + (when (seq shapes) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text "Layout"] + (when (= (count shapes) 1) + [:& copy-button {:data (copy-data (first shapes))}])] + + (for [shape shapes] + [:& layout-block {:shape shape + :key (:id shape)}])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex_element.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex_element.cljs new file mode 100644 index 000000000..31d7da405 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout_flex_element.cljs @@ -0,0 +1,133 @@ +;; 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.viewer.handoff.attributes.layout-flex-element + (:require + [app.common.data :as d] + [app.main.refs :as refs] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as hooks] + [app.util.code-gen :as cg] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + + +(defn format-margin + [margin-values] + (let [short-hand (fmt/format-padding-margin-shorthand (vals margin-values)) + parsed-values (map #(str/fmt "%spx" %) (vals short-hand))] + (str/join " " parsed-values))) + +(def properties [:layout-item-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} + :layout-item-h-sizing ;; :fill-width :fix-width :auto-width + :layout-item-v-sizing ;; :fill-height :fix-height :auto-height + :layout-item-max-h ;; num + :layout-item-min-h ;; num + :layout-item-max-w ;; num + :layout-item-min-w ;; num + :layout-item-align-self ;; :start :end :center :strech :baseline + ]) + + +(def layout-flex-item-params + {:props [:layout-item-margin + :layout-item-h-sizing + :layout-item-v-sizing + :layout-item-max-h + :layout-item-min-h + :layout-item-max-w + :layout-item-min-w + :layout-item-align-self] + :to-prop {:layout-item-margin "margin" + :layout-item-h-sizing "width" + :layout-item-v-sizing "height" + :layout-item-align-self "align-self" + :layout-item-max-h "max. height" + :layout-item-min-h "min. height" + :layout-item-max-w "max. width" + :layout-item-min-w "min. width"} + :format {:layout-item-margin format-margin + :layout-item-h-sizing name + :layout-item-v-sizing name + :layout-item-align-self name}}) + +(defn copy-data + ([shape] + (apply copy-data shape properties)) + + ([shape & properties] + (cg/generate-css-props shape properties layout-flex-item-params))) + +(mf/defc manage-margin + [{:keys [margin type]}] + (let [values (fmt/format-padding-margin-shorthand (vals margin))] + [:div.attributes-value + (for [[k v] values] + [:span.items {:key (str type "-" k "-" v)} v "px"])])) + +(mf/defc layout-element-block + [{:keys [shape]}] + [:* + [:div.attributes-unit-row + [:div.attributes-label "Width"] + [:div.attributes-value (str/capital (d/name (:layout-item-h-sizing shape)))] + [:& copy-button {:data (copy-data shape :layout-item-h-sizing)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Height"] + [:div.attributes-value (str/capital (d/name (:layout-item-v-sizing shape)))] + [:& copy-button {:data (copy-data shape :layout-item-v-sizing)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Align self"] + [:div.attributes-value (str/capital (d/name (:layout-item-align-self shape)))] + [:& copy-button {:data (copy-data shape :layout-item-align-self)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Margin"] + [:& manage-margin {:margin (:layout-item-margin shape) :type "margin"}] + [:& copy-button {:data (copy-data shape :layout-item-margin)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Max. width"] + [:div.attributes-value (fmt/format-pixels (:layout-item-max-w shape))] + [:& copy-button {:data (copy-data shape :layout-item-max-w)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Min. width"] + [:div.attributes-value (fmt/format-pixels (:layout-item-min-w shape))] + [:& copy-button {:data (copy-data shape :layout-item-min-w)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Max. height"] + [:div.attributes-value (fmt/format-pixels (:layout-item-max-h shape))] + [:& copy-button {:data (copy-data shape :layout-item-max-h)}]] + + [:div.attributes-unit-row + [:div.attributes-label "Min. height"] + [:div.attributes-value (fmt/format-pixels (:layout-item-min-w shape))] + [:& copy-button {:data (copy-data shape :layout-item-min-h)}]]]) + +(defn get-flex-elements [page-id shapes] + (let [ids (mapv :id shapes) + ids (hooks/use-equal-memo ids) + get-layout-children-refs (mf/use-memo (mf/deps ids page-id) #(refs/get-flex-child-viewer? ids page-id))] + + (mf/deref get-layout-children-refs))) + +(mf/defc layout-flex-element-panel + [{:keys [shapes]}] + (let [route (mf/deref refs/route) + page-id (:page-id (:query-params route)) + shapes (get-flex-elements page-id shapes)] + (when (and (= (count shapes) 1) (seq shapes)) + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text "Flex element"] + [:& copy-button {:data (copy-data (first shapes))}]] + + [:& layout-element-block {:shape (first shapes)}]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index c9177130d..78b9ccbd4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -18,14 +18,16 @@ [rumext.v2 :as mf])) (def layout-item-attrs - [:layout-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} - :layout-margin-type ;; :simple :multiple - :layout-h-behavior ;; :fill :fix :auto - :layout-v-behavior ;; :fill :fix :auto - :layout-max-h ;; num - :layout-min-h ;; num - :layout-max-w ;; num - :layout-min-w ]) ;; num + [:layout-item-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} + :layout-item-margin-type ;; :simple :multiple + :layout-item-h-sizing ;; :fill :fix :auto + :layout-item-v-sizing ;; :fill :fix :auto + :layout-item-max-h ;; num + :layout-item-min-h ;; num + :layout-item-max-w ;; num + :layout-item-min-w ;; num + :layout-item-align-self ;; :start :end :center :strech :baseline + ]) (mf/defc margin-section [{:keys [values change-margin-style on-margin-change] :as props}] diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs index 37d398e04..804d9af4b 100644 --- a/frontend/src/app/util/code_gen.cljs +++ b/frontend/src/app/util/code_gen.cljs @@ -55,9 +55,9 @@ :shadow {:props [:shadow] :to-prop {:shadow :box-shadow} :format {:shadow #(str/join ", " (map shadow->css %1))}} - :blur {:props [:blur] - :to-prop {:blur "filter"} - :format {:blur #(str/fmt "blur(%spx)" (:value %))}}}) + :blur {:props [:blur] + :to-prop {:blur "filter"} + :format {:blur #(str/fmt "blur(%spx)" (:value %))}}}) (def style-text {:props [:fill-color