├── .gitignore
├── LICENSE
├── README.md
├── deps.edn
├── doc
└── css.md
├── project.clj
└── src
├── main
└── shadow
│ ├── css.clj
│ ├── css.cljs
│ └── css
│ ├── aliases.edn
│ ├── analyzer.cljc
│ ├── build.cljc
│ ├── colors.edn
│ ├── preflight.css
│ └── specs.cljc
└── test
└── shadow
└── css
└── analyzer_test.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | *.iml
5 | profiles.clj
6 | pom.xml
7 | pom.xml.asc
8 | *.jar
9 | *.class
10 | /.cpcache
11 | /.lein-*
12 | /.nrepl-port
13 | /.prepl-port
14 | /.idea
15 | .hgignore
16 | .hg/
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of New York and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # shadow-css
2 |
3 | [](https://clojars.org/com.thheller/shadow-css)
4 |
5 | CSS-in-Clojure(Script). `shadow.css` is essentially a mini DSL for writing CSS directly in Clojure(Script) Code, allowing you to directly write CSS where it is used.
6 |
7 | Play with it live directly in your browser via the [shadow-grove Playground](https://code.thheller.com/shadow-grove-playground/0.4.0/).
8 |
9 | Jump to:
10 | - [Using the CSS macro](#usage)
11 | - [Building CSS](#build)
12 | - [Known Limitations / Trade-Offs](#limits)
13 |
14 | ## Status
15 |
16 | The syntax for defining the CSS is **stable and pretty much final**, same goes for the `css` macro. I won't rule out potential additions, but what is here now will very likely stay as it is forever.
17 |
18 | The build side is rough. It works perfectly fine, but the developer experience could be better.
19 |
20 | If you want to see actual projects using this you may look at the [shadow-cljs UI](https://github.com/thheller/shadow-cljs/tree/master/src/main/shadow/cljs/ui) sources. Just search for `(css`.
21 |
22 | ## Rationale
23 |
24 | Writing and maintaining CSS is a burden. Editing CSS in actual `.css` files means you have to come up with names for everything so the HTML can actually reference the styles. Coming up with the names in the first place is hard, and maintaining them over time is even harder. Many naming conventions (eg. [BEM](http://getbem.com/)) exist, which helps shrink the problem but does not eliminate it. Writing actual CSS also requires constantly context switching since the syntax is much different from Clojure.
25 |
26 | Nowadays, [tailwindcss](https://tailwindcss.com/) has become very popular alternative, which sort of flips the naming problem. Instead, you have a lot of predefined aliases for commonly used CSS properties and use those to style elements. This works great and using Tailwind in CLJ(S) projects actually works quite well. However, it does require installing JS tools and running them. IMHO Tailwind also went a bit [overboard with some things](https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values), which it kinda had to because it is limited to what can be actual CSS classnames inside a `class` HTML attribute. `shadow-css` is much more flexible, so most of that is not required or supported.
27 |
28 | ## Goals
29 |
30 | I wanted something that ...
31 |
32 | - **has close to zero (or actually zero) runtime impact and code size (for frontend CLJS builds)**
33 | - gives me the expressive power of Tailwind aliases, while still giving me full access to all of CSS
34 | - stays entirely in the CLJ(S) space with no outside dependencies (`node` not required)
35 | - is usable in all CLJ(S) projects and libraries
36 | - is completely framework-agnostic, with no expectations for how the HTML/DOM is actually generated
37 | - integrates seamlessly with CSS written by other means
38 |
39 | It is not a goal to hide or abstract CSS in any way other than a slightly friendlier syntax and developer experience.
40 |
41 | It is also not a goal to express every single thing CSS can potentially do. Sometimes, it is easier to just write some CSS (eg. [@keyframes](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes)). The build tooling makes it easy to include basic CSS directly.
42 |
43 |
44 | # Using the CSS macro
45 |
46 | *Knowledge of [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) is required to make anything meaningful with this. No CSS explanation is done here. There are no predefined "components".*
47 |
48 | The basis for everything is the `shadow.css/css` macro. It accepts a subset of Clojure to define the CSS and returns a classname for use in place of HTML `class` attribute
49 |
50 | ```clojure
51 | (ns my.app
52 | (:require [shadow.css :refer (css)]))
53 |
54 | (defn hiccup-example []
55 | [:div {:class (css :px-4 :shadow {:color "green"})}
56 | "Hello World"])
57 | ```
58 |
59 | This will generate HTML like `
Hello World
`. The generated classname is derived from the location used in the code, but may be optimized later by the build tools. The generated name is of no concern when writing the code, you can ignore it entirely. This eliminates the naming problem. Moving or deleting the `(css ...)` rule also means the CSS is updated accordingly.
60 |
61 | The generated CSS for the above will look something like this.
62 |
63 | ```css
64 | .my_app__L5C16 {
65 | box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
66 | color: green;
67 | padding-left: 1rem;
68 | padding-right: 1rem;
69 | }
70 | ```
71 |
72 | Note that this is subject to change once optimizations are applied. The nice thing is that is something that will just happen automatically, and is not something you need to worry about yourself.
73 |
74 | ### Naming sometimes useful
75 |
76 | Naming things is sometimes useful. Instead of naming the CSS class, we just use Clojure for that. So, for example we can just use `let` to define local bindings. Each name declared that way is just referencing a string holding the generated classname.
77 |
78 | ```clojure
79 | (defn hiccup-example [key val]
80 | (let [$row (css ...)
81 | $key (css ...)
82 | $val (css ...)]
83 | [:div {:class $row}
84 | [:div {:class $key} key]
85 | [:div {:class $val} val]]))
86 | ```
87 |
88 | `(def $my-button (css ...))` of course also works if you want to share styles in multiple places.
89 |
90 | I personally prefer prefixing css classnames with `$` in names, just to distinguish them easily in the code. This is of course entirely optional, it is just a regular CLJ(S) symbol with no special meaning otherwise.
91 |
92 | ## CSS Syntax
93 |
94 | As mentioned earlier the `css` macro only accepts a subset of the Clojure. It must be entirely static and cannot take any dynamic code. Symbols or lists used inside `css` will result in an error. You'd have the same limitation when using actual CSS files, so you lost nothing.
95 |
96 | The syntax is limited to the following, and each element is merged in order from left to right. Later values will override earlier values, in case they define the same properties.
97 |
98 | ### Keywords
99 |
100 | ```clojure
101 | (css :px-4 :shadow :flex)
102 | ```
103 |
104 | Keywords represent aliases. For the most part they are identical to tailwind aliases, you can refer to the excellent tailwind documentation (eg. [px-4](https://tailwindcss.com/docs/padding)). They are just shortcuts for common CSS property values. When the CSS is generated each alias is replaced with its value. `:px-4` is just short for `{:padding-left 4 :padding-right 4}`. There are thousands of pre-defined aliases, and you may define your own. They are also entirely optional, you can just use maps if you want.
105 |
106 | ### Maps
107 |
108 | ```clojure
109 | (css :px-4 {:color "green"})
110 | ```
111 |
112 | Each key in a map must be a keyword, which maps directly to a CSS property name. Their name is just passed though as-is and no conversion is done. Conveniently, CSS uses the same name notation and everything maps to idiomatic Clojure names naturally (eg. `:padding-left` is `padding-left` in CSS).
113 |
114 | The value for each key can be
115 | - A string which is used as-is. The example above just generates `color: green;` in the CSS. They are not converted in any way.
116 | - Numbers are converted with some basic rules, these rules are shared with the aliases. `(css :p-4)` is identical to `(css {:padding 4})`, which both end up as `padding: 1rem;` in the CSS. They map exactly to the default [spacing scale used by tailwind](https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale). Note that most numbers in CSS require a unit, so you use Strings in their place. `:padding 4` does not mean `4px`. You'd use `:padding "4px"` for that.
117 | - Keywords are again aliases. `(css {:color :primary-color})` allows you to define a `:primary-color` alias when building the CSS, instead of hardcoding the value in the code.
118 |
119 | ### Strings
120 |
121 | ```clojure
122 | (css "my-class" :px-4)
123 | ```
124 |
125 | Strings are used as pass-through values and will not affect the CSS generated by `shadow.css`. It will however affect the string generated by the `css` macro. This is a useful shortcut if you want to integrate with other existing CSS.
126 |
127 | ```clojure
128 | [:div {:class (css "foo bar" :px-4)} "Hello!"]
129 | ;; same as
130 | [:div {:class (str "foo bar " (css :px-4))} "Hello!"]
131 | ```
132 | generates
133 | ```
134 | Hello!
135 | ```
136 |
137 | ### Vectors
138 |
139 | ```clojure
140 | (css
141 | {:color "green"}
142 | [:hover {:color "red"}])
143 | ```
144 |
145 | Vectors represent sub-selectors and can be used to target pseudo-elements, nested elements or media queries.
146 |
147 | The first element is either string or a keyword, which again is a pre-defined aliases that will be replaced by its value when building. A String must either start with a `@` when representing a media query or contain a `&`, which will refer back to the actual generated classname.
148 |
149 | `:hover` in the above maps to `"&:hover"` which in the final CSS would be `.my_app__LxCx:hover { ... }`.
150 |
151 | Media Queries are just passed through and the emitted CSS rules are grouped accordingly.
152 |
153 | ```clojure
154 | (css :px-4 ["@media (min-width: 1024px)" :px-8])
155 | ;; or using predefined alias
156 | (css :px-4 [:lg :px-8])
157 | ```
158 |
159 | Writing those repeatedly would get annoying, so the default [tailwind responsive breakpoints](https://tailwindcss.com/docs/responsive-design) are provided as aliases by default. Which of course can be overridden or extended via custom aliases.
160 |
161 | Each element in the vector following the first will be part of that sub-selector and only accepts keyword aliases, maps or other vectors. Pass-through Strings are not allowed here.
162 |
163 | ## Dynamic Uses
164 |
165 | By design the `css` macro is very static and does not accept any dynamic values. This does not mean we can't do dynamic things. We are still just writing Clojure code, we can just use other means to gain back all dynamic things we may need.
166 |
167 | There are many options to choose from and all resemble exactly what you'd be doing with regular CSS. You could just replace all `(css ...)` here with a `"my-class"` referencing CSS defined in actual CSS files, and the code would otherwise stay the same.
168 |
169 | ```clojure
170 | ;; assigning different classnames based on arguments
171 | (defn ui-example [selected?]
172 | (let [$selected (css ...)
173 | $regular (css ...)]
174 | [:div {:class (if selected? $selected $regular)}
175 | ...]))
176 |
177 | ;; adding additional rules based on argument
178 | (defn ui-example [selected?]
179 | (let [$base (css ...)
180 | $selected (css ...)]
181 | [:div {:class (str $base (when selected? (str " " $selected)))}
182 | ...]))
183 |
184 | ;; depending on what you use to generated html that may already have
185 | ;; convenience helpers for you to make it a little less verbose
186 | ;; remember that all you are working with here is a string
187 | (defn ui-example [selected?]
188 | (let [$base (css ...)
189 | $selected (css ...)]
190 | [:div {:class [$base (when selected? $selected)]}
191 | ...]))
192 |
193 | ;; using a sub-selector
194 | ;; &.selected here targets elements having BOTH the selected and & classes
195 | ;; remember that & here is replaced with the actual classname the css macro
196 | ;; generates. So it is identical to a .foo.selected {} selector in CSS
197 | (def $thing
198 | (css
199 | ...
200 | ["&.selected"
201 | ...]))
202 |
203 | (defn ui-example [selected?]
204 | ;; so the code just conditionally assigns the two classnames
205 | [:div {:class (str $thing (when selected? " selected"))}
206 | ...])
207 |
208 | ;; using style attributes
209 | (defn ui-example [selected?]
210 | [:div {:class (css ...)
211 | :style {:color (if selected? "red" "green")}}
212 | ...])
213 | ```
214 |
215 | You can also use [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) of course. I won't go into this here further since that would blow the scope of this document.
216 |
217 | I think all dynamic uses are covered just fine and the `css` macro itself doesn't need to do any of it.
218 |
219 | ## Custom CSS Includes
220 |
221 | There are situations where you may want to write some old-school manual CSS and include it in the generated output. This can be done via the `:shadow.css/include` metadata in a `ns` that is part of the build.
222 |
223 | ```clojure
224 | (ns my.app
225 | {:shadow.css/include ["my/app.css"]}
226 | (:require ...))
227 | ```
228 |
229 | The shadow-cljs UI does this [here](https://github.com/thheller/shadow-cljs/blob/531b64f193f6bd26747907aa87c48209c31d9c90/src/main/shadow/cljs/ui/main.cljs#L3) to include the [main.css](https://github.com/thheller/shadow-cljs/tree/531b64f193f6bd26747907aa87c48209c31d9c90/src/main/shadow/cljs/ui) in the same directory as the `main.cljs` file.
230 |
231 | The value of `:shadow.css/include` should be a vector of [classpath resource paths](https://code.thheller.com/blog/shadow-cljs/2021/05/13/paths-paths-paths.html). The build process will just include the files as they are. They will be minified, if the build is minified, but no other processing is done.
232 |
233 |
234 | # Building CSS
235 |
236 | All of this is subject to change. Currently, the only way to build the CSS is by writing a Clojure function.
237 |
238 | I'd suggest using a new namespace since the `shadow.css.build` namespace is not needed at runtime and should not be required by any of your actual application namespaces. I'm generally using `build` in a `src/dev/build.clj` file.
239 |
240 | ```clojure
241 | (ns build
242 | (:require
243 | [shadow.css.build :as cb]
244 | [clojure.java.io :as io]))
245 |
246 | (defn css-release [& args]
247 | (let [build-state
248 | (-> (cb/start)
249 | (cb/index-path (io/file "src" "main") {})
250 | (cb/generate
251 | '{:ui
252 | {:entries [my.app]}})
253 | (cb/minify)
254 | (cb/write-outputs-to (io/file "public" "css")))]
255 |
256 | (doseq [mod (vals (:chunks build-state))
257 | {:keys [warning-type] :as warning} (:warnings mod)]
258 | (prn [:CSS (name warning-type) (dissoc warning :warning-type)]))))
259 | ```
260 |
261 | Here is a breakdown of what all this does:
262 |
263 | - `(cb/start)` creates the initial build state and creates all the default aliases. It returns a map representing the build state. Basically each subsequent steps modifies this map or generates .css files from it.
264 |
265 | - `(cb/index-path (io/file "src" "main"))` finds all `.clj`, `.cljs`, `.cljc` files in the specified `src/main` directory and extracts `(css ...)` forms from it. You can call this multiple times if you have additional directories that should be indexed.
266 |
267 | ### Build Structure
268 |
269 | The `cb/generate` configures the basic build structure. CSS output is configured in "chunks". Basically think of the entire build as producing one `.css` file. It may sometimes be more desirable to split that file into smaller chunks when needed. This example only has one, here the `:ui` chunk will later create the `ui.css` file. Don't worry about splitting too much for now, it is often not necessary.
270 |
271 |
272 | ### Chunk :entries
273 |
274 | The generator needs to know which CSS should go into which chunk. For this `shadow-css` uses the namespace structure from your code. The `:entries [my.app]` option means, that it will take the `my.app` namespace, include all CSS found in the file for that namespace, and then also follow all the required namespaces from `my.app` to also include those and follow them as well. Assuming `my.app` actually required all your namespaces, which it will for most projects, either directly or indirectly, then this would be enough to create CSS for all your files. You may add additional namespaces into the `:entries` vector when needed.
275 |
276 | ### Chunk :include
277 | You may also use the `:include [my.app*]` instead, or in addition to `:entries`. This will end up including all namespaces with the `my.app` prefix, but does not follow any other required namespaces. You may also add multiple patterns into this vector. A `*` at the end is used for a wildcard prefix match, the `*` may only be at the end for now. You may also just put a regular namespace here like `:include [my.app]`, which will only include the CSS for that namespace and nothing else.
278 |
279 |
280 | ### Next steps
281 |
282 | - `cb/minify` strips all comments and whitespace from the generated outputs.
283 | - `cb/write-outputs-to` writes the actual `.css` files to the supplied dir. For the `:ui` chunk it generates a `public/css/ui.css` in this case.
284 | - The `doseq` is for printing warnings (e.g. missing aliases). A bit rough but works for now.
285 |
286 | At this point the build is done, files were written to disk and the build state can be discarded.
287 |
288 | ### Custom CSS aliases
289 |
290 | Custom aliases or other things can be added before `generate` is called. Adding or overriding an alias is just `(assoc-in build-state [:aliases :px-4] {:color "green"})`.
291 |
292 | The default available aliases can be found [in this file](https://github.com/thheller/shadow-css/blob/main/src/main/shadow/css/aliases.edn).
293 |
294 | The `build-state` threaded through the `->` form is just a map, which contains everything interesting about the build. Feel free to explore, e.g. via `tap>` in the shadow-cljs Inspect UI. It can become fairly large, but it is pretty self-explanatory.
295 |
296 | ## Running the actual CSS build
297 |
298 | `css-release` ultimately is just a regular Clojure function. You may run this from the REPL, `lein run -m build/css-release`, `shadow-cljs run build/css-release` or `clj -X build/css-release`.
299 |
300 | ## Development Setup
301 |
302 | The above works totally fine, but is a bit manual. During development I prefer to just have something watching my source files and automatically rebuilding my CSS on change. This coupled with the hot-reload for CSS provided by `shadow-cljs` (or `figwheel`) makes for a very nice workflow.
303 |
304 | I use this basic construct to integrate the CSS building into my regular REPL workflow. This is using the `fs-watch` utility provided by `shadow-cljs`, but any file watcher will do. Since I use shadow-cljs anyway, I just start my work by using the [run feature](https://shadow-cljs.github.io/docs/UsersGuide.html#clj-run):
305 |
306 | ```
307 | npx shadow-cljs run repl/start
308 | ```
309 |
310 | That'll start `shadow-cljs` and CSS building starts with it. You can start CLJS builds from the `start` function as shown, so this basically replaces the normal `npx shadow-cljs watch your-build`
311 |
312 | ```clojure
313 | (ns repl
314 | (:require
315 | [clojure.java.io :as io]
316 | [build]
317 | [shadow.cljs.devtools.api :as shadow]
318 | [shadow.cljs.devtools.server.fs-watch :as fs-watch]))
319 |
320 | (defonce css-watch-ref (atom nil))
321 |
322 | (defn start
323 | {:shadow/requires-server true}
324 | []
325 |
326 | ;; this is optional
327 | ;; if using shadow-cljs you can start a watch from here
328 | ;; same as running `shadow-cljs watch your-build` from the command line
329 | (shadow/watch :your-build)
330 |
331 | ;; build css once on start
332 | (build/css-release)
333 |
334 | ;; then setup the watcher that rebuilds everything on change
335 | (reset! css-watch-ref
336 | (fs-watch/start
337 | {}
338 | [(io/file "src" "main")]
339 | ["cljs" "cljc" "clj"]
340 | (fn [_]
341 | (try
342 | (build/css-release)
343 | (catch Exception e
344 | (prn [:css-failed e]))))))
345 |
346 | ::started)
347 |
348 | (defn stop []
349 | (when-some [css-watch @css-watch-ref]
350 | (fs-watch/stop css-watch))
351 |
352 | ::stopped)
353 |
354 | (defn go []
355 | (stop)
356 | (start))
357 | ```
358 |
359 | If you are used to the common CLJ REPL workflow setups this should fit right in. You can modify this to fit your needs.
360 |
361 | It is possible to set up the development builds in a more efficient way. For me this has not been necessary given that most builds complete within ~100ms. If your development builds are becoming slow let me know, I can provide a more detailed guide on how to potentially make it faster.
362 |
363 |
364 | # Known Limitations / Trade-Offs
365 |
366 | ## Why so static?
367 |
368 | Doing anything more dynamic would require doing things at runtime.
369 |
370 | This violates the first stated goal, since it would require a substantial amount of code to actually generate the CSS at runtime. Instead, all CSS is generated by parsing and extracting `(css ...)` forms from actual source code files on disk. It becomes part of you build process, and you are left with generated ready to use `.css` files.
371 |
372 | This also means it is easy to integrate into existing CSS build systems. No need to throw away all your existing styles.
373 |
374 | It suffers none of the known issues that other systems, which build CSS at runtime, have. You are paying for the generation cost at build time. Instead of your users paying it every time they open your webpage. Leading to substantially better performance overall.
375 |
376 | ## REPL
377 |
378 | I don't think this is actually a problem, but might be for some REPL-heavy workflows.
379 |
380 | All CSS is built from actual files on disk. It cannot possibly see what you do at the REPL. Remember this does almost nothing at runtime. Everything you write and load from disk works just fine (eg. `require` and `load-file`) but evaluating individual forms may become a problem. You may of course evaluate `(css ...)` forms at the REPL, they'll however be somewhat useless since no actual CSS is generated. Not that any REPL needs CSS anyway.
381 |
382 | ```
383 | (require '[shadow.css :refer (css)])
384 | => nil
385 | (css :px-4)
386 | => "user__L1_C1"
387 | ```
388 |
389 | The problem shows itself when you put something in a file but then redefine it at the REPL. Suppose, somewhere you have:
390 |
391 | ```clojure
392 | (defn ui-component []
393 | (html [:div {:class (css :px-4)} "Hello World"]))
394 | ```
395 |
396 | change something and re-define it at the REPL, without saving the file to disk. The location of the `(css` form may have changed and therefore generated classname no longer matches. When the HTML is loaded it'll point to a classname that does not exist and the element may appear unstyled.
397 |
398 | Again, I don't believe this to be a problem, and is a trade-off I'm willing to make either way.
399 |
400 |
401 | ## Static Code Analysis
402 |
403 | The current build tooling just looks for `(css ...)` forms in source files. It just assumes that you have a `:refer (css)` and that all `(css ...)` uses are actual CSS forms it should process. It could be a little smarter and respect qualified uses or other aliases, but doesn't as of now.
404 |
405 | Since the code is not evaluated during builds it also cannot find any CSS generated by other macros. There are ways those macros could provide the necessary data in theory, but it cannot be inferred automatically by the current tooling.
406 |
407 | I also don't believe this to be a problem, but I'm eager to see someone come up with something shorter/friendlier than the current `(css ...)` forms.
408 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths
2 | ["src/main"]
3 |
4 | :deps
5 | {org.clojure/tools.reader {:mvn/version "1.3.6"}}
6 |
7 | :aliases
8 | {:dev
9 | {:extra-deps {org.clojure/clojure {:mvn/version "1.11.1"}
10 | org.clojure/clojurescript {:mvn/version "1.11.60"}}
11 | :extra-paths ["src/dev" "src/test"]}}}
12 |
--------------------------------------------------------------------------------
/doc/css.md:
--------------------------------------------------------------------------------
1 | # shadow.css - rough design draft
2 |
3 | Trying to build a solution for CSS-in-CLJ(S).
4 |
5 | Using [tailwindcss](https://www.tailwindcss.com) style aliases to reduce user typing and improving consistency throughout styles. However, tailwind is rather constrained by the fact that it needs to fit in a `class` attribute string. We don't want that limitation, as sometimes it is more expressive to write actual CSS.
6 |
7 | Writing actual CSS however requires context switching and jumping between files, we instead want styles to be defined where they are used. Directly in the namespaces, alongside other code.
8 |
9 | Other CSS-in-X solution often require naming too many things which can get annoying, especially for utility layout elements. Names here are entirely optional and are covered by other CLJ constructs (eg. `def`, `let`, etc) instead.
10 |
11 | ## Requirements
12 |
13 | - must be usable in CLJ, CLJS and any other dialect (for now focusing on CLJ+CLJS)
14 | - must be able to combine styles from all .clj, .cljc, .cljs, .cljd sources
15 | - each platform must be aware of other platforms styles (CLJ<->CLJS), if they may end up on the same context (eg. webpage)
16 | - must be usable in libraries
17 | - must have an option to generate zero client side JS code (and should do so by default)
18 | - should emit well-structured CSS
19 | - should be statically analyzable (better tool support)
20 | - should be combinable with other methods (post-compilation)
21 | - should be possible to DCE
22 |
23 | # Syntax
24 |
25 | Abstract:
26 | ```
27 | (css )
28 |
29 | =
30 |
31 | |
32 | |
33 | |
34 |
35 | =
36 |
37 | |
38 | |
39 |
40 | = keyword?
41 | = string?
42 | = map-of keyword?
43 | = string? | number? |
44 | ]
45 | = |
46 | = string including & | css media query starting with @
47 | ```
48 |
49 | In Clojure terms, everything is done via the provided `css` macro.
50 |
51 | ```clojure
52 | ;; keywords represent pre-defined aliases
53 | ;; maps are literal CSS definitions
54 | (css :px-4 {:color "red"})
55 |
56 | ;; all the macro generates is a classname. the macro does not generate any css itself
57 | "some_ns__Lx_Cx"
58 | ;; optionally, optimized combination of util classes (with minified names) may be generated instead
59 | "px-4 aA"
60 |
61 | ;; strings are passed through as a convenience when using other CSS techs (eg. tailwind)
62 | (css "px-4 my-2" {:background "#123"})
63 | ;; basically just a shorter version of
64 | (str "px-4 my-2 " (css {:background "#123"}))
65 |
66 | ;; both yield
67 | "px-4 my-2 some_ns__Lx_Cx"
68 |
69 | ;; map keys as keywords represent literal CSS rule keys, they are not modified in any way
70 | ;; map values are string, numbers, or aliases
71 | ;; strings are passed through as is
72 | ;; numbers are translated using the same numbering scheme the aliases use
73 | ;; there are exceptions where numbers are just used as is, eg {:flex 1}
74 | ;; if px or other specific units are required they should be expressed as a string
75 | (css :px-4)
76 | ;; just short for
77 | (css {:padding-left 4 :padding-right 4})
78 |
79 | ;; sub-selectors can be used to target nested elements or pseudo-classes
80 | ;; & is replaced with the actual generated classname and must be present
81 | (css ["&:hover" {:color "green"}])
82 |
83 | ;; except for strings starting with @ representing media queries
84 | (css ["@media (min-width: 1024px)" :px-8])
85 |
86 | ;; aliases are also allowed in the selector place for commonly used selectors
87 | (css :px-4 [:lg :px-8])
88 |
89 | ;; if using an alias it must resolve to a string, otherwise yields an invalid style
90 | (css :px-4 [:px-8]) ;; invalid
91 |
92 | ;; sub-selectors may be nested
93 | (css
94 | :px-4
95 | ["&:hover" {:color "red"}]
96 | [:lg
97 | :px-8
98 | ["&:hover" {:color "green"}]])
99 | ```
100 |
101 | All rules are combined sequentially per selector, later values may override previous ones.
102 |
103 | Symbols are not used. They might confuse tools or users as to what they may resolve to, when in fact no resolving is ever done. No other forms are valid. `css` definitions are entirely static, no local code may influence them. Aliases however provide a way to customize what CSS is actually generated.
104 |
105 | The intent here is to have something in the code that can be extracted just by parsing the source, without actually eval-ing anything. This is necessary because often styles need to be generated and served before the actual page HTML. Generating styles during evaluation is too late, but should be an option for more dynamic uses.
106 |
107 | # Library Use
108 |
109 | A primary goal of this is making it usable in libraries, for any platform.
110 |
111 | Traditionally libraries will just bundle their own required CSS, which often leads to unnecessary duplication or just makes things harder to optimize. With alias keywords it also becomes much easier to customize library css rules. CSS Variables can also be used of course, but aliases offer even more flexibility.
112 |
113 | Instead, libraries will contain a resource file that should contain all their CSS definitions as their raw CLJ form. These resource files will be generated by the shadow.css tooling and loaded when the actual final .css file is generated in projects. Only styles from referenced namespaces will be included and everything can be optimized together if needed. The resource files reduce the need for additional processing of sources. Discovery of files in Jars can end up very expensive, and is wasted work for libraries that don't actually contain any CSS.
114 |
115 | # Cross Platform Use
116 |
117 | CSS is generated as a build step, not at runtime.
118 |
119 | Often there will be some CLJ server side code generating HTML+CSS, and then some additional CLJS client side code doing the same. The styles need to be generated before either is processed, so they can be served up as a regular (and cacheable) `.css` files. Dynamic generation at runtime should be possible, but not the default as it is much less efficient.
120 |
121 | For CLJS `:advanced` compilation may help eliminate unused rules and purge them from the generated .css file.
122 |
123 | CSS rules from namespaces that aren't referenced must not be included. References may come from source code or build configs.
124 |
125 | # Integration with other Systems
126 |
127 | It is entirely fine to use the shadow.css output and feeding it into another CSS tool pipeline. Some library may just want to use some CSS but without `(css ...)`. ns metadata or build configuration can be used to instruct the shadow.css tools to include CSS from other sources.
128 |
129 | ```clojure
130 | (ns some.library
131 | {:shadow.css/include
132 | ["some/library/static.css"]} ;; referencing other resources on the classpath
133 | ...)
134 | ```
135 |
136 | These references however are not processed in any way, and are just included in the final output unmodified. There should be no assumption that these will be processed by some other specific tool again.
137 |
138 | # Problems
139 |
140 | ## Static Analysis
141 |
142 | The files are just parsed and not eval'd, so there can never be macros that can expand to include `(css ...)` forms. Location data would also be a problem for those. Not sure how much this will come back to haunt the whole thing. For now this is a trade-off I'm willing to try and see how it scales.
143 |
144 | In theory the macro expansion itself could store information about what it did as a side effect somewhere. That data could then be used to generate the CSS. This data will become much harder to collect, but would be a little more flexible since it would allow macros to emit css rules.
145 |
146 | ## REPL, actually just the Read part
147 |
148 | One problem with analyzing the files statically and not at runtime is the REPL. We absolutely need the accurate source location (ns + line + column), since that is the css-id the runtime macro will generate.
149 |
150 | `load-file` and `require` work fine since they parse the whole file on disk and have the correct locations.
151 |
152 | Evaluating a single form however is a problem since the location data changes. `ns` is likely still accurate but line/column may be different. Either due to the editor not providing it, or being unable to. The reading usually starts at line 1 column 1 which does not match the actual file location and as such ends up generating the wrong css-id used for lookups later.
153 |
154 | The problem will be gone if tooling/repl impls fix that. Until then forms using `css` cannot be evaluated in the REPL and still provide accurate CSS. This is probably fine for most things since CSS is never needed at the REPL directly.
155 |
156 | ## Build Timing
157 |
158 | Another issue is the problem of timing. This is not an issue for release builds since everything is just a part of the build step long before the code actually runs.
159 |
160 | In development however the code often runs continuously and is eval'd on the fly via the REPL or hot-reload. Saving a file means the CSS processor needs to find that change and emit the proper updated CSS, and then the code needs to update. For CLJ these can just run side by side watches since the time the CSS is loaded counts, not the time the HTML is generated. For CLJS more integration is needed.
161 |
162 | ## Self-Hosting
163 |
164 | I don't have a clue how this would work in a self-hosted CLJS setup. Not sure there enough necessary hooks to get at the code at the correct time. Not sure how it would get the library indexes to it can generate CSS for those. Making the CSS compiler self-hostable shouldn't be an issue though.
165 |
166 | # Possible Optimizations
167 |
168 | As of now each `css` will end up generating one classname, using the namespace and line/column information for the naming purposes. However, as mentioned earlier this doesn't need to be so. In addition to the `.css` file a lookup table may be generated. That way the class that ends up getting used in the code can be overridden. For CLJ that can be used at runtime. For CLJS this would need to be done at build time as it otherwise doesn't benefit from DCE.
169 |
170 | Also, there may be multiple places in a codebase that have `(css :px-4)` and there is absolutely no need to have a specific class for each. So, one optimization is just emitting utility classnames ala tailwind. There'll also be common combinations that may also generate their own utility.
171 |
172 | Instead of the long verbose names it could also generate short names, similar to what `:advanced` produces. For development however these long names are actually useful since they tell you exactly where they were defined just by looking at the name.
173 |
174 | Testing should also be done if shortening is even necessary. GZIP is very good at optimizating repetition after all.
175 |
176 | Might be useful to let use supply a list of prefixes that should be stripped. So instead of `shadow_cljs_ui_components_common_LxCx` you get `common_LxCx` with the rest stripped?
177 |
178 |
179 | # Observations
180 |
181 | Observations made while porting the shadow-cljs UI from tailwind to purely shadow.css.
182 |
183 | Sometimes becomes verbose
184 | ```clojure
185 | [:div.px-2]
186 | [:div {:class "px-2"}]
187 | ;; becomes
188 | [:div {:class (css :px-2)}]
189 | ```
190 |
191 | Not bad compared to the `:class` variant which I prefer, so not a big deal overall. Could be alleviated by making the "indexer" extensible and able to collect other forms. As long as the picked id matches the id the macro will generate any form can be collected.
192 |
193 | ## CSS Size
194 |
195 | Prior with tailwind the generated CSS was GZIP 6.0KB (normal 23.4KB) including all the minification tailwind does.
196 |
197 | The totally not-optimized-or-minified-still-includes-comments CSS from shadow.css gets up to GZIP 8.2 KB (normal 44.1 KB). So not at all bad. GZIP shines here, so the long classnames really don't seem to matter all that much. I expect that optimizations and duplicate removal will bring this at least to tailwind level, potentially smaller.
198 |
199 | Running the generated CSS through an online CSS minifier shrinks everything to GZIP 5.4KB (normal 26.9 KB), which is then already smaller than Tailwind without even trying to minify classnames, removing duplicates or emitting all media-query'd rules grouped. Seems very promising.
200 |
201 | ## Build Tooling
202 |
203 | Build tooling is rough, but I get warnings for undefined aliases and noticed that the code I had ported had some classes that aren't defined in tailwind and did nothing. Tailwind never warned me about those.
204 |
205 | ### Includes are nice
206 |
207 | ```clojure
208 | (ns shadow.cljs.ui.components.code-editor
209 | {:shadow.css/include ["shadow/cljs/ui/components/code-editor.css"]}
210 | ...)
211 | ```
212 | So, when this namespace is included in the build it just includes this resource in the generated CSS as well. Similar to what webpack `require("./some.css")` does I guess. In the above case this is including all the CodeMirror related styles which is nice since those are not portable to `shadow.css`.
213 |
214 | They also provide a way to add CSS that `shadow.css` might not be capable of generating yet.
215 |
216 | ## Flexibility
217 |
218 | Loving the flexibility of just defining stuff on the fly and not being constrained to be something tailwind can recognize. No need for `w-[30px]` typeof classes when you can just `{:width "30px"}` in the `css` definition.
219 |
220 | Having full access to CSS also means more freedom. Not fully taking advantage of that yet since I just wanted to port the code over simply without having to worry about styling for now. Yet, I already feel like I can do more than before without having to jump to actual CSS files.
221 |
222 | ## Getting more Clojurey
223 |
224 | I think I like a new naming pattern of using `$whatever` names for css classes. So that the `$` differentiates them from other local names.
225 |
226 | ```clojure
227 | (let [$row (css ...)
228 | $key (css ...)
229 | $val (css ...)]
230 |
231 | (<< [:div {:class $row}
232 | [:div {:class $key} key]
233 | [:div {:class $val} val]]))
234 |
235 | ;; could extend the fragment macro magic to make things shorter again
236 |
237 | (let [$row (css ...)
238 | $key (css ...)
239 | $val (css ...)]
240 |
241 | (<< [:div$row
242 | [:div$key key]
243 | [:div$val val]]))
244 | ```
245 |
246 | Downside to this is that tools (eg. Cursive) don't recognize this, so it shows all the `$` locals as unused which is not ideal. Maybe tools could be taught that in some way?
247 |
248 | Using a new `$` prefix since `#` or `.` wouldn't be valid clojure, and we also still want those available so `:div$local#app.class` is valid.
249 |
250 | Introducing local names that way certainly beats having to come up with global names such as `defstyled` did. Local names actually makes things more readable and usable, making the intent of elements clearer. Of course that could have been done before with just `(let [$row "..."])`, `(def $thing ...)` of course also works, sometimes global names are desirable.
251 |
252 |
253 | # On Extensibility
254 |
255 | Based on the rough draft of this document a few people have expressed concerns over the non-REPL friendliness and static analysis of `(css ...)` forms. After thinking about this for a while I believe this is an absolute none issue.
256 |
257 | The tooling currently creates an "index" of namespaces and the css forms it contains. It currently is collected by a simple analysis pass that parses CLJ(S) source files and looks for `(css ...)` forms. That index can either be used directly in the build process or it can be stored in a EDN file to be shipped as part of a library for example. Currently, this would be a `shadow-css-index.edn` file at the classpath root, added to the published `.jar`. This is then read at build time and added to the build index.
258 |
259 | ## Index is just Data
260 |
261 | The index is just a CLJ map of `{ns-sym ns-info}`. `ns-info` is another map of roughly this structure
262 |
263 | ```clojure
264 | {:ns shadow.cljs.ui.components.inspect,
265 | :ns-meta {}, ;; for :shadow.css/include etc
266 | :requires [] ;; not collected yet, just the :require namespaces in order
267 | :css
268 | [{:line 46,
269 | :column 27,
270 | :end-line 46,
271 | :end-column 74,
272 | :form [:w-full :h-full :font-mono :border-t :p-4]}
273 | {:line 50,
274 | :column 27,
275 | :end-line 50,
276 | :end-column 74,
277 | :form [:w-full :h-full :font-mono :border-t :p-4]}]}
278 | ```
279 |
280 | So, each `(css ...)` form is just collected and stored in the index with the relevant metadata. The current tooling will generate the classname to use from the `:ns` `:line` `:column` by default, it could just use a `:class` string if already provided instead.
281 |
282 | ## Index + Config then make CSS
283 |
284 | Only the index data and some configuration is then used to construct the actual CSS needed. The idea is that the build config specifies which namespaces should be included. It could supply custom aliases or override predefined ones.
285 |
286 | I haven't figured out how to do the tooling part yet. So, this is all very rough. For development of the shadow-cljs UI I created this [bit of helper code](https://github.com/thheller/shadow-cljs/blob/4115a2be68160b02e88a70409000f696008663e8/src/dev/repl.clj#L39-L60) that just runs as part of my normal REPL workflow. It watches my `src/main` and updates each namespace in the index when the file is modified. To make a "proper" file I run [this](https://github.com/thheller/shadow-cljs/blob/4115a2be68160b02e88a70409000f696008663e8/src/dev/build.clj#L6-L22).
287 |
288 | Not a great build API but works for now.
289 |
290 | ## Indexing is customizable
291 |
292 | All this really needs then is an extensible index generation mechanism. Instead of the regular `index-file` or `index-path` functions you could call your own. All you have to do is `update` a CLJ map, which is simple.
293 |
294 | You don't even have to use the `shadow.css/css` macro at all. If you can provide data for the index you are good to go.
295 |
296 | Maybe `shadow.css` then just becomes sort of a standard way to express CSS-in-CLJ.
297 |
298 | ### Don't forget about includes
299 |
300 | The index can also include the `:ns-meta {:shadow.css/include ["already/generated.css"]}` directive, which is currently collected from the actual `ns` metadata. If you generate the index yourself you can supply that from wherever.
301 |
302 | So, there already is a way to have pre-generated CSS and still have it participate in the overall CSS building by `shadow.css.build`.
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject com.thheller/shadow-css "0.6.1"
2 | :description "CSS-in-CLJ(S)"
3 | :url "https://github.com/thheller/shadow-css"
4 |
5 | :license
6 | {:name "Eclipse Public License"
7 | :url "http://www.eclipse.org/legal/epl-v10.html"}
8 |
9 | :repositories
10 | {"clojars" {:url "https://clojars.org/repo"
11 | :sign-releases false}}
12 |
13 | :dependencies
14 | [[org.clojure/clojure "1.11.1" :scope "provided"]
15 | [org.clojure/clojurescript "1.11.132" :scope "provided"]
16 | [org.clojure/tools.reader "1.4.2"]]
17 |
18 | :source-paths
19 | ["src/main"])
20 |
--------------------------------------------------------------------------------
/src/main/shadow/css.clj:
--------------------------------------------------------------------------------
1 | (ns shadow.css
2 | (:require
3 | [shadow.css.specs :as s]
4 | [clojure.string :as str]))
5 |
6 | (def class-defs-ref
7 | (atom {}))
8 |
9 | ;; for clojure we just do lookups at runtime
10 | ;; by default is empty but in case something has minified the css
11 | ;; it can provide those lookups and put them in the class-defs-ref above
12 | (defn get-class [class]
13 | (get @class-defs-ref class class))
14 |
15 | (defmacro css
16 | "generates css classnames
17 |
18 | using a subset of Clojure/EDN to define css rules, no dynamic code allowed whatsoever"
19 | [& body]
20 |
21 | ;; FIXME: errors are not pretty
22 | (s/conform! &form)
23 |
24 | (let [{:keys [line column]}
25 | (meta &form)
26 |
27 | ns-str
28 | (str *ns*)
29 |
30 | ns-meta
31 | (meta *ns*)
32 |
33 | ;; this must generate a unique identifier right here
34 | ;; using only information that can be taken from the css form
35 | ;; itself. It must not look at any other location and the id
36 | ;; generated must be deterministic.
37 |
38 | ;; this unfortunately makes it pretty much unusable in the REPL
39 | ;; this is fine since there is no need for CSS in the REPL
40 | ;; but may end up emitting invalid references in code
41 | ;; which again is fine in JS since it'll just be undefined
42 | css-id
43 | (s/generate-id ns-str ns-meta line column)
44 |
45 | passthrough
46 | (->> body
47 | (filter string?)
48 | (str/join " "))]
49 |
50 | ;; using analyzer data is hard to combine with CLJ data
51 | ;; so instead just using an external thing that finds (css ...) calls
52 | ;; and generates the stuff we need
53 | #_class-def
54 | #_(assoc conformed
55 | :ns (symbol ns-str)
56 | :line line
57 | :column column
58 | :css-id css-id)
59 |
60 | ;; FIXME: no idea what to do about self-host yet
61 | (if-not (:ns &env)
62 | (if (seq passthrough)
63 | `(str ~(str passthrough " ") (get-class ~css-id))
64 | `(get-class ~css-id))
65 | (if (seq passthrough)
66 | `(~'js* "(~{} + shadow.css.sel(~{}))" ~(str passthrough " ") ~css-id)
67 | (with-meta
68 | `(~'js* "(shadow.css.sel(~{}))" ~css-id)
69 | (assoc (meta &form) :tag 'shadow.css/css-id)
70 | )))))
71 |
72 | (comment
73 |
74 | (require 'clojure.pprint)
75 |
76 | @class-defs-ref
77 |
78 | (clojure.pprint/pprint
79 | (macroexpand
80 | '(css :foo
81 | "yo" {:hello "world"})
82 | )))
--------------------------------------------------------------------------------
/src/main/shadow/css.cljs:
--------------------------------------------------------------------------------
1 | (ns shadow.css
2 | (:require-macros [shadow.css]))
3 |
4 | ;; GOAL: zero runtime size. no css generation in client side code
5 |
6 | ;; calls directly emitted by css macro
7 | ;; this will be replaced with id generator values once classname minification/replacement is done
8 | ;; and the call is removed entirely by :advanced
9 | (defn sel
10 | ;; {:jsdoc ["@idGenerator {mapped}"]}
11 | [id]
12 | id)
--------------------------------------------------------------------------------
/src/main/shadow/css/aliases.edn:
--------------------------------------------------------------------------------
1 | ;; credit for all of these goes to https://tailwindcss.com/
2 | ;; these are straight up copies, only adjusting names if they violate EDN naming rules in some way.
3 | ;; not going for completeness, some aliases IMHO are just better expressed as maps
4 |
5 | ;; default breakpoints
6 | {:sm "@media (min-width: 640px)"
7 | :md "@media (min-width: 768px)"
8 | :lg "@media (min-width: 1024px)"
9 | :xl "@media (min-width: 1280px)"
10 | :xxl "@media (min-width: 1536px)"
11 |
12 | ;; can't decide which alias to use, since these cost nothing just adding a few more
13 | :sm+ "@media (min-width: 640px)"
14 | :md+ "@media (min-width: 768px)"
15 | :lg+ "@media (min-width: 1024px)"
16 | :xl+ "@media (min-width: 1280px)"
17 | :xxl+ "@media (min-width: 1536px)"
18 |
19 | :dark "@media (prefers-color-scheme: dark)"
20 | :light "@media (prefers-color-scheme: light)"
21 |
22 | :&hover "&:hover"
23 | :hover "&:hover"
24 | :focus "&:focus"
25 | :focus-within "&:focus-within"
26 | :focus-visible "&:focus-visible"
27 | :active "&:active"
28 | :visited "&:visited"
29 | :disabled "&:disabled"
30 | :checked "&:checked"
31 | :target "&:target"
32 | :first "&:first-child"
33 | :last "&:last-child"
34 | :only "&:only-child"
35 | :odd "&:nth-child(odd)"
36 | :even "&:nth-child(even)"
37 |
38 | :container
39 | [:w-full
40 | [:sm {:max-width "640px"}]
41 | [:md {:max-width "768px"}]
42 | [:lg {:max-width "1024px"}]
43 | [:xl {:max-width "1280px"}]
44 | [:xxl {:max-width "1536px"}]]
45 |
46 | ;; color naming scheme based on
47 | ;; https://material.io/design/color/the-color-system.html#color-usage-and-palettes
48 | ;; https://material.io/resources/color/
49 |
50 | ;; shortened, so less typing since these will be repeated a lot
51 | ;; cv for actual color value only, then used in actual rules via
52 | ;; c-text for text color, c-bg for background-color, c-border for border-color
53 | ;; 1/2 for primary/secondary
54 | ;; l/d for light/dark
55 |
56 | ;; based on css variables by default?
57 | ;; overridable in build config with fixed color?
58 |
59 | ;; I'm not a designer, I don't have the slightest clue if this makes any sense
60 |
61 | ;; color values, for use in maps
62 | ;; using some default until I can sort out the variable stuff more
63 |
64 | ;; https://material.io/resources/color/#!/?view.left=0&view.right=0&primary.color=F5F5F5&secondary.color=4CAF50
65 | ;; grey 100
66 | :cv-1 "#f5f5f5" ;; "var(--color-primary)"
67 | :cv-1l "#fff" ;; "var(--color-primary-light)"
68 | :cv-1d "#c2c2c2" ;; "var(--color-primary-dark)"
69 |
70 | :cv-contrast-1 "#000" ;; "var(--color-contrast-primary)"
71 | :cv-contrast-1l "#000" ;; "var(--color-contrast-primary-light)"
72 | :cv-contrast-1d "#000" ;; "var(--color-contrast-primary-dark)"
73 |
74 | ;; green 500
75 | :cv-2 "#4caf50" ;; "var(--color-secondary)"
76 | :cv-2l "#80e27e" ;; "var(--color-secondary-light)"
77 | :cv-2d "#087f23" ;; "var(--color-secondary-dark)"
78 |
79 | :cv-contrast-2 "#000" ;; "var(--color-contrast-secondary)"
80 | :cv-contrast-2l "#000" ;; "var(--color-contrast-secondary-light)"
81 | :cv-contrast-2d "#fff" ;; "var(--color-contrast-secondary-dark)"
82 |
83 | ;; aliases
84 | :c-container-1 {:color :cv-contrast-1 :background-color :cv-1 :border-color :cv-constrast-1}
85 | :c-container-1l {:color :cv-contrast-1l :background-color :cv-1l :border-color :cv-contrast-1l}
86 | :c-container-1d {:color :cv-contrast-1d :background-color :cv-1d :border-color :cv-contrast-1d}
87 |
88 | :c-container-2 {:color :cv-contrast-2 :background-color :cv-2 :border-color :cv-constrast-2}
89 | :c-container-2l {:color :cv-contrast-2l :background-color :cv-2l :border-color :cv-contrast-2l}
90 | :c-container-2d {:color :cv-contrast-2d :background-color :cv-2d :border-color :cv-contrast-2d}
91 |
92 | ;; text
93 | :c-text-1 {:color :cv-contrast-1}
94 | :c-text-1l {:color :cv-contrast-1l}
95 | :c-text-1d {:color :cv-contrast-1d}
96 |
97 | ;; background-color
98 | :c-bg-1 {:background-color :cv-1}
99 | :c-bg-1l {:background-color :cv-1l}
100 | :c-bg-1d {:background-color :cv-1d}
101 |
102 | ;; border-color
103 | :c-border-1 {:border-color :cv-contrast-1}
104 | :c-border-1l {:border-color :cv-contrast-1l}
105 | :c-border-1d {:border-color :cv-contrast-1d}
106 |
107 | ;; text
108 | :c-text-2 {:color :cv-contrast-2}
109 | :c-text-2l {:color :cv-contrast-2l}
110 | :c-text-2d {:color :cv-contrast-2d}
111 |
112 | ;; background-color
113 | :c-bg-2 {:background-color :cv-2}
114 | :c-bg-2l {:background-color :cv-2l}
115 | :c-bg-2d {:background-color :cv-2d}
116 |
117 | ;; border-color
118 | :c-border-2 {:border-color :cv-contrast-2}
119 | :c-border-2l {:border-color :cv-contrast-2l}
120 | :c-border-2d {:border-color :cv-contrast-2d}
121 |
122 | ;; display
123 | :block {:display "block"}
124 | :hidden {:display "none"}
125 | :inline {:display "inline"}
126 | :inline-block {:display "inline-block"}
127 | :inline-flex {:display "inline-flex"}
128 | :grid {:display "grid"}
129 | :inline-grid {:display "inline-grid"}
130 |
131 | :grid-cols-1 {:grid-template-columns "repeat(1, minmax(0, 1fr))"}
132 | :grid-cols-10 {:grid-template-columns "repeat(10, minmax(0, 1fr))"}
133 | :grid-cols-11 {:grid-template-columns "repeat(11, minmax(0, 1fr))"}
134 | :grid-cols-12 {:grid-template-columns "repeat(12, minmax(0, 1fr))"}
135 | :grid-cols-2 {:grid-template-columns "repeat(2, minmax(0, 1fr))"}
136 | :grid-cols-3 {:grid-template-columns "repeat(3, minmax(0, 1fr))"}
137 | :grid-cols-4 {:grid-template-columns "repeat(4, minmax(0, 1fr))"}
138 | :grid-cols-5 {:grid-template-columns "repeat(5, minmax(0, 1fr))"}
139 | :grid-cols-6 {:grid-template-columns "repeat(6, minmax(0, 1fr))"}
140 | :grid-cols-7 {:grid-template-columns "repeat(7, minmax(0, 1fr))"}
141 | :grid-cols-8 {:grid-template-columns "repeat(8, minmax(0, 1fr))"}
142 | :grid-cols-9 {:grid-template-columns "repeat(9, minmax(0, 1fr))"}
143 | :grid-cols-none {:grid-template-columns "none"}
144 |
145 | ;; margin
146 | :m-auto {:margin "auto"}
147 | :mx-auto {:margin-left "auto" :margin-right "auto"}
148 | :my-auto {:margin-top "auto" :margin-bottom "auto"}
149 | :mt-auto {:margin-top "auto"}
150 | :mb-auto {:margin-bottom "auto"}
151 | :ml-auto {:margin-left "auto"}
152 | :mr-auto {:margin-right "auto"}
153 |
154 | ;; flexbox
155 | :flex {:display "flex"}
156 | :flex-inline {:display "inline-flex"}
157 |
158 | ;; flex
159 | :flex-1 {:flex "1 1 0%"}
160 | :flex-auto {:flex "1 1 auto"}
161 | :flex-initial {:flex "0 1 auto"}
162 | :flex-none {:flex "none"}
163 |
164 | ;; flex direction
165 | :flex-row {:flex-direction "row"}
166 | :flex-row-reverse {:flex-direction "row-reverse"}
167 | :flex-col {:flex-direction "column"}
168 | :flex-col-reverse {:flex-direction "column-reverse"}
169 |
170 | ;; flex wrap
171 | :flex-wrap {:flex-wrap "wrap"}
172 | :flex-wrap-reverse {:flex-wrap "wrap-reverse"}
173 | :flex-nowrap {:flex-wrap "nowrap"}
174 |
175 |
176 | :flex-shrink {:flex-shrink "1"}
177 | :flex-shrink-0 {:flex-shrink "0"}
178 | :shrink {:flex-shrink "1"}
179 | :shrink-0 {:flex-shrink "0"}
180 |
181 | :grow {:flex-grow "1"}
182 | :grow-0 {:flex-grow "0"}
183 |
184 |
185 | ;; flex align-items
186 | :items-baseline {:align-items "baseline"}
187 | :items-center {:align-items "center"}
188 | :items-end {:align-items "flex-end"}
189 | :items-start {:align-items "flex-start"}
190 | :items-stretch {:align-items "stretch"}
191 |
192 | :self-auto {:align-self "auto"}
193 | :self-baseline {:align-self "baseline"}
194 | :self-center {:align-self "center"}
195 | :self-end {:align-self "flex-end"}
196 | :self-start {:align-self "flex-start"}
197 | :self-stretch {:align-self "stretch"}
198 |
199 | :justify-items-center {:justify-items "center"}
200 | :justify-items-end {:justify-items "end"}
201 | :justify-items-start {:justify-items "start"}
202 | :justify-items-stretch {:justify-items "stretch"}
203 |
204 | :justify-self-auto {:justify-self "auto"}
205 | :justify-self-center {:justify-self "center"}
206 | :justify-self-end {:justify-self "end"}
207 | :justify-self-start {:justify-self "start"}
208 | :justify-self-stretch {:justify-self "stretch"}
209 |
210 | :justify-around {:justify-content "space-around"}
211 | :justify-between {:justify-content "space-between"}
212 | :justify-center {:justify-content "center"}
213 | :justify-end {:justify-content "flex-end"}
214 | :justify-evenly {:justify-content "space-evenly"}
215 | :justify-start {:justify-content "flex-start"}
216 |
217 | :align-baseline {:vertical-align "baseline"}
218 | :align-bottom {:vertical-align "bottom"}
219 | :align-middle {:vertical-align "middle"}
220 | :align-sub {:vertical-align "sub"}
221 | :align-super {:vertical-align "super"}
222 | :align-text-bottom {:vertical-align "text-bottom"}
223 | :align-text-top {:vertical-align "text-top"}
224 | :align-top {:vertical-align "top"}
225 |
226 | :whitespace-normal {:white-space "normal"}
227 | :whitespace-nowrap {:white-space "nowrap"}
228 | :whitespace-pre {:white-space "pre"}
229 | :whitespace-pre-line {:white-space "pre-line"}
230 | :whitespace-pre-wrap {:white-space "pre-wrap"}
231 |
232 | ;; font weight
233 | :font-thin {:font-weight "100"}
234 | :font-extralight {:font-weight "200"}
235 | :font-light {:font-weight "300"}
236 | :font-normal {:font-weight "400"}
237 | :font-medium {:font-weight "500"}
238 | :font-semibold {:font-weight "600"}
239 | :font-bold {:font-weight "700"}
240 | :font-extrabold {:font-weight "800"}
241 | :font-black {:font-weight "900"}
242 |
243 | ;; font style
244 | :italic {:font-style "italic"}
245 | :not-italic {:font-style "normal"}
246 |
247 | ;; font
248 | :font-sans {:font-family "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\""}
249 | :font-serif {:font-family "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif"}
250 | :font-mono {:font-family "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"}
251 |
252 | :leading-10 {:line-height "2.5rem"}
253 | :leading-3 {:line-height ".75rem"}
254 | :leading-4 {:line-height "1rem"}
255 | :leading-5 {:line-height "1.25rem"}
256 | :leading-6 {:line-height "1.5rem"}
257 | :leading-7 {:line-height "1.75rem"}
258 | :leading-8 {:line-height "2rem"}
259 | :leading-9 {:line-height "2.25rem"}
260 | :leading-loose {:line-height "2"}
261 | :leading-none {:line-height "1"}
262 | :leading-normal {:line-height "1.5"}
263 | :leading-relaxed {:line-height "1.625"}
264 | :leading-snug {:line-height "1.375"}
265 | :leading-tight {:line-height "1.25"}
266 |
267 | :tracking-normal {:letter-spacing "0em"}
268 | :tracking-tight {:letter-spacing "-0.025em"}
269 | :tracking-tighter {:letter-spacing "-0.05em"}
270 | :tracking-wide {:letter-spacing "0.025em"}
271 | :tracking-wider {:letter-spacing "0.05em"}
272 | :tracking-widest {:letter-spacing "0.1em"}
273 |
274 | :capitalize {:text-transform "capitalize"}
275 | :lowercase {:text-transform "lowercase"}
276 | :normal-case {:text-transform "none"}
277 | :uppercase {:text-transform "uppercase"}
278 |
279 | ;; font size
280 | :text-xs {:font-size "0.75rem" :line-height "1rem"}
281 | :text-sm {:font-size "0.875rem" :line-height "1.25rem"}
282 | :text-base {:font-size "1rem" :line-height "1.5rem"}
283 | :text-lg {:font-size "1.125rem" :line-height "1.75rem"}
284 | :text-xl {:font-size "1.25rem" :line-height "1.75rem"}
285 | :text-2xl {:font-size "1.5rem" :line-height "2rem"}
286 | :text-3xl {:font-size "1.875rem" :line-height "2.25rem"}
287 | :text-4xl {:font-size "2.25rem" :line-height "2.5rem"}
288 | :text-5xl {:font-size "3rem" :line-height "1"}
289 | :text-6xl {:font-size "3.75rem" :line-height "1"}
290 | :text-7xl {:font-size "4.5rem" :line-height "1"}
291 | :text-8xl {:font-size "6rem" :line-height "1"}
292 | :text-9xl {:font-size "8rem" :line-height "1"}
293 |
294 | ;; text overflow
295 | :truncate {:overflow "hidden" :text-overflow "ellipsis" :white-space "nowrap"}
296 | :text-ellipsis {:text-overflow "ellipsis"}
297 | :text-clip {:text-overflow "clip"}
298 |
299 | ;; overflow
300 | :overflow-auto {:overflow "auto"}
301 | :overflow-clip {:overflow "clip"}
302 | :overflow-hidden {:overflow "hidden"}
303 | :overflow-scroll {:overflow "scroll"}
304 | :overflow-visible {:overflow "visible"}
305 | :overflow-x-auto {:overflow-x "auto"}
306 | :overflow-x-clip {:overflow-x "clip"}
307 | :overflow-x-hidden {:overflow-x "hidden"}
308 | :overflow-x-scroll {:overflow-x "scroll"}
309 | :overflow-x-visible {:overflow-x "visible"}
310 | :overflow-y-auto {:overflow-y "auto"}
311 | :overflow-y-clip {:overflow-y "clip"}
312 | :overflow-y-hidden {:overflow-y "hidden"}
313 | :overflow-y-scroll {:overflow-y "scroll"}
314 | :overflow-y-visible {:overflow-y "visible"}
315 |
316 | ;; border
317 | :border {:border-width "1px"}
318 | :border-0 {:border-width "0px"}
319 | :border-2 {:border-width "2px"}
320 | :border-4 {:border-width "4px"}
321 | :border-8 {:border-width "8px"}
322 | :border-b {:border-bottom-width "1px"}
323 | :border-b-0 {:border-bottom-width "0px"}
324 | :border-b-2 {:border-bottom-width "2px"}
325 | :border-b-4 {:border-bottom-width "4px"}
326 | :border-b-8 {:border-bottom-width "8px"}
327 | :border-l {:border-left-width "1px"}
328 | :border-l-0 {:border-left-width "0px"}
329 | :border-l-2 {:border-left-width "2px"}
330 | :border-l-4 {:border-left-width "4px"}
331 | :border-l-8 {:border-left-width "8px"}
332 | :border-r {:border-right-width "1px"}
333 | :border-r-0 {:border-right-width "0px"}
334 | :border-r-2 {:border-right-width "2px"}
335 | :border-r-4 {:border-right-width "4px"}
336 | :border-r-8 {:border-right-width "8px"}
337 | :border-t {:border-top-width "1px"}
338 | :border-t-0 {:border-top-width "0px"}
339 | :border-t-2 {:border-top-width "2px"}
340 | :border-t-4 {:border-top-width "4px"}
341 | :border-t-8 {:border-top-width "8px"}
342 | :border-x {:border-left-width "1px", :border-right-width "1px"}
343 | :border-x-0 {:border-left-width "0px", :border-right-width "0px"}
344 | :border-x-2 {:border-left-width "2px", :border-right-width "2px"}
345 | :border-x-4 {:border-left-width "4px", :border-right-width "4px"}
346 | :border-x-8 {:border-left-width "8px", :border-right-width "8px"}
347 | :border-y {:border-top-width "1px", :border-bottom-width "1px"}
348 | :border-y-0 {:border-top-width "0px", :border-bottom-width "0px"}
349 | :border-y-2 {:border-top-width "2px", :border-bottom-width "2px"}
350 | :border-y-4 {:border-top-width "4px", :border-bottom-width "4px"}
351 | :border-y-8 {:border-top-width "8px", :border-bottom-width "8px"}
352 |
353 | ;; shadows
354 | :shadow {:box-shadow "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"}
355 | :shadow-2xl {:box-shadow "0 25px 50px -12px rgb(0 0 0 / 0.25)"}
356 | :shadow-inner {:box-shadow "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"}
357 | :shadow-lg {:box-shadow "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"}
358 | :shadow-md {:box-shadow "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"}
359 | :shadow-none {:box-shadow "0 0 #0000"}
360 | :shadow-sm {:box-shadow "0 1px 2px 0 rgb(0 0 0 / 0.05)"}
361 | :shadow-xl {:box-shadow "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"}
362 |
363 |
364 | ;; box sizing
365 | :box-border {:box-sizing "border-box"}
366 | :box-content {:box-sizing "box-content"}
367 |
368 | ;; outline
369 | :outline {:outline-style "solid"}
370 | :outline-dashed {:outline-style "dashed"}
371 | :outline-dotted {:outline-style "dotted"}
372 | :outline-double {:outline-style "double"}
373 | :outline-hidden {:outline-style "hidden"}
374 | :outline-none {:outline "2px solid transparent", :outline-offset "2px"}
375 |
376 | ;; z-index
377 | :z-0 {:z-index 0}
378 | :z-10 {:z-index 10}
379 | :z-20 {:z-index 20}
380 | :z-30 {:z-index 30}
381 | :z-40 {:z-index 40}
382 | :z-50 {:z-index 50}
383 | :z-auto {:z-index "auto"}
384 |
385 |
386 | ;; height, rest auto-generated
387 | :h-auto {:height "auto"}
388 | :h-full {:height "100%"}
389 | :h-screen {:height "100vh"}
390 | :h-min {:height "min-content"}
391 | :h-max {:height "max-content"}
392 | :h-fit {:height "fit-content"}
393 |
394 | :h-1of2 {:height "50%"}
395 |
396 | :h-1of3 {:height "33.333333%"}
397 | :h-2of3 {:height "66.666667%"}
398 |
399 | :h-1of4 {:height "25%"}
400 | :h-2of4 {:height "50%"}
401 | :h-3of4 {:height "75%"}
402 |
403 | :h-1of5 {:height "20%"}
404 | :h-2of5 {:height "40%"}
405 | :h-3of5 {:height "60%"}
406 | :h-4of5 {:height "80%"}
407 |
408 | :h-1of6 {:height "16.666667%"}
409 | :h-2of6 {:height "33.333333%"}
410 | :h-3of6 {:height "50%"}
411 | :h-4of6 {:height "66.666667%"}
412 | :h-5of6 {:height "83.333333%"}
413 |
414 | :min-h-0 {:min-height "0px"}
415 | :min-h-fit {:min-height "fit-content"}
416 | :min-h-full {:min-height "100%"}
417 | :min-h-max {:min-height "max-content"}
418 | :min-h-min {:min-height "min-content"}
419 | :min-h-screen {:min-height "100vh"}
420 |
421 | :max-h-fit {:max-height "fit-content"}
422 | :max-h-full {:max-height "100%"}
423 | :max-h-max {:max-height "max-content"}
424 | :max-h-min {:max-height "min-content"}
425 | :max-h-screen {:max-height "100vh"}
426 |
427 | ;; width, rest auto-generated
428 | :w-auto {:width "auto"}
429 | :w-full {:width "100%"}
430 | :w-screen {:width "100vw"}
431 | :w-min {:width "min-content"}
432 | :w-max {:width "max-content"}
433 | :w-fit {:width "fit-content"}
434 |
435 | :w-1of2 {:width "50%"}
436 |
437 | :w-1of3 {:width "33.333333%"}
438 | :w-2of3 {:width "66.666667%"}
439 |
440 | :w-1of4 {:width "25%"}
441 | :w-2of4 {:width "50%"}
442 | :w-3of4 {:width "75%"}
443 |
444 | :w-1of5 {:width "20%"}
445 | :w-2of5 {:width "40%"}
446 | :w-3of5 {:width "60%"}
447 | :w-4of5 {:width "80%"}
448 |
449 | :w-1of6 {:width "16.666667%"}
450 | :w-2of6 {:width "33.333333%"}
451 | :w-3of6 {:width "50%"}
452 | :w-4of6 {:width "66.666667%"}
453 | :w-5of6 {:width "83.333333%"}
454 |
455 | :w-1of12 {:width "8.333333%"}
456 | :w-2of12 {:width "16.666667%"}
457 | :w-3of12 {:width "25%"}
458 | :w-4of12 {:width "33.333333%"}
459 | :w-5of12 {:width "41.666667%"}
460 | :w-6of12 {:width "50%"}
461 | :w-7of12 {:width "58.333333%"}
462 | :w-8of12 {:width "66.666667%"}
463 | :w-9of12 {:width "75%"}
464 | :w-10of12 {:width "83.333333%"}
465 | :w-11of12 {:width "91.666667%"}
466 |
467 |
468 | :min-w-0 {:min-width "0px"}
469 | :min-w-fit {:min-width "fit-content"}
470 | :min-w-full {:min-width "100%"}
471 | :min-w-max {:min-width "max-content"}
472 | :min-w-min {:min-width "min-content"}
473 | :min-w-screen {:min-width "100vw"}
474 |
475 | :max-w-fit {:max-width "fit-content"}
476 | :max-w-full {:max-width "100%"}
477 | :max-w-max {:max-width "max-content"}
478 | :max-w-min {:max-width "min-content"}
479 | :max-w-screen {:max-width "100vw"}
480 |
481 | ;; text align
482 | :text-center {:text-align "center"}
483 | :text-end {:text-align "end"}
484 | :text-justify {:text-align "justify"}
485 | :text-left {:text-align "left"}
486 | :text-right {:text-align "right"}
487 | :text-start {:text-align "start"}
488 |
489 | ;; cursor
490 | :cursor-alias {:cursor "alias"}
491 | :cursor-all-scroll {:cursor "all-scroll"}
492 | :cursor-auto {:cursor "auto"}
493 | :cursor-cell {:cursor "cell"}
494 | :cursor-col-resize {:cursor "col-resize"}
495 | :cursor-context-menu {:cursor "context-menu"}
496 | :cursor-copy {:cursor "copy"}
497 | :cursor-crosshair {:cursor "crosshair"}
498 | :cursor-default {:cursor "default"}
499 | :cursor-e-resize {:cursor "e-resize"}
500 | :cursor-ew-resize {:cursor "ew-resize"}
501 | :cursor-grab {:cursor "grab"}
502 | :cursor-grabbing {:cursor "grabbing"}
503 | :cursor-help {:cursor "help"}
504 | :cursor-move {:cursor "move"}
505 | :cursor-n-resize {:cursor "n-resize"}
506 | :cursor-ne-resize {:cursor "ne-resize"}
507 | :cursor-nesw-resize {:cursor "nesw-resize"}
508 | :cursor-no-drop {:cursor "no-drop"}
509 | :cursor-none {:cursor "none"}
510 | :cursor-not-allowed {:cursor "not-allowed"}
511 | :cursor-ns-resize {:cursor "ns-resize"}
512 | :cursor-nw-resize {:cursor "nw-resize"}
513 | :cursor-nwse-resize {:cursor "nwse-resize"}
514 | :cursor-pointer {:cursor "pointer"}
515 | :cursor-progress {:cursor "progress"}
516 | :cursor-row-resize {:cursor "row-resize"}
517 | :cursor-s-resize {:cursor "s-resize"}
518 | :cursor-se-resize {:cursor "se-resize"}
519 | :cursor-sw-resize {:cursor "sw-resize"}
520 | :cursor-text {:cursor "text"}
521 | :cursor-vertical-text {:cursor "vertical-text"}
522 | :cursor-w-resize {:cursor "w-resize"}
523 | :cursor-wait {:cursor "wait"}
524 | :cursor-zoom-in {:cursor "zoom-in"}
525 | :cursor-zoom-out {:cursor "zoom-out"}
526 |
527 |
528 | ;; select
529 | :select-none {:user-select "none"}
530 | :select-text {:user-select "text"}
531 | :select-all {:user-select "all"}
532 | :select-auto {:user-select "auto"}
533 |
534 | ;; pointer events
535 | :pointer-events-none {:pointer-events "none"}
536 | :pointer-events-auto {:pointer-events "auto"}
537 |
538 | ;; position
539 | :absolute {:position "absolute"}
540 | :fixed {:position "fixed"}
541 | :relative {:position "relative"}
542 | :static {:position "static"}
543 | :sticky {:position "sticky"}
544 |
545 | ;; position
546 | :inset-0 {:top "0px" :right "0px" :bottom "0px" :left "0px"}
547 | :top-full {:top "100%"}
548 | :right-full {:right "100%"}
549 | :bottom-full {:bottom "100%"}
550 | :left-full {:left "100%"}
551 |
552 | ;; border-radius
553 | :rounded {:border-radius "0.25rem"}
554 | :rounded-2xl {:border-radius "1rem"}
555 | :rounded-3xl {:border-radius "1.5rem"}
556 | :rounded-b {:border-bottom-right-radius "0.25rem" :border-bottom-left-radius "0.25rem"}
557 | :rounded-b-2xl {:border-bottom-right-radius "1rem" :border-bottom-left-radius "1rem"}
558 | :rounded-b-3xl {:border-bottom-right-radius "1.5rem" :border-bottom-left-radius "1.5rem"}
559 | :rounded-b-full {:border-bottom-right-radius "9999px", :border-bottom-left-radius "9999px"}
560 | :rounded-b-lg {:border-bottom-right-radius "0.5rem" :border-bottom-left-radius "0.5rem"}
561 | :rounded-b-md {:border-bottom-right-radius "0.375rem" :border-bottom-left-radius "0.375rem"}
562 | :rounded-b-none {:border-bottom-right-radius "0px", :border-bottom-left-radius "0px"}
563 | :rounded-b-sm {:border-bottom-right-radius "0.125rem" :border-bottom-left-radius "0.125rem"}
564 | :rounded-b-xl {:border-bottom-right-radius "0.75rem" :border-bottom-left-radius "0.75rem"}
565 | :rounded-bl {:border-bottom-left-radius "0.25rem"}
566 | :rounded-bl-2xl {:border-bottom-left-radius "1rem"}
567 | :rounded-bl-3xl {:border-bottom-left-radius "1.5rem"}
568 | :rounded-bl-full {:border-bottom-left-radius "9999px"}
569 | :rounded-bl-lg {:border-bottom-left-radius "0.5rem"}
570 | :rounded-bl-md {:border-bottom-left-radius "0.375rem"}
571 | :rounded-bl-none {:border-bottom-left-radius "0px"}
572 | :rounded-bl-sm {:border-bottom-left-radius "0.125rem"}
573 | :rounded-bl-xl {:border-bottom-left-radius "0.75rem"}
574 | :rounded-br {:border-bottom-right-radius "0.25rem"}
575 | :rounded-br-2xl {:border-bottom-right-radius "1rem"}
576 | :rounded-br-3xl {:border-bottom-right-radius "1.5rem"}
577 | :rounded-br-full {:border-bottom-right-radius "9999px"}
578 | :rounded-br-lg {:border-bottom-right-radius "0.5rem"}
579 | :rounded-br-md {:border-bottom-right-radius "0.375rem"}
580 | :rounded-br-none {:border-bottom-right-radius "0px"}
581 | :rounded-br-sm {:border-bottom-right-radius "0.125rem"}
582 | :rounded-br-xl {:border-bottom-right-radius "0.75rem"}
583 | :rounded-full {:border-radius "9999px"}
584 | :rounded-l {:border-top-left-radius "0.25rem" :border-bottom-left-radious "0.25rem"}
585 | :rounded-l-2xl {:border-top-left-radius "1rem" :border-bottom-left-radius "1rem"}
586 | :rounded-l-3xl {:border-top-left-radius "1.5rem" :border-bottom-left-radius "1.5rem"}
587 | :rounded-l-full {:border-top-left-radius "9999px", :border-bottom-left-radius "9999px"}
588 | :rounded-l-lg {:border-top-left-radius "0.5rem" :border-bottom-left-radius "0.5rem"}
589 | :rounded-l-md {:border-top-left-radius "0.375rem" :border-bottom-left-radius "0.375rem"}
590 | :rounded-l-none {:border-top-left-radius "0px", :border-bottom-left-radius "0px"}
591 | :rounded-l-sm {:border-top-left-radius "0.125rem" :border-bottom-left-radius "0.125rem"}
592 | :rounded-l-xl {:border-top-left-radius "0.75rem" :border-bottom-left-radius "0.75rem"}
593 | :rounded-lg {:border-radius "0.5rem"}
594 | :rounded-md {:border-radius "0.375rem"}
595 | :rounded-none {:border-radius "0px"}
596 | :rounded-r {:border-top-right-radius "0.25rem" :border-bottom-right-radius "0.25rem"}
597 | :rounded-r-2xl {:border-top-right-radius "1rem" :border-bottom-right-radius "1rem"}
598 | :rounded-r-3xl {:border-top-right-radius "1.5rem" :border-bottom-right-radius "1.5rem"}
599 | :rounded-r-full {:border-top-right-radius "9999px", :border-bottom-right-radius "9999px"}
600 | :rounded-r-lg {:border-top-right-radius "0.5rem" :border-bottom-right-radius "0.5rem"}
601 | :rounded-r-md {:border-top-right-radius "0.375rem" :border-bottom-right-radius "0.375rem"}
602 | :rounded-r-none {:border-top-right-radius "0px", :border-bottom-right-radius "0px"}
603 | :rounded-r-sm {:border-top-right-radius "0.125rem" :border-bottom-right-radius "0.125rem"}
604 | :rounded-r-xl {:border-top-right-radius "0.75rem" :border-bottom-right-radius "0.75rem"}
605 | :rounded-sm {:border-radius "0.125rem"}
606 | :rounded-t {:border-top-left-radius "0.25rem" :border-top-right-radius "0.25rem"}
607 | :rounded-t-2xl {:border-top-left-radius "1rem" :border-top-right-radius "1rem"}
608 | :rounded-t-3xl {:border-top-left-radius "1.5rem" :border-top-right-radius "1.5rem"}
609 | :rounded-t-full {:border-top-left-radius "9999px", :border-top-right-radius "9999px"}
610 | :rounded-t-lg {:border-top-left-radius "0.5rem" :border-top-right-radius "0.5rem"}
611 | :rounded-t-md {:border-top-left-radius "0.375rem" :border-top-right-radius "0.375rem"}
612 | :rounded-t-none {:border-top-left-radius "0px" :border-top-right-radius "0px"}
613 | :rounded-t-sm {:border-top-left-radius "0.125rem" :border-top-right-radius "0.125rem"}
614 | :rounded-t-xl {:border-top-left-radius "0.75rem" :border-top-right-radius "0.75rem"}
615 | :rounded-tl {:border-top-left-radius "0.25rem"}
616 | :rounded-tl-2xl {:border-top-left-radius "1rem"}
617 | :rounded-tl-3xl {:border-top-left-radius "1.5rem"}
618 | :rounded-tl-full {:border-top-left-radius "9999px"}
619 | :rounded-tl-lg {:border-top-left-radius "0.5rem"}
620 | :rounded-tl-md {:border-top-left-radius "0.375rem"}
621 | :rounded-tl-none {:border-top-left-radius "0px"}
622 | :rounded-tl-sm {:border-top-left-radius "0.125rem"}
623 | :rounded-tl-xl {:border-top-left-radius "0.75rem"}
624 | :rounded-tr {:border-top-right-radius "0.25rem"}
625 | :rounded-tr-2xl {:border-top-right-radius "1rem"}
626 | :rounded-tr-3xl {:border-top-right-radius "1.5rem"}
627 | :rounded-tr-full {:border-top-right-radius "9999px"}
628 | :rounded-tr-lg {:border-top-right-radius "0.5rem"}
629 | :rounded-tr-md {:border-top-right-radius "0.375rem"}
630 | :rounded-tr-none {:border-top-right-radius "0px"}
631 | :rounded-tr-sm {:border-top-right-radius "0.125rem"}
632 | :rounded-tr-xl {:border-top-right-radius "0.75rem"}
633 | :rounded-xl {:border-radius "0.75rem"}
634 |
635 | :not-sr-only {:position "static", :width "auto", :height "auto", :padding "0", :margin "0", :overflow "visible", :clip "auto", :white-space "normal"}
636 | :sr-only {:clip "rect(0, 0, 0, 0)", :white-space "nowrap", :overflow "hidden", :width "1px", :border-width "0", :padding "0", :position "absolute", :height "1px", :margin "-1px"}
637 |
638 | :divide-dashed [["& > * + *" {:border-style "dashed"}]]
639 | :divide-dotted [["& > * + *" {:border-style "dotted"}]]
640 | :divide-double [["& > * + *" {:border-style "double"}]]
641 | :divide-none [["& > * + *" {:border-style "none"}]]
642 | :divide-solid [["& > * + *" {:border-style "solid"}]]
643 |
644 | :divide-x [["& > * + *" {:border-right-width "0px", :border-left-width "1px"}]]
645 | :divide-x-0 [["& > * + *" {:border-right-width "0px", :border-left-width "0px"}]]
646 | :divide-x-2 [["& > * + *" {:border-right-width "0px", :border-left-width "2px"}]]
647 | :divide-x-4 [["& > * + *" {:border-right-width "0px", :border-left-width "4px"}]]
648 | :divide-x-8 [["& > * + *" {:border-right-width "0px", :border-left-width "8px"}]]
649 | :divide-y [["& > * + *" {:border-top-width "1px", :border-bottom-width "0px"}]]
650 | :divide-y-0 [["& > * + *" {:border-top-width "0px", :border-bottom-width "0px"}]]
651 | :divide-y-2 [["& > * + *" {:border-top-width "2px", :border-bottom-width "0px"}]]
652 | :divide-y-4 [["& > * + *" {:border-top-width "4px", :border-bottom-width "0px"}]]
653 | :divide-y-8 [["& > * + *" {:border-top-width "8px", :border-bottom-width "0px"}]]
654 |
655 | ;; opacity
656 | :opacity-0 {:opacity "0"}
657 | :opacity-5 {:opacity "0.05"}
658 | :opacity-10 {:opacity "0.1"}
659 | :opacity-20 {:opacity "0.2"}
660 | :opacity-25 {:opacity "0.25"}
661 | :opacity-30 {:opacity "0.3"}
662 | :opacity-40 {:opacity "0.4"}
663 | :opacity-50 {:opacity "0.5"}
664 | :opacity-60 {:opacity "0.6"}
665 | :opacity-70 {:opacity "0.7"}
666 | :opacity-75 {:opacity "0.75"}
667 | :opacity-80 {:opacity "0.8"}
668 | :opacity-90 {:opacity "0.9"}
669 | :opacity-95 {:opacity "0.95"}
670 | :opacity-100 {:opacity "1"}
671 |
672 | ;; transforms
673 | :transition-none {:transition-property "none"}
674 | :transition-all {:transition-property "all"
675 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
676 | :transition "150ms"}
677 | :transition {:transition-property "color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter"
678 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
679 | :transition "150ms"}
680 | :transition-colors {:transition-property "color, background-color, border-color, text-decoration-color, fill, stroke"
681 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
682 | :transition "150ms"}
683 | :transition-opacity {:transition-property "opacity"
684 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
685 | :transition "150ms"}
686 | :transition-shadow {:transition-property "box-shadow"
687 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
688 | :transition "150ms"}
689 | :transition-transform {:transition-property "transform"
690 | :transition-timing-function "cubic-bezier(0.4,0,0.2,1)"
691 | :transition "150ms"}
692 |
693 | :duration-0 {:transition-duration "0s"}
694 | :duration-75 {:transition-duration "75ms"}
695 | :duration-100 {:transition-duration "100ms"}
696 | :duration-150 {:transition-duration "150ms"}
697 | :duration-200 {:transition-duration "200ms"}
698 | :duration-300 {:transition-duration "300ms"}
699 | :duration-500 {:transition-duration "500ms"}
700 | :duration-700 {:transition-duration "700ms"}
701 | :duration-1000 {:transition-duration "1000ms"}
702 |
703 |
704 | :ease-linear {:transition-timing-function "linear"}
705 | :ease-in {:transition-timing-function "cubic-bezier(0.4,0,1,1)"}
706 | :ease-out {:transition-timing-function "cubic-bezier(0,0,0.2,1)"}
707 | :ease-in-out {:transition-timing-function "cubic-bezier(0.4,0,0.2,1)"}
708 |
709 | :delay-0 {:transition-delay "0s"}
710 | :delay-75 {:transition-delay "75ms"}
711 | :delay-100 {:transition-delay "100ms"}
712 | :delay-150 {:transition-delay "150ms"}
713 | :delay-200 {:transition-delay "200ms"}
714 | :delay-300 {:transition-delay "300ms"}
715 | :delay-500 {:transition-delay "500ms"}
716 | :delay-700 {:transition-delay "700ms"}
717 | :delay-1000 {:transition-delay "1000ms"}
718 |
719 |
720 | :origin-center {:transform-origin "center"}
721 | :origin-top {:transform-origin "top"}
722 | :origin-top-right {:transform-origin "top right"}
723 | :origin-right {:transform-origin "right"}
724 | :origin-bottom-right {:transform-origin "bottom right"}
725 | :origin-bottom {:transform-origin "bottom"}
726 | :origin-bottom-left {:transform-origin "bottom left"}
727 | :origin-left {:transform-origin "left"}
728 | :origin-top-left {:transform-origin "top left"}
729 |
730 | ;; visibility
731 | :visible {:visibility "visible"}
732 | :invisible {:visibility "hidden"}
733 |
734 | ;; aspect ratio
735 | :aspect-auto {:aspect-ratio "auto"}
736 | :aspect-square {:aspect-ratio "1 / 1"}
737 | :aspect-video {:aspect-ratio "16 / 9"}
738 |
739 | ;; text decoration
740 | :underline {:text-decoration-line "underline"}
741 | :overline {:text-decoration-line "overline"}
742 | :line-through {:text-decoration-line "line-through"}
743 | :no-underline {:text-decoration-line "none"}}
744 |
--------------------------------------------------------------------------------
/src/main/shadow/css/analyzer.cljc:
--------------------------------------------------------------------------------
1 | (ns shadow.css.analyzer
2 | (:require
3 | [clojure.string :as str]
4 | [clojure.tools.reader :as reader]
5 | [clojure.tools.reader.reader-types :as reader-types]
6 | ))
7 |
8 | (defn reduce-> [init rfn coll]
9 | (reduce rfn init coll))
10 |
11 | (defn reduce-kv-> [init rfn coll]
12 | (reduce-kv rfn init coll))
13 |
14 | (defn lookup-alias [svc alias-kw]
15 | (get-in svc [:aliases alias-kw]))
16 |
17 | (def plain-numeric-props
18 | #{:flex :order :flex-shrink :flex-grow :z-index :opacity})
19 |
20 | (defn convert-num-val [index prop num]
21 | (if (contains? plain-numeric-props prop)
22 | (str num)
23 | (or (get-in index [:svc :spacing num])
24 | (throw
25 | (ex-info
26 | (str "invalid numeric value for prop " prop)
27 | {:prop prop :num num})))))
28 |
29 | (defn add-warning [svc form warning-type warning-vals]
30 | (update form :warnings conj (assoc warning-vals :warning-type warning-type)))
31 |
32 | (declare add-part)
33 |
34 | (defn add-alias [svc form alias-kw]
35 | (let [alias-val (lookup-alias svc alias-kw)]
36 | (cond
37 | (not alias-val)
38 | (add-warning svc form ::missing-alias {:alias alias-kw})
39 |
40 | (map? alias-val)
41 | (add-part svc form alias-val)
42 |
43 | (vector? alias-val)
44 | (reduce #(add-part svc %1 %2) form alias-val)
45 |
46 | :else
47 | (add-warning svc form ::invalid-alias-replacement {:alias alias-kw :val alias-val})
48 | )))
49 |
50 | (defn add-map [svc form defs]
51 | (reduce-kv
52 | (fn [form prop val]
53 | (let [[form val]
54 | (cond
55 | ;; {:thing "val"}
56 | (string? val)
57 | [form val]
58 |
59 | ;; {:thing 4}
60 | (number? val)
61 | [form (convert-num-val svc prop val)]
62 |
63 | ;; {:thing :alias}
64 | (keyword? val)
65 | (let [alias-value (lookup-alias svc val)]
66 | (cond
67 | (nil? alias-value)
68 | [(add-warning svc form ::missing-alias {:alias val})
69 | nil]
70 |
71 | (and (map? alias-value) (contains? alias-value prop))
72 | [form (get alias-value prop)]
73 |
74 | (string? alias-value)
75 | [form alias-value]
76 |
77 | (number? alias-value)
78 | [form (convert-num-val form prop alias-value)]
79 |
80 | :else
81 | [(add-warning svc form ::invalid-map-val {:prop prop :val val})
82 | nil]
83 | )))]
84 |
85 | (assoc-in form [:rules (:sel form) prop] val)))
86 |
87 | form
88 | defs))
89 |
90 | (defn make-sub-rule [{:keys [stack rules] :as form}]
91 | (update form :sub-rules assoc stack rules))
92 |
93 | (defn add-group* [svc form group-sel group-parts]
94 | (cond
95 | (not (string? group-sel))
96 | (add-warning svc form ::invalid-group-sel {:sel group-sel})
97 |
98 | ;; at block rules such as @media or @starting-style
99 | (str/starts-with? group-sel "@")
100 | (let [{:keys [rules block]} form
101 |
102 | ;; FIXME: add back support for combining media queries?
103 | ;; I generalized this so @starting-style works but removed @media "and" combining, so this may now nest media queries
104 | ;; this is fine, but looks a bit prettier when combined. don't think its a common occurence to start, so no bothering for now
105 |
106 | ;; FIXME: @starting-style can be nested inside the main selector, maybe that should be its own special case?
107 |
108 | ;; @starting-style {
109 | ;; .foo { ... }
110 | ;; }
111 |
112 | ;; vs
113 |
114 | ;; .foo {
115 | ;; @starting-style {
116 | ;; ...
117 | ;; }}
118 |
119 | new-block
120 | (conj block group-sel)]
121 |
122 | (-> form
123 | (assoc :rules {} :block new-block)
124 | (reduce-> #(add-part svc %1 %2) group-parts)
125 | ((fn [{:keys [rules] :as form}]
126 | (assoc-in form [:at-rules new-block] rules)))
127 | (assoc :rules rules :block block)))
128 |
129 | (str/index-of group-sel "&")
130 | (let [{:keys [rules sel]} form]
131 |
132 | (if (not= sel "&")
133 | (throw (ex-info "tbd, combining &" {:sel sel :group-sel group-sel}))
134 | (-> form
135 | (assoc :sel group-sel)
136 | (reduce-> #(add-part svc %1 %2) group-parts)
137 | (assoc :sel sel))))
138 |
139 | :else
140 | (add-warning svc form ::invalid-group-sel {:sel group-sel})))
141 |
142 | (defn add-group [svc form [sel & parts]]
143 | (if (keyword? sel)
144 | (if-some [alias-value (lookup-alias svc sel)]
145 | (add-group* svc form alias-value parts)
146 | (add-warning svc form ::group-sel-alias-not-found {:alias sel}))
147 | (add-group* svc form sel parts)))
148 |
149 | (defn add-part [svc form part]
150 | (cond
151 | (string? part) ;; "other-class", passthrough, ignored here, handled in macro
152 | form
153 |
154 | (keyword? part) ;; :px-4 alias
155 | (add-alias svc form part)
156 |
157 | (map? part) ;; {:padding 4}
158 | (add-map svc form part)
159 |
160 | (vector? part) ;; ["&:hover" :px-4] subgroup
161 | (add-group svc form part)
162 |
163 | :else
164 | (add-warning svc form ::invalid-part part)))
165 |
166 | (defn process-form [svc {:keys [form] :as form-info}]
167 | (-> form-info
168 | (assoc :sel "&" :block [] :rules {} :at-rules {} :warnings [])
169 | (reduce-> #(add-part svc %1 %2) form)
170 | (dissoc :stack :sel :block)))
171 |
172 | (defn flatten-prefix-lists [form]
173 | (let [prefix-parts (take-while #(not (keyword? %)) form)
174 | prefix-count (count prefix-parts)
175 | args (drop prefix-count form)]
176 |
177 | (if-not (even? (count args))
178 | (throw (ex-info "failed to parse ns require" {:form form}))
179 | (let [args-map (apply array-map args)]
180 | (if (= 1 prefix-count)
181 | [(assoc args-map :require (first form))]
182 | (let [[prefix & suffixes] prefix-parts]
183 | (when (string? prefix)
184 | (throw (ex-info "failed to parse ns require, string requires can't have prefix lists" {:form form})))
185 |
186 | (loop [expanded
187 | (if-not (seq args)
188 | []
189 | [(assoc args-map :require (first form))])
190 | suffixes suffixes]
191 |
192 | (if-not (seq suffixes)
193 | expanded
194 | (let [[part & more] suffixes]
195 | (cond
196 | (symbol? part)
197 | (recur
198 | (conj expanded {:require (symbol (str prefix "." part))})
199 | more)
200 |
201 | (sequential? part)
202 | (recur
203 | (into expanded
204 | (flatten-prefix-lists
205 | (cons
206 | (symbol (str prefix "." (first part)))
207 | (rest part))))
208 | more)
209 |
210 | :else
211 | (throw (ex-info "failed to parse ns form, unexpected prefix part" {:form form :part part}))))))))))))
212 |
213 | (defn parse-ns-require-part [state part]
214 | (cond
215 | (symbol? part)
216 | (update state :requires conj part)
217 |
218 | (sequential? part)
219 | (reduce
220 | (fn [state {:keys [require as-alias as refer rename] :as opts}]
221 | ;; non-loading, only [some.ns :as-alias foo], do not add to :requires
222 | (if (and (= 2 (count opts)) as-alias)
223 | (update state :require-aliases assoc as-alias require)
224 | (-> state
225 | (update :requires conj require)
226 | (cond->
227 | as
228 | (update :require-aliases assoc as require)
229 | as-alias
230 | (update :require-aliases assoc as-alias require)
231 | (seq refer)
232 | (reduce->
233 | ;; not constructing a {css shadow.css/css} qualified symbol here
234 | ;; since require may be a string in CLJS
235 | #(assoc-in %1 [:refer %2] {:require require :sym %2})
236 | refer)
237 | (seq rename)
238 | (reduce-kv->
239 | (fn [state from to]
240 | (update state :refer
241 | (fn [m]
242 | (-> m
243 | (cond->
244 | ;; only remove from refer if it came from same form
245 | ;; [a :refer (css)]
246 | ;; [b :refer (css) :rename {css c}]
247 | ;; FIXME: verify this is actually what clojure does?
248 | (contains? refer from)
249 | (dissoc from))
250 | (assoc to {:require require :sym from})))))
251 | rename)))))
252 | state
253 | (flatten-prefix-lists part))
254 |
255 | ;; ignore parts like :reload :reload-all etc
256 | :else
257 | state))
258 |
259 | (defn parse-ns-require-form [state [require-kw & parts]]
260 | (reduce parse-ns-require-part state parts))
261 |
262 | (defn parse-ns [state form]
263 | (let [[_ ns maybe-meta]
264 | form
265 |
266 | ;; FIXME: should this filter shadow.css/* already? don't really need other meta
267 | ns-meta
268 | (merge
269 | ;; don't care about the reader metadata, only added stuff
270 | (dissoc (meta ns) :source :line :column :end-line :end-column)
271 | (when (map? maybe-meta)
272 | maybe-meta))
273 |
274 | ns-requires
275 | (->> form
276 | (drop 2)
277 | (filter #(and (list? %) (= :require (first %)))))]
278 |
279 | (-> state
280 | (assoc :ns ns :ns-meta ns-meta)
281 | (reduce-> parse-ns-require-form ns-requires))))
282 |
283 | (defn parse-hiccup-kw [state kw]
284 | (let [s (name kw)
285 | start (str/index-of s ".")
286 | s (subs s (inc start))
287 | end (or (str/index-of s "#") (count s))
288 | s (subs s 0 end)]
289 |
290 | (update state :hiccup-classes into (str/split s #"\."))))
291 |
292 | (comment
293 | (parse-hiccup-kw {:hiccup-classes #{}} :div#foo.bar.baz)
294 | (parse-hiccup-kw {:hiccup-classes #{}} :div.foo.bar.baz#id))
295 |
296 | (defn find-css-calls [state form]
297 | (cond
298 | (map? form)
299 | (reduce find-css-calls state (vals form))
300 |
301 | (list? form)
302 | (case (first form)
303 | ;; (ns foo {:maybe "meta") ...)
304 | ns
305 | (parse-ns state form)
306 |
307 | ;; don't traverse into (comment ...)
308 | comment
309 | state
310 |
311 | ;; thing we actually look for
312 | ;; FIXME: make this use require-aliases/refers, should find aliased uses
313 | ;; FIXME: also make this extensible in some way so it can find
314 | ;; other forms that maybe expand to css via some macro
315 | css
316 | (update state :css conj
317 | (-> (meta form)
318 | (dissoc :source)
319 | ;; want [:px-4] instead of (css :px-4)
320 | ;; don't really care about the (css ...) part later
321 | ;; other forms also maybe won't have this
322 | (assoc :form (vec (rest form)))))
323 |
324 | ;; any other list
325 | (reduce find-css-calls state form))
326 |
327 | ;; potential hiccup vector
328 | (vector? form)
329 | (let [kw (first form)]
330 | (if (and (simple-keyword? kw) (str/index-of (name kw) "."))
331 | (-> state
332 | (parse-hiccup-kw kw)
333 | (reduce-> find-css-calls form))
334 |
335 | ;; vector not starting with keyword
336 | (reduce find-css-calls state form)))
337 |
338 | ;; sets, vectors
339 | (coll? form)
340 | (reduce find-css-calls state form)
341 |
342 | :else
343 | state))
344 |
345 | (defn find-css-in-source [{:keys [hiccup-sugar] :as build-state} src]
346 | ;; shortcut if src doesn't contain any css, faster than parsing all forms
347 | ;; still need to parse whole file when in hiccup-sugar mode, as there may not be any (css ...) uses
348 | (let [has-css? (str/index-of src "(css")
349 | reader (reader-types/source-logging-push-back-reader src)
350 | eof #?(:clj (Object.) :cljs (js-obj))]
351 |
352 | (loop [ns-found false
353 | state
354 | {:css []
355 | :ns nil
356 | :ns-meta {}
357 | :require-aliases {}
358 | :requires []
359 | :hiccup-classes #{}}]
360 |
361 | (let [form
362 | (binding
363 | [reader/*default-data-reader-fn*
364 | (fn [tag data] data)
365 |
366 | ;; used for ::alias/keywords
367 | reader/*alias-map*
368 | (fn [sym]
369 | (get (:require-aliases state) sym sym))
370 |
371 | ;; used for ::keywords
372 | ;; don't know actual ns until ns form is parsed
373 | *ns*
374 | (create-ns (or (:ns state) 'user))]
375 |
376 | (try
377 | (reader/read {:eof eof :read-cond :preserve} reader)
378 | (catch #?(:clj Exception :cljs :default) e
379 | (throw (ex-info "failed to parse ns" {:ns (:ns state)} e)))))]
380 |
381 | (if (identical? form eof)
382 | state
383 |
384 | (let [next-state (find-css-calls state form)]
385 | (cond
386 | (and (not ns-found) (not (:ns next-state)))
387 | ;; do not continue without ns form being first, just don't look for css
388 | next-state
389 |
390 | (and (not has-css?) (not hiccup-sugar))
391 | next-state
392 |
393 | :else
394 | (recur true next-state))))))))
--------------------------------------------------------------------------------
/src/main/shadow/css/build.cljc:
--------------------------------------------------------------------------------
1 | (ns shadow.css.build
2 | (:require
3 | [shadow.css.specs :as s]
4 | [shadow.css.analyzer :as ana]
5 | [clojure.set :as set]
6 | [clojure.string :as str]
7 | #?@(:clj
8 | [[clojure.edn :as edn]
9 | [clojure.java.io :as io]]
10 | :cljs
11 | [[cljs.reader :as edn]]))
12 | #?(:clj
13 | (:import [java.io File Writer StringWriter])
14 | :cljs
15 | (:import [goog.string StringBuffer])))
16 |
17 | (defn generate-spacing-aliases [{:keys [alias-groups spacing] :as build-state}]
18 | (update build-state :aliases
19 | (fn [aliases]
20 | (reduce-kv
21 | (fn [aliases space-num space-val]
22 | (reduce-kv
23 | (fn [aliases prefix props]
24 | (if (string? prefix)
25 | (assoc aliases (keyword (str prefix space-num)) (reduce #(assoc %1 %2 (if (= \- (first prefix))
26 | (str "-" space-val)
27 | space-val)) {} props))
28 | (let [[prefix sub-sel] prefix]
29 | (assoc aliases (keyword (str prefix space-num)) [[sub-sel (reduce #(assoc %1 %2 space-val) {} props)]])
30 | )))
31 | aliases
32 | (:spacing alias-groups)))
33 | aliases
34 | spacing))))
35 |
36 | (defn generate-color-aliases* [aliases alias-groups colors]
37 | (reduce
38 | (fn [aliases [color-name suffix color]]
39 | (reduce-kv
40 | (fn [aliases alias-prefix props]
41 | (assoc aliases
42 | (keyword (str alias-prefix color-name suffix))
43 | (cond
44 | (keyword? props)
45 | {props color}
46 |
47 | (fn? props)
48 | (props color)
49 |
50 | :else
51 | nil)))
52 | aliases
53 | (:color alias-groups)))
54 | aliases
55 | (for [[name vals] colors
56 | [suffix color] vals]
57 | [name suffix color])))
58 |
59 | (defn generate-color-aliases [{:keys [alias-groups colors] :as build-state}]
60 | (update build-state :aliases generate-color-aliases* alias-groups colors))
61 |
62 | ;; helper methods for eventual data collection for source mapping
63 | (defn emits
64 | #?(:clj
65 | ([^Writer w ^String s]
66 | (.write w s))
67 | :cljs
68 | ([w s]
69 | (.append w s)))
70 | ([w s & more]
71 | (emits w s)
72 | (doseq [s more]
73 | (emits w s))))
74 |
75 | (defn emitln
76 | #?(:clj
77 | ([^Writer w]
78 | (.write w "\n"))
79 | :cljs
80 | ([sb]
81 | (.append sb "\n")))
82 | ([w & args]
83 | (doseq [s args]
84 | (emits w s))
85 | (emitln w)))
86 |
87 | (defn emit-rule [w sel rules]
88 | (doseq [[group-sel group-rules] rules]
89 | (emitln w (str/replace group-sel #"&" sel) " {")
90 | (doseq [prop (sort (keys group-rules))]
91 | (emitln w " " (name prop) ": " (get group-rules prop) ";"))
92 | (emitln w "}")))
93 |
94 | (defn emit-def [w {:keys [sel rules at-rules ns line column rules] :as def}]
95 | ;; (emitln w (str "/* " ns " " line ":" column " */"))
96 |
97 | (emit-rule w sel rules)
98 |
99 | (doseq [[nesting rules] at-rules]
100 |
101 | (doseq [lvl nesting]
102 | (emitln w lvl " {"))
103 | (emit-rule w sel rules)
104 | (doseq [_ nesting]
105 | (emitln w "}"))))
106 |
107 | (defn build-css-for-chunk [build-state chunk-id]
108 | (update-in build-state [:chunks chunk-id]
109 | (fn [{:keys [base rules classpath-includes] :as chunk}]
110 | (assoc chunk
111 | :css
112 | (let [sw #?(:clj (StringWriter.) :cljs (StringBuffer.))]
113 | (when base
114 | (when-let [pre (:preflight-src build-state)]
115 | (emitln sw pre)))
116 |
117 | #?@(:clj
118 | [(doseq [inc classpath-includes]
119 | (emitln sw (slurp (io/resource inc))))])
120 |
121 | (doseq [def rules]
122 | (emit-def sw def))
123 | (.toString sw))))))
124 |
125 | (defn collect-namespaces-for-chunk
126 | [{:keys [include entries] :as chunk} {:keys [namespaces] :as build-state}]
127 | (let [namespace-matchers
128 | (->> include
129 | (map (fn [x]
130 | (cond
131 | (string? x)
132 | (let [re (re-pattern x)]
133 | #(re-find re (name %)))
134 |
135 | (not (symbol? x))
136 | (throw (ex-info "invalid include pattern" {:x x}))
137 |
138 | :else
139 | (let [s (str x)]
140 | ;; FIXME: allow more patterns that can be expressed as string?
141 | ;; foo.bar.*.views?
142 |
143 | (if (str/ends-with? s "*")
144 | ;; foo.bar.* - prefix match
145 | (let [prefix (subs s 0 (-> s count dec))]
146 | (fn [ns]
147 | (str/starts-with? (str ns) prefix)))
148 |
149 | ;; exact match
150 | (fn [ns]
151 | (= x ns))
152 | )))))
153 | (into []))
154 |
155 |
156 | {entry-namespaces :namespaces}
157 | (reduce
158 | (fn step-fn [{:keys [visited] :as m} ns]
159 | (cond
160 | (contains? visited ns)
161 | m
162 |
163 | ;; npm support later
164 | (string? ns)
165 | m
166 |
167 | (str/includes? (str ns) "*")
168 | (throw (ex-info ":entries only takes full namespace names, not wildcards" {:ns ns}))
169 |
170 | :else
171 | (let [ns-info (get namespaces ns)]
172 | (-> m
173 | (update :namespaces conj ns)
174 | (update :visited conj ns)
175 | (ana/reduce-> step-fn (:requires ns-info))))))
176 |
177 | {:visited #{}
178 | :namespaces #{}}
179 | entries)
180 |
181 | included-namespaces
182 | (->> (keys namespaces)
183 | (filter (fn [ns]
184 | (or (contains? entry-namespaces ns)
185 | (some (fn [matcher] (matcher ns)) namespace-matchers))))
186 | (into []))]
187 |
188 | (assoc chunk :namespaces included-namespaces)))
189 |
190 | (defn build-css-for-chunks
191 | [{:keys [namespaces] :as build-state}]
192 | (reduce-kv
193 | (fn [build-state chunk-id chunk]
194 | (let [all-rules
195 | (->> (for [ns (:chunk-namespaces chunk)
196 | :let [{:keys [ns ns-meta css] :as ns-info} (get namespaces ns)]
197 | {:keys [line column] :as form-info} css
198 | :let [css-id (s/generate-id ns ns-meta line column)]]
199 | (-> (ana/process-form build-state form-info)
200 | (assoc
201 | :ns ns
202 | :css-id css-id
203 | ;; FIXME: when adding optimization pass selector won't be based on css-id anymore
204 | :sel (str "." css-id))))
205 | (into []))
206 |
207 | cp-includes
208 | (into #{} (for [ns (:chunk-namespaces chunk)
209 | :let [{:keys [ns-meta]} (get namespaces ns)]
210 | include (:shadow.css/include ns-meta)]
211 | (do (when-not (io/resource include)
212 | (throw (ex-info
213 | (str "css include \"" include "\" could not be found on the classpath, it was included in namespace " ns)
214 | {:ns ns
215 | :include include})))
216 | include)))
217 |
218 | warnings
219 | (vec
220 | (for [{:keys [warnings ns line column]} all-rules
221 | warning warnings]
222 | (assoc warning :ns ns :line line :column column)))]
223 |
224 | (-> build-state
225 | (update-in [:chunks chunk-id] assoc
226 | :warnings warnings
227 | :classpath-includes cp-includes
228 | :rules all-rules)
229 | (build-css-for-chunk chunk-id))))
230 |
231 | build-state
232 | (:chunks build-state)))
233 |
234 | (defn build-hiccup-sugar [{:keys [namespaces base-chunk aliases] :as build-state}]
235 | ;; FIXME: this could be optimized to emit to do this per chunk
236 | ;; but for now keeping everything in the base chunk
237 | (let [hiccup-classes
238 | (into #{}
239 | (for [chunk (vals (:chunks build-state))
240 | ns (:chunk-namespaces chunk)
241 | class (get-in namespaces [ns :hiccup-classes])]
242 | class))
243 |
244 | hiccup-css
245 | (let [sw #?(:clj (StringWriter.) :cljs (StringBuffer.))]
246 | (doseq [class hiccup-classes
247 | :let [hiccup-kw (keyword class)
248 | rules (get aliases hiccup-kw)]
249 | :when rules]
250 |
251 | (let [def (-> {:sel "&" :block [] :rules {} :at-rules {} :warnings []}
252 | (ana/reduce-> #(ana/add-part build-state %1 %2) [hiccup-kw])
253 | (assoc :sel (str "." class)))]
254 | (emit-def sw def)))
255 | (.toString sw))]
256 |
257 | (-> build-state
258 | (assoc :hiccup-classes hiccup-classes :hiccup-css hiccup-css)
259 | (update-in [:chunks base-chunk :css] str "\n" hiccup-css))))
260 |
261 | (defn trim-chunks [build-state]
262 | (update build-state :chunks
263 | (fn [chunks]
264 | (reduce-kv
265 | (fn [chunks chunk-id {:keys [depends-on namespaces] :as chunk}]
266 | (if-not (seq depends-on)
267 | (assoc-in chunks [chunk-id :chunk-namespaces] (set namespaces))
268 | (let [provided-by-deps
269 | (reduce
270 | (fn step-fn [ns-set module-id]
271 | (let [{:keys [namespaces] :as other} (get chunks module-id)]
272 | (-> ns-set
273 | (set/union (set namespaces))
274 | (ana/reduce-> step-fn (:depends-on other)))))
275 | #{}
276 | depends-on)]
277 | (assoc-in chunks [chunk-id :chunk-namespaces] (set/difference (set namespaces) provided-by-deps))
278 | )))
279 | chunks
280 | chunks))))
281 |
282 | (defn generate [build-state chunks]
283 | ;; FIXME: actually support chunks, similar to CLJS with :depends-on #{:other-chunk}
284 | ;; so chunks don't repeat everything, for that needs to analyze chunks first
285 | ;; then produce output
286 | (-> build-state
287 | (assoc :chunks {})
288 | (ana/reduce-kv->
289 | (fn [build-state chunk-id chunk]
290 | (let [base?
291 | (not (contains? chunk :depends-on))
292 |
293 | chunk
294 | (-> chunk
295 | (assoc :chunk-id chunk-id)
296 | (cond->
297 | base?
298 | (assoc :base true))
299 | (collect-namespaces-for-chunk build-state))]
300 |
301 | (-> build-state
302 | (assoc-in [:chunks chunk-id] chunk)
303 | (cond->
304 | base?
305 | (assoc :base-chunk chunk-id)))))
306 | chunks)
307 | (trim-chunks)
308 | (build-css-for-chunks)
309 | (cond->
310 | (:hiccup-sugar build-state)
311 | (build-hiccup-sugar))))
312 |
313 | ;; simplistic regexp based css minifier
314 | ;; it'll destroy some stuff for sure
315 | ;; but for now it seems to be ok and doesn't require parsing css
316 | (defn minify-chunk [chunk]
317 | #?(:cljs
318 | ;; FIXME: I don't know why the below regexp breaks in JS, look into it
319 | ;; currently only using JS variant in self-hosted grove examples app, which doesn't minify anyways
320 | chunk
321 | :clj
322 | (update chunk :css
323 | (fn [css]
324 | (-> css
325 | ;; collapse multiple whitespace to one first
326 | (str/replace #"\s+" " ")
327 | ;; remove comments
328 | (str/replace #"\/\*(.*?)\*\/" "")
329 | ;; remove a few more whitespace
330 | (str/replace #"\s\{\s" "{")
331 | (str/replace #";\s+\}\s*" "}")
332 | (str/replace #";\s+" ";")
333 | (str/replace #":\s+" ":")
334 | (str/replace #"\s*,\s*" ",")
335 | )))))
336 |
337 | (defn minify [build-state]
338 | (update build-state :chunks update-vals minify-chunk))
339 |
340 | (defn index-source [build-state src]
341 | (let [{:keys [ns] :as contents}
342 | (ana/find-css-in-source build-state src)]
343 |
344 | (if (not contents)
345 | build-state
346 | ;; index every namespace so we can follow requires properly
347 | ;; without anything else having to parse everything again
348 | ;; even though :css might be empty
349 | (assoc-in build-state [:namespaces ns] contents))))
350 |
351 | (defn init []
352 | {:namespaces
353 | {}
354 |
355 | :alias-groups
356 | ;; same naming patterns tailwind uses
357 | {:color
358 | {"bg-" :background-color
359 | "border-" :border-color
360 | "outline-" :outline-color
361 | "stroke-" :stroke
362 | "fill-" :fill
363 | "text-" :color
364 | "divide-"
365 | (fn [color]
366 | [["& > * + *" {:border-color color}]])}
367 |
368 | :spacing
369 | ;; padding
370 | {"p-" [:padding]
371 | "px-" [:padding-left :padding-right]
372 | "py-" [:padding-top :padding-bottom]
373 | "pt-" [:padding-top]
374 | "pb-" [:padding-bottom]
375 | "pl-" [:padding-left]
376 | "pr-" [:padding-right]
377 |
378 | ;; margin
379 | "m-" [:margin]
380 | "mx-" [:margin-left :margin-right]
381 | "my-" [:margin-top :margin-bottom]
382 | "mt-" [:margin-top]
383 | "mb-" [:margin-bottom]
384 | "ml-" [:margin-left]
385 | "mr-" [:margin-right]
386 |
387 | ;; positions
388 | "top-" [:top]
389 | "right-" [:right]
390 | "bottom-" [:bottom]
391 | "left-" [:left]
392 | "-top-" [:top]
393 | "-right-" [:right]
394 | "-bottom-" [:bottom]
395 | "-left-" [:left]
396 |
397 | "inset-x-" [:left :right]
398 | "inset-y-" [:top :bottom]
399 | "inset-" [:top :right :bottom :left]
400 | "-inset-x-" [:left :right]
401 | "-inset-y-" [:top :bottom]
402 | "-inset-" [:top :right :bottom :left]
403 |
404 | ;; width
405 | "w-" [:width]
406 | "max-w-" [:max-width]
407 | "min-w-" [:min-width]
408 |
409 | ;; height
410 | "h-" [:height]
411 | "max-h-" [:max-height]
412 | "min-h-" [:min-height]
413 |
414 | ;; flex
415 | "basis-" [:flex-basis]
416 |
417 | ;; grid
418 | "gap-" [:gap]
419 | "gap-x-" [:column-gap]
420 | "gap-y-" [:row-gap]
421 |
422 | ["space-x-" "& > * + *"] [:padding-left :padding-right]
423 | ["space-y-" "& > * + *"] [:padding-top :padding-bottom]}}
424 |
425 | :aliases
426 | {}
427 |
428 | ;; https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
429 | :spacing
430 | {0 "0"
431 | 0.5 "0.125rem"
432 | 1 "0.25rem"
433 | 1.5 "0.375rem"
434 | 2 "0.5rem"
435 | 2.5 "0.625rem"
436 | 3 "0.75rem"
437 | 3.5 "0.875rem"
438 | 4 "1rem"
439 | 5 "1.25rem"
440 | 6 "1.5rem"
441 | 7 "1.75rem"
442 | 8 "2rem"
443 | 9 "2.25rem"
444 | 10 "2.5rem"
445 | 11 "2.75rem"
446 | 12 "3rem"
447 | 13 "3.25rem"
448 | 14 "3.5rem"
449 | 15 "3.75rem"
450 | 16 "4rem"
451 | 17 "4.25rem"
452 | 18 "4.5rem"
453 | 19 "4.75rem"
454 | 20 "5rem"
455 | 24 "6rem"
456 | 28 "7rem"
457 | 32 "8rem"
458 | 36 "9rem"
459 | 40 "10rem"
460 | 44 "11rem"
461 | 48 "12rem"
462 | 52 "13rem"
463 | 56 "14rem"
464 | 60 "15rem"
465 | 64 "16rem"
466 | 68 "17rem"
467 | 72 "18rem"
468 | 76 "19rem"
469 | 80 "20rem"
470 | 96 "24rem"
471 | "none" "none"
472 | "px" "1px"
473 | "full" "100%"
474 | "3xs" "16rem"
475 | "2xs" "18rem"
476 | "xs" "20rem"
477 | "sm" "24rem"
478 | "md" "28rem"
479 | "lg" "32rem"
480 | "xl" "36rem"
481 | "2xl" "42rem"
482 | "3xl" "48rem"
483 | "4xl" "56rem"
484 | "5xl" "64rem"
485 | "6xl" "72rem"
486 | "7xl" "80rem"
487 | }})
488 |
489 | ;; IO stuff not available in CLJS environments
490 |
491 | #?(:clj
492 | (do (defn clj-file? [filename]
493 | ;; .clj .cljs .cljc .cljd
494 | (re-matches #".+\.clj[cs]?$" filename))
495 |
496 | (defn index-file [build-state ^File file]
497 | (let [src (slurp file)]
498 | (index-source build-state src)))
499 |
500 | (defn index-path
501 | [build-state ^File root config]
502 | (let [files
503 | (->> root
504 | (file-seq)
505 | (filter #(clj-file? (.getName ^File %)))
506 | (remove #(.isHidden ^File %)))]
507 |
508 | ;; FIXME: reducers/parallel?
509 | ;; takes ~80ms for entire shadow-cljs codebase which is fine
510 | ;; but also doesn't contain many sources with css, could be slow on bigger frontend projects
511 | ;; this can easily spread work in threads, just needs to merge namespaces after
512 | (reduce index-file build-state files)))
513 |
514 | (defn safe-pr-str
515 | "cider globally sets *print-length* for the nrepl-session which messes with pr-str when used to print cache or other files"
516 | [x]
517 | (binding [*print-length* nil
518 | *print-level* nil
519 | *print-namespace-maps* nil
520 | *print-meta* nil]
521 | (pr-str x)))
522 |
523 | (defn write-index-to [{:keys [namespaces] :as build-state} ^File output-to]
524 | (io/make-parents output-to)
525 | (spit output-to (safe-pr-str {:version 1 :namespaces namespaces}))
526 | build-state)
527 |
528 | (defn write-outputs-to [build-state ^File output-dir]
529 | (reduce-kv
530 | (fn [_ chunk-id {:keys [css] :as chunk}]
531 | (let [output-file (io/file output-dir (str (name chunk-id) ".css"))]
532 | (io/make-parents output-file)
533 | (spit output-file css)))
534 | nil
535 | (:chunks build-state))
536 |
537 | build-state)
538 |
539 | (defn load-indexes-from-classpath [build-state]
540 | (reduce
541 | (fn [build-state url]
542 | (let [{:keys [version namespaces] :as contents}
543 | (-> (slurp url)
544 | (edn/read-string))]
545 |
546 | ;; FIXME: validate version?
547 |
548 | (-> build-state
549 | (assoc-in [:sources url] contents)
550 | (update :namespaces merge namespaces))))
551 | build-state
552 | (-> (Thread/currentThread)
553 | (.getContextClassLoader)
554 | (.getResources "shadow-css-index.edn")
555 | (enumeration-seq))))
556 |
557 | (defn merge-left [left right]
558 | (merge right left))
559 |
560 | (defn load-default-aliases-from-classpath [build-state]
561 | (update build-state :aliases merge-left (edn/read-string (slurp (io/resource "shadow/css/aliases.edn")))))
562 |
563 | (defn load-colors-from-classpath [build-state]
564 | (update build-state :colors merge-left (edn/read-string (slurp (io/resource "shadow/css/colors.edn")))))
565 |
566 | (defn load-preflight-from-classpath [build-state]
567 | (assoc build-state
568 | :preflight-src
569 | (slurp (io/resource "shadow/css/preflight.css"))))
570 |
571 | (defn start
572 | ([]
573 | (start (init)))
574 | ([build-state]
575 | (-> build-state
576 | (load-preflight-from-classpath)
577 | (load-default-aliases-from-classpath)
578 | (load-colors-from-classpath)
579 | (load-indexes-from-classpath)
580 | (generate-color-aliases)
581 | (generate-spacing-aliases))))))
--------------------------------------------------------------------------------
/src/main/shadow/css/colors.edn:
--------------------------------------------------------------------------------
1 | ;; tailwind colors https://tailwindcss.com/docs/customizing-colors
2 | ;; taken from https://github.com/tailwindlabs/tailwindcss/blob/master/src/public/colors.js
3 |
4 | {"inherit"
5 | {"" "inherit"}
6 | "current"
7 | {"" "currentColor"}
8 | "transparent"
9 | {"" "transparent"}
10 | "black"
11 | {"" "#000"}
12 | "white"
13 | {"" "#fff"}
14 | "slate"
15 | {"-50" "#f8fafc"
16 | "-100" "#f1f5f9"
17 | "-200" "#e2e8f0"
18 | "-300" "#cbd5e1"
19 | "-400" "#94a3b8"
20 | "-500" "#64748b"
21 | "-600" "#475569"
22 | "-700" "#334155"
23 | "-800" "#1e293b"
24 | "-900" "#0f172a"}
25 | "gray"
26 | {"-50" "#f9fafb"
27 | "-100" "#f3f4f6"
28 | "-200" "#e5e7eb"
29 | "-300" "#d1d5db"
30 | "-400" "#9ca3af"
31 | "-500" "#6b7280"
32 | "-600" "#4b5563"
33 | "-700" "#374151"
34 | "-800" "#1f2937"
35 | "-900" "#111827"}
36 | "zinc"
37 | {"-50" "#fafafa"
38 | "-100" "#f4f4f5"
39 | "-200" "#e4e4e7"
40 | "-300" "#d4d4d8"
41 | "-400" "#a1a1aa"
42 | "-500" "#71717a"
43 | "-600" "#52525b"
44 | "-700" "#3f3f46"
45 | "-800" "#27272a"
46 | "-900" "#18181b"}
47 | "neutral"
48 | {"-50" "#fafafa"
49 | "-100" "#f5f5f5"
50 | "-200" "#e5e5e5"
51 | "-300" "#d4d4d4"
52 | "-400" "#a3a3a3"
53 | "-500" "#737373"
54 | "-600" "#525252"
55 | "-700" "#404040"
56 | "-800" "#262626"
57 | "-900" "#171717"}
58 | "stone"
59 | {"-50" "#fafaf9"
60 | "-100" "#f5f5f4"
61 | "-200" "#e7e5e4"
62 | "-300" "#d6d3d1"
63 | "-400" "#a8a29e"
64 | "-500" "#78716c"
65 | "-600" "#57534e"
66 | "-700" "#44403c"
67 | "-800" "#292524"
68 | "-900" "#1c1917"}
69 | "red"
70 | {"-50" "#fef2f2"
71 | "-100" "#fee2e2"
72 | "-200" "#fecaca"
73 | "-300" "#fca5a5"
74 | "-400" "#f87171"
75 | "-500" "#ef4444"
76 | "-600" "#dc2626"
77 | "-700" "#b91c1c"
78 | "-800" "#991b1b"
79 | "-900" "#7f1d1d"}
80 | "orange"
81 | {"-50" "#fff7ed"
82 | "-100" "#ffedd5"
83 | "-200" "#fed7aa"
84 | "-300" "#fdba74"
85 | "-400" "#fb923c"
86 | "-500" "#f97316"
87 | "-600" "#ea580c"
88 | "-700" "#c2410c"
89 | "-800" "#9a3412"
90 | "-900" "#7c2d12"}
91 | "amber"
92 | {"-50" "#fffbeb"
93 | "-100" "#fef3c7"
94 | "-200" "#fde68a"
95 | "-300" "#fcd34d"
96 | "-400" "#fbbf24"
97 | "-500" "#f59e0b"
98 | "-600" "#d97706"
99 | "-700" "#b45309"
100 | "-800" "#92400e"
101 | "-900" "#78350f"}
102 | "yellow"
103 | {"-50" "#fefce8"
104 | "-100" "#fef9c3"
105 | "-200" "#fef08a"
106 | "-300" "#fde047"
107 | "-400" "#facc15"
108 | "-500" "#eab308"
109 | "-600" "#ca8a04"
110 | "-700" "#a16207"
111 | "-800" "#854d0e"
112 | "-900" "#713f12"}
113 | "lime"
114 | {"-50" "#f7fee7"
115 | "-100" "#ecfccb"
116 | "-200" "#d9f99d"
117 | "-300" "#bef264"
118 | "-400" "#a3e635"
119 | "-500" "#84cc16"
120 | "-600" "#65a30d"
121 | "-700" "#4d7c0f"
122 | "-800" "#3f6212"
123 | "-900" "#365314"}
124 | "green"
125 | {"-50" "#f0fdf4"
126 | "-100" "#dcfce7"
127 | "-200" "#bbf7d0"
128 | "-300" "#86efac"
129 | "-400" "#4ade80"
130 | "-500" "#22c55e"
131 | "-600" "#16a34a"
132 | "-700" "#15803d"
133 | "-800" "#166534"
134 | "-900" "#14532d"}
135 | "emerald"
136 | {"-50" "#ecfdf5"
137 | "-100" "#d1fae5"
138 | "-200" "#a7f3d0"
139 | "-300" "#6ee7b7"
140 | "-400" "#34d399"
141 | "-500" "#10b981"
142 | "-600" "#059669"
143 | "-700" "#047857"
144 | "-800" "#065f46"
145 | "-900" "#064e3b"}
146 | "teal"
147 | {"-50" "#f0fdfa"
148 | "-100" "#ccfbf1"
149 | "-200" "#99f6e4"
150 | "-300" "#5eead4"
151 | "-400" "#2dd4bf"
152 | "-500" "#14b8a6"
153 | "-600" "#0d9488"
154 | "-700" "#0f766e"
155 | "-800" "#115e59"
156 | "-900" "#134e4a"}
157 | "cyan"
158 | {"-50" "#ecfeff"
159 | "-100" "#cffafe"
160 | "-200" "#a5f3fc"
161 | "-300" "#67e8f9"
162 | "-400" "#22d3ee"
163 | "-500" "#06b6d4"
164 | "-600" "#0891b2"
165 | "-700" "#0e7490"
166 | "-800" "#155e75"
167 | "-900" "#164e63"}
168 | "sky"
169 | {"-50" "#f0f9ff"
170 | "-100" "#e0f2fe"
171 | "-200" "#bae6fd"
172 | "-300" "#7dd3fc"
173 | "-400" "#38bdf8"
174 | "-500" "#0ea5e9"
175 | "-600" "#0284c7"
176 | "-700" "#0369a1"
177 | "-800" "#075985"
178 | "-900" "#0c4a6e"}
179 | "blue"
180 | {"-50" "#eff6ff"
181 | "-100" "#dbeafe"
182 | "-200" "#bfdbfe"
183 | "-300" "#93c5fd"
184 | "-400" "#60a5fa"
185 | "-500" "#3b82f6"
186 | "-600" "#2563eb"
187 | "-700" "#1d4ed8"
188 | "-800" "#1e40af"
189 | "-900" "#1e3a8a"}
190 | "indigo"
191 | {"-50" "#eef2ff"
192 | "-100" "#e0e7ff"
193 | "-200" "#c7d2fe"
194 | "-300" "#a5b4fc"
195 | "-400" "#818cf8"
196 | "-500" "#6366f1"
197 | "-600" "#4f46e5"
198 | "-700" "#4338ca"
199 | "-800" "#3730a3"
200 | "-900" "#312e81"}
201 | "violet"
202 | {"-50" "#f5f3ff"
203 | "-100" "#ede9fe"
204 | "-200" "#ddd6fe"
205 | "-300" "#c4b5fd"
206 | "-400" "#a78bfa"
207 | "-500" "#8b5cf6"
208 | "-600" "#7c3aed"
209 | "-700" "#6d28d9"
210 | "-800" "#5b21b6"
211 | "-900" "#4c1d95"}
212 | "purple"
213 | {"-50" "#faf5ff"
214 | "-100" "#f3e8ff"
215 | "-200" "#e9d5ff"
216 | "-300" "#d8b4fe"
217 | "-400" "#c084fc"
218 | "-500" "#a855f7"
219 | "-600" "#9333ea"
220 | "-700" "#7e22ce"
221 | "-800" "#6b21a8"
222 | "-900" "#581c87"}
223 | "fuchsia"
224 | {"-50" "#fdf4ff"
225 | "-100" "#fae8ff"
226 | "-200" "#f5d0fe"
227 | "-300" "#f0abfc"
228 | "-400" "#e879f9"
229 | "-500" "#d946ef"
230 | "-600" "#c026d3"
231 | "-700" "#a21caf"
232 | "-800" "#86198f"
233 | "-900" "#701a75"}
234 | "pink"
235 | {"-50" "#fdf2f8"
236 | "-100" "#fce7f3"
237 | "-200" "#fbcfe8"
238 | "-300" "#f9a8d4"
239 | "-400" "#f472b6"
240 | "-500" "#ec4899"
241 | "-600" "#db2777"
242 | "-700" "#be185d"
243 | "-800" "#9d174d"
244 | "-900" "#831843"}
245 | "rose"
246 | {"-50" "#fff1f2"
247 | "-100" "#ffe4e6"
248 | "-200" "#fecdd3"
249 | "-300" "#fda4af"
250 | "-400" "#fb7185"
251 | "-500" "#f43f5e"
252 | "-600" "#e11d48"
253 | "-700" "#be123c"
254 | "-800" "#9f1239"
255 | "-900" "#881337"}}
--------------------------------------------------------------------------------
/src/main/shadow/css/preflight.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | https://tailwindcss.com/docs/preflight
4 |
5 | based on https://unpkg.com/tailwindcss@3.1.6/src/css/preflight.css
6 |
7 | taken as is, only removed the tailwind specific theme functions
8 | dunno what to do about those yet
9 |
10 | */
11 |
12 | /*
13 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
14 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
15 | */
16 |
17 | *,
18 | ::before,
19 | ::after {
20 | box-sizing: border-box; /* 1 */
21 | border-width: 0; /* 2 */
22 | border-style: solid; /* 2 */
23 | border-color: #e5e7eb; /* 2 */
24 | }
25 |
26 | /*
27 | 1. Use a consistent sensible line-height in all browsers.
28 | 2. Prevent adjustments of font size after orientation changes in iOS.
29 | 3. Use a more readable tab size.
30 | 4. Use the user's configured `sans` font-family by default.
31 | */
32 |
33 | html {
34 | line-height: 1.5; /* 1 */
35 | -webkit-text-size-adjust: 100%; /* 2 */
36 | -moz-tab-size: 4; /* 3 */
37 | tab-size: 4; /* 3 */
38 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
39 | }
40 |
41 | /*
42 | 1. Remove the margin in all browsers.
43 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
44 | */
45 |
46 | body {
47 | margin: 0; /* 1 */
48 | line-height: inherit; /* 2 */
49 | }
50 |
51 | /*
52 | 1. Add the correct height in Firefox.
53 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
54 | 3. Ensure horizontal rules are visible by default.
55 | */
56 |
57 | hr {
58 | height: 0; /* 1 */
59 | color: inherit; /* 2 */
60 | border-top-width: 1px; /* 3 */
61 | }
62 |
63 | /*
64 | Add the correct text decoration in Chrome, Edge, and Safari.
65 | */
66 |
67 | abbr:where([title]) {
68 | text-decoration: underline dotted;
69 | }
70 |
71 | /*
72 | Remove the default font size and weight for headings.
73 | */
74 |
75 | h1,
76 | h2,
77 | h3,
78 | h4,
79 | h5,
80 | h6 {
81 | font-size: inherit;
82 | font-weight: inherit;
83 | }
84 |
85 | /*
86 | Reset links to optimize for opt-in styling instead of opt-out.
87 | */
88 |
89 | a {
90 | color: inherit;
91 | text-decoration: inherit;
92 | }
93 |
94 | /*
95 | Add the correct font weight in Edge and Safari.
96 | */
97 |
98 | b,
99 | strong {
100 | font-weight: bolder;
101 | }
102 |
103 | /*
104 | 1. Use the user's configured `mono` font family by default.
105 | 2. Correct the odd `em` font sizing in all browsers.
106 | */
107 |
108 | code,
109 | kbd,
110 | samp,
111 | pre {
112 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
113 | font-size: 1em; /* 2 */
114 | }
115 |
116 | /*
117 | Add the correct font size in all browsers.
118 | */
119 |
120 | small {
121 | font-size: 80%;
122 | }
123 |
124 | /*
125 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
126 | */
127 |
128 | sub,
129 | sup {
130 | font-size: 75%;
131 | line-height: 0;
132 | position: relative;
133 | vertical-align: baseline;
134 | }
135 |
136 | sub {
137 | bottom: -0.25em;
138 | }
139 |
140 | sup {
141 | top: -0.5em;
142 | }
143 |
144 | /*
145 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
146 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
147 | 3. Remove gaps between table borders by default.
148 | */
149 |
150 | table {
151 | text-indent: 0; /* 1 */
152 | border-color: inherit; /* 2 */
153 | border-collapse: collapse; /* 3 */
154 | }
155 |
156 | /*
157 | 1. Change the font styles in all browsers.
158 | 2. Remove the margin in Firefox and Safari.
159 | 3. Remove default padding in all browsers.
160 | */
161 |
162 | button,
163 | input,
164 | optgroup,
165 | select,
166 | textarea {
167 | font-family: inherit; /* 1 */
168 | font-size: 100%; /* 1 */
169 | font-weight: inherit; /* 1 */
170 | line-height: inherit; /* 1 */
171 | color: inherit; /* 1 */
172 | margin: 0; /* 2 */
173 | padding: 0; /* 3 */
174 | }
175 |
176 | /*
177 | Remove the inheritance of text transform in Edge and Firefox.
178 | */
179 |
180 | button,
181 | select {
182 | text-transform: none;
183 | }
184 |
185 | /*
186 | 1. Correct the inability to style clickable types in iOS and Safari.
187 | 2. Remove default button styles.
188 | */
189 |
190 | button,
191 | [type='button'],
192 | [type='reset'],
193 | [type='submit'] {
194 | -webkit-appearance: button; /* 1 */
195 | background-color: transparent; /* 2 */
196 | background-image: none; /* 2 */
197 | }
198 |
199 | /*
200 | Use the modern Firefox focus style for all focusable elements.
201 | */
202 |
203 | :-moz-focusring {
204 | outline: auto;
205 | }
206 |
207 | /*
208 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
209 | */
210 |
211 | :-moz-ui-invalid {
212 | box-shadow: none;
213 | }
214 |
215 | /*
216 | Add the correct vertical alignment in Chrome and Firefox.
217 | */
218 |
219 | progress {
220 | vertical-align: baseline;
221 | }
222 |
223 | /*
224 | Correct the cursor style of increment and decrement buttons in Safari.
225 | */
226 |
227 | ::-webkit-inner-spin-button,
228 | ::-webkit-outer-spin-button {
229 | height: auto;
230 | }
231 |
232 | /*
233 | 1. Correct the odd appearance in Chrome and Safari.
234 | 2. Correct the outline style in Safari.
235 | */
236 |
237 | [type='search'] {
238 | -webkit-appearance: textfield; /* 1 */
239 | outline-offset: -2px; /* 2 */
240 | }
241 |
242 | /*
243 | Remove the inner padding in Chrome and Safari on macOS.
244 | */
245 |
246 | ::-webkit-search-decoration {
247 | -webkit-appearance: none;
248 | }
249 |
250 | /*
251 | 1. Correct the inability to style clickable types in iOS and Safari.
252 | 2. Change font properties to `inherit` in Safari.
253 | */
254 |
255 | ::-webkit-file-upload-button {
256 | -webkit-appearance: button; /* 1 */
257 | font: inherit; /* 2 */
258 | }
259 |
260 | /*
261 | Add the correct display in Chrome and Safari.
262 | */
263 |
264 | summary {
265 | display: list-item;
266 | }
267 |
268 | /*
269 | Removes the default spacing and border for appropriate elements.
270 | */
271 |
272 | blockquote,
273 | dl,
274 | dd,
275 | h1,
276 | h2,
277 | h3,
278 | h4,
279 | h5,
280 | h6,
281 | hr,
282 | figure,
283 | p,
284 | pre {
285 | margin: 0;
286 | }
287 |
288 | fieldset {
289 | margin: 0;
290 | padding: 0;
291 | }
292 |
293 | legend {
294 | padding: 0;
295 | }
296 |
297 | ol,
298 | ul,
299 | menu {
300 | list-style: none;
301 | margin: 0;
302 | padding: 0;
303 | }
304 |
305 | /*
306 | Prevent resizing textareas horizontally by default.
307 | */
308 |
309 | textarea {
310 | resize: vertical;
311 | }
312 |
313 | /*
314 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
315 | 2. Set the default placeholder color to the user's configured gray 400 color.
316 | */
317 |
318 | input::placeholder,
319 | textarea::placeholder {
320 | opacity: 1; /* 1 */
321 | color: #9ca3af; /* 2 */
322 | }
323 |
324 | /*
325 | Set the default cursor for buttons.
326 | */
327 |
328 | button,
329 | [role="button"] {
330 | cursor: pointer;
331 | }
332 |
333 | /*
334 | Make sure disabled buttons don't get the pointer cursor.
335 | */
336 | :disabled {
337 | cursor: default;
338 | }
339 |
340 | /*
341 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
342 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
343 | This can trigger a poorly considered lint error in some tools but is included by design.
344 | */
345 |
346 | img,
347 | svg,
348 | video,
349 | canvas,
350 | audio,
351 | iframe,
352 | embed,
353 | object {
354 | display: block; /* 1 */
355 | vertical-align: middle; /* 2 */
356 | }
357 |
358 | /*
359 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
360 | */
361 |
362 | img,
363 | video {
364 | max-width: 100%;
365 | height: auto;
366 | }
--------------------------------------------------------------------------------
/src/main/shadow/css/specs.cljc:
--------------------------------------------------------------------------------
1 | (ns shadow.css.specs
2 | (:require [clojure.spec.alpha :as s]
3 | [clojure.string :as str]))
4 |
5 | (s/def ::alias keyword?)
6 |
7 | (defn non-empty-string? [s]
8 | (and (string? s) (not (str/blank? s))))
9 |
10 | (s/def ::str-part
11 | ;; str or alias
12 | #(or (non-empty-string? %) (keyword? %)))
13 |
14 | (s/def ::str-concat
15 | (s/coll-of ::str-part :kind list?))
16 |
17 | (s/def ::val
18 | (s/or
19 | :string non-empty-string?
20 | :number number?
21 | :alias ::alias
22 | :concat ::str-concat))
23 |
24 | (s/def ::passthrough
25 | non-empty-string?)
26 |
27 | (s/def ::map
28 | (s/map-of simple-keyword? ::val))
29 |
30 | (defn sub-selector? [x]
31 | (and (non-empty-string? x)
32 | (or (str/starts-with? x "@")
33 | (str/index-of x "&"))))
34 |
35 | (s/def ::group-selector
36 | (s/or
37 | :string
38 | sub-selector?
39 | :alias
40 | ::alias
41 | ))
42 |
43 | (s/def ::group
44 | (s/and
45 | vector?
46 | (s/cat
47 | :sel
48 | ::group-selector
49 | :parts
50 | (s/+ ::part))))
51 |
52 | (s/def ::part
53 | (s/or
54 | :alias
55 | ::alias
56 |
57 | :map
58 | ::map
59 |
60 | :group
61 | ::group))
62 |
63 | (s/def ::root-part
64 | (s/or
65 | :alias
66 | ::alias
67 |
68 | :passthrough
69 | ::passthrough
70 |
71 | :map
72 | ::map
73 |
74 | :group
75 | ::group))
76 |
77 | (s/def ::class-def
78 | (s/cat
79 | :parts
80 | (s/+ ::root-part)))
81 |
82 | (defn conform! [[_ & body :as form]]
83 | (let [conformed (s/conform ::class-def body)]
84 | (when (= conformed ::s/invalid)
85 | (throw (ex-info "failed to parse class definition"
86 | (assoc (s/explain-data ::class-def body)
87 | :tag ::invalid-class-def
88 | :input body))))
89 | conformed))
90 |
91 | (defn conform [[_ & body :as form]]
92 | (let [conformed (s/conform ::class-def body)]
93 | (if (= conformed ::s/invalid)
94 | {:parts [] :invalid true :body body :spec (s/explain-data ::class-def body)}
95 | conformed)))
96 |
97 | (defn generate-id [ns ns-meta line column]
98 | (str (or (:shadow.css/alias ns-meta)
99 | (-> (str ns)
100 | (str/replace #"\." "_")
101 | (munge)))
102 | "__"
103 | "L" line
104 | "_"
105 | "C" column))
--------------------------------------------------------------------------------
/src/test/shadow/css/analyzer_test.clj:
--------------------------------------------------------------------------------
1 | (ns shadow.css.analyzer-test
2 | (:require
3 | [clojure.string :as str]
4 | [clojure.pprint :refer (pprint)]
5 | [clojure.test :as ct :refer (deftest is)]
6 | [shadow.css.analyzer :as ana]
7 | [shadow.css.build :as build]
8 | [shadow.css.specs :as s]
9 | [clojure.java.io :as io]))
10 |
11 | (deftest analyze-form
12 | (pprint
13 | (ana/process-form
14 | (build/start)
15 | {:ns 'foo.bar
16 | :line 1
17 | :column 2
18 | :form '(css :px-4 :my-2
19 | "pass"
20 | :c-text-1
21 | [:&hover :py-10]
22 | [:md+ :px-6
23 | ["@media print" :px-2]]
24 | [:lg+ :px-8
25 | [:&hover :py-12]])})))
26 |
27 |
28 | ;; TODO: CLI API for these?
29 | ;; or just clojure API to be used from REPL/tools.build?
30 |
31 | ;; library side index generation to be included in jar
32 | (deftest build-start
33 | (time
34 | (tap>
35 | (build/start))))
36 |
37 | (deftest index-src-main
38 | (time
39 | (tap>
40 | (-> (build/start)
41 | (build/index-path (io/file "src" "main") {})))))
42 |
43 | ;; project side to generate actual css
44 | (deftest build-src-main
45 | (time
46 | (tap>
47 | (-> (build/start)
48 | (build/index-path (io/file "src" "main") {})
49 | (build/generate '{:main {:include [shadow.cljs.ui.components*]}
50 | :test {:entries [shadow.cljs.ui.main] :depends-on #{:main}}})))))
51 |
52 | (defn parse-tailwind [[tbody tbody-attrs & rows]]
53 | (reduce
54 | (fn [all row]
55 | (let [[_ td-key td-val] row
56 | [_ _ key] td-key
57 | [_ _ val] td-val
58 |
59 | rules
60 | (->> (str/split val #"\n")
61 | (reduce
62 | (fn [rules prop+val]
63 | (let [[prop val] (str/split prop+val #": " 2)]
64 | (assoc rules (keyword prop) (subs val 0 (dec (count val))))))
65 | {}
66 | ))]
67 |
68 | (assoc all (keyword key) rules)))
69 | {}
70 | rows))
71 |
72 | (deftest test-parse-tailwind
73 | (let [s
74 | nil
75 |
76 |
77 |
78 | rules (parse-tailwind s)]
79 | (doseq [rule (sort (keys rules))]
80 | (println (str rule " " (pr-str (get rules rule))))
81 | )))
82 |
83 | (deftest test-parse-ns
84 | (let [state
85 | {:requires []
86 | :require-aliases {}
87 | :refers {}}
88 |
89 | form
90 | '(ns ^{:foo "bar"} foo.bar
91 | {:bar "foo"}
92 | (:require
93 | just-a-sym
94 | [shadow.grove :refer (css << defc)]
95 | [shadow.css :rename {css c}]
96 | [just-another-sym]
97 | [some.thing :as x]
98 | [some.other-thing :refer (x)]
99 | [not.loading :as-alias foo]
100 | ["js-require" :as npm]
101 | ["js-require-blank"]
102 | ;; FIXME: not even sure this is valid, prefix lists are terrible
103 | [bad.prefix list]
104 | [terrible.prefix
105 | [list
106 | [nested :as n]
107 | :as y]
108 | :as z]))]
109 |
110 | (pprint (ana/parse-ns state form))
111 | ))
112 |
113 |
114 | (deftest test-parse-finds-namespaced-keywords
115 | (let [result (ana/find-css-in-source "(ns what (:require [thing :as x])) (css ::x/foo ::baz)")]
116 |
117 | (pprint result)
118 | ))
--------------------------------------------------------------------------------