├── source_maps.jpg
├── release.edn
├── .gitignore
├── shadow-cljs.edn
├── CHANGELOG.md
├── src
└── uix
│ ├── css.cljs
│ ├── css
│ ├── adapter
│ │ ├── reagent.cljs
│ │ └── uix.cljc
│ └── lib.cljc
│ └── css.clj
├── package.json
├── deps.edn
├── .github
└── workflows
│ └── test.yml
├── dev
└── release.clj
├── pom.xml
├── test
├── core_test_sample.cljc
└── core_test.clj
├── logo.svg
└── README.md
/source_maps.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roman01la/uix.css/master/source_maps.jpg
--------------------------------------------------------------------------------
/release.edn:
--------------------------------------------------------------------------------
1 | {:group-id "com.github.roman01la"
2 | :artifact-id "uix.css"
3 | :version "0.4.0"
4 | :scm-url "https://github.com/roman01la/uix.css"}
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | server_render_test
2 | node_modules
3 | *.iml
4 | .idea
5 | .cpcache
6 | .shadow-cljs
7 | .nrepl-port
8 | .styles
9 | public
10 | target
11 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps true
2 | :builds {:test
3 | {:target :node-script
4 | :output-dir "out"
5 | :output-to "out/test.js"
6 | :main core-test-sample/-main
7 | :build-hooks [(uix.css/hook)]
8 | :modules {:test {:entries [core-test-sample]}}}}}
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.4.0
4 |
5 | - Added support for SSR on JVM
6 |
7 | ## 0.3.1
8 |
9 | - Added Reagent adapter
10 | - Added output-dir to build config
11 |
12 | ## 0.3.0
13 |
14 | - Support code splitting (shadow's modules)
15 |
16 | ## 0.2.1
17 |
18 | - Static styles are sorted by location in code
19 | - Fixed pseudo-selectors and media queries in global styles
20 |
21 | ## 0.2.0
22 |
23 | - Support styles composition
24 | - Global styles
25 |
26 | ## 0.1.0
27 |
28 | Initial release
29 |
--------------------------------------------------------------------------------
/src/uix/css.cljs:
--------------------------------------------------------------------------------
1 | (ns uix.css
2 | (:require-macros [uix.css])
3 | (:require [uix.css.lib]
4 | [goog.style])
5 | (:import [goog.html SafeStyleSheet]
6 | [goog.string Const]))
7 |
8 | (defn load-stylesheet [path]
9 | (js/Promise.resolve
10 | (when-not (js/document.querySelector (str "link[href*='" path "']"))
11 | (let [el (js/document.createElement "link")]
12 | (set! (.-rel el) "stylesheet")
13 | (set! (.-href el) path)
14 | (js/document.head.append el)))))
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uix.css",
3 | "version": "1.0.0",
4 | "description": "
",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/roman01la/uix.css.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/roman01la/uix.css/issues"
21 | },
22 | "homepage": "https://github.com/roman01la/uix.css#readme",
23 | "dependencies": {
24 | "process": "^0.11.10",
25 | "react": "^19.2.1",
26 | "react-dom": "^19.2.1",
27 | "source-map-support": "^0.5.21"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 | :aliases {:dev {:extra-paths ["dev" "test"]
3 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"}
4 | org.clojure/clojurescript {:mvn/version "1.11.60"}
5 | thheller/shadow-cljs {:mvn/version "2.25.8"}
6 | clj-diffmatchpatch/clj-diffmatchpatch {:mvn/version "0.0.9.3"}
7 | reagent/reagent {:mvn/version "1.3.0"}
8 | com.pitch/uix.core {:mvn/version "1.4.8"}
9 | com.pitch/uix.dom {:mvn/version "1.4.8"}}}
10 | :release {:extra-paths ["dev"]
11 | :extra-deps {appliedscience/deps-library {:mvn/version "0.3.4"}
12 | org.apache.maven/maven-model {:mvn/version "3.6.3"}}
13 | :main-opts ["-m" "release"]}}}
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | name: ${{ matrix.os }}
13 | runs-on: ${{ matrix.os }}
14 |
15 | strategy:
16 | fail-fast: true
17 | matrix:
18 | os: [ubuntu-latest]
19 |
20 | steps:
21 | - name: Install dependencies
22 | run: |
23 | sudo apt-get update
24 | sudo apt-get install -y curl nodejs npm
25 | curl -s "https://get.sdkman.io" | bash
26 | source "/home/runner/.sdkman/bin/sdkman-init.sh"
27 | sdk install java 25.0.1-graalce
28 | curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh
29 | chmod +x linux-install.sh
30 | sudo ./linux-install.sh
31 |
32 | - name: Run tests
33 | run: |
34 | clojure -A:dev -M -m core-test
35 |
--------------------------------------------------------------------------------
/dev/release.clj:
--------------------------------------------------------------------------------
1 | (ns release
2 | (:require [clojure.java.io :as io]
3 | [deps-library.release :as dl.release])
4 | (:import (org.apache.maven.model License)
5 | (org.apache.maven.model.io.xpp3 MavenXpp3Reader MavenXpp3Writer)))
6 |
7 | (defn add-license []
8 | (let [pom-reader (MavenXpp3Reader.)
9 | pom-writer (MavenXpp3Writer.)
10 | file (io/file "pom.xml")
11 | model (->> (io/reader file)
12 | (.read pom-reader))
13 | license (License.)]
14 | (.setName license "Eclipse Public License - Version 2.0")
15 | (.setUrl license "https://www.eclipse.org/legal/epl-2.0/")
16 | (.addLicense model license)
17 | (.write pom-writer (io/writer file) model)))
18 |
19 | (alter-var-root #'dl.release/pom
20 | (fn [f]
21 | (fn [& args]
22 | (let [ret (apply f args)]
23 | (add-license)
24 | ret))))
25 |
26 | (defn -main [& args]
27 | (apply dl.release/main args))
28 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | com.github.roman01la
6 | uix.css
7 | 0.4.0
8 | uix.css
9 |
10 |
11 | Eclipse Public License - Version 2.0
12 | https://www.eclipse.org/legal/epl-2.0/
13 |
14 |
15 |
16 | scm:git:git@github.com:roman01la/uix.css.git
17 | scm:git:git@github.com:roman01la/uix.css.git
18 | 28c4754759657c116a189b69ee14477ef27a4ff1
19 | https://github.com/roman01la/uix.css
20 |
21 |
22 | src
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/uix/css/adapter/reagent.cljs:
--------------------------------------------------------------------------------
1 | (ns uix.css.adapter.reagent
2 | (:require [reagent.impl.template :as tmpl]))
3 |
4 | (defonce make-element tmpl/make-element)
5 |
6 | (set! tmpl/make-element
7 | (fn [this argv component jsprops first-child]
8 | (when (js/Array.isArray (.-style jsprops))
9 | (let [class-names (atom [])
10 | inline-styles (atom {})]
11 | (doseq [style (.-style jsprops)]
12 | (if (:uixCss style)
13 | (let [{:keys [class vars]} (:uixCss style)]
14 | (swap! class-names conj class)
15 | (swap! inline-styles into vars))
16 | (swap! inline-styles into style)))
17 | (set! (.-className jsprops)
18 | (reagent.core/class-names (.-className jsprops) @class-names))
19 | (if (empty? @inline-styles)
20 | (js-delete jsprops "style")
21 | (set! (.-style jsprops)
22 | (tmpl/convert-props @inline-styles #js {})))))
23 | (make-element this argv component jsprops first-child)))
24 |
--------------------------------------------------------------------------------
/test/core_test_sample.cljc:
--------------------------------------------------------------------------------
1 | (ns core-test-sample
2 | (:require [uix.core :refer [defui $]]
3 | [uix.dom.server :as dom.server]
4 | [uix.css :refer [css]]
5 | [uix.css.adapter.uix]
6 | #?(:cljs [fs])))
7 |
8 | (def border-color "#000")
9 | (def hover-bg "yellow")
10 | (def v (atom 90))
11 |
12 | (def styles
13 | (let [p-xl 32]
14 | (css {:margin 64
15 | :padding (inc p-xl)
16 | :border (str "1px solid " border-color)
17 | :&:hover {:color :blue
18 | :background hover-bg
19 | :width @v}
20 | "&:hover > div" {:border-radius p-xl}
21 | "@media (max-width: 800px)" {:color hover-bg
22 | :width (+ 8 9)
23 | :&:hover {:color hover-bg
24 | :width (+ @v 89)}}})))
25 |
26 | (defui component []
27 | ($ :div {:style styles}))
28 |
29 | #?(:cljs
30 | (defn -main [& args]
31 | (let [html (dom.server/render-to-string ($ component))
32 | path "server_render_test/html/test.html"]
33 | (fs/writeFileSync path html))
34 | (let [html (dom.server/render-to-static-markup ($ component))
35 | path "server_render_test/markup/test.html"]
36 | (fs/writeFileSync path html))))
--------------------------------------------------------------------------------
/src/uix/css/lib.cljc:
--------------------------------------------------------------------------------
1 | (ns uix.css.lib
2 | (:require [clojure.string :as str]))
3 |
4 | (def unitless-prop
5 | #{:animation-iteration-count
6 | :border-image-outset
7 | :border-image-slice
8 | :border-image-width
9 | :box-flex
10 | :box-flex-group
11 | :box-ordinal-group
12 | :column-count
13 | :columns
14 | :flex
15 | :flex-grow
16 | :flex-positive
17 | :flex-shrink
18 | :flex-negative
19 | :flex-order
20 | :grid-area
21 | :grid-row
22 | :grid-row-end
23 | :grid-row-span
24 | :grid-row-start
25 | :grid-column
26 | :grid-column-end
27 | :grid-column-span
28 | :grid-column-start
29 | :font-weight
30 | :line-clamp
31 | :line-height
32 | :opacity
33 | :order
34 | :orphans
35 | :tab-size
36 | :widows
37 | :z-index
38 | :zoom
39 | ;; SVG-related properties
40 | :fill-opacity
41 | :flood-opacity
42 | :stop-opacity
43 | :stroke-dasharray
44 | :stroke-dashoffset
45 | :stroke-miterlimit
46 | :stroke-opacity
47 | :stroke-width})
48 |
49 | (defn interpret-value [k v]
50 | (if (and (number? v) (not (unitless-prop k)))
51 | (str v "px")
52 | v))
53 |
54 | #?(:clj
55 | (do
56 | (declare class-names)
57 |
58 | (defn class-names-coll [classes]
59 | (let [classes (reduce (fn [a c]
60 | (if c
61 | (->> (if (keyword? c) (name c) (class-names c))
62 | (conj a))
63 | a))
64 | []
65 | classes)]
66 | (when (pos? (count classes))
67 | (str/join " " classes))))
68 |
69 | (defn class-names
70 | "Merges a collection of class names into a string"
71 | ([a]
72 | (cond
73 | (coll? a) (class-names-coll a)
74 | (keyword? a) (name a)
75 | :else a))
76 | ([a b]
77 | (if a
78 | (if b
79 | (str (class-names a) " " (class-names b))
80 | (class-names a))
81 | (class-names b)))
82 | ([a b & rst]
83 | (reduce class-names (class-names a b) rst)))))
--------------------------------------------------------------------------------
/src/uix/css/adapter/uix.cljc:
--------------------------------------------------------------------------------
1 | (ns uix.css.adapter.uix
2 | (:require #?@(:cljs [[uix.compiler.alpha]
3 | [uix.compiler.attributes :as uix.attrs]])
4 | #?@(:clj [[uix.dom.server]
5 | [uix.css.lib]])))
6 |
7 | ;; uix.css adapter
8 | ;; 1. Intercepts element creation
9 | ;; 2. Extracts `uixCssClass` and `uixCssVars` fields from `props.style`
10 | ;; 3. Merges generated class name with the rest of class names in `props.className` field
11 | ;; 4. Merges generated CSS Vars map with the rest of styles in `props.style` object
12 |
13 | #?(:cljs
14 | (defonce create-element uix.compiler.alpha/create-element))
15 |
16 | #?(:cljs
17 | (set! uix.compiler.alpha/create-element
18 | (fn [args children]
19 | (when-let [^js props (aget args 1)]
20 | (when (and (uix.compiler.alpha/pojo? props)
21 | (js/Array.isArray (.-style props)))
22 | (let [class-names #js []
23 | inline-styles (atom {})]
24 | (doseq [style (.-style props)]
25 | (if (:uixCss style)
26 | (let [{:keys [class vars]} (:uixCss style)]
27 | (.push class-names class)
28 | (swap! inline-styles into vars))
29 | (swap! inline-styles into style)))
30 | (set! (.-className props)
31 | (uix.attrs/class-names (.-className props) class-names))
32 | (set! (.-style props)
33 | (uix.compiler.attributes/convert-prop-value-shallow @inline-styles)))))
34 | (create-element args children))))
35 |
36 | #?(:clj
37 | (alter-var-root #'uix.dom.server/render-attrs!
38 | (fn [f]
39 | (fn [tag attrs sb]
40 | (let [props (atom attrs)]
41 | (when (vector? (:style attrs))
42 | (let [class-names (atom [])
43 | inline-styles (atom {})]
44 | (doseq [style (:style attrs)]
45 | (if (:uixCss style)
46 | (let [{:keys [class vars]} (:uixCss style)]
47 | (swap! class-names conj class)
48 | (swap! inline-styles into vars))
49 | (swap! inline-styles into style)))
50 | (swap! props assoc :class
51 | (uix.css.lib/class-names (:class attrs) @class-names))
52 | (swap! props assoc :style @inline-styles)))
53 | (f tag @props sb))))))
--------------------------------------------------------------------------------
/test/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns core-test
2 | (:require [clojure.java.io :as io]
3 | [clojure.java.shell :as shell]
4 | [clojure.string :as str]
5 | [clojure.test :refer :all]
6 | [clj-diffmatchpatch :as diff]
7 | [uix.dom.server :as dom.server]
8 | [uix.core :refer [$]]
9 | [core-test-sample]))
10 |
11 | (defn diff [s1 s2]
12 | (->> (diff/wdiff s1 s2)
13 | (map (fn [[op text]]
14 | (case op
15 | :delete (str "\033[37;41;1m" text "\033[0m")
16 | :insert (str "\033[37;42;1m" text "\033[0m")
17 | :equal text)))
18 | (str/join)))
19 |
20 | (def css-out
21 | ".core-test-sample-14-5{margin:64px;padding:33px;border:1px solid #000;}.core-test-sample-14-5:hover{color:blue;background:yellow;width:var(--core-test-sample-20-0);}.core-test-sample-14-5:hover > div{border-radius:32px;}@media (max-width: 800px){.core-test-sample-14-5{color:yellow;width:17px;}.core-test-sample-14-5:hover{color:yellow;width:var(--core-test-sample-27-0);}}\n/*# sourceMappingURL=main.css.map */")
22 |
23 | (def css-out-release
24 | (->> [".k1{margin:64px;padding:33px;border:1px solid #000;}"
25 | ".k1:hover{color:blue;background:yellow;width:var(--v2);}"
26 | ".k1:hover > div{border-radius:32px;}"
27 | "@media (max-width: 800px){.k1{color:yellow;width:17px;}.k1:hover{color:yellow;width:var(--v3);}}"
28 | "\n"
29 | "/*# sourceMappingURL=main.css.map */"]
30 | (str/join "")))
31 |
32 | (defn after []
33 | (.delete (io/file "out/main.css"))
34 | (.delete (io/file "out/main.css.map"))
35 | (run! io/delete-file (reverse (file-seq (io/file ".styles"))))
36 | (run! io/delete-file (reverse (file-seq (io/file ".shadow-cljs"))))
37 | (run! io/delete-file (reverse (file-seq (io/file "out")))))
38 |
39 | (defn exec [& cmd]
40 | (testing cmd
41 | (println "Running" (str "\"" (str/join " " cmd) "\""))
42 | (let [{:keys [exit out err]} (apply shell/sh cmd)]
43 | (is (= exit 0))
44 | (when-not (str/blank? err)
45 | (binding [*out* *err*]
46 | (println err)))
47 | (when-not (str/blank? out)
48 | (println out)))))
49 |
50 | (deftest test-css-compilation
51 | (testing "generated CSS should match snapshot"
52 | (exec "clojure" "-A:dev" "-M" "-m" "shadow.cljs.devtools.cli" "compile" "test")
53 | (let [out-css (slurp "out/main.css")]
54 | (is (= css-out out-css)
55 | (diff css-out out-css)))
56 | (after))
57 | (testing "generated minified CSS should match snapshot"
58 | (exec "clojure" "-A:dev" "-M" "-m" "shadow.cljs.devtools.cli" "release" "test")
59 | (let [out-css (slurp "out/main.css")]
60 | (is (= css-out-release out-css)
61 | (diff css-out-release out-css)))
62 | (after)))
63 |
64 | (def render-dir "server_render_test")
65 |
66 | (deftest test-ssr-compat-between-jvm-and-js
67 | (doseq [^java.io.File f (reverse (file-seq (io/file render-dir)))]
68 | (when (.exists f)
69 | (.delete f)))
70 | (.mkdir (io/file render-dir))
71 | (.mkdir (io/file render-dir "html"))
72 | (.mkdir (io/file render-dir "markup"))
73 | (exec "clojure" "-A:dev" "-M" "-m" "shadow.cljs.devtools.cli" "compile" "test")
74 | (exec "node" "out/test.js")
75 | (let [cljs-html (slurp (str render-dir "/html/test.html"))
76 | clj-html (dom.server/render-to-string ($ core-test-sample/component))]
77 | (is (= cljs-html clj-html) (diff cljs-html clj-html)))
78 | (let [cljs-html (slurp (str render-dir "/markup/test.html"))
79 | clj-html (dom.server/render-to-static-markup ($ core-test-sample/component))]
80 | (is (= cljs-html clj-html) (diff cljs-html clj-html)))
81 | (after))
82 |
83 | (defn -main [& args]
84 | (run-tests 'core-test))
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://clojars.org/com.github.roman01la/uix.css)
4 |
5 | CSS-in-CLJS library
6 |
7 | ```clojure
8 | (ns my.app
9 | (:require [uix.core :as uix :refer [defui $]]
10 | [uix.css :refer [css]]
11 | [uix.css.adapter.uix]))
12 |
13 | (defn button []
14 | ($ :button {:style (css {:font-size "14px"
15 | :background "#151e2c"})}))
16 | ```
17 |
18 | - Discuss at #uix on Clojurians Slack
19 |
20 | ## Installation
21 |
22 | ```clojure
23 | {:deps {com.github.roman01la/uix.css {:mvn/version "0.4.0"}}}
24 | ```
25 |
26 | ## Motivation
27 |
28 | I love inline styles, unfortunately they are quite limited, there's no way to specify states (hover, active, etc) or use media queries. Essentially, inline styles won't let you use all of CSS.
29 |
30 | `uix.css` is a successor of [cljss](https://github.com/clj-commons/cljss), similar library that I created some years ago.
31 |
32 | ## Usage
33 |
34 | The library relies on shadow-cljs to generate and bundle CSS. `css` macro accepts a map of styles and returns a tuple of a class name and a map of dynamic inline styles.
35 |
36 | In the example below I'm using [UIx](https://github.com/pitch-io/uix). The library can be used with any other React wrapper as long as a proper adapter is provided (take a look at `uix.css.adapter.uix` ns to learn how to build adapters). The adapter takes care of applying whatever `css` macro returns.
37 |
38 | ```clojure
39 | (ns my.app
40 | (:require [uix.core :as uix :refer [defui $]]
41 | [uix.css :refer [css]]
42 | [uix.css.adapter.uix]))
43 |
44 | (def border-color "blue")
45 |
46 | (defn button []
47 | ($ :button {:style (css {:font-size "14px"
48 | :background "#151e2c"
49 | :padding "8px 32px"
50 | :border (str "1px solid " border-color)
51 | :&:hover {:background "green"}
52 | "@media (max-width: 800px)" {:padding "4px 12px"}
53 | "& > strong" {:font-weight 600}})}))
54 | ```
55 |
56 | `uix.css/hook` build hook takes care of generating CSS and creating a bundle.
57 |
58 | ```clojure
59 | ;; shadow-cljs.edn
60 | {:deps true
61 | :dev-http {8080 "public"}
62 | :builds {:website
63 | {:target :browser
64 | :build-hooks [(uix.css/hook)]
65 | :modules {:website {:entries [my.app]}}}}}
66 | ```
67 |
68 | When compiled, static part of the styles map is dumped into CSS bundle, but dynamic part of it (see `border-color` example above) is deferred to runtime, where values are assigned via CSS Variables API.
69 |
70 | ### Styles composition
71 |
72 | `css` macro takes arbitrary number of styles
73 |
74 | ```clojure
75 | (defui button [{:keys [style children]}]
76 | ($ :button
77 | {:style (css {:color :red
78 | :padding "8px 16px"}
79 | style)}
80 | children))
81 |
82 | ($ button {:style (css {:background :yellow})}
83 | "press me")
84 | ```
85 |
86 | When a map of styles is passed at runtime, it will be applied as normal inline styles. This behaviour exists specifically for a case when you have UI styled with inline CSS and want to migrate to `uix.css` gradually.
87 |
88 | In this example existing `button` component was updated with `css` macro, but all usage places are still passing inline styles, meaning that updating internals of the component won't break its users.
89 |
90 | ```clojure
91 | (defui button [{:keys [style children]}]
92 | ($ :button
93 | {:style (css {:color :red
94 | :padding "8px 16px"}
95 | style)}
96 | children))
97 |
98 | ;; these styles will be applied as inline styles
99 | ($ button {:style {:background :yellow}}
100 | "press me")
101 | ```
102 |
103 | ### Global styles
104 |
105 | Styles passed under `:global` keyword are not scoped to current element, also global styles do not support dynamic values. This exists as a convenience, to avoid creating CSS file just for global styles.
106 |
107 | ```clojure
108 | (defui app []
109 | ($ :div {:style (css {:width "100vw"
110 | :min-height "100vh"
111 | :background "#10121e"
112 | :color "#d7dbf1"
113 | :global {:html {:box-sizing :border-box}
114 | "html *" {:box-sizing :inherit}
115 | :body {:-webkit-font-smoothing :antialiased
116 | :-moz-osx-font-smoothing :grayscale
117 | :-moz-font-feature-settings "\"liga\" on"
118 | :text-rendering :optimizelegibility
119 | :margin 0
120 | :font "400 16px / 1.4 Inter, sans-serif"}}})}))
121 | ```
122 |
123 | ## Source maps
124 |
125 | While generated class names are quite descriptive (`auix-core-L18-C20` — ns + line + column), we also generate CSS source maps to improve debugging experience.
126 |
127 | 
128 |
129 | ## Evaluators
130 |
131 | `uix.css` tries to inline constant values and pure expressions to reduce the number of dynamic styles, this is especially useful when you have a set of shared design tokens in code, like colors, font sizes, spacing, etc.
132 |
133 | In this example the value of `border-color` var will be inlined, as well as `(str "1px solid " border-color)` expression. `css` macro analyzes the code and evaluates well known functions given that their arguments are constant values.
134 |
135 | ```clojure
136 | (def border-color "blue")
137 |
138 | (def m-xl 64)
139 |
140 | (css {:border (str "1px solid " border-color)
141 | :margin m-xl})
142 | ```
143 |
144 | ## Code-splitting
145 |
146 | Starting from v0.3.0 uix.css follows shadow's code splitting via modules. Here's UIx example:
147 |
148 | ```clojure
149 | ;; main module
150 | (ns app.core
151 | (:require [uix.core :as uix :refer [$ defui]]
152 | [uix.css :refer [css]]
153 | [uix.css.adapter.uix]
154 | [shadow.lazy]))
155 |
156 | ;; create loadable var
157 | (def loadable-settings
158 | (shadow.lazy/loadable app.settings/view))
159 |
160 | (def settings
161 | ;; creates lazy React component
162 | (uix.core/lazy
163 | ;; loads CSS bundle of the settings module
164 | #(uix.css/load-before app.settings
165 | ;; loads settings module
166 | (shadow.lazy/load loadable-settings))))
167 |
168 | (defui root-layout []
169 | ($ :div {:style (css {:padding 24})}
170 | ;; Suspense component displays the fallback UI while
171 | ;; lazy component is being loaded
172 | ($ uix.core/suspense {:fallback "loading settings..."}
173 | ($ settings))))
174 |
175 | (defn init []
176 | ;; render
177 | )
178 |
179 | ;; settings module
180 | (ns app.settings
181 | (:require [uix.core :as uix :refer [$ defui]]
182 | [uix.css :refer [css]]
183 | [uix.css.adapter.uix]))
184 |
185 | (defui view []
186 | ($ :div {:style (css {:padding 16})}))
187 |
188 | ;; shadow-cljs.edn build config
189 | {:app {:target :browser
190 | :module-loader true
191 | :modules {:main {:entries [app.core]
192 | :init-fn app.core/init}
193 | :settings {:entries [app.settings]
194 | :depends-on #{:main}}}
195 | :build-hooks [(uix.css/hook)]}}
196 | ```
197 |
198 | Building this example will output two CSS bundles next to JavaScript bundles: `main.css` and `settings.css`.
199 |
200 | Same as for splitted JavaScript, you have to load initial CSS bundle explicitly, by declaring it via `` element in HTML. But for dynamically loaded modules you need to use `uix.css/load-before` function to load CSS bundle of the specified module before the module itself.
201 |
202 | ## Server-side rendering
203 |
204 | Since v0.4.0 uix.css supports server rendering on JVM, which plays nicely with [UIx SSR support](https://github.com/pitch-io/uix/blob/master/docs/server-side-rendering.md).
205 |
206 | No extra setup is required. When rendering UIx components in Clojure JVM, the `css` macro won't build styles. Intead it will just emit class names needed at runtime in a browser, where prebuilt CSS is loaded.
207 |
--------------------------------------------------------------------------------
/src/uix/css.clj:
--------------------------------------------------------------------------------
1 | (ns uix.css
2 | (:require [cljs.analyzer :as ana]
3 | [cljs.analyzer.api :as ana-api]
4 | [cljs.env :as env]
5 | [cljs.source-map]
6 | [cljs.vendor.clojure.data.json :as json]
7 | [clojure.java.io :as io]
8 | [clojure.string :as str]
9 | [clojure.tools.reader.edn :as edn]
10 | [uix.css.lib :as css.lib])
11 | (:import (java.io File FileNotFoundException)))
12 |
13 | (defn cljs? [env]
14 | (some? (:ns env)))
15 |
16 | (defn -resolve [env v]
17 | (if (cljs? env)
18 | (ana-api/resolve env v)
19 | (meta (resolve env v))))
20 |
21 | (defmacro debug [& body]
22 | `(binding [*out* *err*]
23 | ~@body))
24 |
25 | ;; var name -> ast node
26 | (def defs (atom {}))
27 |
28 | (defonce parse-def (get-method ana/parse 'def))
29 |
30 | (defmethod ana/parse 'def [op env form name opts]
31 | (let [ast (parse-def op env form name opts)]
32 | (swap! defs assoc (:name ast) ast)
33 | ast))
34 |
35 | (defn compile-rule [k v]
36 | (assert (not (or (symbol? v) (list? v))) "Value can't be a symbol or expression here, this is a bug")
37 | (str (name k) ":"
38 | (cond
39 | (and (number? v) (not (css.lib/unitless-prop k)))
40 | (str v "px")
41 |
42 | (keyword? v)
43 | (name v)
44 |
45 | :else v)
46 | ";"))
47 |
48 | (defn compile-styles [class-name selector styles & {:keys [global?]}]
49 | (str (if global?
50 | (str/replace (name selector) "&" class-name)
51 | (str/replace (name selector) "&" (str "." class-name)))
52 | "{"
53 | (str/join "" (map #(apply compile-rule %) styles))
54 | "}"))
55 |
56 | (defn styles-by-type [styles]
57 | (group-by #(let [k (-> % key name)]
58 | (cond
59 | (str/starts-with? k "&") :blocks
60 | (str/starts-with? k"@") :media
61 | (= "global" k) :global
62 | :else :self))
63 | styles))
64 |
65 | (defn walk-styles-compile [class-name styles & {:keys [global?]}]
66 | (let [{:keys [self blocks media global]} (styles-by-type styles)]
67 | (str/join ""
68 | (concat
69 | ;; global styles
70 | (->> global
71 | (map second)
72 | (apply merge-with merge)
73 | (mapv (fn [[selector styles]]
74 | (walk-styles-compile (name selector) styles :global? true))))
75 | ;; element styles
76 | (mapv #(apply compile-styles class-name (concat % [:global? global?])) (into [["&" self]] blocks))
77 | ;; element media queries
78 | ;; FIXME: media styles should be in the end of file
79 | ;; FIXME: keyframes
80 | (mapv (fn [[media styles]]
81 | (str media "{" (walk-styles-compile class-name styles :global? global?) "}"))
82 | media)))))
83 |
84 | (def ^:dynamic *build-state*)
85 |
86 | (defn release? []
87 | (-> *build-state*
88 | :mode
89 | (= :release)))
90 |
91 | (defn root-path []
92 | ;; TODO: include build name dir
93 | (str ".styles/"
94 | (if (release?) "release" "dev")))
95 |
96 | (defn styles-modules []
97 | (->> (file-seq (io/file (root-path)))
98 | (filter #(.isFile ^File %))))
99 |
100 | (defn write-modules! [styles-reg]
101 | (doseq [[file styles] styles-reg]
102 | (let [path (str (root-path) "/" file ".edn")]
103 | (io/make-parents path)
104 | (spit path (str styles)))))
105 |
106 | (defn write-source-map! [styles css-str output-to]
107 | (let [file-name (peek (str/split output-to #"/"))
108 | styles (->> styles
109 | (reduce-kv (fn [ret class v]
110 | (assoc ret class (assoc v :css-loc (.indexOf ^String css-str ^String class))))
111 | {}))
112 | sm (->> (vals styles)
113 | (reduce (fn [ret {:keys [file line column css-loc]}]
114 | (assoc-in ret [file (dec line) column] [{:gline 0 :gcol css-loc}]))
115 | {}))
116 | sources (->> (vals styles)
117 | (map (fn [{:keys [file]}]
118 | [file (-> file io/resource slurp)]))
119 | (into #{})
120 | vec)
121 | sources-content (map second sources)
122 | source-files (map first sources)
123 | sm (into (cljs.source-map/encode* sm {:file file-name
124 | :sources-content sources-content})
125 | {"sources" source-files})]
126 | (spit (str output-to ".map") (json/write-str sm :escape-slash false))))
127 |
128 | (defn write-bundle! [[output-name styles] output-dir]
129 | (let [styles (into {} styles)
130 | styles-strs (->> styles
131 | (sort-by (comp #(->> (str/split % #"-") (take-last 2) (str/join "."))
132 | key))
133 | (map (comp :css-str val)))
134 | sm-path (str output-name ".map")
135 | output-to (str output-dir "/" output-name)
136 | out (str (str/join "" styles-strs)
137 | "\n/*# sourceMappingURL=" sm-path " */")]
138 | (write-source-map! styles out output-to)
139 | (try
140 | (when (not= (slurp output-to) out)
141 | (spit output-to out))
142 | (catch FileNotFoundException e
143 | (spit output-to out)))))
144 |
145 | (defn write-bundles! [state {:keys [output-dir]}]
146 | (let [output-dir (or output-dir (->> state :shadow.build/config :output-dir))
147 | build-sources (->> (:build-sources state)
148 | (map second)
149 | (filter string?)
150 | (into #{}))
151 | modules (->> (vals (:shadow.build.modules/modules state))
152 | (map (fn [{:keys [module-name sources]}]
153 | {:output-name (str/replace module-name #"\.js$" ".css")
154 | :sources (->> sources
155 | (filter (comp #{:shadow.build.classpath/resource} first))
156 | (keep (comp :ns (:sources state)))
157 | (into #{}))})))
158 | used (->> (styles-modules)
159 | (filter #(-> (.getPath ^File %)
160 | (str/replace #"^\.styles\/(dev|release)/" "")
161 | (str/replace #"\.edn$" "")
162 | build-sources)))
163 | styles (->> used
164 | (map (comp edn/read-string slurp))
165 | (apply merge)
166 | (reduce-kv (fn [ret class styles]
167 | (assoc ret class (->> (walk-styles-compile class (:styles styles))
168 | (assoc styles :css-str))))
169 | {}))
170 | style-modules (->> styles
171 | (group-by (fn [[_ {:keys [ns]}]]
172 | (->> modules
173 | (reduce #(when (contains? (:sources %2) ns)
174 | (reduced (:output-name %2)))
175 | nil)))))]
176 | (.mkdirs (io/file output-dir))
177 | (run! #(write-bundle! % output-dir) style-modules)))
178 |
179 | (defn- build-state->styles-reg [{:keys [compiler-env]}]
180 | (->> (:cljs.analyzer/namespaces compiler-env)
181 | vals
182 | (map :uix/css)
183 | (apply merge)))
184 |
185 | (defn write-styles! [state config]
186 | (binding [*build-state* state]
187 | (write-modules! (build-state->styles-reg state))
188 | (write-bundles! state config)))
189 |
190 | (defn eval-symbol [env v]
191 | (if-not (cljs? env)
192 | (if-let [var (resolve env v)]
193 | (if (or (number? @var) (string? @var))
194 | @var
195 | ::nothing)
196 | (or (some-> (env v) .init .eval)
197 | ::nothing))
198 | (let [ast (ana-api/resolve env v)]
199 | (cond
200 | (and (= :local (:op ast))
201 | (-> ast :init :op (= :const)))
202 | (-> ast :init :val)
203 |
204 | (= :var (:op ast))
205 | (let [ast (@defs (:name ast))]
206 | (when (-> ast :init :op (= :const))
207 | (-> ast :init :val)))
208 |
209 | :else ::nothing))))
210 |
211 | (def evaluators
212 | {'cljs.core/inc inc
213 | 'cljs.core/dec dec
214 | 'cljs.core/+ +
215 | 'cljs.core/- -
216 | 'cljs.core/* *
217 | 'cljs.core// (comp float /)
218 | 'cljs.core/str str
219 |
220 | 'inc inc
221 | 'dec dec
222 | '+ +
223 | '- -
224 | '* *
225 | '/ (comp float /)
226 | 'str str})
227 |
228 | ;; TODO: add a walker for well known forms: when, if, etc
229 |
230 | (declare eval-css-value)
231 |
232 | (defn eval-expr [env [f & args :as expr]]
233 | (cond
234 | (symbol? f)
235 | (if-let [eval-fn (->> f (-resolve env) :name evaluators)]
236 | (let [args (map #(eval-css-value env %) args)]
237 | (if (every? (complement #{::nothing}) args)
238 | (apply eval-fn args)
239 | ::nothing))
240 | ::nothing)
241 |
242 | :else ::nothing))
243 |
244 | (defn eval-value [env v]
245 | (if-not (cljs? env)
246 | (if (or (number? v) (string? v))
247 | v
248 | ::nothing)
249 | (ana-api/no-warn
250 | (let [ast (ana-api/analyze env v)]
251 | (if (= :const (:op ast))
252 | (:val ast)
253 | ::nothing)))))
254 |
255 | (def release-counter (atom 0))
256 |
257 | (defn dyn-var-name [env v line]
258 | (if (release?)
259 | (str "--v" (swap! release-counter inc))
260 | (let [v (cond-> v
261 | (and (list? v) (= 'clojure.core/deref (first v)))
262 | second)
263 | {:keys [file ns]
264 | :or {file *file* ns *ns*}}
265 | (if (symbol? v)
266 | (-resolve env v)
267 | (meta v))
268 | column 0
269 | ns (if ns
270 | (-> (str ns) (str/replace #"\." "-"))
271 | (-> file
272 | (str/replace #"\.clj(s|c)?$" "")
273 | (str/replace #"(/|_)" "-")))]
274 | (str "--" ns "-" line "-" column))))
275 |
276 | (defn eval-css-value [env v]
277 | (or (cond
278 | (symbol? v) (eval-symbol env v)
279 | (list? v) (eval-expr env v)
280 | :else (eval-value env v))
281 | ::nothing))
282 |
283 | (def ^:dynamic *global-context?* false)
284 |
285 | (defn walk-map [f m]
286 | (letfn [(walk [x]
287 | (if (map? x)
288 | (into {} (->> x (map (fn [[k v]]
289 | (if-not (and (= :global k) (map? v))
290 | (f [k (walk v)])
291 | (binding [*global-context?* true]
292 | (f [k (walk v)])))))))
293 | x))]
294 | (walk m)))
295 |
296 | (defn- env-with-loc [env form]
297 | (let [loc (select-keys (meta form) [:line :column])]
298 | (cond-> env (seq loc) (into loc))))
299 |
300 | (defn find-dyn-styles [styles env line]
301 | (let [dyn-input-styles (atom {})
302 | line (atom line)]
303 | [(walk-map (fn [[k v]]
304 | (swap! line inc)
305 | (if-not (or (symbol? v) (list? v) (instance? clojure.lang.Cons v))
306 | [k v]
307 | (let [ret (eval-css-value env v)]
308 | (when (and *global-context?* (= ::nothing ret))
309 | (ana/warning ::global-styles-dynamic-vars (env-with-loc env v) {}))
310 | (if (= ::nothing ret)
311 | (let [var-name (dyn-var-name env v @line)]
312 | (swap! dyn-input-styles assoc var-name `(uix.css.lib/interpret-value ~k ~v))
313 | [k (str "var(" var-name ")")])
314 | [k ret]))))
315 | styles)
316 | @dyn-input-styles]))
317 |
318 | (let [env (atom {})]
319 | (defn get-env []
320 | (or env/*compiler* env)))
321 |
322 | (defn make-styles [styles env form]
323 | (let [ns (or (-> env :ns :name) (ns-name *ns*))
324 | file (or (-> env :ns :meta :file) *file*)
325 | {:keys [line column]} (meta form)
326 | class (if (release?)
327 | (str "k" (swap! release-counter inc))
328 | (str (-> ns (str/replace "." "-")) "-" line "-" column))
329 | [evaled-styles dyn-input-styles] (find-dyn-styles styles env line)]
330 | (swap! (get-env) assoc-in [:cljs.analyzer/namespaces ns :uix/css file class]
331 | {:styles evaled-styles
332 | :file file
333 | :ns ns
334 | :line line
335 | :column column
336 | :dyn-input-styles dyn-input-styles})
337 | {:uixCss {:class class
338 | :vars dyn-input-styles}}))
339 |
340 | (defmacro css [& styles]
341 | (if (cljs? &env)
342 | (binding [*build-state* (:shadow.build.cljs-bridge/state @env/*compiler*)]
343 | (let [styles (->> styles
344 | (mapv (fn [v]
345 | (if (map? v)
346 | `(cljs.core/array ~(make-styles v &env &form))
347 | `(let [v# ~v]
348 | (if (map? v#)
349 | (cljs.core/array v#)
350 | v#))))))]
351 | `(.concat ~@styles)))
352 | (binding [*build-state* (atom {})]
353 | (let [styles (->> styles
354 | (mapv (fn [v]
355 | (if (map? v)
356 | [(make-styles v &env &form)]
357 | `(let [v# ~v]
358 | (if (map? v#)
359 | [v#]
360 | v#))))))]
361 | `(vec (concat ~@styles))))))
362 |
363 |
364 | (defn hook
365 | {:shadow.build/stage :compile-finish}
366 | [build-state & [config]]
367 | (write-styles! build-state config)
368 | build-state)
369 |
370 | (defmethod ana/error-message ::global-styles-dynamic-vars [_ _]
371 | "Global styles can't have dynamic values")
372 |
373 | (defmacro load-before [ns promise]
374 | (let [asset-path (->> (:shadow.build.cljs-bridge/state @env/*compiler*)
375 | :shadow.build/config
376 | :asset-path)
377 | path (->> (:shadow.build.cljs-bridge/state @env/*compiler*)
378 | :shadow.build.modules/modules
379 | vals
380 | (some #(when (contains? (set (:entries %)) ns)
381 | (str asset-path "/" (str/replace (:module-name %) #"\.js$" ".css")))))]
382 | `(-> (load-stylesheet ~path)
383 | (.then (fn [] ~promise)))))
384 |
385 | (comment
386 | (require '[uix.core :refer [$]])
387 | (require 'uix.dom.server)
388 |
389 | (def xx (atom 90))
390 | (let [x (atom 1)]
391 | (css {:font-size "14px" :flex (+ @xx 89)})
392 | (uix.dom.server/render-to-string
393 | ($ :div.flex.flex-col.items-center {:style (css {:font-size "14px" :flex (+ @xx 89)})}
394 | ($ :ul.flex.gap-2.text-sm.py-1.font-medium)))))
--------------------------------------------------------------------------------