mirror of
https://github.com/penpot/penpot.git
synced 2025-02-10 09:08:31 -05:00
✨ CSS code generation first draft
This commit is contained in:
parent
5d6b07f2a7
commit
28f90da70e
10 changed files with 237 additions and 108 deletions
|
@ -978,6 +978,12 @@
|
|||
"en" : "Left"
|
||||
}
|
||||
},
|
||||
"handoff.attributes.layout.radius" : {
|
||||
"used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ],
|
||||
"translations" : {
|
||||
"en" : "Radius"
|
||||
}
|
||||
},
|
||||
"handoff.attributes.layout.rotation" : {
|
||||
"used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ],
|
||||
"translations" : {
|
||||
|
|
|
@ -264,8 +264,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.code-block {
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid $color-gray-60;
|
||||
|
||||
.code-row-lang {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -307,6 +309,7 @@
|
|||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
background: $color-gray-60;
|
||||
user-select: text;
|
||||
|
||||
.hljs-attr {
|
||||
color: #a6e22e;
|
||||
|
@ -319,5 +322,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.element-options :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
(mf/defc layer-blur-filter
|
||||
[{:keys [filter-id params]}]
|
||||
|
||||
[:feGaussianBlur {:stdDeviation (/ (:value params) 2)
|
||||
[:feGaussianBlur {:stdDeviation (:value params)
|
||||
:result filter-id}])
|
||||
|
||||
(mf/defc image-fix-filter [{:keys [filter-id]}]
|
||||
|
|
|
@ -18,27 +18,6 @@
|
|||
[app.main.ui.shapes.gradients :as grad]
|
||||
[app.main.ui.context :as muc]))
|
||||
|
||||
(mf/defc background-blur [{:keys [shape]}]
|
||||
(when-let [background-blur-filters (->> shape :blur (remove #(= (:type %) :layer-blur)) (remove :hidden))]
|
||||
(for [filter background-blur-filters]
|
||||
[:*
|
||||
|
||||
|
||||
[:foreignObject {:key (str "blur_" (:id filter))
|
||||
:pointerEvents "none"
|
||||
:x (:x shape)
|
||||
:y (:y shape)
|
||||
:width (:width shape)
|
||||
:height (:height shape)
|
||||
:transform (geom/transform-matrix shape)}
|
||||
[:style ""]
|
||||
[:div.backround-blur
|
||||
{:style {:width "100%"
|
||||
:height "100%"
|
||||
;; :backdrop-filter (str/format "blur(%spx)" (:value filter))
|
||||
:filter (str/format "blur(4px")
|
||||
}}]]])))
|
||||
|
||||
(mf/defc shape-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -51,23 +30,12 @@
|
|||
(obj/clone)
|
||||
(obj/without ["shape" "children"])
|
||||
(obj/set! "className" "shape")
|
||||
(obj/set! "data-type" (:type shape))
|
||||
(obj/set! "filter" (filters/filter-str filter-id shape)))
|
||||
|
||||
;;group-props (if (seq (:blur shape))
|
||||
;; (obj/set! group-props "clip-path" (str/fmt "url(#%s)" (str "blur_" render-id)))
|
||||
;; group-props)
|
||||
]
|
||||
(obj/set! "filter" (filters/filter-str filter-id shape)))]
|
||||
[:& (mf/provider muc/render-ctx) {:value render-id}
|
||||
[:> :g group-props
|
||||
[:defs
|
||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
|
||||
|
||||
#_(when (:blur shape)
|
||||
[:clipPath {:id (str "blur_" render-id)}
|
||||
children])]
|
||||
|
||||
[:& background-blur {:shape shape}]
|
||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
|
||||
|
||||
children]]))
|
||||
|
|
|
@ -16,38 +16,13 @@
|
|||
[app.util.color :as uc]
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.code-gen :as code]
|
||||
[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 {}}}]
|
||||
(defn copy-cb [values properties & {:keys [to-prop format] :as params}]
|
||||
(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)]
|
||||
|
||||
(let [result (code/generate-css-props values properties params)]
|
||||
(wapi/write-to-clipboard result))))
|
||||
|
||||
(mf/defc color-row [{:keys [color format on-copy on-change-format]}]
|
||||
|
@ -79,3 +54,4 @@
|
|||
|
||||
(when on-copy
|
||||
[:button.attributes-copy-button {:on-click on-copy} i/copy])]))
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
(defn copy-layout [shape]
|
||||
(copy-cb shape
|
||||
[:width :height :x :y :rotation]
|
||||
:to-prop {:x "left" :y "top" :rotation "transform"}
|
||||
[:width :height :x :y :radius :rx]
|
||||
:to-prop {:x "left" :y "top" :rotation "transform" :rx "border-radius"}
|
||||
:format {:rotation #(str/fmt "rotate(%sdeg)" %)}))
|
||||
|
||||
(mf/defc layout-block
|
||||
|
@ -55,6 +55,14 @@
|
|||
{:on-click (copy-cb shape :y :to-prop "top")}
|
||||
i/copy]])
|
||||
|
||||
(when (not= (:rx shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
|
||||
[:button.attributes-copy-button
|
||||
{:on-click (copy-cb shape :rx :to-prop "border-radius")}
|
||||
i/copy]])
|
||||
|
||||
(when (not= (:rotation shape 0) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
||||
|
|
|
@ -12,21 +12,13 @@
|
|||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.code-gen :as cg]
|
||||
[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
|
||||
|
@ -52,7 +44,7 @@
|
|||
{:on-click (copy-cb shadow
|
||||
:style
|
||||
:to-prop "box-shadow"
|
||||
:format #(shadow->css shadow))}
|
||||
:format #(cg/shadow->css shadow))}
|
||||
i/copy]]
|
||||
[:& color-row {:color (:color shadow)
|
||||
:format @color-format
|
||||
|
@ -64,7 +56,7 @@
|
|||
(copy-cb (first shapes)
|
||||
:shadow
|
||||
:to-prop "box-shadow"
|
||||
:format #(str/join ", " (map shadow->css (:shadow (first shapes))))))]
|
||||
:format #(str/join ", " (map cg/shadow->css (:shadow (first shapes))))))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[:div.attributes-block-title
|
||||
|
|
|
@ -10,31 +10,33 @@
|
|||
(ns app.main.ui.viewer.handoff.code
|
||||
(:require
|
||||
["highlight.js" :as hljs]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.color :as uc]
|
||||
[app.util.webapi :as wapi]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.common.geom.shapes :as gsh]))
|
||||
|
||||
(def css-example
|
||||
"/* text layer name */
|
||||
.shape {
|
||||
width: 142px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: var(--tiffany-blue);
|
||||
}")
|
||||
|
||||
(def svg-example
|
||||
"<g class=\"shape\">
|
||||
<rect fill=\"#ffffff\" fill-opacity=\"1\" x=\"629\" y=\"169\" id=\"shape-eee5fa10-5336-11ea-
|
||||
8394-2dd26e322db3\" width=\"176\" height=\"211\">
|
||||
</rect>
|
||||
</g>")
|
||||
"<rect
|
||||
x=\"629\"
|
||||
y=\"169\"
|
||||
width=\"176\"
|
||||
height=\"211\"
|
||||
fill=\"#ffffff\"
|
||||
fill-opacity=\"1\">
|
||||
</rect>")
|
||||
|
||||
|
||||
(defn generate-markup-code [type shapes]
|
||||
svg-example)
|
||||
|
||||
(mf/defc code-block [{:keys [code type]}]
|
||||
(let [block-ref (mf/use-ref)]
|
||||
(mf/use-effect
|
||||
(mf/deps block-ref)
|
||||
(mf/deps code type block-ref)
|
||||
(fn []
|
||||
(hljs/highlightBlock (mf/ref-val block-ref))))
|
||||
[:pre.code-display {:class type
|
||||
|
@ -42,39 +44,46 @@
|
|||
|
||||
(mf/defc code
|
||||
[{:keys [shapes frame]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
(let [style-type (mf/use-state "css")
|
||||
markup-type (mf/use-state "svg")
|
||||
|
||||
locale (mf/deref i18n/locale)
|
||||
shapes (->> shapes
|
||||
(map #(gsh/translate-to-frame % frame)))]
|
||||
(map #(gsh/translate-to-frame % frame)))
|
||||
|
||||
style-code (cg/generate-style-code @style-type shapes)
|
||||
markup-code (generate-markup-code @markup-type shapes)]
|
||||
[:div.element-options
|
||||
[:div.code-block
|
||||
[:div.code-row-lang
|
||||
[:select.code-selection
|
||||
[:option "CSS"]
|
||||
[:option "SASS"]
|
||||
[:option "Less"]
|
||||
[:option "Stylus"]]
|
||||
[:option {:value "css"} "CSS"]
|
||||
#_[:option {:value "sass"} "SASS"]
|
||||
#_[:option {:value "less"} "Less"]
|
||||
#_[:option {:value "stylus"} "Stylus"]]
|
||||
|
||||
[:button.attributes-copy-button
|
||||
{:on-click #(prn "??")}
|
||||
{:on-click #(wapi/write-to-clipboard style-code)}
|
||||
i/copy]]
|
||||
|
||||
[:div.code-row-display
|
||||
[:& code-block {:type "css"
|
||||
:code css-example}]]]
|
||||
[:& code-block {:type @style-type
|
||||
:code style-code}]]]
|
||||
|
||||
[:div.code-block
|
||||
[:div.code-row-lang
|
||||
[:select.code-selection
|
||||
[:option "SVG"]
|
||||
[:option "HTML"]]
|
||||
#_[:option "HTML"]]
|
||||
|
||||
[:button.attributes-copy-button
|
||||
{:on-click #(prn "??")}
|
||||
{:on-click #(wapi/write-to-clipboard markup-code)}
|
||||
i/copy]]
|
||||
|
||||
[:div.code-row-display
|
||||
[:& code-block {:type "svg"
|
||||
:code svg-example}]]]
|
||||
[:& code-block {:type @markup-type
|
||||
:code markup-code}]]]
|
||||
|
||||
]))
|
||||
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
(mf/defc right-sidebar
|
||||
[{:keys [frame]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
section (mf/use-state :info #_:code)
|
||||
section (mf/use-state #_:info :code)
|
||||
selected-ref (mf/use-memo (make-selected-shapes-iref))
|
||||
shapes (mf/deref selected-ref)]
|
||||
[:aside.settings-bar.settings-bar-right
|
||||
|
|
164
frontend/src/app/util/code_gen.cljs
Normal file
164
frontend/src/app/util/code_gen.cljs
Normal file
|
@ -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.util.code-gen
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.common.math :as mth]
|
||||
[app.util.text :as ut]
|
||||
[app.util.color :as uc]))
|
||||
|
||||
(declare format-fill-color)
|
||||
(declare format-stroke)
|
||||
(declare shadow->css)
|
||||
|
||||
(def styles-data
|
||||
{:layout {:props [:width :height :x :y :radius :rx]
|
||||
:to-prop {:x "left" :y "top" :rotation "transform" :rx "border-radius"}
|
||||
:format {:rotation #(str/fmt "rotate(%sdeg)" %)}}
|
||||
:fill {:props [:fill-color :fill-color-gradient]
|
||||
:to-prop {:fill-color "background" :fill-color-gradient "background"}
|
||||
:format {:fill-color format-fill-color :fill-color-gradient format-fill-color}}
|
||||
:stroke {:props [:stroke-color]
|
||||
:to-prop {:stroke-color "border"}
|
||||
:format {:stroke-color format-stroke}}
|
||||
: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 %))}}})
|
||||
|
||||
(def style-text
|
||||
{:props [:fill-color
|
||||
:font-family
|
||||
:font-style
|
||||
:font-size
|
||||
:line-height
|
||||
:letter-spacing
|
||||
:text-decoration
|
||||
:text-transform]
|
||||
:to-prop {:fill-color "color" }
|
||||
:format {: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 format-fill-color}})
|
||||
|
||||
(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))))
|
||||
|
||||
|
||||
(defn format-fill-color [_ shape]
|
||||
(let [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)}]
|
||||
(uc/color->background color)))
|
||||
|
||||
(defn format-stroke [_ shape]
|
||||
(let [width (:stroke-width shape)
|
||||
style (name (:stroke-style shape))
|
||||
color {: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)}]
|
||||
(str/format "%spx %s %s" width style (uc/color->background color))))
|
||||
|
||||
|
||||
(defn generate-css-props [values properties params]
|
||||
(let [{:keys [to-prop format tab-size] :or {to-prop {} tab-size 0}} params
|
||||
;; 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))
|
||||
format-fn (or (prop format) default-format)]
|
||||
(str
|
||||
(str/repeat " " tab-size)
|
||||
(str/fmt "%s: %s;" css-prop (format-fn (prop values) values)))))]
|
||||
|
||||
(->> properties
|
||||
(remove #(let [value (get values %)]
|
||||
(or (nil? value) (= value 0))))
|
||||
(map format-property)
|
||||
(str/join "\n"))))
|
||||
|
||||
(defn shape->properties [shape]
|
||||
(let [props (->> styles-data vals (mapcat :props))
|
||||
to-prop (->> styles-data vals (map :to-prop) (reduce merge))
|
||||
format (->> styles-data vals (map :format) (reduce merge))]
|
||||
(generate-css-props shape props {:to-prop to-prop
|
||||
:format format
|
||||
:tab-size 2})))
|
||||
(defn text->properties [shape]
|
||||
(let [text-shape-style (select-keys styles-data [:layout :shadow :blur])
|
||||
|
||||
shape-props (->> text-shape-style vals (mapcat :props))
|
||||
shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge))
|
||||
shape-format (->> text-shape-style vals (map :format) (reduce merge))
|
||||
|
||||
|
||||
text-values (->> (ut/search-text-attrs (:content shape) (:props style-text))
|
||||
(merge ut/default-text-attrs))]
|
||||
|
||||
(str/join
|
||||
"\n"
|
||||
[(generate-css-props shape
|
||||
shape-props
|
||||
{:to-prop shape-to-prop
|
||||
:format shape-format
|
||||
:tab-size 2})
|
||||
(generate-css-props text-values
|
||||
(:props style-text)
|
||||
{:to-prop (:to-prop style-text)
|
||||
: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)
|
||||
selector (if (str/starts-with? selector "-") (subs selector 1) selector)]
|
||||
(str/join "\n" [(str/fmt "/* %s */" name)
|
||||
(str/fmt ".%s {" selector)
|
||||
properties
|
||||
"}"])))
|
||||
|
||||
(defn generate-style-code [type shapes]
|
||||
(let [generate-style-fn (case type
|
||||
"css" generate-css)]
|
||||
(->> shapes
|
||||
(map generate-style-fn)
|
||||
(str/join "\n\n"))))
|
Loading…
Add table
Reference in a new issue