From 16eea3b13a63a7857e13818ce421661e7d438e92 Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Mon, 23 Nov 2020 09:57:34 -0500 Subject: [PATCH 1/8] Experiment: add special syntax for CSS variables At compile time, keywords with the namespace "var" get transformed to refer to CSS variables. Map keys like `:var/font-size` become `:--font-size` and map values become `:var(font-size)`. This syntax seems nice since, at least in the "resolve" version, it *looks like* the equivalent CSS. However, this syntax means that all vars are global, unnamespaced, which is perhaps not ideal. So, this is sort of a proof of concept. Perhaps a better syntax would be eg `::*font-size*`. This is somewhat familiar due to clojure's use of earmuffs for dynamic vars, and also allows namespacing with native clojure namespaces. "Global" vars if desired could still be created as eg `:global/*font-size*`. --- dev/spade/demo.cljs | 5 +++-- src/spade/core.cljc | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/dev/spade/demo.cljs b/dev/spade/demo.cljs index eb0fe82..1412d88 100644 --- a/dev/spade/demo.cljs +++ b/dev/spade/demo.cljs @@ -12,7 +12,8 @@ ["100%" {:opacity end}]) (defglobal background - [:body {:background "#333"}]) + [:body {:var/my-var "22pt" + :background "#333"}]) (defglobal text [:body {:color "#fff"}]) @@ -22,7 +23,7 @@ {:padding "80px"}) {:padding "8px"} - [:.title {:font-size "22pt" + [:.title {:font-size :var/my-var :animation [[(parameterized-anim-frames 0 0.5) "560ms" 'ease-in-out]]}]) (defclass colorized-with-key [color] diff --git a/src/spade/core.cljc b/src/spade/core.cljc index 0057d85..8d44c1d 100644 --- a/src/spade/core.cljc +++ b/src/spade/core.cljc @@ -34,6 +34,32 @@ element)) style)) +(defn- css-var? [element] + (and (keyword? element) + (= "var" (namespace element)))) + +(defn- varify-key [element] + (keyword (str "--" (name element)))) + +(defn- varify-val [element] + (keyword (str "var(--" (name element) ")"))) + +(defn- rename-vars [style] + (postwalk + (fn [element] + (if (map-entry? element) + (let [var-key? (css-var? (key element)) + var-val? (css-var? (val element))] + (if (or var-key? var-val?) + (-> element + (update 0 (if var-key? varify-key identity)) + (update 1 (if var-val? varify-val identity))) + + element)) + + element)) + style)) + (defn- extract-composes [style] (if-let [composes (when (map? (first style)) (:composes (first style)))] @@ -94,7 +120,7 @@ (defn- transform-named-style [style params style-name-var params-var] (let [[composition style] (extract-composes style) style-var (gensym "style") - style (prefix-at-media style) + style (->> style prefix-at-media rename-vars) [base-style-var name-var name-let] (build-style-naming-let style params style-name-var params-var) @@ -106,7 +132,8 @@ ~(with-composition composition name-var style-var)))) (defn- transform-keyframes-style [style params style-name-var params-var] - (let [[style-var name-var style-naming-let] (build-style-naming-let + (let [style (->> style prefix-at-media rename-vars) + [style-var name-var style-naming-let] (build-style-naming-let style params style-name-var params-var) info-map `{:css (spade.runtime/compile-css @@ -127,7 +154,7 @@ (let [style (replace-at-forms style)] (cond (#{:global} mode) - `{:css (spade.runtime/compile-css ~(vec style)) + `{:css (spade.runtime/compile-css ~(vec (rename-vars style))) :name ~style-name-var} ; keyframes are a bit of a special case From ae4637382c4bfa2c4d4b4306b56b0b9b86c5dc4d Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Tue, 24 Nov 2020 09:52:33 -0500 Subject: [PATCH 2/8] Switch to the proposed :*var* syntax --- dev/spade/demo.cljs | 4 ++-- src/spade/core.cljc | 21 +++++++++++++++++---- src/spade/runtime.cljs | 6 +++++- test/spade/core_test.cljs | 27 ++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/dev/spade/demo.cljs b/dev/spade/demo.cljs index 1412d88..e959268 100644 --- a/dev/spade/demo.cljs +++ b/dev/spade/demo.cljs @@ -12,7 +12,7 @@ ["100%" {:opacity end}]) (defglobal background - [:body {:var/my-var "22pt" + [:body {:*my-var* "22pt" :background "#333"}]) (defglobal text @@ -23,7 +23,7 @@ {:padding "80px"}) {:padding "8px"} - [:.title {:font-size :var/my-var + [:.title {:font-size :*my-var* :animation [[(parameterized-anim-frames 0 0.5) "560ms" 'ease-in-out]]}]) (defclass colorized-with-key [color] diff --git a/src/spade/core.cljc b/src/spade/core.cljc index 8d44c1d..d6560d7 100644 --- a/src/spade/core.cljc +++ b/src/spade/core.cljc @@ -1,5 +1,6 @@ (ns spade.core - (:require [clojure.walk :refer [postwalk]] + (:require [clojure.string :as str] + [clojure.walk :refer [postwalk]] [spade.util :refer [factory->name build-style-name]])) (defn- extract-key [style] @@ -34,15 +35,27 @@ element)) style)) +(defn- clean-property-name [n] + (when n + (str/replace n #"[^a-zA-Z0-9_-]" "-"))) + (defn- css-var? [element] (and (keyword? element) - (= "var" (namespace element)))) + (let [n (name element)] + (and (str/starts-with? n "*") + (str/ends-with? n "*"))))) (defn- varify-key [element] - (keyword (str "--" (name element)))) + (let [space (namespace element) + n (name element)] + (keyword (str (when space "--") + (clean-property-name (namespace element)) + "--" + (clean-property-name + (subs n 1 (dec (count n)))))))) (defn- varify-val [element] - (keyword (str "var(--" (name element) ")"))) + `(spade.runtime/->css-var ~(varify-key element))) (defn- rename-vars [style] (postwalk diff --git a/src/spade/runtime.cljs b/src/spade/runtime.cljs index 476aacd..83ef45f 100644 --- a/src/spade/runtime.cljs +++ b/src/spade/runtime.cljs @@ -1,6 +1,7 @@ (ns spade.runtime (:require [clojure.string :as str] - [garden.core :as garden])) + [garden.core :as garden] + [garden.types :refer [->CSSFunction]])) (defonce ^{:private true @@ -10,6 +11,9 @@ (defonce ^:dynamic *css-compile-flags* {:pretty-print? goog.DEBUG}) +(defn ->css-var [n] + (->CSSFunction "var" n)) + (defn compile-css [elements] (garden/css *css-compile-flags* elements)) diff --git a/test/spade/core_test.cljs b/test/spade/core_test.cljs index 661cf1b..43e24e3 100644 --- a/test/spade/core_test.cljs +++ b/test/spade/core_test.cljs @@ -3,6 +3,12 @@ [clojure.string :as str] [spade.core :refer [defattrs defclass defglobal defkeyframes]])) +; for the linter's sake: +(declare with-media-factory$ + class-with-vars-factory$ + composed-factory$ + composed-attrs-factory$) + (defclass computed-key [color] ^{:key (str/upper-case color)} {:color color}) @@ -20,6 +26,12 @@ {:background "blue"} [:.nested {:background "red"}])) +(defclass class-with-vars [] + {:*my-var* "42pt" + ::*namespaced* "blue" + :font-size :*my-var* + :background ::*namespaced*}) + (deftest defclass-test (testing "computed-key test" (is (= "spade-core-test-computed-key_BLUE" @@ -41,7 +53,20 @@ generated (str "@media (max-width: 50px) { " ".with-media { background: blue; } " - ".with-media .nested { background: red; }")))))) + ".with-media .nested { background: red; }"))))) + + (testing "CSS var declaration and usage" + (let [generated (-> (class-with-vars-factory$ "class-with-vars" []) + :css + (str/replace #"\s+" " "))] + (is (str/includes? + generated + (str ".class-with-vars {" + " --my-var: 42pt;" + " --spade-core-test--namespaced: blue;" + " font-size: var(--my-var);" + " background: var(--spade-core-test--namespaced);" + " }")))))) (defattrs fixed-style-attrs [] From f988d7eb3b393c7dea708841e6432ef419787bcf Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Tue, 24 Nov 2020 12:58:39 -0500 Subject: [PATCH 3/8] Handle css vars within forms, eg `[[:*var* :!important]]` --- src/spade/core.cljc | 11 ++++++++--- test/spade/core_test.cljs | 16 +++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/spade/core.cljc b/src/spade/core.cljc index d6560d7..03962c4 100644 --- a/src/spade/core.cljc +++ b/src/spade/core.cljc @@ -1,6 +1,6 @@ (ns spade.core (:require [clojure.string :as str] - [clojure.walk :refer [postwalk]] + [clojure.walk :refer [postwalk prewalk]] [spade.util :refer [factory->name build-style-name]])) (defn- extract-key [style] @@ -58,9 +58,10 @@ `(spade.runtime/->css-var ~(varify-key element))) (defn- rename-vars [style] - (postwalk + (prewalk (fn [element] - (if (map-entry? element) + (cond + (map-entry? element) (let [var-key? (css-var? (key element)) var-val? (css-var? (val element))] (if (or var-key? var-val?) @@ -70,6 +71,10 @@ element)) + (css-var? element) + (varify-val element) + + :else element)) style)) diff --git a/test/spade/core_test.cljs b/test/spade/core_test.cljs index 43e24e3..783adae 100644 --- a/test/spade/core_test.cljs +++ b/test/spade/core_test.cljs @@ -30,7 +30,8 @@ {:*my-var* "42pt" ::*namespaced* "blue" :font-size :*my-var* - :background ::*namespaced*}) + :background ::*namespaced* + :color [[:*my-var* :!important]]}) (deftest defclass-test (testing "computed-key test" @@ -55,7 +56,7 @@ ".with-media { background: blue; } " ".with-media .nested { background: red; }"))))) - (testing "CSS var declaration and usage" + (testing "CSS var declaration and usage" (let [generated (-> (class-with-vars-factory$ "class-with-vars" []) :css (str/replace #"\s+" " "))] @@ -66,6 +67,7 @@ " --spade-core-test--namespaced: blue;" " font-size: var(--my-var);" " background: var(--spade-core-test--namespaced);" + " color: var(--my-var) !important;" " }")))))) @@ -159,15 +161,7 @@ (is (= ["spade-core-test-computed-key_BLUE" "spade-core-test-composed-attrs_blue" "spade-core-test-compose-ception"] - (str/split (compose-ception) #" "))) - - (let [generated (:css (composed-attrs-factory$ "" ["blue"] "blue"))] - (is (false? (str/includes? generated - "color:"))) - (is (false? (str/includes? generated - "composes:"))) - (is (true? (str/includes? generated - "background:")))))) + (str/split (compose-ception) #" "))))) (defclass destructured [{:keys [c b]}] ^{:key (str c "_" b)} From 6a2bb38b11ecd0af44c44ff3d67cf0b41a98b8f7 Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Tue, 24 Nov 2020 13:08:10 -0500 Subject: [PATCH 4/8] Fill out tests for var transform across all macros --- test/spade/core_test.cljs | 43 ++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/test/spade/core_test.cljs b/test/spade/core_test.cljs index 783adae..aec7b69 100644 --- a/test/spade/core_test.cljs +++ b/test/spade/core_test.cljs @@ -6,8 +6,10 @@ ; for the linter's sake: (declare with-media-factory$ class-with-vars-factory$ + fixed-style-attrs-factory$ composed-factory$ - composed-attrs-factory$) + composed-attrs-factory$ + parameterized-key-frames-factory$) (defclass computed-key [color] ^{:key (str/upper-case color)} @@ -72,16 +74,29 @@ (defattrs fixed-style-attrs [] - {:color "blue"}) + {:*my-var* "blue" + :color :*my-var*}) (deftest defattrs-test (testing "Return map from defattrs" (is (= {:class "spade-core-test-fixed-style-attrs"} - (fixed-style-attrs))))) + (fixed-style-attrs)))) + + (testing "CSS var declaration and usage" + (let [generated (-> (fixed-style-attrs-factory$ "with-vars" []) + :css + (str/replace #"\s+" " "))] + (is (str/includes? + generated + (str ".with-vars {" + " --my-var: blue;" + " color: var(--my-var);" + " }")))))) (defglobal global-1 - [:body {:background "blue"}]) + [":root" {:*background* "blue"}] + [:body {:background :*background*}]) (defglobal global-2 (at-media {:min-width "42px"} @@ -90,7 +105,8 @@ (deftest defglobal-test (testing "Declare const var with style from global" (is (string? global-1)) - (is (= "body {\n background: blue;\n}" + (is (= (str ":root {\n --background: blue;\n}\n\n" + "body {\n background: var(--background);\n}") global-1))) (testing "Support at-media automatically" @@ -103,7 +119,8 @@ [:from {:opacity 0}]) (defkeyframes parameterized-key-frames [from] - [:from {:opacity from}]) + [:from {::*from* from + :opacity ::*from*}]) (deftest defkeyframes-test (testing "Return keyframes name from defkeyframes" @@ -114,7 +131,19 @@ (testing "Return dynamic keyframes name from parameterized defkeyframes" (is (fn? key-frames)) (is (= (str "spade-core-test-parameterized-key-frames_" (hash [0])) - (parameterized-key-frames 0))))) + (parameterized-key-frames 0)))) + + (testing "CSS var declaration and usage" + (let [generated (-> (parameterized-key-frames-factory$ + "with-vars" [42] 42) + :css + (str/replace #"\s+" " "))] + (is (str/includes? + generated + (str "from {" + " --spade-core-test--from: 42;" + " opacity: var(--spade-core-test--from);" + " }")))))) (defclass composed [color] ^{:key color} From ea803e9603fba9e1f0dfe6657571cae20742d5ec Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Tue, 24 Nov 2020 13:26:43 -0500 Subject: [PATCH 5/8] Document earmuff variables in the README --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 1e45d0f..75c2d5a 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,59 @@ appropriate CSS into the DOM on-demand, and returns the animation identifier: {:animation [[(anim-frames) "560ms" 'ease-in-out]]}) ``` +### Syntactic Sugar + +Spade also provides some extra syntactic sugar, performed at compile time +for "zero-cost" abstractions. + +#### CSS Custom Properties + +CSS custom properties (AKA variables) can be a convenient way to, for +example, define dynamic theme colors once and reuse them throughout the +codebase. Spade provides some extra sugar to make using them feel more +idiomatic: + +```clojure +(defglobal light-dark + (at-media {:prefers-color-scheme 'dark} + [":root" {:theme/*background* "#000" + :theme/*text* "#E0EBFF"}]) + [":root" {:theme/*background* "#fff" + :theme/*text* "#000"}]) + +(defclass page [] + {:background :theme/*background* + :color :theme/*text*}) +``` + +Notice how our declaration and usage sites are identical, and that +they're "just" normal keywords. However, by using `*earmuffs*` around +the name of the keyword, Spade knows that it is meant to be a variable, +and applies the correct CSS styling based on the position within the +style. Normal keyword namespace semantics apply, so you can expect that +`:theme/*text*` and `:crew.quarters/*text*` will result in distinct +variables. + +The above example will generate the following CSS: + +```css +@media (prefers-color-scheme: dark) { + :root { + --theme--background: #000; + --theme--text: #E0EBFF; + } +} + +:root { + --theme--background: #fff; + --theme--text: #000; +} + +.page { + background: var(--theme--background); + color: var(--theme--text); +} +``` ## Development From 504c7d297ec385de3e3d64be9bfb659065c487f5 Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Tue, 24 Nov 2020 13:30:30 -0500 Subject: [PATCH 6/8] Add a bit more info to the CSS vars documentation --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 75c2d5a..b1a73ff 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ for "zero-cost" abstractions. CSS custom properties (AKA variables) can be a convenient way to, for example, define dynamic theme colors once and reuse them throughout the -codebase. Spade provides some extra sugar to make using them feel more -idiomatic: +codebase. Spade provides some extra sugar to make using them easier and +more idiomatic: ```clojure (defglobal light-dark @@ -176,9 +176,15 @@ Notice how our declaration and usage sites are identical, and that they're "just" normal keywords. However, by using `*earmuffs*` around the name of the keyword, Spade knows that it is meant to be a variable, and applies the correct CSS styling based on the position within the -style. Normal keyword namespace semantics apply, so you can expect that +style. This naming was chosen because it is reminiscent of the naming +of dynamic Clojure vars, and because `*` is not valid in a CSS property +name, so the meaning is unambiguous. + +Normal keyword namespace semantics apply, so you can expect that `:theme/*text*` and `:crew.quarters/*text*` will result in distinct -variables. +variables. In fact, you don't need any namespace at all; `:*text*` will +also result in a perfectly valid CSS variable, disinct from any of the +other two mentioned above. The above example will generate the following CSS: From 11f9af4f20fc11c5568f8ace5552bdcd3f1b8d3f Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Sat, 20 Mar 2021 10:39:01 -0400 Subject: [PATCH 7/8] Explicitly call out CSS Custom Property browser support --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b1a73ff..0812432 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,8 @@ The above example will generate the following CSS: } ``` +Note that [CSS Custom Properties are not supported on all browsers][4], and this syntax compiles to that feature directly without any attempt at backwards compatibility—if CSS Custom Properties are not supported on a browser you are targetting, this syntax will also not be supported. + ## Development To get an interactive development environment run: @@ -231,3 +233,4 @@ Distributed under the Eclipse Public License either version 1.0 [1]: https://github.com/css-modules/css-modules [2]: https://github.com/noprompt/garden/ [3]: https://github.com/roosta/herb +[4]: https://caniuse.com/css-variables From 9e29689d2dcb03df29c9c37e45d75efd47b4d9fc Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Sat, 20 Mar 2021 10:44:42 -0400 Subject: [PATCH 8/8] Restore deleted test cases Don't remember why I removed these, but the tests do pass --- test/spade/core_test.cljs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/spade/core_test.cljs b/test/spade/core_test.cljs index aec7b69..eee3347 100644 --- a/test/spade/core_test.cljs +++ b/test/spade/core_test.cljs @@ -190,7 +190,15 @@ (is (= ["spade-core-test-computed-key_BLUE" "spade-core-test-composed-attrs_blue" "spade-core-test-compose-ception"] - (str/split (compose-ception) #" "))))) + (str/split (compose-ception) #" "))) + + (let [generated (:css (composed-attrs-factory$ "" ["blue"] "blue"))] + (is (false? (str/includes? generated + "color:"))) + (is (false? (str/includes? generated + "composes:"))) + (is (true? (str/includes? generated + "background:")))))) (defclass destructured [{:keys [c b]}] ^{:key (str c "_" b)}