diff --git a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc index 1d035998c..8871045f6 100644 --- a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc +++ b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc @@ -464,8 +464,13 @@ column-add-auto (/ free-column-space column-autos) row-add-auto (/ free-row-space row-autos) - column-tracks (add-auto-size column-tracks column-add-auto) - row-tracks (add-auto-size row-tracks row-add-auto) + column-tracks (cond-> column-tracks + (= :stretch (:layout-align-content parent)) + (add-auto-size column-add-auto)) + + row-tracks (cond-> row-tracks + (= :stretch (:layout-justify-content parent)) + (add-auto-size row-add-auto)) column-total-size (tracks-total-size column-tracks) row-total-size (tracks-total-size row-tracks) @@ -556,8 +561,7 @@ :column-total-size column-total-size :column-total-gap column-total-gap :row-total-size row-total-size - :row-total-gap row-total-gap - })) + :row-total-gap row-total-gap})) (defn get-cell-data [{:keys [origin row-tracks column-tracks shape-cells]} _transformed-parent-bounds [_ child]] diff --git a/frontend/resources/styles/main/layouts/inspect.scss b/frontend/resources/styles/main/layouts/inspect.scss index 8b13d09f7..e26df3ece 100644 --- a/frontend/resources/styles/main/layouts/inspect.scss +++ b/frontend/resources/styles/main/layouts/inspect.scss @@ -106,9 +106,6 @@ $width-settings-bar: 256px; .settings-bar { transition: width 0.2s; width: $width-settings-bar; - &.expanded { - width: $width-settings-bar * 3; - } &.settings-bar-right, &.settings-bar-left { diff --git a/frontend/resources/styles/main/partials/inspect.scss b/frontend/resources/styles/main/partials/inspect.scss index 32ce5a9e7..8693da4e5 100644 --- a/frontend/resources/styles/main/partials/inspect.scss +++ b/frontend/resources/styles/main/partials/inspect.scss @@ -328,6 +328,7 @@ } .code-block { + position: relative; margin-top: 0.5rem; border-top: 1px solid $color-gray-60; @@ -353,17 +354,86 @@ .copy-button { margin-top: 8px; } + + .custom-select { + border: 1px solid $color-gray-40; + border-radius: 3px; + cursor: pointer; + padding: 0.25rem 1.5rem 0.25rem 0.25rem; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + + .dropdown-button { + position: absolute; + right: 0.25rem; + top: 7px; + + svg { + fill: $color-gray-40; + height: 10px; + width: 10px; + } + } + } + + .custom-select-dropdown { + background-color: $color-white; + border-radius: 3px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); + left: 0; + max-height: 30rem; + min-width: 7rem; + position: absolute; + overflow-y: auto; + top: 30px; + z-index: 12; + + li { + color: $color-gray-60; + cursor: pointer; + font-size: 0.875rem; + display: flex; + gap: 0 10px; + justify-content: flex-start; + padding: 0.5rem; + + .checked-element { + padding-left: 0; + } + } + + svg { + visibility: hidden; + width: 8px; + height: 8px; + background: none; + margin: 0.25rem; + fill: $color-black; + } + + .is-selected svg { + visibility: visible; + } + } + } .code-row-display { + line-height: 1; margin: 0.5rem; font-size: $fs14; + max-height: var(--code-height, 400px); + overflow: auto; .code-display { + font-family: monospace; border-radius: $br4; - padding: 1rem; + padding: 0.5rem 1rem; overflow: hidden; - white-space: pre-wrap; + white-space: pre; + min-width: fit-content; background: $color-gray-60; user-select: text; @@ -378,6 +448,15 @@ } } } + .resize-area { + width: 100%; + position: absolute; + bottom: -15px; + left: 0; + height: 18px; + z-index: 1; + cursor: ns-resize; + } } .element-options > :first-child { diff --git a/frontend/resources/styles/main/partials/tab-container.scss b/frontend/resources/styles/main/partials/tab-container.scss index 41490f896..33c759e0a 100644 --- a/frontend/resources/styles/main/partials/tab-container.scss +++ b/frontend/resources/styles/main/partials/tab-container.scss @@ -35,6 +35,10 @@ overflow-x: hidden; } +.inspect .tab-container-content { + overflow: hidden; +} + .tab-element, .tab-element-content { height: 100%; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 595d2d831..9f5b8104f 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -48,14 +48,11 @@ $height-palette-max: 80px; } .settings-bar.settings-bar-right { - transition: width 0.2s; - min-width: $width-settings-bar; - max-width: $width-settings-bar * 3; - width: $width-settings-bar; + width: var(--width, $width-settings-bar); grid-area: right-sidebar; - &.expanded { - width: $width-settings-bar * 3; + &.not-expand { + max-width: $width-settings-bar; } } diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 33e019b99..0de1233fb 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -88,9 +88,9 @@ (defn ^:export reinit [] - (mf/unmount (dom/get-element "app")) - (mf/unmount (dom/get-element "modal")) - (st/emit! (ev/initialize)) + #_(mf/unmount (dom/get-element "app")) + #_(mf/unmount (dom/get-element "modal")) + #_(st/emit! (ev/initialize)) (init-ui)) (defn ^:dev/after-load after-load diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index b2e9436e7..ce0cf18a7 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -2160,20 +2160,6 @@ (let [orphans (set (into [] (keys (wsh/find-orphan-shapes state))))] (rx/of (relocate-shapes orphans uuid/zero 0 true)))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Inspect -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - - -(defn set-inspect-expanded - [expanded?] - (ptk/reify ::set-inspect-expanded - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :inspect-expanded] expanded?)))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Sitemap ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index bba3d9bda..15873ee1c 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -59,9 +59,9 @@ :layout-gap-type :multiple :layout-gap {:row-gap 0 :column-gap 0} :layout-align-items :start - :layout-align-content :start :layout-justify-items :start - :layout-justify-content :start + :layout-align-content :stretch + :layout-justify-content :stretch :layout-padding-type :simple :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0} :layout-grid-cells {} diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index 1782048ca..6dd334678 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -9,6 +9,7 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.logging :as log] + [app.common.math :as mth] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.util.dom :as dom] @@ -65,11 +66,19 @@ start-size (mf/ref-val start-size-ref) new-size (-> (+ start-size delta) (max min-val) (min max-val))] (reset! size-state new-size) - (swap! storage assoc-in [::saved-resize current-file-id key] new-size)))))] + (swap! storage assoc-in [::saved-resize current-file-id key] new-size))))) + + set-size + (mf/use-callback + (fn [new-size] + (let [new-size (mth/clamp new-size min-val max-val)] + (reset! size-state new-size) + (swap! storage assoc-in [::saved-resize current-file-id key] new-size))))] {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move :parent-ref parent-ref + :set-size set-size :size @size-state})) (defn use-resize-observer diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index 6bfcdf675..56fa88488 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -48,6 +48,18 @@ (mf/ref-val fonts-css-ref))) +(mf/defc fontfaces-style-html + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} + [props] + + (let [fonts (obj/get props "fonts") + + ;; Fetch its CSS fontfaces + fonts-css (use-fonts-css fonts)] + + [:style fonts-css])) + (mf/defc fontfaces-style-render {::mf/wrap-props false ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} @@ -63,7 +75,6 @@ (mf/deps fonts-css) #(fonts/extract-fontface-urls fonts-css)) - ;; Calculate the data-uris for these fonts fonts-embed (embed/use-data-uris fonts-urls) diff --git a/frontend/src/app/main/ui/shapes/text/html_text.cljs b/frontend/src/app/main/ui/shapes/text/html_text.cljs index d60810e16..399d57d3e 100644 --- a/frontend/src/app/main/ui/shapes/text/html_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/html_text.cljs @@ -83,19 +83,24 @@ [props ref] (let [shape (obj/get props "shape") grow-type (obj/get props "grow-type") - {:keys [id x y width height content]} shape] + code? (obj/get props "code?") + {:keys [id x y width height content]} shape + + style + (when-not code? + #js {:position "fixed" + :left 0 + :top 0 + :background "white" + :width (if (#{:auto-width} grow-type) 100000 width) + :height (if (#{:auto-height :auto-width} grow-type) 100000 height)})] [:div.text-node-html {:id (dm/str "html-text-node-" id) :ref ref :data-x x :data-y y - :style {:position "fixed" - :left 0 - :top 0 - :background "white" - :width (if (#{:auto-width} grow-type) 100000 width) - :height (if (#{:auto-height :auto-width} grow-type) 100000 height)}} + :style style} ;; We use a class here because react has a bug that won't use the appropriate selector for ;; `background-clip` [:style ".text-node { background-clip: text; diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 5ad25a7df..891cca1ee 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -48,7 +48,8 @@ [_shape data] (let [line-height (:line-height data 1.2) text-align (:text-align data "start") - base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") + base #js {;; Fix a problem when exporting HTML + :fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px") :lineHeight (:line-height data (:line-height txt/default-text-attrs)) :margin 0}] (cond-> base diff --git a/frontend/src/app/main/ui/viewer/inspect/code.cljs b/frontend/src/app/main/ui/viewer/inspect/code.cljs index 2d97af2ed..fc3f4dd6d 100644 --- a/frontend/src/app/main/ui/viewer/inspect/code.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/code.cljs @@ -7,41 +7,36 @@ (ns app.main.ui.viewer.inspect.code (:require ["js-beautify" :as beautify] - ["react-dom/server" :as rds] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst] + [app.config :as cfg] [app.main.data.events :as ev] + [app.main.fonts :as fonts] [app.main.refs :as refs] - [app.main.render :as render] [app.main.store :as st] [app.main.ui.components.code-block :refer [code-block]] [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.select :refer [select]] [app.main.ui.hooks :as hooks] + [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] + [app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]] [app.util.code-gen :as cg] + [app.util.http :as http] + [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk] [rumext.v2 :as mf])) -(defn generate-markup-code [objects shapes] - ;; Here we can render specific HTML code - (->> shapes - (map (fn [shape] - (dm/str - "" - (rds/renderToStaticMarkup - (mf/element - render/object-svg - #js {:objects objects - :object-id (-> shape :id)}))))) - (str/join "\n\n"))) - (defn format-code [code type] (let [code (-> code (str/replace "" "") (str/replace "><" ">\n<"))] (cond-> code - (= type "svg") (beautify/html #js {"indent_size" 2})))) + (or (= type "svg") (= type "html")) (beautify/html #js {"indent_size" 2})))) (defn get-flex-elements [page-id shapes from] (let [ids (mapv :id shapes) @@ -62,46 +57,139 @@ (refs/get-viewer-objects))))] (mf/deref page-objects-ref))) +(defn shapes->images + [shapes] + (->> shapes + (keep + (fn [shape] + (when-let [data (or (:metadata shape) (:fill-image shape))] + [(:id shape) (cfg/resolve-file-media data)]))))) + +(defn replace-map + [value map] + (reduce + (fn [value [old new]] + (str/replace value old new)) + value map)) + (mf/defc code [{:keys [shapes frame on-expand from]}] - (let [style-type (mf/use-state "css") - markup-type (mf/use-state "svg") + (let [style-type* (mf/use-state "css") + markup-type* (mf/use-state "html") + fontfaces-css* (mf/use-state nil) + images-data* (mf/use-state nil) + + style-type (deref style-type*) + markup-type (deref markup-type*) + fontfaces-css (deref fontfaces-css*) + images-data (deref images-data*) + shapes (->> shapes (map #(gsh/translate-to-frame % frame))) + route (mf/deref refs/route) page-id (:page-id (:query-params route)) flex-items (get-flex-elements page-id shapes from) objects (get-objects from) + + ;; TODO REMOVE THIS shapes (->> shapes (map #(assoc % :parent (get objects (:parent-id %)))) (map #(assoc % :flex-items flex-items))) - style-code (-> (cg/generate-style-code @style-type shapes) - (format-code "css")) + + all-children (->> shapes + (map :id) + (cph/selected-with-children objects) + (ctst/sort-z-index objects) + (map (d/getf objects))) + + + shapes (hooks/use-equal-memo shapes) + all-children (hooks/use-equal-memo all-children) + + fonts (-> (shapes->fonts all-children) + (hooks/use-equal-memo)) + + images-urls (-> (shapes->images all-children) + (hooks/use-equal-memo)) + + style-code + (mf/use-memo + (mf/deps fontfaces-css style-type all-children) + (fn [] + (dm/str + fontfaces-css "\n" + (-> (cg/generate-style-code objects style-type all-children) + (format-code style-type))))) markup-code - (-> (mf/use-memo (mf/deps shapes) #(generate-markup-code objects shapes)) - (format-code "svg")) + (mf/use-memo + (mf/deps markup-type shapes images-data) + (fn [] + (-> (cg/generate-markup-code objects markup-type (map :id shapes)) + (format-code markup-type)))) on-markup-copied (mf/use-callback - (mf/deps @markup-type) + (mf/deps markup-type) (fn [] (st/emit! (ptk/event ::ev/event {::ev/name "copy-inspect-code" - :type @markup-type})))) + :type markup-type})))) on-style-copied (mf/use-callback - (mf/deps @style-type) + (mf/deps style-type) (fn [] (st/emit! (ptk/event ::ev/event {::ev/name "copy-inspect-style" - :type @style-type}))))] + :type style-type})))) + + {on-code-pointer-down :on-pointer-down + on-code-lost-pointer-capture :on-lost-pointer-capture + on-code-pointer-move :on-pointer-move + code-size :size} + (use-resize-hook :code 400 100 800 :y false :bottom) + + set-style + (mf/use-callback + (fn [value] + (reset! style-type* value))) + + set-markup + (mf/use-callback + (fn [value] + (reset! markup-type* value)))] + + (mf/use-effect + (mf/deps fonts) + #(->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/reduce conj []) + (rx/subs + (fn [result] + (let [css (str/join "\n" result)] + (reset! fontfaces-css* css)))))) + + (mf/use-effect + (mf/deps images-urls) + #(->> (rx/from images-urls) + (rx/merge-map + (fn [[_ uri]] + (->> (http/fetch-data-uri uri true) + (rx/catch (fn [_] (rx/of (hash-map uri uri))))))) + (rx/reduce conj {}) + (rx/subs + (fn [result] + (reset! images-data* result))))) [:div.element-options [:div.code-block - [:div.code-row-lang "CSS" - + [:div.code-row-lang + [:& select {:default-value style-type + :class "custom-select" + :options [{:label "CSS" :value "css"}] + :on-change set-style}] [:button.expand-button {:on-click on-expand} i/full-screen] @@ -109,19 +197,31 @@ [:& copy-button {:data style-code :on-copied on-style-copied}]] - [:div.code-row-display - [:& code-block {:type @style-type - :code style-code}]]] + [:div.code-row-display {:style #js {"--code-height" (str (or code-size 400) "px")}} + [:& code-block {:type style-type + :code style-code}]] + + [:div.resize-area {:on-pointer-down on-code-pointer-down + :on-lost-pointer-capture on-code-lost-pointer-capture + :on-pointer-move on-code-pointer-move}]] + [:div.code-block - [:div.code-row-lang "SVG" + [:div.code-row-lang + [:& select {:default-value markup-type + :class "input-option" + :options [{:label "HTML" :value "html"} + {:label "SVG" :value "svg"}] + :on-change set-markup}] [:button.expand-button {:on-click on-expand} i/full-screen] - [:& copy-button {:data markup-code + [:& copy-button {:data (replace-map markup-code images-data) :on-copied on-markup-copied}]] + + [:div.code-row-display - [:& code-block {:type @markup-type + [:& code-block {:type markup-type :code markup-code}]]]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs index e3d03bc3f..2cceadd34 100644 --- a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs @@ -6,9 +6,7 @@ (ns app.main.ui.viewer.inspect.right-sidebar (:require - [app.main.data.workspace :as dw] [app.main.refs :as refs] - [app.main.store :as st] [app.main.ui.components.shape-icon :as si] [app.main.ui.components.tabs-container :refer [tabs-container tabs-element]] [app.main.ui.icons :as i] @@ -38,10 +36,9 @@ :data local}))))) (mf/defc right-sidebar - [{:keys [frame page file selected shapes page-id file-id share-id from] + [{:keys [frame page file selected shapes page-id file-id share-id from on-change-section on-expand] :or {from :inspect}}] - (let [expanded (mf/use-state false) - section (mf/use-state :info #_:code) + (let [section (mf/use-state :info #_:code) shapes (or shapes (resolve-shapes (:objects page) selected)) @@ -49,9 +46,29 @@ page-id (or page-id (:id page)) file-id (or file-id (:id file)) - libraries (get-libraries from)] + libraries (get-libraries from) - [:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")} + handle-change-tab + (mf/use-callback + (mf/deps from on-change-section) + (fn [new-section] + (reset! section new-section) + (when on-change-section + (on-change-section new-section)))) + + handle-expand + (mf/use-callback + (mf/deps on-expand) + (fn [] + (when on-expand (on-expand))))] + + (mf/use-effect + (mf/deps shapes handle-change-tab) + (fn [] + (when-not (seq shapes) + (handle-change-tab :info)))) + + [:aside.settings-bar.settings-bar-right [:div.settings-bar-inside (if (seq shapes) [:div.tool-window @@ -77,12 +94,8 @@ ;; inspect.tabs.code.selected.text [:span.tool-window-bar-title (:name first-shape)]])] [:div.tool-window-content.inspect - [:& tabs-container {:on-change-tab #(do - (reset! expanded false) - (reset! section %) - (when (= from :workspace) - (st/emit! (dw/set-inspect-expanded false)))) - :selected @section} + [:& tabs-container {:on-change-tab handle-change-tab + :selected @section} [:& tabs-element {:id :info :title (tr "inspect.tabs.info")} [:& attributes {:page-id page-id :file-id file-id @@ -95,10 +108,7 @@ [:& tabs-element {:id :code :title (tr "inspect.tabs.code")} [:& code {:frame frame :shapes shapes - :on-expand (fn [] - (when (= from :workspace) - (st/emit! (dw/set-inspect-expanded (not @expanded)))) - (swap! expanded not)) + :on-expand handle-expand :from from}]]]]] [:div.empty [:span.tool-window-bar-icon i/code] diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 870487b71..f4ed014a7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -26,6 +26,7 @@ [app.main.ui.workspace.sidebar.sitemap :refer [sitemap]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [app.util.object :as obj] [rumext.v2 :as mf])) ;; --- Left Sidebar (Component) @@ -134,16 +135,45 @@ is-history? (contains? layout :document-history) is-inspect? (= section :inspect) - expanded? (mf/deref refs/inspect-expanded) + ;;expanded? (mf/deref refs/inspect-expanded) + ;;prev-expanded? (hooks/use-previous expanded?) + + current-section* (mf/use-state :info) + current-section (deref current-section*) + can-be-expanded? (and (not is-comments?) (not is-history?) - is-inspect?)] + is-inspect? + (= current-section :code)) - (mf/with-effect [can-be-expanded?] - (when (not can-be-expanded?) - (st/emit! (dw/set-inspect-expanded false)))) + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move set-size size]} + (use-resize-hook :code 256 256 768 :x true :right) - [:aside.settings-bar.settings-bar-right {:class (when (and can-be-expanded? expanded?) "expanded")} + handle-change-section + (mf/use-callback + (fn [section] + (reset! current-section* section))) + + handle-expand + (mf/use-callback + (mf/deps size) + (fn [] + (set-size (if (> size 256) 256 768)))) + + props + (-> props + (obj/clone) + (obj/set! "on-change-section" handle-change-section) + (obj/set! "on-expand" handle-expand))] + + [:aside.settings-bar.settings-bar-right + {:class (when (not can-be-expanded?) "not-expand") + :style #js {"--width" (when can-be-expanded? (dm/str size "px"))}} + (when can-be-expanded? + [:div.resize-area + {:on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move}]) [:div.settings-bar-inside (cond (true? is-comments?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 6151dade2..19d98b8ac 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -67,7 +67,7 @@ (mf/defc options-content {::mf/wrap [mf/memo]} - [{:keys [selected section shapes shapes-with-children page-id file-id]}] + [{:keys [selected section shapes shapes-with-children page-id file-id on-change-section on-expand]}] (let [drawing (mf/deref refs/workspace-drawing) objects (mf/deref refs/workspace-page-objects) shared-libs (mf/deref refs/workspace-libraries) @@ -83,9 +83,8 @@ on-change-tab (fn [options-mode] - (st/emit! (udw/set-options-mode options-mode) - (udw/set-inspect-expanded false)) - (if (= options-mode :inspect) ;;TODO maybe move this logic to set-options-mode + (st/emit! (udw/set-options-mode options-mode)) + (if (= options-mode :inspect) (st/emit! :interrupt (udw/set-workspace-read-only true)) (st/emit! :interrupt (udw/set-workspace-read-only false))))] @@ -94,7 +93,7 @@ [:& tabs-container {:on-change-tab on-change-tab :selected section} [:& tabs-element {:id :design - :title (tr "workspace.options.design")} + :title (tr "workspace.options.design")} [:div.element-options [:& align-options] [:& bool-options] @@ -143,13 +142,16 @@ [:& interactions-menu {:shape (first shapes)}]]] [:& tabs-element {:id :inspect - :title (tr "workspace.options.inspect")} - [:div.element-options - [:& hrs/right-sidebar {:page-id page-id - :file-id file-id - :frame shape-parent-frame - :shapes selected-shapes - :from :workspace}]]]]]])) + :title (tr "workspace.options.inspect")} + + [:div.element-options.element-options-inspect + [:& hrs/right-sidebar {:page-id page-id + :file-id file-id + :frame shape-parent-frame + :shapes selected-shapes + :on-change-section on-change-section + :on-expand on-expand + :from :workspace}]]]]]])) ;; TODO: this need optimizations, selected-objects and ;; selected-objects-with-children are derefed always but they only @@ -161,6 +163,8 @@ [props] (let [section (obj/get props "section") selected (obj/get props "selected") + on-change-section (obj/get props "on-change-section") + on-expand (obj/get props "on-expand") page-id (mf/use-ctx ctx/current-page-id) file-id (mf/use-ctx ctx/current-file-id) shapes (mf/deref refs/selected-objects) @@ -171,4 +175,6 @@ :shapes-with-children shapes-with-children :file-id file-id :page-id page-id - :section section}])) + :section section + :on-change-section on-change-section + :on-expand on-expand}])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 9cb756546..16eaaa871 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -125,6 +125,7 @@ :justify-items (if is-col? (case val + :stretch i/grid-justify-content-column-around :start i/grid-justify-content-column-start :end i/grid-justify-content-column-end :center i/grid-justify-content-column-center @@ -133,6 +134,7 @@ :space-evenly i/grid-justify-content-column-between) (case val + :stretch i/grid-justify-content-column-around :start i/grid-justify-content-row-start :end i/grid-justify-content-row-end :center i/grid-justify-content-row-center @@ -407,7 +409,7 @@ [{:keys [is-col? justify-items set-justify] :as props}] (let [type (if is-col? :column :row)] [:div.justify-content-style - (for [align [:start :center :end :space-around :space-between :space-evenly]] + (for [align [:stretch :start :center :end :space-around :space-between]] [:button.align-start.tooltip {:class (dom/classnames :active (= justify-items align) :tooltip-bottom-left (not= align :start) @@ -748,7 +750,7 @@ align-items-row (:layout-align-items values) align-items-column (:layout-justify-items values) - set-align-grid + set-items-grid (fn [value type] (if (= type :row) (st/emit! (dwsl/update-layout ids {:layout-align-items value})) @@ -758,7 +760,7 @@ grid-justify-content-row (:layout-align-content values) grid-justify-content-column (:layout-justify-content values) - set-justify-grid + set-content-grid (mf/use-callback (mf/deps ids) (fn [value type] @@ -833,25 +835,25 @@ [:& grid-edit-mode {:id (first ids)}]])]] [:div.layout-row - [:div.align-items-grid.row-title "Align"] + [:div.align-items-grid.row-title "Items"] [:div.btn-wrapper.align-grid [:& align-grid-row {:is-col? false :align-items align-items-row - :set-align set-align-grid}] + :set-align set-items-grid}] [:& align-grid-row {:is-col? true :align-items align-items-column - :set-align set-align-grid}]]] + :set-align set-items-grid}]]] [:div.layout-row - [:div.jusfiy-content-grid.row-title "Justify"] + [:div.jusfiy-content-grid.row-title "Content"] [:div.btn-wrapper.align-grid [:& justify-grid-row {:is-col? true :justify-items grid-justify-content-column - :set-justify set-justify-grid}] + :set-justify set-content-grid}] [:& justify-grid-row {:is-col? false :justify-items grid-justify-content-row - :set-justify set-justify-grid}]]] + :set-justify set-content-grid}]]] [:& grid-columns-row {:is-col? true :expanded? @grid-columns-open? :toggle toggle-columns-info diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs index f3cf5eea4..24d680320 100644 --- a/frontend/src/app/util/code_gen.cljs +++ b/frontend/src/app/util/code_gen.cljs @@ -6,14 +6,19 @@ (ns app.util.code-gen (:require + ["react-dom/server" :as rds] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.common.types.shape.layout :as ctl] + [app.config :as cfg] + [app.main.render :as render] [app.main.ui.formats :as fmt] + [app.main.ui.shapes.text.html-text :as text] [app.util.color :as uc] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [rumext.v2 :as mf])) (defn shadow->css [shadow] (let [{:keys [style offset-x offset-y blur spread]} shadow @@ -24,9 +29,14 @@ (defn fill-color->background [fill] - (uc/color->background {:color (:fill-color fill) - :opacity (:fill-opacity fill) - :gradient (:fill-color-gradient fill)})) + (cond + (not= (:fill-opacity fill) 1) + (uc/color->background {:color (:fill-color fill) + :opacity (:fill-opacity fill) + :gradient (:fill-color-gradient fill)}) + + :else + (str/upper (:fill-color fill)))) (defn format-fill-color [_ shape] (let [fills (:fills shape) @@ -41,7 +51,7 @@ [(fill-color->background first-fill)])] (str/join ", " colors))) -(defn format-stroke [_ shape] +(defn format-stroke [shape] (let [first-stroke (first (:strokes shape)) width (:stroke-width first-stroke) style (let [style (:stroke-style first-stroke)] @@ -52,16 +62,32 @@ (when-not (= :none (:stroke-style first-stroke)) (str/format "%spx %s %s" width style (uc/color->background color))))) -(defn format-position [_ shape] - (let [relative? (cph/frame-shape? shape) - absolute? (or (empty? (:flex-items shape)) - (and (ctl/any-layout? (:parent shape)) (ctl/layout-absolute? shape)))] +(defn format-position [objects] + (fn [_ shape] (cond - absolute? "absolute" - relative? "relative" + (and (ctl/any-layout-immediate-child? objects shape) + (not (ctl/layout-absolute? shape)) + (or (cph/group-shape? shape) + (cph/frame-shape? shape))) + "relative" - ;; static is default value in css - :else nil))) + (and (ctl/any-layout-immediate-child? objects shape) + (not (ctl/layout-absolute? shape))) + nil + + :else + "absolute"))) + +(defn mk-grid-coord + [objects prop span-prop] + + (fn [_ shape] + (when (ctl/grid-layout-immediate-child? objects shape) + (let [parent (get objects (:parent-id shape)) + cell (ctl/get-cell-by-shape-id parent (:id shape))] + (if (> (get cell span-prop) 1) + (dm/str (get cell prop) " / " (+ (get cell prop) (get cell span-prop))) + (get cell prop)))))) (defn get-size [type values] @@ -75,14 +101,35 @@ (fmt/format-size :width value values) (fmt/format-size :heigth value values)))) +(defn make-format-absolute-pos + [objects shape coord] + (fn [value] + (let [parent-id (dm/get-in objects [(:id shape) :parent-id]) + parent-value (dm/get-in objects [parent-id :selrect coord])] + (when-not (or (cph/root-frame? shape) + (ctl/any-layout-immediate-child? objects shape) + (ctl/layout-absolute? shape)) + (fmt/format-pixels (- value parent-value)))))) + +(defn format-tracks + [tracks] + (str/join + " " + (->> tracks (map (fn [{:keys [type value]}] + (case type + :flex (dm/str (fmt/format-number value) "fr") + :percent (fmt/format-percent (/ value 100)) + :auto "auto" + (fmt/format-pixels value))))))) + (defn styles-data - [shape] + [objects shape] {:position {:props [:type] :to-prop {:type "position"} - :format {:type format-position}} + :format {:type (format-position objects)}} :layout {:props (if (or (empty? (:flex-items shape)) (ctl/layout-absolute? shape)) - [:width :height :x :y :radius :rx :r1] + [:x :y :width :height :radius :rx :r1] [:width :height :radius :rx :r1]) :to-prop {:x "left" :y "top" @@ -92,30 +139,60 @@ :format {:rotation #(str/fmt "rotate(%sdeg)" %) :r1 #(apply str/fmt "%spx %spx %spx %spx" %) :width #(get-size :width %) - :height #(get-size :height %)} + :height #(get-size :height %) + :x (make-format-absolute-pos objects shape :x) + :y (make-format-absolute-pos objects shape :y)} :multi {:r1 [:r1 :r2 :r3 :r4]}} :fill {:props [:fills] - :to-prop {:fills (if (> (count (:fills shape)) 1) "background-image" "background-color")} + :to-prop {:fills (cond + (or (cph/path-shape? shape) + (cph/mask-shape? shape) + (cph/bool-shape? shape) + (cph/svg-raw-shape? shape) + (some? (:svg-attrs shape))) + nil + + (> (count (:fills shape)) 1) + "background-image" + + (and (= (count (:fills shape)) 1) + (some? (:fill-color-gradient (first (:fills shape))))) + "background" + + :else + "background-color")} :format {:fills format-fill-color}} :stroke {:props [:strokes] :to-prop {:strokes "border"} - :format {:strokes format-stroke}} + :format {:strokes (fn [_ shape] + (when-not (or (cph/path-shape? shape) + (cph/mask-shape? shape) + (cph/bool-shape? shape) + (cph/svg-raw-shape? shape) + (some? (:svg-attrs shape))) + (format-stroke shape)))}} :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 %))}} + :layout-flex {:props [:layout :layout-flex-dir :layout-align-items + :layout-justify-items + :layout-align-content :layout-justify-content :layout-gap :layout-padding :layout-wrap-type] + :gen-props [:flex-shrink] :to-prop {:layout "display" :layout-flex-dir "flex-direction" :layout-align-items "align-items" + :layout-align-content "align-content" + :layout-justify-items "justify-items" :layout-justify-content "justify-content" :layout-wrap-type "flex-wrap" :layout-gap "gap" @@ -123,10 +200,24 @@ :format {:layout d/name :layout-flex-dir d/name :layout-align-items d/name + :layout-align-content d/name + :layout-justify-items d/name :layout-justify-content d/name :layout-wrap-type d/name :layout-gap fmt/format-gap - :layout-padding fmt/format-padding}}}) + :layout-padding fmt/format-padding + :flex-shrink (fn [_ shape] (when (ctl/flex-layout-immediate-child? objects shape) 0))}} + + :layout-grid {:props [:layout-grid-rows + :layout-grid-columns] + :gen-props [:grid-column + :grid-row] + :to-prop {:layout-grid-rows "grid-template-rows" + :layout-grid-columns "grid-template-columns"} + :format {:layout-grid-rows format-tracks + :layout-grid-columns format-tracks + :grid-column (mk-grid-coord objects :column :column-span) + :grid-row (mk-grid-coord objects :row :row-span)}}}) (def style-text {:props [:fills @@ -190,9 +281,12 @@ (defn generate-css-props ([values properties] - (generate-css-props values properties nil)) + (generate-css-props values properties [] nil)) - ([values properties params] + ([values properties gen-properties] + (generate-css-props values properties gen-properties nil)) + + ([values properties gen-properties params] (let [{:keys [to-prop format tab-size multi] :or {to-prop {} tab-size 0 multi {}}} params @@ -210,7 +304,7 @@ to-prop) get-value (fn [prop] - (if-let [props (prop multi)] + (if-let [props (get multi prop)] (map #(get values %) props) (get-specific-value values prop))) @@ -220,30 +314,35 @@ (or (nil? value) (= value 0)))) default-format (fn [value] (dm/str (fmt/format-pixels value))) - format-property (fn [prop] - (let [css-prop (or (prop to-prop) (d/name prop)) - format-fn (or (prop format) default-format) - css-val (format-fn (get-value prop) values)] - (when css-val - (dm/str - (str/repeat " " tab-size) - (str/fmt "%s: %s;" css-prop css-val)))))] - (->> properties - (remove #(null? (get-value %))) - (map format-property) - (filter (comp not nil?)) + format-property + (fn [prop] + (let [css-prop (or (get to-prop prop) (d/name prop)) + format-fn (or (get format prop) default-format) + css-val (format-fn (get-value prop) values)] + (when (and css-val (not= css-val "")) + (dm/str + (str/repeat " " tab-size) + (dm/fmt "%: %;" css-prop css-val)))))] + + (->> (concat + (->> properties + (remove #(null? (get-value %)))) + gen-properties) + (keep format-property) (str/join "\n"))))) -(defn shape->properties [shape] - (let [;; This property is added in an earlier step (code.cljs), +(defn shape->properties [objects shape] + (let [;; This property is added in an earlier step (code.cljs), ;; it will come with a vector of flex-items if any. - ;; If there are none it will continue as usual. + ;; If there are none it will continue as usual. flex-items (:flex-items shape) - props (->> (styles-data shape) vals (mapcat :props)) - to-prop (->> (styles-data shape) vals (map :to-prop) (reduce merge)) - format (->> (styles-data shape) vals (map :format) (reduce merge)) - multi (->> (styles-data shape) vals (map :multi) (reduce merge)) + props (->> (styles-data objects shape) vals (mapcat :props)) + to-prop (->> (styles-data objects shape) vals (map :to-prop) (reduce merge)) + format (->> (styles-data objects shape) vals (map :format) (reduce merge)) + multi (->> (styles-data objects shape) vals (map :multi) (reduce merge)) + gen-props (->> (styles-data objects shape) vals (mapcat :gen-props)) + props (cond-> props (seq flex-items) (concat (:props layout-flex-item-params)) (= :wrap (:layout-wrap-type shape)) (concat (:props layout-align-content))) @@ -253,10 +352,14 @@ format (cond-> format (seq flex-items) (merge (:format layout-flex-item-params)) (= :wrap (:layout-wrap-type shape)) (merge (:format layout-align-content)))] - (generate-css-props shape props {:to-prop to-prop - :format format - :multi multi - :tab-size 2}))) + (generate-css-props + shape + props + gen-props + {:to-prop to-prop + :format format + :multi multi + :tab-size 2}))) (defn search-text-attrs [node attrs] @@ -269,36 +372,36 @@ (defn parse-style-text-blocks [node attrs] (letfn - [(rec-style-text-map [acc node style] - (let [node-style (merge style (select-keys node attrs)) - head (or (-> acc first) [{} ""]) - [head-style head-text] head + [(rec-style-text-map [acc node style] + (let [node-style (merge style (select-keys node attrs)) + head (or (-> acc first) [{} ""]) + [head-style head-text] head - new-acc - (cond - (:children node) - (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) + new-acc + (cond + (:children node) + (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) - (not= head-style node-style) - (cons [node-style (:text node "")] acc) + (not= head-style node-style) + (cons [node-style (:text node "")] acc) - :else - (cons [node-style (dm/str head-text "" (:text node))] (rest acc))) + :else + (cons [node-style (dm/str head-text "" (:text node))] (rest acc))) ;; We add an end-of-line when finish a paragraph - new-acc - (if (= (:type node) "paragraph") - (let [[hs ht] (first new-acc)] - (cons [hs (dm/str ht "\n")] (rest new-acc))) - new-acc)] - new-acc))] + new-acc + (if (= (:type node) "paragraph") + (let [[hs ht] (first new-acc)] + (cons [hs (dm/str ht "\n")] (rest new-acc))) + new-acc)] + new-acc))] (-> (rec-style-text-map [] node {}) reverse))) -(defn text->properties [shape] +(defn text->properties [objects shape] (let [flex-items (:flex-items shape) - text-shape-style (select-keys (styles-data shape) [:layout :shadow :blur]) + text-shape-style (d/without-keys (styles-data objects shape) [:fill :stroke]) shape-props (->> text-shape-style vals (mapcat :props)) shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) @@ -315,6 +418,7 @@ (:content shape) (conj (:props style-text) :fill-color-gradient :fill-opacity)) (d/merge txt/default-text-attrs))] + (str/join "\n" [(generate-css-props shape @@ -328,21 +432,128 @@ :format (:format style-text) :tab-size 2})]))) -(defn generate-css [shape] - (let [name (:name shape) - properties (if (= :text (:type shape)) - (text->properties shape) - (shape->properties shape)) - selector (str/css-selector name) +(defn selector-name [shape] + (let [ + name (-> (:name shape) + #_(subs 0 (min 10 (count (:name shape))))) + ;; selectors cannot start with numbers + name (if (re-matches #"^\d.*" name) (dm/str "c-" name) name) + id (-> (dm/str (:id shape)) + #_(subs 24 36)) + selector (str/css-selector (dm/str name " " id)) selector (if (str/starts-with? selector "-") (subs selector 1) selector)] + selector)) + +(defn generate-css [objects shape] + (let [name (:name shape) + properties (shape->properties objects shape) + selector (selector-name shape)] (str/join "\n" [(str/fmt "/* %s */" name) (str/fmt ".%s {" selector) properties "}"]))) -(defn generate-style-code [type shapes] +(defn generate-svg + [objects shape-id] + (let [shape (get objects shape-id)] + (rds/renderToStaticMarkup + (mf/element + render/object-svg + #js {:objects objects + :object-id (-> shape :id)})))) + +(defn generate-html + ([objects shape-id] + (generate-html objects shape-id 0)) + + ([objects shape-id level] + (let [shape (get objects shape-id) + indent (str/repeat " " level) + maybe-reverse (if (ctl/any-layout? shape) reverse identity)] + (cond + (cph/text-shape? shape) + (let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape #js {:shape shape :code? true}))] + (dm/fmt "%
\n%\n%
" + indent + (selector-name shape) + text-shape-html + indent)) + + (cph/image-shape? shape) + (let [data (or (:metadata shape) (:fill-image shape)) + image-url (cfg/resolve-file-media data)] + (dm/fmt "%\n%" + indent + image-url + (selector-name shape) + indent)) + + (or (cph/path-shape? shape) + (cph/mask-shape? shape) + (cph/bool-shape? shape) + (cph/svg-raw-shape? shape) + (some? (:svg-attrs shape))) + (let [svg-markup (rds/renderToStaticMarkup (mf/element render/object-svg #js {:objects objects :object-id (:id shape) :render-embed? false}))] + (dm/fmt "%
\n%\n%
" + indent + (selector-name shape) + svg-markup + indent)) + + (empty? (:shapes shape)) + (dm/fmt "%
\n%
" + indent + (selector-name shape) + indent) + + :else + (dm/fmt "%
\n%\n%
" + indent + (selector-name shape) + (->> (:shapes shape) + (maybe-reverse) + (map #(generate-html objects % (inc level))) + (str/join "\n")) + indent))))) + +(defn generate-markup-code [objects type shapes] + (let [generate-markup-fn (case type + "html" generate-html + "svg" generate-svg)] + (->> shapes + (map #(generate-markup-fn objects % 0)) + (str/join "\n")))) + +(defn generate-style-code [objects type shapes] (let [generate-style-fn (case type "css" generate-css)] - (->> shapes - (map generate-style-fn) - (str/join "\n\n")))) + (dm/str + "html, body { + background-color: #E8E9EA; + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +body { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; +} + +svg { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +* { + box-sizing: border-box; +} +\n" + (->> shapes + (map (partial generate-style-fn objects)) + (str/join "\n\n")))))