├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── dev.cljs.edn ├── dev ├── build.sh ├── tailwind │ └── examples.cljs └── user.clj ├── docs ├── css │ └── examples.css ├── index.html └── js │ └── examples.js ├── resources ├── public │ └── index.html └── tailwind.edn └── src └── tailwind ├── base.clj ├── config.clj ├── core.clj ├── core.cljs ├── defaults.edn ├── transform.clj └── util.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .idea 3 | *.iml 4 | /resources/public/js 5 | /resources/public/css 6 | node_modules 7 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Modified work Copyright (c) Michael McClintock 4 | Copyright (c) Adam Wathan 5 | Copyright (c) Jonathan Reinink 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## tailwind-clj 2 | 3 | A clojure library that processes [tailwindcss][tailwind] 4 | utility classes, generates css rules and either writes the output to a 5 | css file or returns css data (suitable for css-in-js libraries such as 6 | [emotion]). 7 | 8 | When developing client applications with ClojureScript and `tailwind-clj` 9 | 10 | * you can use macros to only generate css for the tailwind utilities that you actually use 11 | * you don't have to integrate any nodejs tooling into your dev flow 12 | * customize tailwind with a `tailwind.edn` file on the classpath 13 | * fits nicely with figwheel based development 14 | * get a lot of the benefits of tailwind (see rationale below) 15 | 16 | ### Project Status 17 | 18 | The motivation for this library is to investigate the generation of 19 | Tailwind like utility classes on the fly directly with Clojure[Script] 20 | and is based on v1.0.0 of tailwind. 21 | 22 | `tailwind-clj` does not support 23 | 24 | * autoprefixer 25 | * css minification 26 | * tailwind plugins like [custom-forms](https://github.com/tailwindcss/custom-forms) 27 | * tailwind UI which requires a custom tailwind plugin / custom-forms 28 | 29 | If any of the above are requirements then you'll likely be better off using 30 | the official tooling which **does** work with ClojureScript. For an example 31 | see https://github.com/mrmcc3/tailwind-cljs-example 32 | 33 | If you're not interested in the nodejs tooling then read on. 34 | 35 | ### Example 36 | 37 | ```clojure 38 | {:deps {mrmcc3/tailwind-clj {:git/url "https://github.com/mrmcc3/tailwind-clj.git" 39 | :sha "67dc8999aef155dc197b4f207932b658e4496d39"}}} 40 | ``` 41 | 42 | ```clojure 43 | (ns tailwind.example 44 | (:require [tailwind.core :refer [tw! spit-css!]])) 45 | 46 | (tw! "flex flex-col items-center" "py-3 m-4" :text-gray-800) ;; strings or keywords 47 | ;; => "flex flex-col items-center py-3 m-4 text-gray-800" 48 | 49 | (spit-css! "styles.css") 50 | ``` 51 | 52 | ```base 53 | $ clojure -m cljs.main -c tailwind.example 54 | ``` 55 | 56 | ```css 57 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{box-sizing:border-box;font-family:sans-serif}*,::after,::before{box-sizing:inherit}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,p,pre{margin:0}button{background:0 0;padding:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:-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";line-height:1.5}*,::after,::before{border-width:0;border-style:solid;border-color:#e2e8f0;}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:inherit;opacity:.5}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:"Ubuntu Mono",monospace;}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto} 58 | 59 | .flex{display:flex;} 60 | .flex-col{flex-direction:column;} 61 | .items-center{align-items:center;} 62 | .m-4{margin:1rem;} 63 | .py-3{padding-top:0.75rem;padding-bottom:0.75rem;} 64 | .text-gray-800{color:#2d3748;} 65 | ``` 66 | 67 | You can also test the `tw!` macro at the command line 68 | 69 | ``` 70 | $ clj -m tailwind.core tw! font-mono 71 | .font-mono{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;} 72 | ``` 73 | 74 | `dev/tailwind/examples.cljs` has some example components from the tailwind site rendered 75 | using [uix] and [emotion]. [result][examples] 76 | 77 | ### Rationale 78 | 79 | If you're unfamiliar with the rationale behind tailwind css 80 | read the [utility first][utility-first] page from the tailwind docs. 81 | In short the idea is that you can generate a whole bunch of utility 82 | classes that in most cases correspond to a single css rule. By combining 83 | these classes in various ways you can create complex user interfaces. 84 | At first it seems cumbersome to add numerous classes to your markup 85 | but in practice some really nice benefits fall out of it. 86 | 87 | * for the most you don't have to write or maintain any css 88 | * you don't have to keep inventing class names 89 | * you are constrained to the provided set of utility classes establishing 90 | a predefined design system. The result is visually consistent UIs 91 | * After a while you get familiar with the standard utility classes speeding 92 | up the design process 93 | * re-using standard utilities means your CSS stops growing over time 94 | * making changes feels safer 95 | 96 | One downside is that Tailwind has to generate css for every utility **and** its variants. 97 | Variants include a combination of pseudo classes and media queries which means the 98 | size of the resulting css has a combinatorial explosion. Tailwind minimizes the 99 | issue by carefully choosing the default set of utilities and disabling all but 100 | the most used variants. Even then you're looking at ~400KB of uncompressed css. 101 | A good portion of that is most likely unused. Tools like 102 | [purgecss] can help remove the unused classes. 103 | 104 | In ClojureScript we can just generate the utility classes as we need them using 105 | macros at compile time. While we're at it we can make customization simpler by 106 | just dropping a `tailwind.edn` file somewhere on the classpath. 107 | 108 | ### Configuration 109 | 110 | The tailwind config/design system is built by first defining some base attributes 111 | like colors and spacing. Then the config is expanded by using the base definitions 112 | to define attributes like border-color, padding and margin. 113 | 114 | * The default config before expansion is at `src/tailwind/defaults.edn`. 115 | * To view the expanded default config `clj -m tailwind.core default` 116 | * To drill down into the config pass extra args `clj -m tailwind.core default colors blue` 117 | 118 | ### User customization 119 | 120 | If you would like to customize the configuration then place a `tailwind.edn` 121 | file on the classpath with your customizations. This file will be read and 122 | merged with the default config **before** expansion using 123 | [meta-merge]. 124 | 125 | For example `{"spacing" {"perfect" "23px"}}` would add `perfect` to `spacing` 126 | and all attributes that depend on it like `margin` and `padding` 127 | 128 | ``` 129 | $ clj -m tailwind.core tw! mb-perfect px-perfect 130 | .mb-perfect{margin-bottom:23px;} 131 | .px-perfect{padding-left:23px;padding-right:23px;} 132 | ``` 133 | 134 | If you prefer to completely replace the default spacing scale then the 135 | meta-merge hint ^:replace is what you want 136 | `{"spacing" ^:replace {"perfect" "23px"}}` 137 | 138 | If you want to skip the expansion mechanism you can add an extra `^:final` 139 | hint to a calculated attribute. For example 140 | `{"padding" ^:replace ^:final {"perfect" "23px"}}` 141 | 142 | [tailwind]: https://tailwindcss.com/ 143 | [examples]: https://mrmcc3.github.io/tailwind-clj/ 144 | [utility-first]: https://tailwindcss.com/docs/utility-first 145 | [purgecss]: https://github.com/FullHuman/purgecss 146 | [meta-merge]: https://github.com/weavejester/meta-merge 147 | [emotion]: https://emotion.sh/docs/introduction 148 | [uix]: https://github.com/roman01la/uix 149 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | org.clojure/core.match {:mvn/version "0.3.0"} 4 | com.sangupta/murmur {:mvn/version "1.0.0"} 5 | meta-merge {:mvn/version "1.0.0"}} 6 | 7 | :aliases {:dev {:extra-paths ["dev" "resources"] 8 | :extra-deps {org.clojure/clojurescript {:mvn/version "1.10.520"} 9 | com.bhauman/figwheel-main {:mvn/version "0.2.1-SNAPSHOT"} 10 | cljsjs/emotion {:mvn/version "10.0.6-0"} 11 | uix.core {:git/url "https://github.com/roman01la/uix.git" 12 | :deps/root "core" 13 | :sha "ecf04297986071e7b89283bb2835f423987e7d36"} 14 | uix.dom {:git/url "https://github.com/roman01la/uix.git" 15 | :deps/root "dom" 16 | :sha "ecf04297986071e7b89283bb2835f423987e7d36"}}}}} 17 | -------------------------------------------------------------------------------- /dev.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:watch-dirs ["dev" "src"] 2 | :open-url false 3 | :css-dirs ["resources/public/css"]} 4 | {:main tailwind.examples 5 | :output-to "resources/public/js/examples.js" 6 | :output-dir "resources/public/js/build" 7 | :asset-path "/js/build"} -------------------------------------------------------------------------------- /dev/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | clojure -Adev -m figwheel.main -O advanced -bo dev 4 | CSS="resources/public/css/examples.css" 5 | postcss ${CSS} -u autoprefixer -r --no-map 6 | cleancss -O2 ${CSS} -o ${CSS} 7 | rm -rf resources/public/js/build docs 8 | cp -rf resources/public docs -------------------------------------------------------------------------------- /dev/tailwind/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-hooks tailwind.examples 2 | (:require 3 | [cljsjs.emotion] 4 | [uix.core.alpha :as uix] 5 | [uix.dom.alpha :as uix.dom] 6 | [tailwind.core :refer [tw tw! spit-css!]])) 7 | 8 | ;; tw + uix/add-transform-fn for emotion 9 | 10 | ;; tw! outputs to tailwind.css 11 | 12 | ;; Alerts 13 | 14 | ; https://tailwindcss.com/components/alerts/#modern-with-badge 15 | (defn modern-alert [] 16 | [:div {:css (tw "bg-indigo-900 text-center py-4 lg:px-4")} 17 | [:div {:css (tw "p-2 bg-indigo-800 items-center text-indigo-100 leading-none" 18 | "lg:rounded-full flex lg:inline-flex") 19 | :role "alert"} 20 | [:span {:css (tw "flex rounded-full bg-indigo-500 uppercase px-2 py-1" 21 | "text-xs font-bold mr-3")} "New"] 22 | [:span {:css (tw "font-semibold mr-2 text-left flex-auto")} 23 | "Get the coolest t-shirts from our brand new store"] 24 | [:svg {:css (tw "fill-current opacity-75 h-4 w-4") 25 | :xmlns "http://www.w3.org/2000/svg" 26 | :viewBox "0 0 20 20"} 27 | [:path {:d "M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z"}]]]]) 28 | 29 | ;; Cards 30 | 31 | ; https://tailwindcss.com/components/cards#stacked 32 | (defn card-1 [] 33 | (let [s1 (tw "inline-block bg-gray-200 rounded-full px-3 py-1 " 34 | "text-sm font-semibold text-gray-700")] 35 | [:div {:css (tw "max-w-sm rounded overflow-hidden shadow-lg")} 36 | [:img {:css (tw "w-full") :src "https://tailwindcss.com/img/card-top.jpg"}] 37 | [:div {:css (tw "px-6 py-4")} 38 | [:div {:css (tw "font-bold text-xl mb-2")} "The Coldest Sunset"] 39 | [:p {:css (tw "text-gray-700 text-base")} 40 | "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil"]] 41 | [:div {:css (tw "px-6 py-4")} 42 | [:span {:css [s1 (tw "mr-2")]} "#photography"] 43 | [:span {:css [s1 (tw "mr-2")]} "#travel"] 44 | [:span {:css s1} "#winter"]]])) 45 | 46 | ; https://tailwindcss.com/components/cards#horizontal 47 | (defn card-2 [] 48 | [:div {:class (tw! "max-w-sm w-full lg:max-w-full lg:flex")} 49 | [:div 50 | {:css [{:background-image "url('https://tailwindcss.com/img/card-left.jpg')" 51 | :title "Woman holding a mug"}] 52 | :class (tw! "h-48 lg:h-auto lg:w-48 flex-none bg-cover rounded-t" 53 | "lg:rounded-t-none lg:rounded-l text-center overflow-hidden")}] 54 | [:div {:class (tw! "border-r border-b border-l border-gray-400 lg:border-l-0" 55 | "lg:border-t lg:border-gray-400 bg-white rounded-b" 56 | "lg:rounded-b-none lg:rounded-r p-4 flex flex-col" 57 | "justify-between leading-normal")} 58 | [:div {:class (tw! "mb-8")} 59 | [:p {:class (tw! "text-sm text-gray-600 flex items-center")} 60 | [:svg 61 | {:class (tw! "fill-current text-gray-500 w-3 h-3 mr-2") 62 | :xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20"} 63 | [:path {:d "M4 8V6a6 6 0 1 1 12 0v2h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-8c0-1.1.9-2 2-2h1zm5 6.73V17h2v-2.27a2 2 0 1 0-2 0zM7 6v2h6V6a3 3 0 0 0-6 0z"}]] "Members only"] 64 | [:div {:class (tw! "text-gray-900 font-bold text-xl mb-2")} 65 | "Can coffee make you a better developer?"] 66 | [:p {:class (tw! "text-gray-700 text-base")} 67 | "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil."]] 68 | [:div {:class (tw! "flex items-center")} 69 | [:img {:class (tw! "w-10 h-10 rounded-full mr-4") 70 | :src "https://tailwindcss.com/img/jonathan.jpg" 71 | :alt "Avatar of Jonathan Reinink"}] 72 | [:div {:class (tw! "text-sm")} 73 | [:p {:class (tw! "text-gray-900 leading-none")} "Jonathan Reinink"] 74 | [:p {:class (tw! "text-gray-600")} "Aug 18"]]]]]) 75 | 76 | ;; forms 77 | 78 | ; https://tailwindcss.com/components/forms#inline-form 79 | (defn form-1 [] 80 | (let [d1 (tw! "md:flex md:items-center mb-6") 81 | l1 (tw! "block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4") 82 | i1 (tw! "bg-gray-200 appearance-none border-2 border-gray-200" 83 | "rounded w-full py-2 px-4 text-gray-700 leading-tight" 84 | "focus:outline-none focus:bg-white focus:border-purple-500") 85 | b1 (tw! "shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline" 86 | "focus:outline-none text-white font-bold py-2 px-4 rounded")] 87 | [:form {:class (tw! "w-full max-w-sm")} 88 | [:div {:class d1} 89 | [:div {:class (tw! "md:w-1/3")} 90 | [:label {:for "inline-full-name" :class l1} "Full Name"]] 91 | [:div {:class (tw! "md:w-2/3")} 92 | [:input#inline-full-name 93 | {:type "text" :placeholder "Jane Doe" :class i1}]]] 94 | [:div {:class d1} 95 | [:div {:class (tw! "md:w-1/3")} 96 | [:label {:for "inline-username" :class l1} "Password"]] 97 | [:div {:class (tw! "md:w-2/3")} 98 | [:input#inline-username 99 | {:class i1 :type "password" :placeholder "******************"}]]] 100 | [:div {:class d1} 101 | [:div {:class (tw! "md:w-1/3")}] 102 | [:label {:class (tw! "md:w-2/3 block text-gray-500 font-bold")} 103 | [:input {:type "checkbox" :class (tw! "mr-2 leading-tight")}] 104 | [:span {:class (tw! "text-sm")} "Send me your newsletter!"]]] 105 | [:div {:class (tw! "md:flex md:items-center")} 106 | [:div {:class (tw! "md:w-1/3")}] 107 | [:div {:class (tw! "md:w-2/3")} 108 | [:button {:type "button" :class b1} "Sign Up"]]]])) 109 | 110 | ; https://tailwindcss.com/components/forms#underline-form 111 | (defn form-2 [] 112 | (let [i1 (tw! "appearance-none bg-transparent border-none w-full" 113 | "text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none") 114 | b1 (tw! "flex-shrink-0 bg-teal-500 hover:bg-teal-700 border-teal-500" 115 | "hover:border-teal-700 text-sm border-4 text-white py-1 px-2 rounded") 116 | b2 (tw! "flex-shrink-0 border-transparent border-4 text-teal-500" 117 | "hover:text-teal-800 text-sm py-1 px-2 rounded")] 118 | [:form {:class (tw! "w-full max-w-sm")} 119 | [:div {:class (tw! "flex items-center border-b border-teal-500 py-2")} 120 | [:input {:class i1 :type "text" :placeholder "Jane Doe" :aria-label "Full name"}] 121 | [:button {:class b1 :type "button"} "Sign Up"] 122 | [:button {:class b2 :type "button"} "Cancel"]]])) 123 | 124 | ; https://tailwindcss.com/components/navigation/#responsive-header 125 | (defn header [] 126 | [:nav {:class (tw! "flex items-center justify-between flex-wrap bg-teal-500 p-6")} 127 | [:div {:class (tw! "flex items-center flex-shrink-0 text-white mr-6")} 128 | [:svg {:class (tw! "fill-current h-8 w-8 mr-2") 129 | :width "54" 130 | :height "54" 131 | :viewBox "0 0 54 54" 132 | :xmlns "http://www.w3.org/2000/svg"} 133 | [:path {:d "M13.5 22.1c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05zM0 38.3c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05z"}]] 134 | [:span {:class (tw! "font-semibold text-xl tracking-tight")} "Tailwind CSS"]] 135 | [:div {:class (tw! "block lg:hidden")} 136 | [:button {:class (tw! "flex items-center px-3 py-2 border rounded text-teal-200 border-teal-400 hover:text-white hover:border-white")} 137 | [:svg {:class (tw! "fill-current h-3 w-3") 138 | :viewBox "0 0 20 20" 139 | :xmlns "http://www.w3.org/2000/svg"} 140 | [:title "Menu"] 141 | [:path {:d "M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"}]]]] 142 | [:div {:class (tw! "w-full block flex-grow lg:flex lg:items-center lg:w-auto")} 143 | (let [a (tw! "block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white mr-4")] 144 | [:div {:class (tw! "text-sm lg:flex-grow")} 145 | [:a {:href "#responsive-header" :class a} "Docs"] 146 | [:a {:href "#responsive-header" :class a} "Examples"] 147 | [:a {:href "#responsive-header" :class a} "Blog"]]) 148 | [:div 149 | [:a {:href "#" 150 | :class (tw! "inline-block text-sm px-4 py-2 leading-none border rounded" 151 | "text-white border-white hover:border-transparent hover:text-teal-500" 152 | "hover:bg-white mt-4 lg:mt-0")} 153 | "Download"]]]]) 154 | 155 | (defn spacer [] 156 | [:div {:class (tw! "my-4 w-full border-b border-gray-400")}]) 157 | 158 | (defn examples [] 159 | [:div {:class (tw! "flex flex-col items-center p-4")} 160 | [header] 161 | [spacer] 162 | [card-1] 163 | [spacer] 164 | [card-2] 165 | [spacer] 166 | [modern-alert] 167 | [spacer] 168 | [form-1] 169 | [spacer] 170 | [form-2] 171 | [spacer] 172 | [:pre [:code "(println \"test mono font config\")"]]]) 173 | 174 | ;; setup 175 | 176 | (defn ^:after-load render [] 177 | (uix.dom/render [examples] js/root)) 178 | 179 | ;; from uix.recipes.dynamic-styles 180 | (defn css-uix-transform [attrs] 181 | (if-not (contains? attrs :css) 182 | attrs 183 | (let [classes (:class attrs) 184 | css (:css attrs) 185 | class (->> (clj->js css) 186 | js/emotion.css 187 | (str classes " "))] 188 | (-> (dissoc attrs :css) 189 | (assoc :class class))))) 190 | 191 | (defonce startup 192 | (do (uix/add-transform-fn css-uix-transform) 193 | (render) 194 | true)) 195 | 196 | (spit-css! "resources/public/css/examples.css") 197 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (comment 4 | 5 | ;; start figwheel repl 6 | (do (require '[figwheel.main.api]) 7 | (figwheel.main.api/start "dev")) 8 | 9 | ) 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/css/examples.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */body{margin:0}details,main{display:block}h1{font-size:2em}hr{box-sizing:content-box;height:0;overflow:visible}code,kbd,pre,samp{font-size:1em}a{background-color:transparent;color:inherit;text-decoration:inherit}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto;resize:vertical}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}html{-webkit-text-size-adjust:100%;box-sizing:border-box;font-family:sans-serif;font-family:-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";line-height:1.5}*,::after,::before{box-sizing:inherit;border:0 solid #e2e8f0}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,p,pre{margin:0}button{background:0 0}button:focus{outline:dotted 1px;outline:-webkit-focus-ring-color auto 5px}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}img{border-style:solid}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:inherit;opacity:.5}input::-moz-placeholder,textarea::-moz-placeholder{color:inherit;opacity:.5}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:inherit;opacity:.5}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:inherit;opacity:.5}input::placeholder,textarea::placeholder{color:inherit;opacity:.5}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:"Ubuntu Mono",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.bg-cover{background-size:cover}.bg-gray-200{background-color:#edf2f7}.bg-purple-500{background-color:#9f7aea}.bg-teal-500{background-color:#38b2ac}.bg-transparent{background-color:transparent}.bg-white{background-color:#fff}.block{display:block}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-gray-200{border-color:#edf2f7}.border-gray-400{border-color:#cbd5e0}.border-l{border-left-width:1px}.border-none{border-style:none}.border-r{border-right-width:1px}.border-teal-400{border-color:#4fd1c5}.border-teal-500{border-color:#38b2ac}.border-transparent{border-color:transparent}.border-white{border-color:#fff}.fill-current{fill:currentColor}.flex{display:flex}.flex-col{flex-direction:column}.flex-grow{flex-grow:1}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.flex-wrap{flex-wrap:wrap}.focus\:bg-white:focus{background-color:#fff}.focus\:border-purple-500:focus{border-color:#9f7aea}.focus\:outline-none:focus{outline:0}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgba(66,153,225,.5)}.font-bold{font-weight:700}.font-semibold{font-weight:600}.h-10{height:2.5rem}.h-3{height:.75rem}.h-48{height:12rem}.h-8{height:2rem}.hover\:bg-purple-400:hover{background-color:#b794f4}.hover\:bg-teal-700:hover{background-color:#2c7a7b}.hover\:bg-white:hover{background-color:#fff}.hover\:border-teal-700:hover{border-color:#2c7a7b}.hover\:border-transparent:hover{border-color:transparent}.hover\:border-white:hover{border-color:#fff}.hover\:text-teal-500:hover{color:#38b2ac}.hover\:text-teal-800:hover{color:#285e61}.hover\:text-white:hover{color:#fff}.inline-block{display:inline-block}.items-center{align-items:center}.justify-between{justify-content:space-between}.leading-none{line-height:1}.leading-normal{line-height:1.5}.leading-tight{line-height:1.25}.max-w-sm{max-width:24rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-6{margin-right:1.5rem}.mt-4{margin-top:1rem}.my-4{margin-top:1rem;margin-bottom:1rem}.overflow-hidden{overflow:hidden}.p-4{padding:1rem}.p-6{padding:1.5rem}.pr-4{padding-right:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.rounded{border-radius:.25rem}.rounded-b{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-t{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-base{font-size:1rem}.text-center{text-align:center}.text-gray-500{color:#a0aec0}.text-gray-600{color:#718096}.text-gray-700{color:#4a5568}.text-gray-900{color:#1a202c}.text-sm{font-size:.875rem}.text-teal-200{color:#b2f5ea}.text-teal-500{color:#38b2ac}.text-white{color:#fff}.text-xl{font-size:1.25rem}.tracking-tight{letter-spacing:-.025em}.w-10{width:2.5rem}.w-3{width:.75rem}.w-8{width:2rem}.w-full{width:100%}@media (min-width:768px){.md\:flex{display:flex}.md\:items-center{align-items:center}.md\:mb-0{margin-bottom:0}.md\:text-right{text-align:right}.md\:w-1\/3{width:33.333333%}.md\:w-2\/3{width:66.666667%}}@media (min-width:1024px){.lg\:border-gray-400{border-color:#cbd5e0}.lg\:border-l-0{border-left-width:0}.lg\:border-t{border-top-width:1px}.lg\:flex{display:flex}.lg\:flex-grow{flex-grow:1}.lg\:h-auto{height:auto}.lg\:hidden{display:none}.lg\:inline-block{display:inline-block}.lg\:items-center{align-items:center}.lg\:max-w-full{max-width:100%}.lg\:mt-0{margin-top:0}.lg\:rounded-b-none{border-bottom-left-radius:0;border-bottom-right-radius:0}.lg\:rounded-l{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.lg\:rounded-r{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.lg\:rounded-t-none{border-top-right-radius:0;border-top-left-radius:0}.lg\:w-48{width:12rem}.lg\:w-auto{width:auto}} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/tailwind.edn: -------------------------------------------------------------------------------- 1 | {"font-family" {"mono" ^:replace ["\"Ubuntu Mono\"" "monospace"]}} -------------------------------------------------------------------------------- /src/tailwind/base.clj: -------------------------------------------------------------------------------- 1 | (ns tailwind.base 2 | (:require 3 | [clojure.string :as str] 4 | [tailwind.config :refer [cfg->]])) 5 | 6 | (def normalize "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}") 7 | (def preflight "html{box-sizing:border-box;font-family:sans-serif}*,::after,::before{box-sizing:inherit}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,p,pre{margin:0}button{background:0 0;padding:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:-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\";line-height:1.5}*,::after,::before{border-width:0;border-style:solid;border-color:%s;}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:inherit;opacity:.5}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:%s;}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%%;height:auto}") 8 | 9 | (defn ^String styles [] 10 | (str normalize 11 | (format 12 | preflight 13 | (cfg-> :border-color :default) 14 | (str/join "," (cfg-> :font-family :mono))))) 15 | 16 | -------------------------------------------------------------------------------- /src/tailwind/config.clj: -------------------------------------------------------------------------------- 1 | (ns tailwind.config 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io] 5 | [meta-merge.core :as mm])) 6 | 7 | (defn negate [m] 8 | (reduce-kv #(assoc %1 (str "-" %2) (str "-" %3)) {} m)) 9 | 10 | (defn final [v f & args] 11 | (if (:final (meta v)) v (apply f v args))) 12 | 13 | (defn border-color-default [cfg] 14 | (let [default (get-in cfg ["border-color" "default"])] 15 | (if (vector? default) 16 | (assoc-in cfg ["border-color" "default"] 17 | (get-in cfg default "currentColor")) 18 | cfg))) 19 | 20 | (defn expand [{:strs [colors spacing] :as cfg}] 21 | (-> (border-color-default cfg) 22 | (update "background-color" final merge colors) 23 | (update "border-color" final merge colors) 24 | (update "text-color" final merge colors) 25 | (update "height" final merge spacing) 26 | (update "padding" final merge spacing) 27 | (update "width" final merge spacing) 28 | (update "margin" final merge spacing (negate spacing)))) 29 | 30 | (def init-defaults 31 | (-> "tailwind/defaults.edn" io/resource slurp edn/read-string)) 32 | 33 | (def init-user 34 | (some-> "tailwind.edn" io/resource slurp edn/read-string)) 35 | 36 | (def default-config 37 | (expand init-defaults)) 38 | 39 | (def config 40 | (expand (mm/meta-merge init-defaults init-user))) 41 | 42 | (defn cfg-> [& paths] 43 | (get-in config (map name paths))) 44 | -------------------------------------------------------------------------------- /src/tailwind/core.clj: -------------------------------------------------------------------------------- 1 | (ns tailwind.core 2 | (:require 3 | [clojure.core.match :refer [match]] 4 | [clojure.pprint :refer [pprint]] 5 | [tailwind.config :as cfg :refer [cfg->]] 6 | [tailwind.base :as base] 7 | [tailwind.util :as u] 8 | [tailwind.transform :as t] 9 | [clojure.java.io :as io] 10 | [clojure.string :as str]) 11 | (:import (java.io Writer StringWriter))) 12 | 13 | ;; emotion style 14 | 15 | (defn tw->emotion [strings] 16 | (->> (mapcat u/split-classes strings) 17 | (map u/split-fragments) 18 | (map t/fragments->emotion) 19 | (apply str))) 20 | 21 | (defmacro tw 22 | "Given one or more strings containing whitespace separated tailwind classes 23 | return a string of css. 24 | 25 | The intention is that the result can be processed by a css-in-js library 26 | such as emotion. Example (tw \"w-full max-w-sm my-3\")" 27 | [& strings] 28 | (tw->emotion strings)) 29 | 30 | ;; extract css 31 | 32 | (def empty-rules 33 | (reduce-kv 34 | #(assoc %1 %3 (sorted-map)) 35 | (sorted-map nil (sorted-map)) 36 | (cfg-> :screens))) 37 | 38 | (def rules (atom empty-rules)) 39 | 40 | (def mwm (partial merge-with merge)) 41 | 42 | (defmacro tw! [& strings] 43 | (let [classes (mapcat u/split-classes strings)] 44 | (apply swap! rules mwm (map t/class->css classes)) 45 | (str/join " " classes))) 46 | 47 | (defn write-rules! [^Writer writer rules base?] 48 | (with-open [out writer] 49 | (when base? 50 | (.write out (base/styles)) 51 | (.write out "\n\n")) 52 | (doseq [[bp bp-rules] rules] 53 | (when (seq bp-rules) 54 | (when bp (.write out (format "\n@media (min-width: %spx) {\n" bp))) 55 | (doseq [[_ rule] bp-rules] (.write out (str rule "\n"))) 56 | (when bp (.write out "}\n")))))) 57 | 58 | (defmacro spit-css! [path] 59 | (io/make-parents path) 60 | (write-rules! (io/writer path) @rules true)) 61 | 62 | (defn tw->css 63 | ([strings] (tw->css strings false)) 64 | ([strings base?] 65 | (let [sw (StringWriter.) 66 | rs (->> (mapcat u/split-classes strings) 67 | (map t/class->css) 68 | (apply mwm empty-rules))] 69 | (write-rules! sw rs base?) 70 | (str sw)))) 71 | 72 | (defmacro base [] (base/styles)) 73 | 74 | ;; cli entry point 75 | 76 | (defn -main [& args] 77 | (match (vec args) 78 | ["base"] (println (base/styles)) 79 | ["tw" & rest] (println (tw->emotion rest)) 80 | ["tw!" & rest] (print (tw->css rest)) 81 | ["hash" & rest] (-> rest tw->emotion u/emotion-hash println) 82 | ["default" & rest] (pprint (get-in cfg/default-config rest)) 83 | ["config" "keys"] (-> cfg/config keys sort pprint) 84 | ["config" & rest] (pprint (get-in cfg/config rest)))) 85 | -------------------------------------------------------------------------------- /src/tailwind/core.cljs: -------------------------------------------------------------------------------- 1 | (ns tailwind.core 2 | (:require-macros tailwind.core)) 3 | -------------------------------------------------------------------------------- /src/tailwind/defaults.edn: -------------------------------------------------------------------------------- 1 | {"screens" {"sm" 640 "md" 768 "lg" 1024 "xl" 1280} 2 | 3 | "colors" {"transparent" "transparent" 4 | "black" "#000" 5 | "white" "#fff" 6 | "gray" {"100" "#f7fafc" 7 | "200" "#edf2f7" 8 | "300" "#e2e8f0" 9 | "400" "#cbd5e0" 10 | "500" "#a0aec0" 11 | "600" "#718096" 12 | "700" "#4a5568" 13 | "800" "#2d3748" 14 | "900" "#1a202c"} 15 | "red" {"100" "#fff5f5" 16 | "200" "#fed7d7" 17 | "300" "#feb2b2" 18 | "400" "#fc8181" 19 | "500" "#f56565" 20 | "600" "#e53e3e" 21 | "700" "#c53030" 22 | "800" "#9b2c2c" 23 | "900" "#742a2a"} 24 | "orange" {"100" "#fffaf0" 25 | "200" "#feebc8" 26 | "300" "#fbd38d" 27 | "400" "#f6ad55" 28 | "500" "#ed8936" 29 | "600" "#dd6b20" 30 | "700" "#c05621" 31 | "800" "#9c4221" 32 | "900" "#7b341e"} 33 | "yellow" {"100" "#fffff0" 34 | "200" "#fefcbf" 35 | "300" "#faf089" 36 | "400" "#f6e05e" 37 | "500" "#ecc94b" 38 | "600" "#d69e2e" 39 | "700" "#b7791f" 40 | "800" "#975a16" 41 | "900" "#744210"} 42 | "green" {"100" "#f0fff4" 43 | "200" "#c6f6d5" 44 | "300" "#9ae6b4" 45 | "400" "#68d391" 46 | "500" "#48bb78" 47 | "600" "#38a169" 48 | "700" "#2f855a" 49 | "800" "#276749" 50 | "900" "#22543d"} 51 | "teal" {"100" "#e6fffa" 52 | "200" "#b2f5ea" 53 | "300" "#81e6d9" 54 | "400" "#4fd1c5" 55 | "500" "#38b2ac" 56 | "600" "#319795" 57 | "700" "#2c7a7b" 58 | "800" "#285e61" 59 | "900" "#234e52"} 60 | "blue" {"100" "#ebf8ff" 61 | "200" "#bee3f8" 62 | "300" "#90cdf4" 63 | "400" "#63b3ed" 64 | "500" "#4299e1" 65 | "600" "#3182ce" 66 | "700" "#2b6cb0" 67 | "800" "#2c5282" 68 | "900" "#2a4365"} 69 | "indigo" {"100" "#ebf4ff" 70 | "200" "#c3dafe" 71 | "300" "#a3bffa" 72 | "400" "#7f9cf5" 73 | "500" "#667eea" 74 | "600" "#5a67d8" 75 | "700" "#4c51bf" 76 | "800" "#434190" 77 | "900" "#3c366b"} 78 | "purple" {"100" "#faf5ff" 79 | "200" "#e9d8fd" 80 | "300" "#d6bcfa" 81 | "400" "#b794f4" 82 | "500" "#9f7aea" 83 | "600" "#805ad5" 84 | "700" "#6b46c1" 85 | "800" "#553c9a" 86 | "900" "#44337a"} 87 | "pink" {"100" "#fff5f7" 88 | "200" "#fed7e2" 89 | "300" "#fbb6ce" 90 | "400" "#f687b3" 91 | "500" "#ed64a6" 92 | "600" "#d53f8c" 93 | "700" "#b83280" 94 | "800" "#97266d" 95 | "900" "#702459"}} 96 | 97 | "spacing" {"px" "1px" 98 | "0" "0" 99 | "1" "0.25rem" 100 | "2" "0.5rem" 101 | "3" "0.75rem" 102 | "4" "1rem" 103 | "5" "1.25rem" 104 | "6" "1.5rem" 105 | "8" "2rem" 106 | "10" "2.5rem" 107 | "12" "3rem" 108 | "16" "4rem" 109 | "20" "5rem" 110 | "24" "6rem" 111 | "32" "8rem" 112 | "40" "10rem" 113 | "48" "12rem" 114 | "56" "14rem" 115 | "64" "16rem"} 116 | 117 | "background-color" {} 118 | 119 | "background-size" {"auto" "auto" "cover" "cover" "contain" "contain"} 120 | 121 | "border-color" {"default" ["colors" "gray" "300"]} 122 | 123 | "border-radius" {"none" "0" 124 | "sm" "0.125rem" 125 | "default" "0.25rem" 126 | "lg" "0.5rem" 127 | "full" "9999px"} 128 | 129 | "border-width" {"0" "0" 130 | "2" "2px" 131 | "4" "4px" 132 | "8" "8px" 133 | "default" "1px"} 134 | 135 | "box-shadow" {"default" "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)" 136 | "md" "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)" 137 | "lg" "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)" 138 | "xl" "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)" 139 | "2xl" "0 25px 50px -12px rgba(0, 0, 0, 0.25)" 140 | "inner" "inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)" 141 | "outline" "0 0 0 3px rgba(66, 153, 225, 0.5)" 142 | "none" "none"} 143 | 144 | "container" {} 145 | 146 | "cursor" {"auto" "auto" 147 | "default" "default" 148 | "pointer" "pointer" 149 | "wait" "wait" 150 | "text" "text" 151 | "move" "move" 152 | "not-allowed" "not-allowed"} 153 | "fill" {"current" "currentColor"} 154 | 155 | "flex" {"1" "1 1 0%" 156 | "auto" "1 1 auto" 157 | "initial" "0 1 auto" 158 | "none" "none"} 159 | 160 | "flex-grow" {"0" "0" "default" "1"} 161 | 162 | "flex-shrink" {"0" "0" "default" "1"} 163 | 164 | "font-family" {"sans" ["-apple-system" "BlinkMacSystemFont" "\"Segoe UI\"" "Roboto" 165 | "\"Helvetica Neue\"" "Arial" "\"Noto Sans\"" "sans-serif" 166 | "\"Apple Color Emoji\"" "\"Segoe UI Emoji\"" 167 | "\"Segoe UI Symbol\"" "\"Noto Color Emoji\""] 168 | "serif" ["Georgia" "Cambria" "\"Times New Roman\"" "Times" "serif"] 169 | "mono" ["Menlo" "Monaco" "Consolas" "\"Liberation Mono\"" 170 | "\"Courier New\"" "monospace"]} 171 | 172 | "font-size" {"xs" "0.75rem" 173 | "sm" "0.875rem" 174 | "base" "1rem" 175 | "lg" "1.125rem" 176 | "xl" "1.25rem" 177 | "2xl" "1.5rem" 178 | "3xl" "1.875rem" 179 | "4xl" "2.25rem" 180 | "5xl" "3rem" 181 | "6xl" "4rem"} 182 | 183 | "font-weight" {"hairline" "100" 184 | "thin" "200" 185 | "light" "300" 186 | "normal" "400" 187 | "medium" "500" 188 | "semibold" "600" 189 | "bold" "700" 190 | "extrabold" "800" 191 | "black" "900"} 192 | 193 | "height" {"auto" "auto" "full" "100%" "screen" "100vh"} 194 | 195 | "inset" {"0" "0" "auto" "auto"} 196 | 197 | "letter-spacing" {"tighter" "-0.05em" 198 | "tight" "-0.025em" 199 | "normal" "0" 200 | "wide" "0.025em" 201 | "wider" "0.05em" 202 | "widest" "0.1em"} 203 | 204 | "line-height" {"none" "1" 205 | "tight" "1.25" 206 | "snug" "1.375" 207 | "normal" "1.5" 208 | "relaxed" "1.625" 209 | "loose" "2"} 210 | 211 | "list-style-type" {"none" "none" "disc" "disc" "decimal" "decimal"} 212 | 213 | "margin" {"auto" "auto"} 214 | 215 | "max-height" {"full" "100%" "screen" "100vh"} 216 | 217 | "max-width" {"xs" "20rem" 218 | "sm" "24rem" 219 | "md" "28rem" 220 | "lg" "32rem" 221 | "xl" "36rem" 222 | "2xl" "42rem" 223 | "3xl" "48rem" 224 | "4xl" "56rem" 225 | "5xl" "64rem" 226 | "6xl" "72rem" 227 | "full" "100%"} 228 | "min-height" {"0" "0" "full" "100%" "screen" "100vh"} 229 | 230 | "min-width" {"0" "0" "full" "100%"} 231 | 232 | "object-position" {"right" "right" 233 | "top" "top" 234 | "left-top" "left top" 235 | "center" "center" 236 | "right-bottom" "right bottom" 237 | "right-top" "right top" 238 | "left-bottom" "left bottom" 239 | "bottom" "bottom" 240 | "left" "left"} 241 | 242 | "opacity" {"0" "0" "25" "0.25" "50" "0.5" "75" "0.75" "100" "1"} 243 | 244 | "order" {"first" "-9999" 245 | "none" "0" 246 | "1" "1" 247 | "2" "2" 248 | "3" "3" 249 | "4" "4" 250 | "5" "5" 251 | "6" "6" 252 | "7" "7" 253 | "8" "8" 254 | "9" "9" 255 | "10" "10" 256 | "11" "11" 257 | "12" "12" 258 | "last" "9999"} 259 | 260 | "padding" {} 261 | 262 | "stroke" {"current" "currentColor"} 263 | 264 | "text-color" {} 265 | 266 | "width" {"1/2" "50%" 267 | "1/3" "33.333333%" 268 | "2/3" "66.666667%" 269 | "1/4" "25%" 270 | "2/4" "50%" 271 | "3/4" "75%" 272 | "1/5" "20%" 273 | "2/5" "40%" 274 | "3/5" "60%" 275 | "4/5" "80%" 276 | "1/6" "16.666667%" 277 | "2/6" "33.333333%" 278 | "3/6" "50%" 279 | "4/6" "66.666667%" 280 | "5/6" "83.333333%" 281 | "1/12" "8.333333%" 282 | "2/12" "16.666667%" 283 | "3/12" "25%" 284 | "4/12" "33.333333%" 285 | "5/12" "41.666667%" 286 | "6/12" "50%" 287 | "7/12" "58.333333%" 288 | "8/12" "66.666667%" 289 | "9/12" "75%" 290 | "10/12" "83.333333%" 291 | "11/12" "91.666667%" 292 | "auto" "auto" 293 | "full" "100%" 294 | "screen" "100vw"} 295 | 296 | "z-index" {"auto" "auto" 297 | "0" "0" 298 | "10" "10" 299 | "20" "20" 300 | "30" "30" 301 | "40" "40" 302 | "50" "50"}} -------------------------------------------------------------------------------- /src/tailwind/transform.clj: -------------------------------------------------------------------------------- 1 | (ns tailwind.transform 2 | (:require 3 | [clojure.core.match :refer [match]] 4 | [tailwind.config :refer [cfg->]] 5 | [tailwind.util :as u :refer [rule]] 6 | [clojure.string :as str])) 7 | 8 | ;; tailwind classes -> css 9 | ;; the transform is represented using core.match patterns 10 | 11 | (def fragments->emotion 12 | "Accepts tailwind class fragments and returns the corresponding css (emotion style)." 13 | (memoize 14 | (fn [fragments] 15 | (match fragments 16 | 17 | ;; responsive breakpoints - media queries 18 | 19 | [(s :guard (cfg-> :screens)) & rest] 20 | (format "@media(min-width: %spx){%s}" (cfg-> :screens s) (fragments->emotion rest)) 21 | 22 | ;; pseudo classes 23 | 24 | [(p :guard u/pseudo-classes) & rest] 25 | (format ":%s{%s}" p (fragments->emotion rest)) 26 | 27 | ;; -------------------- LAYOUT ----------------------- 28 | 29 | ;; container 30 | 31 | ["container"] 32 | (reduce-kv 33 | #(str %1 (format "@media(min-width:%spx){max-width:%spx;}" %3 %3)) 34 | "width:100%;" 35 | (cfg-> :screens)) 36 | 37 | ;; display 38 | 39 | ["block"] (rule "display" "block") 40 | ["inline" "block"] (rule "display" "inline-block") 41 | ["inline"] (rule "display" "inline") 42 | ["flex"] (rule "display" "flex") 43 | ["inline" "flex"] (rule "display" "inline-flex") 44 | ["table"] (rule "display" "table") 45 | ["table" "row"] (rule "display" "table-row") 46 | ["table"] (rule "display" "table-cell") 47 | ["hidden"] (rule "display" "none") 48 | 49 | ;; float 50 | 51 | ["float" (f :guard #{"right" "left" "none"})] {"float" f} 52 | ["clearfix"] "&::after{content:\"\";display:table;clear:both;}" 53 | 54 | ;; object fit 55 | 56 | ["object" "scale" "down"] (rule "object-fit" "scale-down") 57 | ["object" (f :guard #{"contain" "cover" "fill" "none"})] 58 | (rule "object-fit" f) 59 | 60 | ;; object position 61 | 62 | ["object" (p :guard #{"top" "left" "right" "bottom" "center"})] 63 | (rule "object-position" p) 64 | ["object" (lr :guard #{"left" "right"}) (tb :guard #{"top" "bottom"})] 65 | (rule "object-position" (str lr " " tb)) 66 | 67 | ;; overflow 68 | 69 | ["overflow" (o :guard u/overflow)] (rule "overflow" o) 70 | ["overflow" (d :guard #{"x" "y"}) (o :guard u/overflow)] 71 | (rule (str "overflow-" d) o) 72 | ["scrolling" (o :guard #{"touch" "auto"})] 73 | (rule "-webkit-overflow-scrolling" o) 74 | 75 | ;; position 76 | 77 | [(p :guard #{"static" "fixed" "absolute" "relative" "sticky"})] 78 | (rule "position" p) 79 | 80 | ;; top left bottom right 81 | 82 | [(p :guard #{"top" "left" "bottom" "right"}) (i :guard (cfg-> :inset))] 83 | (rule p (cfg-> :inset i)) 84 | 85 | ["inset" (i :guard (cfg-> :inset))] 86 | (rule "top" i "left" i "bottom" i "right" i) 87 | 88 | ["inset" "x" (i :guard (cfg-> :inset))] (rule "left" i "right" i) 89 | ["inset" "y" (i :guard (cfg-> :inset))] (rule "top" i "bottom" i) 90 | 91 | ;; visibility 92 | 93 | ["visible"] (rule "visibility" "visible") 94 | ["invisible"] (rule "visibility" "hidden") 95 | 96 | ;; z-index 97 | 98 | ["z" (z :guard (cfg-> :z-index))] 99 | (rule "z-index" (cfg-> :z-index z)) 100 | 101 | ;; ------------------ TYPOGRAPHY --------------------- 102 | 103 | ;; font family 104 | 105 | ["font" (f :guard (cfg-> :font-family))] 106 | (rule "font-family" (str/join "," (cfg-> :font-family f))) 107 | 108 | ;; font size 109 | 110 | ["text" (s :guard (cfg-> :font-size))] 111 | (rule "font-size" (cfg-> :font-size s)) 112 | 113 | ;; font smoothing 114 | 115 | ["antialiased"] 116 | (rule "-webkit-font-smoothing" "antialiased" 117 | "-moz-osx-font-smoothing" "grayscale") 118 | ["subpixel-antialiased"] 119 | (rule "-webkit-font-smoothing" "auto" 120 | "-moz-osx-font-smoothing" "auto") 121 | 122 | ;; font style 123 | 124 | ["italic"] (rule "font-style" "italic") 125 | ["not-italic"] (rule "font-style" "normal") 126 | 127 | ;; font weight 128 | 129 | ["font" (w :guard (cfg-> :font-weight))] 130 | (rule "font-weight" (cfg-> :font-weight w)) 131 | 132 | ;; letter spacing 133 | 134 | ["tracking" (s :guard (cfg-> :letter-spacing))] 135 | (rule "letter-spacing" (cfg-> :letter-spacing s)) 136 | 137 | ;; line height 138 | 139 | ["leading" (h :guard (cfg-> :line-height))] 140 | (rule "line-height" (cfg-> :line-height h)) 141 | 142 | ;; list style type 143 | 144 | ["list" (s :guard #{"none" "disc" "decimal"})] 145 | (rule "list-style-type" s) 146 | 147 | ;; list style position 148 | 149 | ["list" (s :guard #{"inside" "outside"})] 150 | (rule "list-style-position" s) 151 | 152 | ;; text align 153 | 154 | ["text" (c :guard u/text-align)] 155 | (rule "text-align" c) 156 | 157 | ;; text color 158 | 159 | ["text" (c :guard (cfg-> :colors)) & rest] 160 | (rule "color" (apply cfg-> :colors c rest)) 161 | 162 | ;; text decoration 163 | 164 | ["underline"] (rule "text-decoration" "underline") 165 | ["line" "through"] (rule "text-decoration" "line-through") 166 | ["no" "underline"] (rule "text-decoration" "none") 167 | 168 | ;; text transform 169 | 170 | ["uppercase"] (rule "text-transform" "uppercase") 171 | ["lowercase"] (rule "text-transform" "lowercase") 172 | ["capitalize"] (rule "text-transform" "capitalize") 173 | ["normal-case"] (rule "text-transform" "none") 174 | 175 | ;; vertical align 176 | 177 | ["align" (s :guard #{"baseline" "top" "middle" "bottom"})] 178 | (rule "vertical-align" s) 179 | ["align" "text" (s :guard #{"top" "bottom"})] 180 | (rule "vertical-align" (str "text-" s)) 181 | 182 | ;; whitespace 183 | 184 | ["whitespace" "normal"] (rule "white-space" "normal") 185 | ["whitespace" "no" "wrap"] (rule "white-space" "nowrap") 186 | ["whitespace" "pre"] (rule "white-space" "pre") 187 | ["whitespace" "pre" "line"] (rule "white-space" "pre-line") 188 | ["whitespace" "pre" "wrap"] (rule "white-space" "pre-wrap") 189 | 190 | ;; word break 191 | 192 | ["break" "normal"] (rule "word-break" "normal" "overflow-wrap" "normal") 193 | ["break" "words"] (rule "overflow-wrap" "break-word") 194 | ["break" "all"] (rule "word-break" "break-all") 195 | ["truncate"] (rule "overflow" "hidden" 196 | "text-overflow" "ellipsis" 197 | "white-space" "nowrap") 198 | 199 | ;; ------------------ BACKGROUNDS -------------------- 200 | 201 | ;; attachment 202 | 203 | ["bg" (a :guard u/background-attachments)] 204 | (rule "background-attachment" a) 205 | 206 | ;; color 207 | 208 | ["bg" (c :guard (cfg-> :background-color)) & rest] 209 | (rule "background-color" (apply cfg-> :background-color c rest)) 210 | 211 | ;; position 212 | 213 | ["bg" "left" "bottom"] (rule "background-position" "left bottom") 214 | ["bg" "left" "top"] (rule "background-position" "left top") 215 | ["bg" "right" "bottom"] (rule "background-position" "right bottom") 216 | ["bg" "right" "top"] (rule "background-position" "right top") 217 | ["bg" (p :guard #{"bottom" "center" "left" "right" "top"})] 218 | (rule "background-position" p) 219 | 220 | ;; repeat 221 | 222 | ["bg" "repeat"] (rule "background-repeat" "repeat") 223 | ["bg" "no" "repeat"] (rule "background-repeat" "no-repeat") 224 | ["bg" "repeat" "x"] (rule "background-repeat" "repeat-x") 225 | ["bg" "repeat" "y"] (rule "background-repeat" "repeat-y") 226 | ["bg" "repeat" "round"] (rule "background-repeat" "repeat-round") 227 | ["bg" "repeat" "space"] (rule "background-repeat" "repeat-space") 228 | 229 | ;; size 230 | 231 | ["bg" (s :guard (cfg-> :background-size))] 232 | (rule "background-size" (cfg-> :background-size s)) 233 | 234 | ;; -------------------- BORDERS ---------------------- 235 | 236 | ;; color 237 | 238 | ["border" (c :guard (cfg-> :colors)) & rest] 239 | (rule "border-color" (apply cfg-> :colors c rest)) 240 | 241 | ;; style 242 | 243 | ["border" (s :guard u/border-style)] 244 | (rule "border-style" s) 245 | 246 | ;; width 247 | 248 | ["border"] (fragments->emotion ["border" "default"]) 249 | 250 | ["border" (s :guard u/border-sides)] 251 | (rule (format "border-%s-width" (u/border-sides s)) (cfg-> :border-width "default")) 252 | 253 | ["border" (s :guard u/border-sides) (w :guard (cfg-> :border-width))] 254 | (rule (format "border-%s-width" (u/border-sides s)) (cfg-> :border-width w)) 255 | 256 | ["border" (w :guard (cfg-> :border-width))] 257 | (rule "border-width" (cfg-> :border-width w)) 258 | 259 | ;; radius 260 | 261 | ["rounded"] (fragments->emotion ["rounded" "default"]) 262 | ["rounded" (r :guard (cfg-> :border-radius))] 263 | (rule "border-radius" (cfg-> :border-radius r)) 264 | 265 | ["rounded" (c :guard u/corners)] (fragments->emotion ["rounded" c "default"]) 266 | ["rounded" (c :guard u/corners) (r :guard (cfg-> :border-radius))] 267 | (rule (format "border-%s-radius" (u/corners c)) (cfg-> :border-radius r)) 268 | 269 | ["rounded" (s :guard u/sides)] (fragments->emotion ["rounded" s "default"]) 270 | ["rounded" (s :guard u/sides) (r :guard (cfg-> :border-radius))] 271 | (apply str (map #(fragments->emotion ["rounded" % r]) (u/sides s))) 272 | 273 | ;; -------------------- FLEXBOX ---------------------- 274 | 275 | ;; direction 276 | 277 | ["flex" "row"] (rule "flex-direction" "row") 278 | ["flex" "row" "reverse"] (rule "flex-direction" "row-reverse") 279 | ["flex" "col"] (rule "flex-direction" "column") 280 | ["flex" "col" "reverse"] (rule "flex-direction" "column-reverse") 281 | 282 | ;; wrap 283 | 284 | ["flex" "no" "wrap"] (rule "flex-wrap" "no-wrap") 285 | ["flex" "wrap"] (rule "flex-wrap" "wrap") 286 | ["flex" "wrap" "reverse"] (rule "flex-wrap" "wrap-reverse") 287 | 288 | ;; align items 289 | 290 | ["items" "stretch"] (rule "align-items" "stretch") 291 | ["items" "start"] (rule "align-items" "flex-start") 292 | ["items" "center"] (rule "align-items" "center") 293 | ["items" "end"] (rule "align-items" "flex-end") 294 | ["items" "baseline"] (rule "align-items" "baseline") 295 | 296 | ;; align content 297 | 298 | ["content" "start"] (rule "align-content" "flex-start") 299 | ["content" "center"] (rule "align-content" "center") 300 | ["content" "end"] (rule "align-content" "flex-end") 301 | ["content" "between"] (rule "align-content" "space-between") 302 | ["content" "around"] (rule "align-content" "space-around") 303 | 304 | ;; align self 305 | 306 | ["self" "auto"] (rule "align-self" "auto") 307 | ["self" "start"] (rule "align-self" "flex-start") 308 | ["self" "center"] (rule "align-self" "center") 309 | ["self" "end"] (rule "align-self" "flex-end") 310 | ["self" "stretch"] (rule "align-self" "stretch") 311 | 312 | ;; justify content 313 | 314 | ["justify" "start"] (rule "justify-content" "flex-start") 315 | ["justify" "center"] (rule "justify-content" "center") 316 | ["justify" "end"] (rule "justify-content" "flex-end") 317 | ["justify" "between"] (rule "justify-content" "space-between") 318 | ["justify" "around"] (rule "justify-content" "space-around") 319 | 320 | ;; flex 321 | 322 | ["flex" (f :guard (cfg-> :flex))] 323 | (rule "flex" (cfg-> :flex f)) 324 | 325 | ;; grow 326 | 327 | ["flex" "grow"] (fragments->emotion ["flex" "grow" "default"]) 328 | ["flex" "grow" (g :guard (cfg-> :flex-grow))] 329 | (rule "flex-grow" (cfg-> :flex-grow g)) 330 | 331 | ;; shrink 332 | 333 | ["flex" "shrink"] (fragments->emotion ["flex" "shrink" "default"]) 334 | ["flex" "shrink" (s :guard (cfg-> :flex-shrink))] 335 | (rule "flex-shrink" (cfg-> :flex-shrink s)) 336 | 337 | ;; order 338 | 339 | ["order" (o :guard (cfg-> :order))] 340 | (rule "order" (cfg-> :order o)) 341 | 342 | ;; -------------------- SPACING ---------------------- 343 | 344 | ;; padding 345 | 346 | [(v :guard u/padding-fns) (p :guard (cfg-> :padding))] 347 | ((u/padding-fns v) (cfg-> :padding p)) 348 | 349 | ;; margin 350 | 351 | [(v :guard u/margin-fns) (m :guard (cfg-> :margin))] 352 | ((u/margin-fns v) (cfg-> :margin m)) 353 | ["" (v :guard u/margin-fns) (m :guard (cfg-> :margin))] 354 | ((u/margin-fns v) (cfg-> :margin (str "-" m))) 355 | 356 | ;; -------------------- SIZING ----------------------- 357 | 358 | ;; width 359 | 360 | ["w" (w :guard (cfg-> :width))] 361 | (rule "width" (cfg-> :width w)) 362 | 363 | ;; min width 364 | 365 | ["min" "w" (w :guard (cfg-> :min-width))] 366 | (rule "min-width" (cfg-> :min-width w)) 367 | 368 | ;; max width 369 | 370 | ["max" "w" (s :guard (cfg-> :max-width))] 371 | (rule "max-width" (cfg-> :max-width s)) 372 | 373 | ;; height 374 | 375 | ["h" (h :guard (cfg-> :height))] 376 | (rule "height" (cfg-> :height h)) 377 | 378 | ;; min height 379 | 380 | ["min" "h" (h :guard (cfg-> :min-height))] 381 | (rule "min-height" (cfg-> :min-height h)) 382 | 383 | ;; max height 384 | 385 | ["max" "h" (h :guard (cfg-> :max-height))] 386 | (rule "max-height" (cfg-> :max-height h)) 387 | 388 | ;; -------------------- TABLES ----------------------- 389 | 390 | ;; border collapse 391 | 392 | ["border" "collapse"] (rule "border-collapse" "collapse") 393 | ["border" "separate"] (rule "border-collapse" "separate") 394 | 395 | ;; table layout 396 | 397 | ["table" "auto"] (rule "table-layout" "auto") 398 | ["table" "fixed"] (rule "table-layout" "fixed") 399 | 400 | ;; ------------------- EFFECTS ----------------------- 401 | 402 | ;; box shadows 403 | 404 | ["shadow"] (fragments->emotion ["shadow" "default"]) 405 | ["shadow" (s :guard (cfg-> :box-shadow))] 406 | (rule "box-shadow" (cfg-> :box-shadow s)) 407 | 408 | ;; opacity 409 | 410 | ["opacity" (o :guard (cfg-> :opacity))] 411 | (rule "opacity" (cfg-> :opacity o)) 412 | 413 | ;; ---------------- INTERACTIVITY -------------------- 414 | 415 | ;; appearance 416 | 417 | ["appearance" "none"] (rule "appearance" "none") 418 | ["outline" "none"] (rule "outline" "none") 419 | 420 | ;; cursor 421 | 422 | ["cursor" & rest] 423 | (rule "cursor" (cfg-> :cursor (str/join "-" rest))) 424 | 425 | ;; outline 426 | 427 | ["outline" "none"] (rule "outline" "0") 428 | 429 | ;; pointer events 430 | 431 | ["pointer" "events" "none"] (rule "pointer-events" "none") 432 | ["pointer" "events" "auto"] (rule "pointer-events" "auto") 433 | 434 | ;; resize 435 | 436 | ["resize" "none"] (rule "resize" "none") 437 | ["resize"] (rule "resize" "both") 438 | ["resize" "y"] (rule "resize" "vertical") 439 | ["resize" "x"] (rule "resize" "horizontal") 440 | 441 | ;; user select 442 | 443 | ["select" (s :guard #{"none" "text" "all" "auto"})] 444 | (rule "user-select" s) 445 | 446 | ;; --------------------- SVG ------------------------- 447 | 448 | ;; fill 449 | 450 | ["fill" (f :guard (cfg-> :fill))] 451 | (rule "fill" (cfg-> :fill f)) 452 | 453 | ;; stroke 454 | 455 | ["stroke" (s :guard (cfg-> :stroke))] 456 | (rule "stroke" (cfg-> :stroke s)) 457 | 458 | ;; ---------------- screenreader --------------------- 459 | 460 | ["sr" "only"] 461 | (rule "position" "absolute" 462 | "width" "1px" 463 | "height" "1px" 464 | "padding" "0" 465 | "margin" "-1px" 466 | "overflow" "hidden" 467 | "clip" "rect(0, 0, 0, 0)" 468 | "whiteSpace" "nowrap" 469 | "borderWidth" "0") 470 | 471 | ["not" "sr" "only"] 472 | (rule "position" "static" 473 | "width" "auto" 474 | "height" "auto" 475 | "padding" "0" 476 | "margin" "0" 477 | "overflow" "visible" 478 | "clip" "auto" 479 | "whiteSpace" "normal") 480 | 481 | )))) 482 | 483 | 484 | (def fragments->css 485 | "TODO" 486 | (memoize 487 | (fn [class fragments] 488 | (match fragments 489 | 490 | ;; responsive breakpoints - media queries 491 | [(screen :guard (cfg-> :screens)) & rest] 492 | {(cfg-> :screens screen) 493 | (get (fragments->css class rest) nil)} 494 | 495 | ;; pseudo classes 496 | [(p :guard u/pseudo-classes) & rest] 497 | {nil {class (format ".%s:%s{%s}" (u/escape class) p (fragments->emotion rest))}} 498 | 499 | ;; container 500 | ["container"] 501 | (reduce-kv 502 | #(assoc %1 %3 {class (format ".container{max-width:%spx;}" %3)}) 503 | {nil {class ".container{width:100%;}"}} 504 | (cfg-> :screens)) 505 | 506 | ;; everything else 507 | :else 508 | {nil {class (format ".%s{%s}" (u/escape class) (fragments->emotion fragments))}})))) 509 | 510 | (defn class->css [class] 511 | (fragments->css class (u/split-fragments class))) 512 | -------------------------------------------------------------------------------- /src/tailwind/util.clj: -------------------------------------------------------------------------------- 1 | (ns tailwind.util 2 | (:require [clojure.string :as str]) 3 | (:import (com.sangupta.murmur Murmur2))) 4 | 5 | (defn split-classes [s] 6 | (-> (name s) str/trim (str/split #"\s+"))) 7 | 8 | (defn split-fragments [s] 9 | (str/split s #"[:-]")) 10 | 11 | (defn escape [class] 12 | (-> (str/replace class #":" "\\\\:") 13 | (str/replace #"/" "\\\\/"))) 14 | 15 | (defn rule [& kvs] 16 | (->> (partition-all 2 kvs) 17 | (map (fn [[k v]] (str (name k) ":" v ";"))) 18 | (apply str))) 19 | 20 | (def pseudo-classes 21 | #{"group-hover" "focus-within" "hover" "focus" "active" "disabled" "visited"}) 22 | 23 | (def background-attachments 24 | #{"fixed" "local" "scroll"}) 25 | 26 | (def corners 27 | {"tl" "top-left" 28 | "tr" "top-right" 29 | "bl" "bottom-left" 30 | "br" "bottom-right"}) 31 | 32 | (def sides 33 | {"t" #{"tl" "tr"} 34 | "r" #{"tr" "br"} 35 | "b" #{"br" "bl"} 36 | "l" #{"tl" "bl"}}) 37 | 38 | (def text-align 39 | #{"left" "center" "right" "justify"}) 40 | 41 | (defn side-fns [pre v] 42 | (case (second v) 43 | nil #(rule pre %) 44 | \t #(rule (str pre "-top") %) 45 | \b #(rule (str pre "-bottom") %) 46 | \l #(rule (str pre "-left") %) 47 | \r #(rule (str pre "-right") %) 48 | \x #(rule (str pre "-left") % (str pre "-right") %) 49 | \y #(rule (str pre "-top") % (str pre "-bottom") %))) 50 | 51 | (def padding-fns 52 | (let [ps ["p" "px" "py" "pt" "pl" "pr" "pb"]] 53 | (zipmap ps (map (partial side-fns "padding") ps)))) 54 | 55 | (def margin-fns 56 | (let [ps ["m" "mx" "my" "mt" "ml" "mr" "mb"]] 57 | (zipmap ps (map (partial side-fns "margin") ps)))) 58 | 59 | (def overflow 60 | #{"auto" "hidden" "visible" "scroll"}) 61 | 62 | (def border-sides 63 | {"t" "top" "l" "left" "r" "right" "b" "bottom"}) 64 | 65 | (def border-style 66 | #{"solid" "dashed" "dotted" "none" "double"}) 67 | 68 | (defn emotion-hash 69 | "Replicate emotion hash. useful for server side rendering" 70 | [s] 71 | (let [bytes (.getBytes s) 72 | hash (Murmur2/hash bytes (count bytes) (count s))] 73 | (str "css-" (Long/toString hash 36)))) 74 | 75 | --------------------------------------------------------------------------------