├── .VERSION_PREFIX ├── .dir-locals.el ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bb.edn ├── bin ├── kaocha └── proj ├── deps.edn ├── dev ├── build_notebooks.clj └── user.clj ├── notebooks ├── attributes_and_properties.clj ├── demo.clj ├── ornament_next.clj └── template.clj ├── pom.xml ├── repl_sessions ├── cssparser.clj └── poke.clj ├── src ├── .gitkeep └── lambdaisland │ ├── ornament.cljc │ └── ornament │ ├── clerk_util.clj │ └── watcher.clj ├── test ├── .gitkeep └── lambdaisland │ └── ornament_test.cljc └── tests.edn /.VERSION_PREFIX: -------------------------------------------------------------------------------- 1 | 1.16 -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test:byo") 2 | (cider-default-cljs-repl . browser)))) 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: push 4 | 5 | jobs: 6 | Kaocha: 7 | runs-on: ${{matrix.sys.os}} 8 | 9 | strategy: 10 | matrix: 11 | sys: 12 | # - { os: macos-latest, shell: bash } 13 | - { os: ubuntu-latest, shell: bash } 14 | # - { os: windows-latest, shell: powershell } 15 | 16 | defaults: 17 | run: 18 | shell: ${{matrix.sys.shell}} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch_depth: 0 24 | 25 | - name: 🔧 Install java 26 | uses: actions/setup-java@v1 27 | with: 28 | java-version: '11.0.7' 29 | 30 | - name: 🔧 Install clojure 31 | uses: DeLaGuardo/setup-clojure@master 32 | with: 33 | cli: '1.10.3.943' 34 | 35 | - name: 🗝 maven cache 36 | uses: actions/cache@v2 37 | with: 38 | path: | 39 | ~/.m2 40 | ~/.gitlibs 41 | key: ${{ runner.os }}-maven-${{ github.sha }} 42 | restore-keys: | 43 | ${{ runner.os }}-maven- 44 | 45 | - name: 🧪 Run tests 46 | run: bin/kaocha clj 47 | 48 | 49 | Clerk-build: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v2 53 | with: 54 | fetch_depth: 0 55 | 56 | - name: 🔧 Install java 57 | uses: actions/setup-java@v1 58 | with: 59 | java-version: '11.0.7' 60 | 61 | - name: 🔧 Install clojure 62 | uses: DeLaGuardo/setup-clojure@master 63 | with: 64 | cli: '1.10.3.943' 65 | 66 | - name: 🗝 maven cache 67 | uses: actions/cache@v2 68 | with: 69 | path: | 70 | ~/.m2 71 | ~/.gitlibs 72 | key: ${{ runner.os }}-maven-${{ github.sha }} 73 | restore-keys: | 74 | ${{ runner.os }}-maven- 75 | 76 | - name: 🗝 Clerk Cache 77 | uses: actions/cache@v2 78 | with: 79 | path: .clerk 80 | key: ${{ runner.os }}-clerk-cache 81 | 82 | - name: 🏗 Build Clerk Static App with default Notebooks 83 | run: clojure -A:dev:test:byo -M -m build-notebooks '${{ github.sha }}' 84 | 85 | - name: 🔐 Google Auth 86 | uses: google-github-actions/auth@v0 87 | with: 88 | credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_JSON }} 89 | 90 | - name: 🔧 Setup Google Cloud SDK 91 | uses: google-github-actions/setup-gcloud@v0.3.0 92 | 93 | - name: 📠 Copy static build to bucket under SHA 94 | run: | 95 | gsutil cp -r public/build gs://lambdaisland-notebooks/ornament/sha/${{ github.sha }} 96 | gsutil cp -r public/build gs://lambdaisland-notebooks/ornament/branch/${{ github.ref_name }} 97 | 98 | - name: ✅ Add success status to report with link to snapshot 99 | uses: Sibz/github-status-action@v1 100 | with: 101 | authToken: ${{secrets.GITHUB_TOKEN}} 102 | context: 'Browse Clerk Notebooks' 103 | description: 'Ready' 104 | state: 'success' 105 | sha: ${{github.event.pull_request.head.sha || github.sha}} 106 | target_url: https://notebooks.lambdaisland.com/ornament/sha/${{ github.sha }} 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .nrepl-port 3 | target 4 | repl 5 | scratch.clj 6 | .shadow-cljs 7 | target 8 | yarn.lock 9 | node_modules/ 10 | .DS_Store 11 | resources/public/ui 12 | .store 13 | out 14 | .#* 15 | package.json 16 | package-lock.json 17 | .clerk 18 | public 19 | deps.local.edn 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## Added 4 | 5 | ## Fixed 6 | 7 | ## Changed 8 | 9 | # 1.16.141 (2025-04-29 / 8c00784) 10 | 11 | ## Changed 12 | 13 | - Only include compiled CSS in cljs docstrings when the cljs optimization level 14 | is `:none` 15 | 16 | # 1.15.138 (2025-04-24 / 8299d3c) 17 | 18 | ## Fixed 19 | 20 | - Add a require-macros so defstyled can be referred from cljs directly 21 | 22 | # 1.14.134 (2025-04-24 / dadcb61) 23 | 24 | ## Fixed 25 | 26 | - Deal with more edge cases when referencing tokens inside style rules 27 | 28 | # 1.13.130 (2025-04-16 / 83c295f) 29 | 30 | ## Changed 31 | 32 | - [BREAKING] When setting a custom `:ornament/prefix` on the namespace, the 33 | separator `__` is no longer implied, to get the same result add `__` to the 34 | end of your prefix string. 35 | 36 | ## Added 37 | 38 | - Support docstrings, they come after the tagname, before any styles or tokens 39 | - If there's only a zero-arg render function (fn-tail), also emit a one-arg 40 | version that takes HTML attributes to be merged in. 41 | - Add `defrules`, for general garden CSS rules 42 | - Add `defprop`, for CSS custom properties (aka variables) 43 | - Add `defutil`, for standalone utility classes 44 | - Add `import-tokens!`, for importing W3C design token JSON files as properties (as per `defprop`) 45 | - Allow setting metadata on a child list, useful for reagent/react keys 46 | 47 | ## Fixed 48 | 49 | - Fix `defined-garden` 50 | - Use of `defrules` in pure-cljs namespaces 51 | - Fix implementation of ILookup on cljs 52 | 53 | # 1.12.107 (2023-09-27 / 2444e34) 54 | 55 | ## Fixed 56 | 57 | - Fix component resolution inside a set (in a rule of another component) (see tests for example) 58 | 59 | # 1.11.101 (2023-09-13 / 213279d) 60 | 61 | ## Fixed 62 | 63 | - Allow reusing the styles of one component directly inside another (see tests for example) 64 | 65 | # 1.10.94 (2023-08-30 / d1e1c3b) 66 | 67 | ## Fixed 68 | 69 | - Support using `defstyled` components as reagent form-2 components 70 | 71 | # 0.9.87 (2023-04-15 / dac82f4) 72 | 73 | ## Added 74 | 75 | - Added a `:tw-version` flag for the preflight, similar to `set-tokens!` 76 | - Document how to opt-in to Tailwind v3 77 | 78 | # 0.8.84 (2023-02-28 / 8d54daa) 79 | 80 | ## Added 81 | 82 | - Implement inheritance for fn-tails 83 | 84 | # 0.7.77 (2022-11-25 / a1f8d65) 85 | 86 | ## Added 87 | 88 | - Add Clerk garden setup 89 | 90 | ## Fixed 91 | 92 | - improved way to handle girouette v2 and v3 tokens 93 | 94 | # 0.6.69 (2022-10-11 / a629407) 95 | 96 | ## Fixed 97 | 98 | - Fixed an issue withe direct invocation of components with a render function (tail-fn) 99 | 100 | # 0.5.65 (2022-09-20 / 94cbebe) 101 | 102 | ## Added 103 | 104 | - Support attributes when using a top-level fragment in a rendering function 105 | 106 | # 0.4.34 (2022-01-25 / df056c8) 107 | 108 | ## Fixed 109 | 110 | - Fix cljdoc build 111 | 112 | # 0.3.30 (2022-01-25 / d37c5e4) 113 | 114 | ## Fixed 115 | 116 | - Improve ClojureScript support, in particular referencing components in other components style rules 117 | - Support vectors with multiple selectors, plus alternative syntax with sets 118 | 119 | # 0.2.19 (2021-11-29 / 6c8e226) 120 | 121 | ## Fixed 122 | 123 | - Fix issue where girouette tokens were not being applied to child elements. [See Github Issue](https://github.com/lambdaisland/ornament/issues/5) 124 | 125 | ## Changed 126 | 127 | - Bump Girouette to 0.0.6 128 | 129 | # 0.1.12 (2021-10-25 / d0a739b) 130 | 131 | ## Changed 132 | 133 | - Bump Girouette to 0.0.5 134 | 135 | # 0.0.7 (2021-10-01 / 52aa304) 136 | 137 | ## Added 138 | 139 | - Initial implementation 140 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ornament 2 | 3 | 4 | [![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/ornament)](https://cljdoc.org/d/com.lambdaisland/ornament) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/ornament.svg)](https://clojars.org/com.lambdaisland/ornament) 5 | 6 | 7 | CSS-in-Clj(s) 8 | 9 | ## Features 10 | 11 | - Define styled components 12 | - Use Garden syntax for CSS, or Girouette (Tailwind-style) utility class names 13 | - Have styling live close to, but separate from DOM structure 14 | - Compile to plain CSS files as a compile step 15 | - Use components in any Hiccup implementation, frontend or backend 16 | 17 | 18 | ## Installation 19 | 20 | To use the latest release, add the following to your `deps.edn` ([Clojure CLI](https://clojure.org/guides/deps_and_cli)) 21 | 22 | ``` 23 | com.lambdaisland/ornament {:mvn/version "1.16.141"} 24 | ``` 25 | 26 | or add the following to your `project.clj` ([Leiningen](https://leiningen.org/)) 27 | 28 | ``` 29 | [com.lambdaisland/ornament "1.16.141"] 30 | ``` 31 | 32 | 33 | ## Introduction 34 | 35 | Ornament is the culmination of many discussions and explorations with the aim to 36 | find the sweet spot in how to handle styling in large Clojure or ClojureScript 37 | web projects. It takes ideas from CSS-in-JS approaches, and utility-class 38 | libraries, while (in our opinion) improving on both. 39 | 40 | At the heart of ornament is the `defstyled` macro, which defines a "styled 41 | component". It combines a name, a HTML tag, and styling rules. 42 | 43 | ```clojure 44 | (require '[lambdaisland.ornament :as o]) 45 | 46 | (o/defstyled freebies-link :a 47 | {:font-size "1rem" 48 | :color "#cff9cf" 49 | :text-decoration "underline"}) 50 | ``` 51 | 52 | This does two things, first of all it creates a Hiccup component, which combines 53 | the tag (`:a` in this case), with a class name based on the component name. (See 54 | the section on "Choosing a Hiccup Implementation" for more info on where this 55 | syntax is supported.) 56 | 57 | ```clojure 58 | ;; Hiccup 59 | [freebies-link {:href "/episodes/interceptors-concepts"} "Freebies"] 60 | ``` 61 | 62 | Which renders as: 63 | 64 | ```html 65 | Freebies 66 | ``` 67 | 68 | The styling information is rendered during a build step to CSS, and written out, 69 | so it gets served as any other plain CSS file. 70 | 71 | ```clojure 72 | (spit "resource/public/ornament.css" (o/defined-styles)) 73 | ``` 74 | 75 | ```css 76 | .lambdaisland_episodes__freebies_link { 77 | font-size: 1rem; 78 | color: #cff9cf; 79 | text-decoration: underline; 80 | } 81 | ``` 82 | 83 | If you prefer to use [Girouette](https://github.com/green-coder/girouette) 84 | (Tailwind) utility classes (a.k.a. tokens), then you can do that as well, or you 85 | can mix and match. Note that for compatibility reasons Ornament will continue to 86 | default to Tailwind v2 components, but you can opt-in to v3, see the section 87 | below. 88 | 89 | ```clojure 90 | (o/defstyled freebies-link :a 91 | :text-base 92 | :text-green-500 93 | :underline) 94 | ``` 95 | 96 | Finally you can add one or more function bodies to your component, which acts as 97 | a render function, determining how the children of the component will render. 98 | Note that this render function only determines the "inside" of the component, it 99 | will still get wrapped with the tag and class name passed to `defstyled`. 100 | 101 | ```clojure 102 | (o/defstyled page-grid :div 103 | :relative :h-full :md:flex 104 | [:>.content :flex-1 :p-2 :md:p-10 :bg-gray-100] 105 | ([sidebar content] 106 | [:<> 107 | sidebar 108 | [:div.content 109 | content]])) 110 | ``` 111 | 112 | Hopefully these examples have sufficiently whetted your appetite. We'll explain 113 | the syntax and features of `defstyled` in detail down below. But first we need 114 | to explain Ornament's philosophy on how to deal with CSS, to give you accurate 115 | expectations of how it will behave. This is especially relevant for 116 | ClojureScript projects. 117 | 118 | ## Choosing your Tailwind version 119 | 120 | We rely on [Girouette](https://github.com/green-coder/girouette) to provide us 121 | with a re-implementation in Clojure of Tailwinds components, classes, and 122 | styles. At time of writing Girouette is compatible with either Tailwind 2.0.3, 123 | or 3.0.24. 124 | 125 | To use Tailwind 3 tokens (classes), pass an extra `:tw-version 3` option to 126 | `set-tokens!` 127 | 128 | ```clj 129 | (o/set-tokens! {:tw-version 3}) 130 | ``` 131 | 132 | See "Customizing Girouette" below for more info on `set-tokens`, or check the 133 | docstring. 134 | 135 | The `girouette.tw.preflight` namespace provides the Tailwind preflight (reset) 136 | stylesheet for either v2 or v3. If you are using `o/defined-styles` you can also 137 | opt to have it automatically prepended. This function also accepts a similar 138 | `:tw-version` argument (`2` or `3) 139 | 140 | ```clj 141 | (o/defined-styles {:preflight? true :tw-version 3}) 142 | ``` 143 | 144 | ## Choosing a Hiccup Implementation 145 | 146 | Representing HTML using nested Clojure vectors is an approach that was 147 | popularized by the [Hiccup](https://github.com/weavejester/hiccup) library, and 148 | so this is referred to as Hiccup-syntax. Since then many other libaries have 149 | implemented this same syntax, sometimes with small changes or extensions. 150 | 151 | A particular extension popularized by 152 | [Reagent](https://github.com/reagent-project/reagent) is the use of functions as 153 | components. You define functions which return hiccup, and use those functions in 154 | your hiccup where normally you would have a keyword denoting a HTML element tag. 155 | In essence you're no longer calling the function yourself (with parentheses) but 156 | telling Reagent to call it while rendering. 157 | 158 | ```clj 159 | (defn my-component [arg] 160 | [:p "in my component:" arg]) 161 | 162 | (reagent-dom/render [my-component "hello"] (js/document.getElementById "app")) 163 | ``` 164 | 165 | This has significance in React-apps because it allows React/Reagent to manage 166 | the component, but we like it in general to make it clear that something is 167 | conceptually a hiccup component. 168 | 169 | Ornament components are basically the same, they are (or behave like) functions 170 | which return Hiccup. If the implementation you are using supports putting them 171 | in square brackets then you can go with that, or you can just call them yourself 172 | as a function. 173 | 174 | ```clj 175 | (o/defstyled my-component :p 176 | :m-3) 177 | 178 | ;; option 1 179 | [my-component "hello"] 180 | 181 | ;; option 2 182 | (my-component "hello") 183 | ``` 184 | 185 | How do different Hiccup implementations differ? 186 | 187 | - Clojure vs ClojureScript (or both with cljc) 188 | - Support functions as components 189 | - Automatically HTML-escape text (the ones that support this all have different ways of injecting "raw" html strings) 190 | - Support for `:<>` (fragments) 191 | - Have the style property be a map (instead of just a string) 192 | - Pre-compile via macros for better performance 193 | 194 | Some that we know of: 195 | 196 | Clojure: 197 | 198 | - Hiccup 1, beware: does not auto-escape text and so is sensitive to CSRF attacks 199 | - Hiccup 2, fixes the security issue and is generally the better option, despite officially being in alpha 200 | - Enlive (See `net.cgrand.enlive-html/html`) 201 | - [lambdaisland.hiccup](https://github.com/lambdaisland/hiccup) is our take on 202 | this, it is based on Enlive, but adds functions-as-component, fragments, 203 | styles-as-maps, and a convenient syntax for raw html strings 204 | - Hicada (cljc, macro-based) 205 | 206 | ClojureScript 207 | 208 | - Reagent and other React wrappers 209 | - Sablono 210 | - Hicada 211 | 212 | ## Ornament CSS Compilation 213 | 214 | Ornament is written in CLJC, meaning you can call `defstyled` in ClojureScript 215 | exactly the same way as in Clojure. But, there's one important distinction, we 216 | do not add any styling information (be it Garden, Girouette, or CSS), to your 217 | ClojureScript build. 218 | 219 | This is an important design decision, and it's worth elaborating a bit. We 220 | understand the appeal of something like CSS-in-JS from a _syntax and programmer 221 | convenience_ point of view, and we try to offer the same kind of convenience. 222 | However, we question that it is sensible to deal with CSS generation in 223 | JavaScript. We think it's vastly superior to generate CSS once at build/deploy 224 | time, and to then deal with it *as CSS*. 225 | 226 | This pays dividends, we don't need to add all the machinery of Garden and 227 | Girouette to the frontend build, neither do we need all the styling definitions, 228 | so Ornament will have minimal impact on your bundle size. And your CSS can be 229 | served — and cached — as a simple static CSS file. 230 | 231 | What you do get from using `defstyled` in ClojureScript is a component 232 | (function) which you can use in Hiccup (e.g. Reagent) to render HTML. The 233 | component knows about the HTML tag to use, the CSS class name to add, and if you 234 | added a render function, it knows about that as well. 235 | 236 | So where does the styling go? We add that to a registry *during macroexpansion*. 237 | In other words: in Clojure. Once all `defstyled` invocations have been compiled, 238 | you can grab the full CSS with `(o/defined-styles)`, and spit it to a file. 239 | 240 | Before you grab the output of `(o/defined-styles)` you need to make sure all 241 | your styles have in fact been defined. For Clojure projects this merely means 242 | requiring all namespaces. If you have some kind of main application entry point 243 | that loads all your components/views, then load that, and capture the styles 244 | once it has finished. 245 | 246 | ### ClojureScript 247 | 248 | For ClojureScript you have two options, either you define all you components in 249 | `cljc` files, and use the same approach as in Clojure. The alternative is to 250 | first run your ClojureScript build, and then in the same process write out the 251 | styles. You can for instance write your own script that invokes the 252 | ClojureScript build API, then follows it up by writing out the styles, or you 253 | can use something like Shadow-cljs build hooks. 254 | 255 | #### Limitations 256 | 257 | Keep in mind that the style (Garden/CSS) section of a component is only ever 258 | processed in Clojure, even when used in ClojureScript files. This means that it 259 | is not possible to reference ClojureScript variables or functions. 260 | 261 | ```clojure 262 | (def sizes {:s "0.5rem" :m "1.rem" :l "2rem"}) 263 | 264 | (o/defstyled foo :div 265 | {:padding (:m sizes)}) 266 | ``` 267 | 268 | This will work in Clojure, but not ClojureScript. Referencing 269 | variables/functions inside the component body is not a problem. 270 | 271 | ```clojure 272 | (def divider [:hr]) 273 | 274 | (o/defstyled dividers :div 275 | {:padding "1rem"} 276 | ([& children] 277 | (into [:<>] 278 | (interpose divider) 279 | children))) 280 | ``` 281 | 282 | This works in both Clojure and ClojureScript. 283 | 284 | Note that it *is* possible to reference previously defined `defstyled` 285 | components in the style rules section, even in ClojureScript, see the section 286 | "Referencing other components in Rules" below. 287 | 288 | ```clojure 289 | (o/defstyled referenced :div 290 | {:color :blue}) 291 | 292 | (o/defstyled referer :p 293 | [referenced {:color :red}] ;; use as classname 294 | [:.foo referenced]) ;; use as style rule 295 | ``` 296 | 297 | #### Computing values (referencing vars) inside style rules in cljc files 298 | 299 | Style rules are processed during macroexpansion, which happens in Clojure, even 300 | when compiling ClojureScript. This means that any code inside style rules needs 301 | to be able to evaluate in Clojure by the time ClojureScript starts compiling the 302 | `defstyled` form. 303 | 304 | Consider this namespace 305 | 306 | ```clj 307 | ;; components.cljc 308 | (ns my.components 309 | (:require [lambdaisland.ornament :as o])) 310 | 311 | (def my-tokens {:main-color "green"}) 312 | 313 | (o/defstyled with-code :div 314 | {:background-color (-> my-tokens :main-color)}) 315 | ``` 316 | 317 | In Clojure this works as you would expect but when compiling this as a 318 | ClojureScript file it fails. The failure is due to the file never being loaded 319 | as a Clojure namespace, so the Clojure var `#'my.components/my-tokens` doesn't 320 | exist in the Clojure environment which is where macro expansion takes place. 321 | 322 | To fix this you can use the `:require-macros` directive which instructs the 323 | ClojureScript compiler to load a given Clojure namespace (in this case the 324 | current namespace) before continuing the compilation of the current namespace. 325 | 326 | ```clj 327 | (ns my.components 328 | (:require [lambdaisland.ornament :as o]) 329 | #?(:cljs (:require-macros my.components))) 330 | 331 | #?(:clj 332 | (def my-tokens {:main-color "green"})) 333 | 334 | (o/defstyled with-code :div 335 | {:background-color (-> my-tokens :main-color)}) 336 | ``` 337 | 338 | This addresses the problem by instructing the ClojureScript compiler to first 339 | load `my.components` as a clj namespace thereby creating the `#'my-tokens` 340 | Clojure var. The compiler then continues with the cljs compilation and when it 341 | gets to expansion of the `defstyled` macro form (specifically processing of the 342 | style rule `{:background-color ...}`) the `#'my-tokens` var is available in the 343 | clj environment and evaluated for it's value which is then included in the 344 | Ornament style registry. 345 | 346 | Wrapping `my-tokens` in `#?(:clj ...)` is not strictly necessary, but it helps 347 | to emphasize the point that this definition is only ever used on the Clojure 348 | side, you don't need it in your compiled ClojureScript. 349 | 350 | ##### Important note about CLJC files and clojure.core symbols 351 | 352 | If you do __any__ computation in your style rules it is recommended that you 353 | require the file as clj by utilising the `:require-macros` directive to self 354 | require the namespace. 355 | 356 | ```clj 357 | (ns my.components 358 | (:require [lambdaisland.ornament :as o]) 359 | #?(:cljs (:require-macros my.components))) 360 | 361 | (o/defstyled with-code :div 362 | {:background-color (str "red")}) 363 | ``` 364 | 365 | The need for this is a subtle consequence of the same interaction between 366 | Clojure and the ClojureScript compiler outlined in the previous section. A 367 | similar issue will manifest if you try to use `clojure.core` symbols in your 368 | style rules without requiring the clj namespace. This is surprising at first but 369 | makes sense after considering that it is the `ns` form that auto refers all 370 | `clojure.core` symbols into the current namespace. It follows that if the `ns` 371 | form is not evaluated, because we do not require the cljc file as a clj file, 372 | then no symbols mapping to the `clojure.core` symbols will have created in the 373 | current namespace. The `clojure.core` symbols are however interned, and as such 374 | fully qualifying the namespace will work but that is not an ergonomic solution. 375 | 376 | ```clj 377 | (ns my.components 378 | (:require [lambdaisland.ornament :as o])) 379 | 380 | ;; Does not work 381 | (o/defstyled with-code :div 382 | {:background-color (str "red")}) 383 | 384 | ;; Does work but not recommended 385 | (o/defstyled with-code :div 386 | {:background-color (clojure.core/str "red")}) 387 | ``` 388 | 389 | #### Shadow-cljs build hook example 390 | 391 | This is enough to get recompilation of your styles to CSS, which shadow-cljs 392 | will then hot-reload. 393 | 394 | ```clojure 395 | ;; Easiest to just make this a clj file. 396 | (ns my.hooks 397 | (:require [lambdaisland.ornament :as o] 398 | [garden.compiler :as gc] 399 | [girouette.tw.preflight :as girouette-preflight])) 400 | 401 | ;; Optional, but it's common to still have some style rules that are not 402 | ;; component-specific, so you can use Garden directly for that 403 | (def global-styles 404 | [[:html {:font-size "14pt"}]]) 405 | 406 | (defn write-styles-hook 407 | {:shadow.build/stage :flush} 408 | [build-state & args] 409 | ;; In case your global-styles is in a separate clj file you will have to 410 | ;; reload it yourself, shadow only reloads/recompiles cljs/cljc files 411 | #_(require my.styles :reload) 412 | ;; Just writing out the CSS is enough, shadow will pick it up (make sure you 413 | ;; have a ) 414 | (spit "resources/public/styles.css" 415 | (str 416 | ;; `defined-styles` takes a :preflight? flag, but we like to have some 417 | ;; style rules between the preflight and the components. This whole bit 418 | ;; is optional. 419 | (gc/compile-css (concat 420 | girouette-preflight/preflight-v2_0_3 421 | styles/global-styles)) 422 | "\n" 423 | (o/defined-styles))) 424 | build-state) 425 | ``` 426 | 427 | ```clojure 428 | ;; shadow-cljs.edn 429 | {,,, 430 | 431 | ;; For best results, otherwise you will find that some styles are missing after 432 | ;; restarting the shadow process 433 | :cache-blockers #{lambdaisland.ornament} 434 | 435 | :builds 436 | {:main 437 | {:target :browser 438 | ,,, 439 | :build-hooks [(my.hooks/write-styles-hook)]}}} 440 | ``` 441 | 442 | ## Defstyled Component Syntax 443 | 444 | `defstyled` really does two things, the macro expands to a form like 445 | 446 | ```clojure 447 | (def footer (reify StyledComponent ...)) 448 | ``` 449 | 450 | This "styled component" acts as a function, which is what makes it compatible 451 | with Hiccup implementations. 452 | 453 | ```clojure 454 | (footer "hello") 455 | ;;=> 456 | [:footer {:class ["project-discovery_parts__footer"]} "hello"] 457 | ``` 458 | 459 | It also implements various protocol methods. You don't typically need to call 460 | these yourself, but they can be useful for verifying how your component behaves. 461 | 462 | ```clojure 463 | (o/classname footer) 464 | ;;=> "project-discovery_parts__footer" 465 | (o/tag footer) 466 | ;;=> :footer 467 | (o/rules footer) 468 | ;;=> [{:max-width "60rem"}] 469 | (o/as-garden footer) 470 | ;;=> [".project-discovery_parts__footer" {:max-width "60rem"}] 471 | (o/css footer) 472 | ;;=> ".project-discovery_parts__footer{max-width:60rem}" 473 | ``` 474 | 475 | ### Component Name 476 | 477 | The first argument to `defstyled` is the component name, this will create a var 478 | with the given name, containing the component. A function-like object that can 479 | be used to render HTML. 480 | 481 | The name will also determine the class name that will be used in the HTML and 482 | CSS. For this Ornament combines the namespace name with the component name, and 483 | munges them to be valid CSS identifiers. 484 | 485 | ```clojure 486 | (ns my.views 487 | (:require [lambdaisland.ornament :as o])) 488 | 489 | (o/defstyled footer :footer 490 | {:max-width "60rem"}) 491 | 492 | (o/classname footer) 493 | ;; my_views__footer 494 | ``` 495 | 496 | You can use metadata on the namespace to change the namespace prefix. 497 | 498 | ``` clojure 499 | (ns ^{:ornament/prefix "views"} com.company.project.frontend.views 500 | (:require [lambdaisland.ornament :as o])) 501 | 502 | (o/defstyled footer :footer 503 | {:max-width "60rem"}) 504 | 505 | (o/classname footer) 506 | ;; views__footer 507 | ``` 508 | 509 | Using fully qualified var names as class names provides the unexpected benefit 510 | that it becomes trivial to find the component you are looking at in your 511 | browser's inspector. 512 | 513 | When stringifying the component you also get the class name back, this allows 514 | using them to reference a certain class, for instance in Hiccup: 515 | 516 | ```clojure 517 | [:div {:class (str footer)} ...] 518 | 519 | ;; Depending on your Hiccup implementation this can also work 520 | [:div {:class [footer]} ...] 521 | 522 | (js/querySelector (str footer)) 523 | ``` 524 | 525 | ### HTML Tag 526 | 527 | The second argument to `defstyled` is the HTML tag. This is typically a keyword, 528 | like `:section`, `:tr`, or `:div`. This is used when rendering the component as 529 | Hiccup, and so anything that is valid in your Hiccup implementation of choice is 530 | fair game, including `:div#some-id` or `:p.a_class`. You could also use for 531 | instance a Reagent component, assuming it correctly handles receiving a 532 | properties map as its first argument. You can't use `:<>` as the tag, since we 533 | can't add a class name to a fragment. 534 | 535 | As a special case you can use another styled component as the tag. 536 | 537 | ```clojure 538 | (defstyled about-footer footer 539 | ,,,) 540 | ``` 541 | 542 | This will cause the new component to "inherit" both the HTML tag used by the 543 | referenced componet, and any CSS rules it defines. 544 | 545 | ### Rules 546 | 547 | After the name and tag, `defstyled` takes one or more "rules". These can be 548 | maps, vectors, or keywords. 549 | 550 | Maps and vectors are handled by [Garden](https://github.com/noprompt/garden), 551 | and we recommend reading the Garden documentation and getting familiar with the 552 | syntax. Maps define CSS styles as demonstrated before, with vectors you can 553 | apply styles to descendant elements, or handle pseudo-elements. 554 | 555 | ```clojure 556 | (in-ns 'my-nav) 557 | 558 | (o/defstyled menu :nav 559 | {:padding "2rem"} 560 | [:a {:color "blue"}] 561 | [:&:hover {:background-color "#888"}]) 562 | 563 | ;; Inspect the result 564 | (o/css menu) 565 | ``` 566 | 567 | Result: 568 | 569 | ```css 570 | .my_nav__menu{padding:2rem} 571 | .my_nav__menu a{color:blue} 572 | .my_nav__menu:hover{background-color:#888} 573 | ``` 574 | 575 | Keywords are handled by [Girouette](https://github.com/green-coder/girouette). 576 | Girouette uses a grammar to parse utility class names like `:text-green-500`, 577 | and converting the result to Garden syntax. Out of the box it supports all the 578 | same names that Tailwind provides, but you can define custom rules, or adjust 579 | the color palette. (See [Customizing Girouette](#customizing-girouette)). 580 | 581 | Note that you can mix and match these. You should be able to use a Girouette 582 | keyword anywhere where you would use a Garden properties map. 583 | 584 | #### Referencing other components in Rules 585 | 586 | You can use a previously defined `defstyled` component either as a selector, or 587 | as a style rule. 588 | 589 | Consider this "call to action" button. 590 | 591 | ```clojure 592 | (o/defstyled cta :button 593 | {:background-color "red"}) 594 | ``` 595 | 596 | You might use it as part of another component, and add additional styling for 597 | that context. 598 | 599 | ```clojure 600 | (o/defstyled buy-now-section :div 601 | [cta {:padding "2rem"}] 602 | ([] 603 | [:<> 604 | [:p "The best widgest in the world"] 605 | [cta {:value "Buy now!"}]])) 606 | ``` 607 | 608 | Here `cta` is a shorthand for writing the full Ornament class name of the 609 | component. Now the `cta` button will get some extra padding in this context, in 610 | addition to its red background. 611 | 612 | You can also use `cta` as a reusable group of styles. In this case we want to 613 | style the `:a` element with the `cta` styles. 614 | 615 | ```clojure 616 | (o/defstyled pricing-link :span 617 | [:a cta] 618 | ([] 619 | [:a {:href "/pricing"} "Pricing"])) 620 | ``` 621 | 622 | This kind of referencing previously defined components works both in Clojure and 623 | ClojureScript, even though in ClojureScript usage you can't normally reference 624 | vars inside your style declaration. To make these work we resolve these symbols 625 | during compilation based on Ornament's registry of components. 626 | 627 | ## Render functions 628 | 629 | After the component name, tag, and CSS rules, you can optionally put one or more 630 | render functions, consisting of an argument vector, and the function body. 631 | 632 | ```clojure 633 | (o/defstyled with-body :p 634 | :px-5 :py-3 :rounded-xl 635 | {:color "azure"} 636 | ([& children] 637 | (into [:strong] children))) 638 | 639 | [with-body "hello"] 640 | ;;=> 641 | "

hello

" 642 | ``` 643 | 644 | You can put multiple of these to deal with multiple arities 645 | 646 | ```clojure 647 | (o/defstyled multi-arity :p 648 | ([arg1] 649 | [:strong arg1]) 650 | ([arg1 arg2] 651 | [:<> 652 | [:strong arg1] [:em arg2]])) 653 | ``` 654 | 655 | Without render functions a styled component works almost like a plain HTML tag 656 | when using in Hiccup: the first argument, if it's a map, is treated as a map of 657 | HTML attributes, any following arguments are treated as children. 658 | 659 | When you supply your own render function this behavior changes. All arguments 660 | are passed to the render function, which then determines the element's 661 | attributes and children. 662 | 663 | To set custom attributes on the outer element from inside the render function, 664 | you use a properties map together with a fragment `:<>` identifier: 665 | 666 | ```clojure 667 | (o/defstyled my-compo :div 668 | ([props] 669 | [:<> {:title "hello"} "hello!"])) 670 | ``` 671 | 672 | If you pass a `:class` here it will get added to the class that Ornament 673 | generates for the component. 674 | 675 | When using a component that has a custom render function, you can set attributes 676 | by using the special `:lambdaisland.ornament/attrs` keyword. 677 | 678 | ```clojure 679 | [my-compo {:regular-prop 123 ::o/attrs {:title "heyo"}}] 680 | ``` 681 | 682 | Any `:class` or `:style` attributes passed in this way will be added to any 683 | classes or styles set inside the render function with `:<>`. Optionally for 684 | `:class` and `:style` you can replace the values instead of appending by adding 685 | a `^:replace` metadata on the vector / map. 686 | 687 | ```clojure 688 | [my-compo {::o/attrs {:class ^:replace ["one-class" "other-class"] 689 | :style {:text-color "blue"}}}] 690 | ``` 691 | 692 | In previous versions we supported `:class`, `:id` and `:style` at the top of the 693 | properties map, but that's no longer the case. 694 | 695 | There's an additional mechanic for setting attributes from inside the 696 | render-function, through metadata on the return value, but it is considered 697 | deprecated, since it's superseded by `[:<> {,,,attrs,,,}]`. 698 | 699 | ```clojure 700 | (o/defstyled nav-link :a 701 | ([{:keys [id]}] 702 | (let [{:keys [url title description]} (get-route id)] 703 | ^{:href url :title description} 704 | [:<> title]))) 705 | 706 | ;;=> 707 | Videos 708 | ``` 709 | 710 | ## Differences from Garden 711 | 712 | The rules section of a component is essentially 713 | [Garden](https://github.com/noprompt/garden) syntax. We run it through the 714 | Garden compiler, and so things that work in Garden generally work there as well, 715 | with some exceptions. 716 | 717 | Keywords that come first inside a vector are always treated as CSS selectors, as 718 | you would expect, but if they occur elsewhere then we first pass them to 719 | Girouette to expand to style rules class names. If Girouette does not recognize 720 | the keyword as a classname, then it's preserved in the Garden as-is. 721 | 722 | That means that generally things work as expected, since selectors and Girouette 723 | classes don't have much overlap. 724 | 725 | ```clojure 726 | ;; ✔️ :ol is recognized as a selector 727 | 728 | (o/defstyled list-wrapper :div 729 | [:ul :ol {:background-color "blue"}]) 730 | 731 | (o/css list-wrapper) 732 | ;; => ".ot__list_wrapper ul,.ot__list_wrapper ol{background-color:blue}" 733 | 734 | ;; ✔️ :bg-blue-500 is recognized as a utility class 735 | 736 | (o/defstyled list-wrapper :div 737 | [:ul :bg-blue-500]) 738 | 739 | (o/css list-wrapper) 740 | ;; => ".ot__list_wrapper ul{--gi-bg-opacity:1;background-color:rgba(59,130,246,var(--gi-bg-opacity))}" 741 | ``` 742 | 743 | But there is some potential for clashes, e.g. Girouette has a `:table` class. 744 | 745 | ```clojure 746 | ;; ❌ not what we wanted 747 | 748 | (o/defstyled fig-wrapper :div 749 | [:figure :table {:padding "1rem"}]) 750 | 751 | (o/css fig-wrapper) 752 | ;; => ".ot__fig_wrapper figure{display:table;padding:1rem}" 753 | ``` 754 | 755 | Instead use a set to make it explicit that these are multiple selectors. It's 756 | good practice to do this in general since it is more explicit and reduces 757 | ambiguity and chance of clashes. 758 | 759 | ```clojure 760 | (o/defstyled fig-wrapper :div 761 | [#{:figure :table} {:padding "1rem"}]) 762 | 763 | (o/css fig-wrapper) 764 | ;; => ".ot__fig_wrapper figure,.ot__fig_wrapper table{padding:1rem}" 765 | ``` 766 | 767 | ### Garden Extensions 768 | 769 | Ornament does a certain amount of pre-processing before passing the rules over 770 | to Garden for compilation. This allows us to support some extra syntax which we 771 | find more convenient. 772 | 773 | ### Special "tags" 774 | 775 | Use these as the first element in a vector to opt into special handling. Some of 776 | these are used where a selector would be used, others are helpers for defining 777 | property values. 778 | 779 | - `:at-media` 780 | 781 | You can add breakpoints for responsiveness to your components with `:at-media`. 782 | 783 | ```clojure 784 | (o/defstyled eps-container :div 785 | {:display "grid" 786 | :grid-gap "1rem" 787 | :grid-template-columns "repeat(auto-fill, minmax(20rem, 1fr))" 788 | :padding "0 1rem 1rem"} 789 | [:at-media {:min-width "40rem"} 790 | {:grid-gap "2rem" 791 | :padding "0 2rem 2rem"}]) 792 | ``` 793 | 794 | - `:cssfn` 795 | 796 | CSS functions can be invoked with `:cssfn` 797 | 798 | ```clojure 799 | (o/defstyled with-css-fn :a 800 | [:&:after {:content [:cssfn :attr "href"]}]) 801 | ``` 802 | 803 | - `:at-supports` 804 | 805 | Support for feature tests via `@supports` 806 | 807 | ```clojure 808 | (o/defstyled feature-check :div 809 | [:at-supports {:display "grid"} 810 | {:display "grid"}]) 811 | ``` 812 | 813 | - `:rgb` / `:hsl` / `:rgba` / `:hsla` 814 | 815 | Shorthands for color functions 816 | 817 | ```clojure 818 | (o/defstyled color-fns :div 819 | {:color [:rgb 150 30 75] 820 | :background-color [:hsla 235 100 50 0.5]}) 821 | 822 | (o/css color-fns) 823 | ;;=> 824 | ".ot__color_fns{color:#961e4b;background-color:hsla(235,100%,50%,0.5)}" 825 | ``` 826 | 827 | - `:str` 828 | 829 | Turns any strings into quoted strings, for cases where you need to put string content in your CSS. 830 | 831 | ```clojure 832 | (o/defstyled with-css-fn :a 833 | [:&:after {:content [:str " (" [:cssfn :attr "href"] ")"]}]) 834 | ``` 835 | 836 | #### Special property handling 837 | 838 | Some property names we recognize and treat special, mainly to make it less 839 | tedious to define composite values. 840 | 841 | - `:grid-area` / `:border` / `:margin` / `:padding` 842 | 843 | Treat vector values as space-separated lists, e.g. `:padding [10 0 15 0]`. 844 | Non-vector values are passed on unchanged. 845 | 846 | - `:grid-template-areas` 847 | 848 | Use nested vectors to define the areas 849 | 850 | ```clojure 851 | :grid-template-areas [["title" "title" "user"] 852 | ["controlbar" "controlbar" "controlbar"] 853 | ["...." "...." "...."] 854 | ["...." "...." "...."] 855 | ["...." "...." "...."] 856 | ``` 857 | 858 | ## Customizing Girouette 859 | 860 | Girouette is highly customizable. Out of the box it supports the same classes as 861 | Tailwind does, but you can customize the colors, fonts, or add completely new 862 | rules for recognizing class name. 863 | 864 | The `girouette-api` atom contains the result of `giroutte/make-api`. By 865 | replacing it you can customize how keywords are expanded to Garden. We provide a 866 | `set-tokens!` function which makes the common cases straightforward. This 867 | configures Girouette, so that these tokens become available inside Ornament 868 | style declarations. 869 | 870 | `set-tokens!` takes a map with these (optional) keys: 871 | 872 | - `:colors` : map from keyword to 6-digit hex color, without leading `#` 873 | - `:fonts`: map from keyword to font stack (comman separated string) 874 | - `:components`: sequence of Girouette components, each a map with `:id` 875 | (keyword), `:rules` (string, instaparse, can be omitted), and `:garden` (map, 876 | or function taking instaparse results and returning Garden map) 877 | - `:tw-version`: which Girouette defaults to use, either based on Tailwind 878 | v2, or v3. Valid values: `2`, `3`. Defaults to v2. 879 | 880 | ```clojure 881 | (o/set-tokens! {:colors {:primary "001122"} 882 | :fonts {:system "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"} 883 | :components [{:id :full-center 884 | :garden {:display "inline-flex" 885 | :align-items "center"}} 886 | {:id :full-center-bis 887 | :garden [:& :inline-flex :items-center]} 888 | {:id :custom-bullets 889 | :rules "custom-bullets = <'bullets-'> bullet-char 890 | = #\".\"" 891 | :garden (fn [{[bullet-char] :component-data}] 892 | [:& 893 | {:list-style "none" 894 | :padding 0 895 | :margin 0} 896 | [:li 897 | {:padding-left "1rem" 898 | :text-indent "-0.7rem"}] 899 | ["li:before" 900 | {:content bullet-char}]])}]}) 901 | ``` 902 | 903 | Let's go over these. Colors is straightforward, it introduces a new color name, 904 | so now I can use classes like `:text-primary-500` or `:bg-primary`. 905 | 906 | Fonts provide the `:font-` class, so in this case `:font-system`. 907 | 908 | With custom components there's a lot you can do. The first one here, 909 | `:full-center`, only has a `:garden` key, which has plain data as its value. 910 | This basically provides an alias or shorthand, so we can use `:full-center` in 911 | place of `{:display "inline-flex" :align-items "center"}`. The second one, 912 | `:full-center-bis` is essentially the same, but we've used other Girouette 913 | classes. Just as in `defstyled` you can use those too. 914 | 915 | The third one introduces a completely custom rule. It has a `:rules` key, which 916 | gets a string using Instaparse grammar syntax. Here we're definining a grammar 917 | which will recognize any classname starting with "bullets-" and followed by a 918 | single character. 919 | 920 | If `:rules` is omitted we assume this is a static token, and we'll generate a 921 | rule of the form `token-id = <'token-id'>`. That's what happens with the first 922 | two components. 923 | 924 | In this case the `:garden` key gets a function, which receives the parse 925 | information (under the `:component-data` key), and can use it to build up the 926 | Garden styling. Notice that we're using the "bullet-char" that we parsed out of 927 | the class name, to set the `:content` on `:li:before`. 928 | 929 | The end result is that we can do something like this: 930 | 931 | ```clojure 932 | (o/defstyled bear-list :ul 933 | :bullets-🐻) 934 | 935 | [bear-list 936 | [:li "Black"] 937 | [:li "Formosan"]] 938 | ``` 939 | 940 | And get a bullet list which uses bear emojis for the bullets. 941 | 942 | `set-tokens!` will add the new colors, fonts, and components to the defaults that 943 | Girouette provides. You can change that by adding a `^:replace` tag (this uses 944 | meta-merge). e.g. `{:colors ^:replace {...}}`) 945 | 946 | ## Babashka compatibility 947 | 948 | Unfortunately Ornament is not bb-compatible, and most likely never will be. 949 | 950 | Ornament styled components extend the `IFn` interface, babashka only supports 951 | extending protocols. Extending `IFn` is quite crucial to Ornament's design, 952 | since this is what allows us to make Hiccup-compatible components, since they 953 | act as functions. 954 | 955 | There are similar issues with libraries we depend on, notably Garden via 956 | Girouette. Girouette also depends on Instaparse, which is also in its original 957 | form not bb-compatible. 958 | 959 | Making Garden bb-com 960 | 961 | 962 | ## Lambda Island Open Source 963 | 964 | Thank you! ornament is made possible thanks to our generous backers. [Become a 965 | backer on OpenCollective](https://opencollective.com/lambda-island) so that we 966 | can continue to make ornament better. 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 |   975 | 976 | ornament is part of a growing collection of quality Clojure libraries created and maintained 977 | by the fine folks at [Gaiwan](https://gaiwan.co). 978 | 979 | Pay it forward by [becoming a backer on our Open Collective](http://opencollective.com/lambda-island), 980 | so that we may continue to enjoy a thriving Clojure ecosystem. 981 | 982 | You can find an overview of our projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source). 983 | 984 |   985 | 986 |   987 | 988 | 989 | 990 | ## Contributing 991 | 992 | Everyone has a right to submit patches to ornament, and thus become a contributor. 993 | 994 | Contributors MUST 995 | 996 | - adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide) 997 | - write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*` 998 | - agree to license their contributions as EPL 1.0. 999 | - not break the contract with downstream consumers. `**` 1000 | - not break the tests. 1001 | 1002 | Contributors SHOULD 1003 | 1004 | - update the CHANGELOG and README. 1005 | - add tests for new functionality. 1006 | 1007 | If you submit a pull request that adheres to these rules, then it will almost 1008 | certainly be merged immediately. However some things may require more 1009 | consideration. If you add new dependencies, or significantly increase the API 1010 | surface, then we need to decide if these changes are in line with the project's 1011 | goals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template), 1012 | and collecting feedback on it. 1013 | 1014 | `*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution. 1015 | 1016 | `**` As long as this project has not seen a public release (i.e. is not on Clojars) 1017 | we may still consider making breaking changes, if there is consensus that the 1018 | changes are justified. 1019 | 1020 | 1021 | 1022 | ## License 1023 | 1024 | Copyright © 2021-2022 Arne Brasseur and contributors 1025 | 1026 | Available under the terms of the Eclipse Public License 1.0, see LICENSE.txt 1027 | 1028 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" 3 | :git/sha "52d82093bde661cd8c57b2f1e8cfaf854575a583"}}} 4 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ -d node_modules ]] || npm install ws 4 | 5 | clojure -M:test -m kaocha.runner "$@" 6 | -------------------------------------------------------------------------------- /bin/proj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns proj 4 | (:require [lioss.main :as lioss])) 5 | 6 | (lioss/main 7 | {:license :mpl 8 | :inception-year 2021 9 | :description "Clojure Styled Components" 10 | :group-id "com.lambdaisland" 11 | :aliases-as-optional-deps [:byo]}) 12 | 13 | ;; Local Variables: 14 | ;; mode:clojure 15 | ;; End: 16 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | 3 | :deps 4 | {org.clojure/clojure {:mvn/version "1.12.0"} 5 | com.lambdaisland/garden {:mvn/version "1.7.590"} 6 | girouette/girouette {:mvn/version "0.0.10"} 7 | meta-merge/meta-merge {:mvn/version "1.0.0"}} 8 | 9 | :aliases 10 | {:dev 11 | {:extra-paths ["dev"] 12 | :extra-deps {io.github.nextjournal/clerk {:mvn/version "0.17.1102"} 13 | com.lambdaisland/hiccup {:mvn/version "0.14.67"} }} 14 | 15 | :byo 16 | {:extra-deps {hawk/hawk {:mvn/version "0.2.11"} 17 | com.lambdaisland/glogi {:mvn/version "1.3.169"} 18 | io.pedestal/pedestal.log {:mvn/version "0.7.2"}}} 19 | 20 | :test 21 | {:extra-paths ["test"] 22 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} 23 | lambdaisland/kaocha-cljs {:mvn/version "1.5.154"} 24 | org.clojure/clojurescript {:mvn/version "1.11.132"} 25 | com.lambdaisland/glogi {:mvn/version "1.3.169"} 26 | ;; for lambdaisland.hiccup and lambdaisland.thicc, used in testing 27 | lambdaisland/webstuff {:git/url "https://github.com/lambdaisland/webstuff" 28 | :git/sha "f3ae2a2d41a4335d3da1757a3a21aa1dd1125eb1" 29 | #_#_:local/root "/home/arne/github/lambdaisland/webstuff"}}} 30 | 31 | :cssparser 32 | {:extra-deps {net.sourceforge.cssparser/cssparser {:mvn/version "0.9.30"}}} 33 | 34 | :nextjournal/clerk 35 | {:exec-fn nextjournal.clerk/build! 36 | :exec-args {:paths ["notebooks/demo.clj" 37 | "notebooks/attributes_and_properties.clj"]} 38 | :nextjournal.clerk/aliases [:dev]}}} 39 | -------------------------------------------------------------------------------- /dev/build_notebooks.clj: -------------------------------------------------------------------------------- 1 | (ns build-notebooks 2 | "Build notebooks as a static app on CI" 3 | (:require 4 | [clojure.java.io :as io] 5 | [nextjournal.clerk :as clerk])) 6 | 7 | (defn -main [sha] 8 | (clerk/build-static-app! 9 | {:paths (->> (file-seq (io/file "notebooks")) 10 | (remove (memfn ^java.io.File isDirectory)) 11 | (map str)) 12 | :bundle? false 13 | :path-prefix (str "ornament/sha/" sha "/") 14 | :git/sha sha 15 | :git/url "https://github.com/lambdaisland/ornament" 16 | :browse? false})) 17 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (defmacro jit [sym] 4 | `(requiring-resolve '~sym)) 5 | 6 | (defn browse [] 7 | ((jit clojure.java.browse/browse-url) "http://localhost:7777")) 8 | 9 | (def portal-instance (atom nil)) 10 | 11 | (defn portal 12 | "Open a Portal window and register a tap handler for it. The result can be 13 | treated like an atom." 14 | [] 15 | ;; Portal is both an IPersistentMap and an IDeref, which confuses pprint. 16 | (prefer-method @(jit clojure.pprint/simple-dispatch) clojure.lang.IPersistentMap clojure.lang.IDeref) 17 | (let [p ((jit portal.api/open) @portal-instance)] 18 | (reset! portal-instance p) 19 | (add-tap (jit portal.api/submit)) 20 | p)) 21 | 22 | (defn clerk! [] 23 | ((jit nextjournal.clerk/serve!) {:watch-paths ["notebooks"]})) 24 | -------------------------------------------------------------------------------- /notebooks/attributes_and_properties.clj: -------------------------------------------------------------------------------- 1 | (ns notebooks.attributes-and-properties 2 | (:require 3 | [lambdaisland.ornament :as o] 4 | [lambdaisland.ornament.clerk-util :refer [inline-styles 5 | render 6 | expand]])) 7 | 8 | ;; # Attributes and Properties 9 | 10 | ;; When dealing with Ornament components it's important to understand the 11 | ;; distinction between "attributes" and "properties". 12 | 13 | ;;; ## Attributes 14 | 15 | ;; Attributes are a concept from HTML, they are the key-value pairs you provide 16 | ;; to HTML elements in your markup. 17 | 18 | ;; ```html 19 | ;; Go to Example 20 | ;; ``` 21 | 22 | ;; In this example `href` and `title` are attributes. 23 | 24 | ;; When you define a plain Ornament component, one that doesn't have a custom 25 | ;; render function, then you can pass in attributes by providing a map as the 26 | ;; first argument, just like you would do in plain Hiccup. 27 | 28 | ;; Define components 29 | 30 | (o/defstyled strong-link :a 31 | {:font-weight 1000 32 | :border "1px solid #999"}) 33 | 34 | [:a {:href "https://github.com/lambdaisland/open-source"} 35 | "Check out our open source offerings!"] 36 | 37 | (expand 38 | [strong-link 39 | {:href "https://github.com/lambdaisland/open-source"} 40 | "Check out our open source offerings!"]) 41 | 42 | ;; You can see that these two Hiccup forms are basically equivalent, except that 43 | ;; in the case of `strong-link` an extra class is added to the `class` 44 | ;; attribute. 45 | 46 | ;; ## Properties 47 | 48 | ;; When defininig components through a render function, as is common in 49 | ;; React/Reagent, you can also pass a map as the first argument. These are 50 | ;; called "properties" or (in React especially) as "props". 51 | 52 | ;; It's up to the component (the render function) to do something with these. 53 | 54 | (defn user-list [{:keys [users] :as props}] 55 | [:ul 56 | (for [{:keys [name]} users] 57 | [:li name])]) 58 | 59 | (render 60 | [user-list {:users [{:name "Arne"} 61 | {:name "Felipe"}]}]) 62 | 63 | ;; The same is true for Ornament components that contain their own render 64 | ;; function. 65 | 66 | (o/defstyled styled-user-list :ul 67 | [:li {:list-style "square"}] 68 | ([{:keys [users] :as props}] 69 | (for [{:keys [name]} users] 70 | [:li name]))) 71 | 72 | (render 73 | [styled-user-list {:users [{:name "Arne"} 74 | {:name "Felipe"}]}]) 75 | 76 | ;; ## Setting Attributes on Styled Components 77 | 78 | ;; But what if we still want to set certain attributes on this `:ul`, perhaps we 79 | ;; want to indicate that this element is in a different language using the 80 | ;; `lang` attribute. 81 | 82 | ;; Generally this is the responsibility of the component itself, it can return a 83 | ;; fragment (`:<>`), inclduding an attributes map. 84 | 85 | 86 | (o/defstyled name-list-de :ul 87 | [:li {:list-style "square"}] 88 | ([{:keys [users] :as props}] 89 | [:<> {:lang "de"} 90 | (for [{:keys [name]} users] 91 | [:li name])])) 92 | 93 | (expand 94 | [name-list-de {:users [{:name "Goethe"} 95 | {:name "Freud"}]}]) 96 | 97 | 98 | ;; So now we've added a `lang` attribute inside the component. You can ignore the 99 | ;; extra attributes here like `:col` and `:row`, they are a consequence of how 100 | ;; Clerk renders things, combined with the fact that we support a legacy syntax 101 | ;; where the attributes are provided as metadata on the result of the render 102 | ;; function. 103 | 104 | ;; The component could even take a `lang` property, and pass that on as a `lang` 105 | ;; attribute, so that you can decide to set the language at the point where you 106 | ;; are using this component. 107 | 108 | ;; However Ornament also supports a special property, 109 | ;; `:lambdaisland.ornament/attrs`, which will get merged in with the other 110 | ;; attributes. 111 | 112 | (expand 113 | [name-list-de {:users [{:name "Goethe"} 114 | {:name "Freud"}] 115 | ::o/attrs {:title "German Philosophers"}}]) 116 | 117 | ;; These `::o/attrs` will take precedence over attributes set inside the 118 | ;; component. So our component with `lang=de` could be used for a different 119 | ;; language as well. The value inside the component is the default, but can be 120 | ;; overruled when using the components. 121 | 122 | (expand 123 | [name-list-de {:users [{:name "René Magritte"} 124 | {:name "James Ensor"}] 125 | ::o/attrs {:lang "nl" 126 | :title "Belgische Kunstenaars"}}]) 127 | 128 | ;; When merging attributes like this `:class` and `:style` are handled special. 129 | ;; Classes are additive, you get both the classes defined inside the component, 130 | ;; the ones passed in through `::o/attrs`, and the special ornament class that 131 | ;; gets generated to link this component to its styles. You can use either 132 | ;; strings or vectors of strings as the `class` attribute. 133 | 134 | ;; `:style` attributes are merged (assuming they are maps). 135 | 136 | (o/defstyled name-list-klz :ul 137 | [:li {:list-style "square"}] 138 | ([{:keys [users] :as props}] 139 | [:<> {:class "some-class" 140 | :style {:background-color "red"}} 141 | (for [{:keys [name]} users] 142 | [:li name])])) 143 | 144 | (expand 145 | [name-list-klz {:users [{:name "John"}] 146 | ::o/attrs 147 | {:class "other-class" 148 | :style {:text-color "blue"}}}]) 149 | 150 | ;; Class attribute as a vector: 151 | 152 | (expand 153 | [name-list-klz {:users [{:name "John"}] 154 | ::o/attrs {:class ["one-class" "other-class"] 155 | :style {:text-color "blue"}}}]) 156 | 157 | ;; For both `:class` and `:style` you can get the regular merge behavior back, 158 | ;; where the `::o/attrs` value completely replaces the default value specified 159 | ;; in the component, by adding a `^:replace` metadata on the vector or map. 160 | 161 | (expand 162 | [name-list-klz {:users [{:name "John"}] 163 | ::o/attrs {:class ^:replace ["one-class" "other-class"] 164 | :style ^:replace {:text-color "blue"}}}]) 165 | 166 | ;; ## Conclusion 167 | 168 | ;; While we were working on Ornament, and trying it out on various projects, we 169 | ;; found we wanted a large degree of flexibility for setting attributes, either 170 | ;; from within the component, or from without. 171 | 172 | ;; We wanted to stay fairly close to how things work in plain Hiccup, as well as 173 | ;; in Reagent, so people can largely rely on their existing mental models. 174 | 175 | ;; At the same time we had to reconcile some differences with how plain HTML 176 | ;; elements work in Hiccup, vs how rendered components work. We went through a 177 | ;; few iterations, and finally realized that by making a clear distinction 178 | ;; between attributes and properties we could define behavior that is 179 | ;; consistent, explicit, and intuitive, while avoiding "magic" and a 180 | ;; proliferation of special cases. 181 | 182 | ;; While there are some particulars to be aware of, we hope the result will 183 | ;; generally be found to be intuitive, and to yield code that does not present 184 | ;; undue surprises to the reader. 185 | 186 | ^{:nextjournal.clerk/no-cache true 187 | :nextjournal.clerk/visibility #{:fold}} 188 | (inline-styles) 189 | -------------------------------------------------------------------------------- /notebooks/demo.clj: -------------------------------------------------------------------------------- 1 | (ns demo 2 | (:require 3 | [lambdaisland.hiccup :as hiccup] 4 | [lambdaisland.ornament :as o] 5 | [nextjournal.clerk :as clerk])) 6 | 7 | ;; # A Small Demonstration of Ornament 8 | 9 | ;; Helper to render components: 10 | 11 | (defn render [h] 12 | (clerk/html (hiccup/render h {:doctype? false}))) 13 | 14 | ;; A relatively simple component, using Girouette (Tailwind-style) styling, and 15 | ;; leaning into the fact that we can organize our styles however we like, 16 | ;; including splitting things up and adding comments. 17 | 18 | (o/defstyled navbar :nav 19 | ;; layout 20 | :flex :space-x-4 21 | [:a :px-3 :py-2 :my-2] 22 | ;; fonts & borders 23 | :font-sans 24 | [:a :text-sm :font-medium :rounded-md] 25 | ;; colors 26 | :bg-gray-800 27 | [:a :text-gray-300 :hover:bg-gray-700 :hover:text-white 28 | :rounded-md :text-sm :font-medium 29 | [:&.active :bg-gray-900 :text-white]] 30 | ([links] 31 | (for [[{:keys [text href active?]}] links] 32 | [(if active? :a.active :a) 33 | {:href href} 34 | text]))) 35 | 36 | ;; Let's see what that looks 37 | 38 | (render 39 | [navbar 40 | [[{:text "Lambda Island" 41 | :href "https://lambdaisland.com" 42 | :active? true}] 43 | [{:text "Gaiwan" 44 | :href "https://gaiwan.co"}]]]) 45 | 46 | ;; We can also inspect all aspects of the component 47 | 48 | (o/as-garden navbar) 49 | 50 | (o/css navbar) 51 | 52 | (navbar [[{:text "Lambda Island" 53 | :href "https://lambdaisland.com" 54 | :active? true}] 55 | [{:text "Gaiwan" 56 | :href "https://gaiwan.co"}]]) 57 | 58 | ;; Inline our styles last, so all component styles are certainly defined. 59 | 60 | ^{::clerk/no-cache true} 61 | (render [:style (o/defined-styles)]) 62 | -------------------------------------------------------------------------------- /notebooks/ornament_next.clj: -------------------------------------------------------------------------------- 1 | (ns ornament-next 2 | (:require 3 | [lambdaisland.hiccup :as hiccup] 4 | [lambdaisland.ornament :as o] 5 | [nextjournal.clerk :as clerk])) 6 | 7 | (reset! o/registry {}) 8 | (reset! o/rules-registry {}) 9 | (reset! o/props-registry {}) 10 | 11 | ;; Original Ornament was all about styled components, meaning we put 12 | ;; Garden-syntax CSS inside your components to style them. Later on we added 13 | ;; support for Girouette, which means you can use shorthand tags similar to 14 | ;; Tailwind utility classes to define your style rules. 15 | 16 | ;; This is great, but it's not the full story. Ornament Next gives you several 17 | ;; news ways to define and structure your styles, and better dev-time 18 | ;; affordances. 19 | 20 | ;; Here's a regular old Ornament styled component, except that it now sports a 21 | ;; docstring. The docstring that actually gets set on the var also contains the 22 | ;; compiled CSS, and we set `:arglists`, so you can see how to use it aas a 23 | ;; component in your Hiccup. 24 | 25 | (o/defstyled user-form :form 26 | "Form used on the profile page" 27 | :mx-3) 28 | 29 | (:arglists (meta #'user-form)) 30 | (:doc (meta #'user-form)) 31 | 32 | ;; The new macros that follow all support docstrings. 33 | 34 | ;; ## defrules 35 | 36 | ;; The most basic one is `defrules`, which lets you define plain Garden CSS 37 | ;; rules that get prepended to your Ornament styles. Realistically there are 38 | ;; always still things you define globally, and you shouldn't have to jump 39 | ;; through extra hoops to do so. `defrules` still takes a name and optionally a 40 | ;; docstring, so you can split up your styles and document them. 41 | 42 | (o/defrules my-style 43 | "Some common defaults" 44 | [:* {:box-sizing "border-box"}] 45 | [:form :mx-2]) 46 | 47 | ;; Ornament features like tailwing utilities or referencing components work here 48 | ;; too. 49 | 50 | (o/defstyled menu :nav 51 | :hidden) 52 | 53 | (o/defrules toggle-menu 54 | [:body.menu-open 55 | [menu :block]]) 56 | 57 | (o/defined-garden) 58 | 59 | ;; ## defutil 60 | 61 | ;; There's now also `defutil` for defining utility classes. This is in a way 62 | ;; similar, in that it defines global CSS, but you get a handle onto something 63 | ;; that you can use like a CSS class. 64 | 65 | (o/defutil square 66 | "Ensure the element has the same width and height." 67 | {:aspect-ratio 1}) 68 | 69 | ;; This creates a utility class in your CSS. Note that it's namespaced, like all 70 | ;; classes in Ornament, to be collision free. 71 | 72 | (o/defined-styles) 73 | 74 | ;; You can now use this in multiple ways, the simplest is direcly in hiccup. 75 | 76 | (hiccup/render [:img {:class [square]}]) 77 | 78 | 79 | ;; You can also use it in styled components, to pull those additional style 80 | ;; rules into the CSS of the component. 81 | 82 | (o/defstyled avatar :img 83 | "A square avatar" 84 | square) 85 | 86 | (o/css avatar) 87 | 88 | ;; ## defprop 89 | 90 | ;; Modern CSS heavily leans on CSS custom properties, also known as variables. 91 | ;; These are especially useful for defining design tokens. 92 | 93 | ;; These can be defined with or without 94 | 95 | (o/defprop --without-default) 96 | (o/defprop --color-primary "hsla(201, 100%, 50%, 1)") 97 | 98 | 99 | (hiccup/render [:img {:style {:backgroun-color --color-primary}}]) 100 | 101 | (o/defined-styles) 102 | -------------------------------------------------------------------------------- /notebooks/template.clj: -------------------------------------------------------------------------------- 1 | (ns notebooks.template 2 | (:require 3 | [lambdaisland.ornament :as o] 4 | [lambdaisland.ornament.clerk-util :refer [inline-styles render]])) 5 | 6 | ;; # Ornament Notebook Template 7 | 8 | ;; Define components 9 | 10 | (o/defstyled strong-link :a 11 | {:font-weight 1000}) 12 | 13 | ;; Render them with Hiccup 14 | 15 | (render 16 | [strong-link {:href "https://github.com/lambdaisland/open-source"} 17 | "Check out our open source offerings!"]) 18 | 19 | ;; Inline our styles last, so that this happens after all `defstyled`s are 20 | ;; defined. 21 | 22 | ^{:nextjournal.clerk/no-cache true} 23 | (inline-styles) 24 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.lambdaisland 5 | ornament 6 | 1.16.141 7 | ornament 8 | Clojure Styled Components 9 | https://github.com/lambdaisland/ornament 10 | 2021 11 | 12 | Lambda Island 13 | https://lambdaisland.com 14 | 15 | 16 | UTF-8 17 | 18 | 19 | 20 | MPL-2.0 21 | https://www.mozilla.org/media/MPL/2.0/index.txt 22 | 23 | 24 | 25 | https://github.com/lambdaisland/ornament 26 | scm:git:git://github.com/lambdaisland/ornament.git 27 | scm:git:ssh://git@github.com/lambdaisland/ornament.git 28 | afa349c12efda40606247a3629c90be516e07a4a 29 | 30 | 31 | 32 | org.clojure 33 | clojure 34 | 1.12.0 35 | 36 | 37 | com.lambdaisland 38 | garden 39 | 1.7.590 40 | 41 | 42 | girouette 43 | girouette 44 | 0.0.10 45 | 46 | 47 | meta-merge 48 | meta-merge 49 | 1.0.0 50 | 51 | 52 | hawk 53 | hawk 54 | 0.2.11 55 | true 56 | 57 | 58 | com.lambdaisland 59 | glogi 60 | 1.3.169 61 | true 62 | 63 | 64 | io.pedestal 65 | pedestal.log 66 | 0.7.2 67 | true 68 | 69 | 70 | 71 | src 72 | 73 | 74 | src 75 | 76 | 77 | resources 78 | 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-compiler-plugin 84 | 3.8.1 85 | 86 | 1.8 87 | 1.8 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-jar-plugin 93 | 3.2.0 94 | 95 | 96 | 97 | afa349c12efda40606247a3629c90be516e07a4a 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-gpg-plugin 105 | 1.6 106 | 107 | 108 | sign-artifacts 109 | verify 110 | 111 | sign 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | clojars 121 | https://repo.clojars.org/ 122 | 123 | 124 | 125 | 126 | clojars 127 | Clojars repository 128 | https://clojars.org/repo 129 | 130 | 131 | -------------------------------------------------------------------------------- /repl_sessions/cssparser.clj: -------------------------------------------------------------------------------- 1 | (ns repl-sessions.cssparser 2 | (:require [clojure.java.io :as io]) 3 | (:import (com.steadystate.css.parser CSSOMParser SACParserCSS3 HandlerBase ) 4 | (org.w3c.css.sac InputSource DocumentHandler))) 5 | 6 | ;; https://javadoc.io/static/net.sourceforge.cssparser/cssparser/0.9.11/com/steadystate/css/parser/CSSOMParser.html 7 | 8 | (def css3-parser (SACParserCSS3.)) 9 | (def parser (CSSOMParser. css3-parser)) 10 | 11 | (.setDocumentHandler css3-parser 12 | ^DocumentHandler 13 | (proxy [HandlerBase] [] 14 | (ignorableAtRule [x y] 15 | (prn [x y])))) 16 | 17 | (def s 18 | (rand-nth (seq (.getRules 19 | (.getCssRules 20 | (.parseStyleSheet 21 | parser 22 | (InputSource. (io/reader (io/file "/home/arne/ARS/ductile/stylesheets/jit.css"))) 23 | nil 24 | nil)))))) 25 | (bean (.getStyle s)) 26 | -------------------------------------------------------------------------------- /repl_sessions/poke.clj: -------------------------------------------------------------------------------- 1 | (ns poke 2 | (:require 3 | [lambdaisland.ornament :as o] 4 | [lambdaisland.hiccup :as hiccup])) 5 | 6 | (set! *print-namespace-maps* false) 7 | 8 | (o/defstyled freebies-link :a 9 | {:font-size "1rem" 10 | :color "#cff9cf" 11 | :text-decoration "underline"}) 12 | 13 | (o/rules freebies-link) 14 | 15 | (freebies-link {:href "/episodes/interceptors-concepts"} "hello") 16 | 17 | [:a {:class ["poke__freebies_link"] 18 | :href "/episodes/interceptors-concepts"} "hello"] 19 | 20 | (o/defstyled foo :div 21 | {:margin size-2}) 22 | (o/css foo) 23 | (o/defprop size-2 "2rem") 24 | (o/defrules main-styles 25 | "Main application styles" 26 | [:.link {:color "blue"}] 27 | [:.link:visited {:color "purple"}] 28 | [:main 29 | [:.container {:width size-2}]]) 30 | 31 | (garden.compiler/expand size-2) 32 | main-styles 33 | (o/defutil square {:aspect-ratio 1}) 34 | o/props-registry 35 | (o/defined-garden) 36 | (o/defined-styles) 37 | (o/defstyled avatar :img 38 | {size-2 size-2} 39 | #_#_(garden.stylesheet/at-media {"print" true} [:& {:color "blue"}]) 40 | (garden.stylesheet/at-keyframes "myanim" [:100% {:height "10px"}]) 41 | ) 42 | (#'garden.compiler/expand-stylesheet {size-2 size-2}) 43 | (garden.compiler/compile-css [:& {size-2 size-2}]) 44 | 45 | (hiccup/render [avatar {:style {size-2 "3rem"}}]) 46 | (map class (o/process-rules 47 | (o/rules avatar))) 48 | duration | 49 | easing-function | 50 | delay | 51 | iteration-count | 52 | direction | 53 | fill-mode | 54 | play-state | 55 | name 56 | 57 | (o/defanimation pulse 58 | :duration 59 | "2s" 60 | :keyframes 61 | ["0%" "100%" {:opacity 1}] 62 | ["50%" {:opacity 0.5}]) 63 | 64 | (o/css avatar) 65 | 66 | *e 67 | (o/defined-styles) 68 | 69 | (hiccup/render [:div {:class [square]}]) 70 | 71 | () 72 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaisland/ornament/ca7862fdb54f26a2139460b6138284c70ea240d7/src/.gitkeep -------------------------------------------------------------------------------- /src/lambdaisland/ornament.cljc: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.ornament 2 | "CSS-in-clj(s)" 3 | #?@ 4 | (:clj 5 | [(:require 6 | [clojure.string :as str] 7 | [clojure.walk :as walk] 8 | [garden.color :as gcolor] 9 | [garden.compiler :as gc] 10 | [garden.stylesheet :as gs] 11 | [garden.types :as gt] 12 | [garden.util :as gu] 13 | [girouette.tw.color :as girouette-color] 14 | [girouette.tw.core :as girouette] 15 | [girouette.tw.default-api :as girouette-default] 16 | [girouette.tw.preflight :as girouette-preflight] 17 | [girouette.tw.typography :as girouette-typography] 18 | [girouette.version :as girouette-version] 19 | [meta-merge.core :as meta-merge])] 20 | :cljs 21 | [(:require [clojure.string :as str] [garden.util :as gu]) 22 | (:require-macros lambdaisland.ornament)])) 23 | 24 | #?(:clj 25 | (defonce ^{:doc "Registry of styled components 26 | 27 | Keys are fully qualified symbols (var names), values are maps with the 28 | individual `:tag`, `:rules`, `:classname`. We add an `:index` to be able to 29 | iterate over the components/styles in source order. This is now the 30 | preferred way to iterate over all styles (as in [[defined-styles]]), rather 31 | than the old approach of finding all vars with a given metadata attached to 32 | them. 33 | 34 | Clojure-only because we only deal with CSS on the backend, the frontend 35 | only knows about classnames. `:component` points at a StyledComponent 36 | instance that can be used to get the [[css]] for that component."} 37 | registry 38 | (atom {}))) 39 | 40 | #?(:clj 41 | (defonce ^{:doc "Registry of plain CSS (Garden) rules"} 42 | rules-registry 43 | (atom {}))) 44 | 45 | #?(:clj 46 | (defonce ^{:doc "Registry of custom properties"} 47 | props-registry 48 | (atom {}))) 49 | 50 | (def ^:dynamic *strip-prefixes* 51 | "Prefixes to be stripped from class names in generated CSS" 52 | nil) 53 | 54 | (defprotocol StyledComponent 55 | (classname [_] 56 | "The CSS class name for this component, derived from the var and ns name.") 57 | (as-garden [_] 58 | "Return the styles for this component in Garden syntax (i.e. EDN data)") 59 | (css [_] 60 | "Compile this component's styles to CSS") 61 | (rules [_] 62 | "Get the rules passed to this component, without any processing.") 63 | (tag [_] 64 | "HTML tag (keyword) for this component") 65 | (component [_] 66 | "Function which is a Hiccup component, for styled components which have one or more function tails.") 67 | (as-hiccup [_ args] 68 | "Render to hiccup")) 69 | 70 | (declare process-rule) 71 | 72 | #?(:clj 73 | (do 74 | (defonce ^{:doc "Atom containing the return value 75 | of [[girouette/make-api]], making it possible to swap this out for your 76 | own Girouette instance. See also [[set-tokens!]] for a convenient API for 77 | common use cases."} 78 | girouette-api 79 | (atom nil)) 80 | 81 | (def default-tokens-v2 82 | (delay 83 | {:components (-> @(requiring-resolve 'girouette.tw.default-api/all-tw-components) 84 | (girouette-version/filter-components-by-version [:tw 2])) 85 | :colors girouette-color/tw-v2-colors 86 | :fonts girouette-typography/tw-v2-font-family-map})) 87 | 88 | (def default-tokens-v3 89 | (delay 90 | {:components (-> @(requiring-resolve 'girouette.tw.default-api/all-tw-components) 91 | (girouette-version/filter-components-by-version [:tw 3])) 92 | :colors girouette-color/tw-v3-unified-colors-extended 93 | :fonts girouette-typography/tw-v2-font-family-map})) 94 | 95 | (def default-tokens default-tokens-v2) 96 | 97 | (defn set-tokens! 98 | "Set \"design tokens\": colors, fonts, and components 99 | 100 | This configures Girouette, so that these tokens become available inside 101 | Ornament style declarations. 102 | 103 | - `:colors` : map from keyword to 6-digit hex color, without leading `#` 104 | - `:fonts`: map from keyword to font stack (comman separated string) 105 | - `:components`: sequence of Girouette components, each a map with 106 | `:id` (keyword), `:rules` (string, instaparse, can be omitted), and 107 | `:garden` (map, or function taking instaparse results and returning Garden 108 | map) 109 | - `:tw-version`: which Girouette defaults to use, either based on Tailwind 110 | v2, or v3. Valid values: 2, 3. 111 | 112 | If `:rules` is omitted we assume this is a static token, and we'll 113 | generate a rule of the form `token-id = <'token-id'>`. 114 | 115 | `:garden` can be a function, in which case it receives a map with a 116 | `:compoent-data` key containing the instaparse parse tree. Literal maps or 117 | vectors are wrapped in a function, in case the returned Garden is fixed. The 118 | resulting Garden styles are processed again as in `defstyled`, so you can use 119 | other Girouette or other tokens in there as well. Use `[:&]` for returning 120 | multiple tokens/maps/stylesUse `[:&]` for returning multiple 121 | tokens/maps/styles. 122 | 123 | By default these are added to the Girouette defaults, which are in terms 124 | based on the Tailwind defaults. We still default to v2 (to avoid breaking 125 | changes), but you can opt-in to Tailwind v3 by adding `:tw-version 3`. Use 126 | meta-merge annotations (e.g. `{:colors ^:replace {...}}`) to change that 127 | behaviour." 128 | [{:keys [components colors fonts tw-version] 129 | :or {tw-version 2}}] 130 | (let [{:keys [components colors fonts]} 131 | (meta-merge/meta-merge 132 | (case tw-version 133 | 2 @default-tokens-v2 134 | 3 @default-tokens-v3) 135 | {:components 136 | (into (empty components) 137 | (map (fn [{:keys [id rules garden] :as c}] 138 | (cond-> c 139 | (not rules) 140 | (assoc :rules (str "\n" (name id) " = <'" (name id) "'>" "\n")) 141 | (not (fn? garden)) 142 | (assoc :garden (constantly garden)) 143 | 144 | :always 145 | (update :garden #(comp process-rule %))))) 146 | (flatten components)) 147 | :colors (into (empty colors) 148 | (map (juxt (comp name key) val)) 149 | colors) 150 | :fonts (into (empty fonts) 151 | (map (juxt (comp name key) val)) 152 | fonts)})] 153 | (reset! girouette-api 154 | (girouette/make-api 155 | components 156 | {:color-map colors 157 | :font-family-map fonts})))) 158 | 159 | (defonce set-default-tokens (set-tokens! nil)) 160 | 161 | (defn class-name->garden [n] 162 | ((:class-name->garden @girouette-api) n)) 163 | 164 | (defmethod print-method ::styled [x writer] 165 | (.write writer (classname x))) 166 | 167 | (def munge-map 168 | {\@ "_CIRCA_" 169 | \! "_BANG_" 170 | \# "_SHARP_" 171 | \% "_PERCENT_" 172 | \& "_AMPERSAND_" 173 | \' "_SINGLEQUOTE_" 174 | \* "_STAR_" 175 | \+ "_PLUS_" 176 | \- "_" 177 | \/ "_SLASH_" 178 | \: "_COLON_" 179 | \[ "_LBRACK_" 180 | \{ "_LBRACE_" 181 | \< "_LT_" 182 | \\ "_BSLASH_" 183 | \| "_BAR_" 184 | \= "_EQ_" 185 | \] "_RBRACK_" 186 | \} "_RBRACE_" 187 | \> "_GT_" 188 | \^ "_CARET_" 189 | \~ "_TILDE_" 190 | \? "_QMARK_"}) 191 | 192 | (defn munge-str 193 | ([s] 194 | (munge-str s munge-map)) 195 | ([s munge-map] 196 | #?(:clj 197 | (let [sb (StringBuilder.)] 198 | (doseq [ch s] 199 | (if-let [repl (get munge-map ch)] 200 | (.append sb repl) 201 | (.append sb ch))) 202 | (str sb)) 203 | :cljs 204 | (apply str (map #(get munge-map % %) s))))) 205 | 206 | (defn classname-for 207 | "Convert a fully qualified symbol into a CSS classname 208 | 209 | Munges special characters, and honors `:ornament/prefix` metadata on the 210 | namespace." 211 | [varsym] 212 | (let [prefix (or (:ornament/prefix (meta (the-ns (symbol (namespace varsym))))) 213 | (-> varsym 214 | namespace 215 | (str/replace #"\." "_") 216 | (str "__")))] 217 | (str prefix (munge-str (name varsym))))) 218 | 219 | (defn join-vector-by [sep val] 220 | (if (vector? val) 221 | (str/join sep val) 222 | val)) 223 | 224 | (defmulti process-tag 225 | "Support some of our Garden extensions 226 | 227 | Convert tagged vectors in the component rules into plain Garden, e.g. 228 | `[:at-media]` or `[:rgb]`. Default implementation handles using styled 229 | components as selectors, or otherwise simply preserves the tag." 230 | (fn [[tag & _]] tag)) 231 | 232 | (defmethod process-tag :default [v] 233 | (let [tag (first v)] 234 | (into (if (set? tag) 235 | (into [] tag) 236 | [(cond 237 | (= ::styled (type tag)) 238 | (str "." (classname tag)) 239 | (sequential? tag) 240 | (process-rule tag) 241 | :else 242 | tag)]) 243 | (map process-rule (next v))))) 244 | 245 | (defmethod process-tag :at-media [[_ media-queries & rules]] 246 | (gs/at-media media-queries (into [:&] (map process-rule) rules))) 247 | 248 | (defmethod process-tag :cssfn [[_ fn-name & args]] 249 | (gt/->CSSFunction fn-name args)) 250 | 251 | (defmethod process-tag :at-supports [[_ feature-queries & rules]] 252 | (gt/->CSSAtRule 253 | :feature 254 | {:feature-queries feature-queries 255 | :rules (list (into [:&] (map (comp process-rule)) rules))})) 256 | 257 | (defmethod process-tag :rgb [[_ r g b]] 258 | (gcolor/rgb [r g b])) 259 | 260 | (defmethod process-tag :hsl [[_ h s l]] 261 | (gcolor/hsl [h s l])) 262 | 263 | (defmethod process-tag :rgba [[_ r g b a]] 264 | (gcolor/rgba [r g b a])) 265 | 266 | (defmethod process-tag :hsla [[_ h s l a]] 267 | (gcolor/hsla [h s l a])) 268 | 269 | (defmethod process-tag :str [[_ & xs]] 270 | [(map #(if (string? %) (pr-str %) (process-rule %)) xs)]) 271 | 272 | (defmulti process-property 273 | "Special handling of certain CSS properties. E.g. setting `:grid-template-areas` 274 | using a vector." 275 | (fn [prop val] prop)) 276 | 277 | (defmethod process-property :default [_ val] 278 | (if (vector? val) 279 | (process-tag val) 280 | val)) 281 | 282 | (defmethod process-property :grid-template-areas [_ val] 283 | (if (vector? val) 284 | (str/join " " 285 | (map (fn [row] 286 | (pr-str (str/join " " (map name row)))) 287 | val)) 288 | val)) 289 | 290 | (defmethod process-property :grid-area [_ val] (join-vector-by " / " val)) 291 | (defmethod process-property :border [_ val] (join-vector-by " " val)) 292 | (defmethod process-property :margin [_ val] (join-vector-by " " val)) 293 | (defmethod process-property :padding [_ val] (join-vector-by " " val)) 294 | 295 | (defn process-rule 296 | "Process a single \"rule\" into plain Garden 297 | 298 | Components receive a list of rules. These can be Garden-style maps, 299 | Girouette-style keywords, or Garden-style vectors of selectors+rules. This 300 | function together with [[process-tag]] and [[process-property]] defines the 301 | recursive logic to turn this into something we can pass to the Garden 302 | compiler." 303 | [rule] 304 | (cond 305 | (record? rule) ; Prevent some defrecords in garden.types to be fudged 306 | rule 307 | 308 | (simple-keyword? rule) 309 | (let [girouette-garden (class-name->garden (name rule))] 310 | (cond 311 | (nil? girouette-garden) 312 | #_(throw (ex-info "Girouette style expansion failed" {:rule rule})) 313 | rule 314 | 315 | (and (record? girouette-garden) 316 | (= (:identifier girouette-garden) :media)) 317 | (-> girouette-garden 318 | (update-in [:value :rules] (fn [rules] 319 | (map #(into [:&] (rest %)) rules)))) 320 | :else 321 | (second girouette-garden))) 322 | 323 | (map? rule) 324 | (into {} (map (fn [[k v]] [k (process-property k v)])) rule) 325 | 326 | (vector? rule) 327 | (process-tag rule) 328 | 329 | :else 330 | rule)) 331 | 332 | (defn process-rules 333 | "Process the complete set of rules for a component, see [[process-rule]] 334 | 335 | If multiple consecutive rules result in Garden property maps, then they get 336 | merged, to prevent unnecessary bloat of the compiled CSS." 337 | [rules] 338 | (let [add-rule (fn add-rule [acc r] 339 | (cond 340 | (and (vector? r) 341 | (or (= :& (first r)) 342 | (= "&" (first r)))) 343 | (reduce add-rule acc (next r)) 344 | 345 | (and (map? r) 346 | (map? (last acc)) 347 | (not (record? r)) 348 | (not (record? (last acc)))) 349 | (conj (vec (butlast acc)) 350 | (merge (last acc) r)) 351 | 352 | :else 353 | (conj acc r)))] 354 | (seq (reduce add-rule [] (map process-rule rules))))))) 355 | 356 | (defn add-class 357 | "Hiccup helper, add a CSS classname to an existing `:class` property 358 | 359 | We allow components to define `:class` as a string, a vector, or to use a 360 | styled component directly as a class. (This last behavior is to support some 361 | legacy code, we recommend using a wrapping vector in that case). 362 | 363 | This function handles these cases, and will always return a vector of class 364 | names." 365 | [classes class] 366 | (cond 367 | (nil? class) 368 | classes 369 | 370 | (sequential? class) 371 | (reduce add-class classes class) 372 | 373 | (string? classes) 374 | [class classes] 375 | 376 | (= ::styled (:type (meta classes))) 377 | [class (str classes)] 378 | 379 | (and (sequential? classes) (seq classes)) 380 | (vec (cons class classes)) 381 | 382 | :else 383 | [(str class)])) 384 | 385 | ;; vocab note: we call "attributes" the key-value pairs you can supply to a HTML 386 | ;; element, like `class`, `style`, or `href`. We call "properties" the map you 387 | ;; pass as the first child to a Ornament/Hiccup component. For component that 388 | ;; don't have a custom render functions these properties will be used as 389 | ;; attributes. For components that do have a custom render function it depends 390 | ;; on what the render function does. In this case you can still pass in 391 | ;; attributes directly using the special `:lambdaisland.ornament/attrs` 392 | ;; property. 393 | ;; See also the Attributes and Properties notebook. 394 | 395 | (defn merge-attr 396 | "Logic for merging two attribute values for the same key. 397 | - `class` : append the classname(s) 398 | - `style` : merge the right style map into the left" 399 | [k v1 v2] 400 | (case k 401 | :class (if (and (vector? v2) (:replace (meta v2))) 402 | v2 403 | (add-class v2 v1)) 404 | :style (if (or (not (and (map? v1) (map? v2))) 405 | (:replace (meta v2))) 406 | v2 407 | (merge v1 v2)) 408 | v2)) 409 | 410 | (defn merge-attrs 411 | "Combine attribute maps" 412 | ([p1 p2] 413 | (when (or p1 p2) 414 | (let [merge-entry (fn [m e] 415 | (let [k (key e) 416 | v (val e)] 417 | (if (contains? m k) 418 | (assoc m k (merge-attr k (get m k) v)) 419 | (assoc m k v))))] 420 | (reduce merge-entry (or p1 {}) p2)))) 421 | ([p1 p2 & ps] 422 | (reduce merge-attrs (merge-attrs p1 p2) ps))) 423 | 424 | (defn attr-add-class [attrs class] 425 | (if class 426 | (update attrs :class add-class class) 427 | attrs)) 428 | 429 | (defn expand-hiccup-tag-simple 430 | "Expand an ornament component being called directly with child elements, without 431 | custom render function." 432 | [tag css-class children extra-attrs] 433 | (let [child-meta (meta children) 434 | [tag attrs children :as result] 435 | (if (sequential? children) 436 | (as-> children $ 437 | (if (= :<> (first $)) (next $) $) 438 | (if (map? (first $)) 439 | (into [tag (attr-add-class 440 | (merge-attrs (first $) (meta children) extra-attrs) 441 | css-class)] (next $)) 442 | (into [tag (attr-add-class 443 | (merge-attrs (meta children) extra-attrs) 444 | css-class)] 445 | (if (vector? $) (list $) $)))) 446 | [tag (attr-add-class extra-attrs css-class) children]) 447 | result (if child-meta 448 | (with-meta result child-meta) 449 | result)] 450 | (if (and (sequential? children) (= :<> (first children))) 451 | (recur tag nil children attrs) 452 | result))) 453 | 454 | (defn expand-hiccup-tag 455 | "Handle expanding/rendering the component to Hiccup 456 | 457 | For plain [[defstyled]] components this simply adds the CSS class name. For 458 | components with a render function this handles the expansion, and also handles 459 | fragments (`:<>`), optionally with an attributes map, and handles merging 460 | attributes passed in via the `::attrs` property." 461 | [tag css-class args component] 462 | (if component 463 | (let [result (apply component args)] 464 | (if (fn? result) 465 | (fn [& args] 466 | (expand-hiccup-tag-simple tag css-class (apply result args) (::attrs (first args)))) 467 | (expand-hiccup-tag-simple tag css-class result (::attrs (first args))))) 468 | (expand-hiccup-tag-simple tag css-class (seq args) nil))) 469 | 470 | (defn styled 471 | ([varsym css-class tag rules component] 472 | #?(:clj 473 | ^{:type ::styled} 474 | (reify 475 | StyledComponent 476 | (classname [_] 477 | (reduce 478 | (fn [c p] 479 | (if (str/starts-with? (str c) p) 480 | (reduced (subs (str c) (count p))) 481 | c)) 482 | css-class 483 | *strip-prefixes*)) 484 | (as-garden [this] 485 | (into [(str "." (classname this))] 486 | (process-rules rules))) 487 | (css [this] (gc/compile-css 488 | {:pretty-print? false} 489 | (as-garden this))) 490 | (rules [_] rules) 491 | (tag [_] tag) 492 | (component [_] component) 493 | (as-hiccup [this children] 494 | (expand-hiccup-tag tag (classname this) children component)) 495 | 496 | clojure.lang.IFn 497 | (invoke [this] 498 | (as-hiccup this nil)) 499 | (invoke [this a] 500 | (as-hiccup this [a])) 501 | (invoke [this a b] 502 | (as-hiccup this [a b])) 503 | (invoke [this a b c] 504 | (as-hiccup this [a b c])) 505 | (invoke [this a b c d] 506 | (as-hiccup this [a b c d])) 507 | (invoke [this a b c d e] 508 | (as-hiccup this [a b c d e])) 509 | (invoke [this a b c d e f] 510 | (as-hiccup this [a b c d e f])) 511 | (invoke [this a b c d e f g] 512 | (as-hiccup this [a b c d e f g])) 513 | (invoke [this a b c d e f g h] 514 | (as-hiccup this [a b c d e f g h])) 515 | (invoke [this a b c d e f g h i] 516 | (as-hiccup this [a b c d e f g h i])) 517 | (invoke [this a b c d e f g h i j] 518 | (as-hiccup this [a b c d e f g h i j])) 519 | (invoke [this a b c d e f g h i j k] 520 | (as-hiccup this [a b c d e f g h i j k])) 521 | (invoke [this a b c d e f g h i j k l] 522 | (as-hiccup this [a b c d e f g h i j k l])) 523 | (invoke [this a b c d e f g h i j k l m] 524 | (as-hiccup this [a b c d e f g h i j k l m])) 525 | (invoke [this a b c d e f g h i j k l m n] 526 | (as-hiccup this [a b c d e f g h i j k l m n])) 527 | (invoke [this a b c d e f g h i j k l m n o] 528 | (as-hiccup this [a b c d e f g h i j k l m n o])) 529 | (invoke [this a b c d e f g h i j k l m n o p] 530 | (as-hiccup this [a b c d e f g h i j k l m n o p])) 531 | (invoke [this a b c d e f g h i j k l m n o p q] 532 | (as-hiccup this [a b c d e f g h i j k l m n o p q])) 533 | (invoke [this a b c d e f g h i j k l m n o p q r] 534 | (as-hiccup this [a b c d e f g h i j k l m n o p q r])) 535 | (invoke [this a b c d e f g h i j k l m n o p q r s] 536 | (as-hiccup this [a b c d e f g h i j k l m n o p q r s])) 537 | (applyTo [this args] 538 | (as-hiccup this args)) 539 | 540 | Object 541 | (toString [this] (classname this)) 542 | 543 | gc/IExpandable 544 | (expand [this] 545 | (mapcat 546 | (fn [rule] 547 | (gc/expand 548 | (if (map? rule) 549 | [:& rule] 550 | rule))) 551 | rules))) 552 | 553 | :cljs 554 | (let [render-fn 555 | (fn [& children] 556 | (expand-hiccup-tag tag 557 | css-class 558 | children 559 | component)) 560 | component (specify! render-fn 561 | StyledComponent 562 | (classname [_] css-class) 563 | (as-garden [_] ) 564 | (css [_] ) 565 | (rules [_] ) 566 | (tag [_] tag) 567 | (component [_] component) 568 | (as-hiccup [_ children] 569 | (expand-hiccup-tag tag css-class children component)) 570 | 571 | Object 572 | (toString [_] css-class) 573 | 574 | ;; https://ask.clojure.org/index.php/11514/functions-with-metadata-can-not-take-more-than-20-arguments 575 | cljs.core/IMeta 576 | (-meta [_] {:type ::styled}))] 577 | (js/Object.defineProperty component "name" #js {:value (str varsym)}) 578 | component)))) 579 | 580 | #?(:clj 581 | (defn qualify-sym [env s] 582 | (when (symbol? s) 583 | (if (:ns env) 584 | ;; cljs 585 | (if (simple-symbol? s) 586 | (or (some-> env :ns :uses s name (symbol (name s))) 587 | (symbol (name (-> env :ns :name)) (name s))) 588 | (symbol (or (some-> env :ns :requires (get (symbol (namespace s))) name) 589 | (namespace s)) 590 | (name s))) 591 | 592 | ;; clj 593 | (if (simple-symbol? s) 594 | (or (some-> (ns-refers *ns*) (get s) symbol) 595 | (symbol (str *ns*) (str s))) 596 | (let [ns (namespace s) 597 | n (name s) 598 | aliases (ns-aliases *ns*)] 599 | (symbol (or (some-> aliases (get (symbol ns)) ns-name str) ns) n))))))) 600 | 601 | #?(:clj 602 | (defn fn-tail? [o] 603 | (and (list? o) 604 | (vector? (first o))))) 605 | 606 | #?(:clj 607 | (defn update-index [registry varsym] 608 | (update-in registry [varsym :index] (fnil identity (count registry))))) 609 | 610 | #?(:clj 611 | (defn register! [reg varsym m] 612 | ;; We give each style an incrementing index so they get a predictable 613 | ;; order (i.e. source order). If a style is evaluated again (e.g. REPL use) 614 | ;; then it keeps its original index/position. 615 | (swap! reg 616 | (fn [reg] 617 | (-> reg 618 | (update varsym merge m) 619 | (update-index varsym)))))) 620 | 621 | #?(:clj 622 | (defn cljs-optimization-level [] 623 | (some-> 624 | (try (requiring-resolve 'cljs.env/*compiler*) 625 | (catch Exception _)) 626 | deref deref :options :optimizations))) 627 | 628 | #?(:clj 629 | (defn render-docstring 630 | "Add the compiled CSS to the docstring, for easy dev-time reference. Ignored 631 | when `*compile-files*` is true (AOT compiling Clojure), or cljs optimization 632 | level is not `:none` (prod CLJS builds), to prevent CSS from bloating up a 633 | production build." 634 | [docstring rules] 635 | (let [css (gc/compile-css (process-rules rules))] 636 | (str 637 | docstring 638 | (when (and (not *compile-files*) 639 | (#{:none nil} (cljs-optimization-level))) 640 | (str 641 | (when (and (not (str/blank? docstring)) 642 | (not (str/blank? css))) 643 | (str "\n\n")) 644 | css)))))) 645 | 646 | #?(:clj 647 | (defn component->selector [&env s] 648 | (if (symbol? s) 649 | (let [qsym (qualify-sym &env s)] 650 | (if (contains? @registry qsym) 651 | (str "." (get-in @registry [qsym :classname])) 652 | s)) 653 | s))) 654 | 655 | #?(:clj 656 | (defn component->rules [&env s] 657 | (if (symbol? s) 658 | (let [qsym (qualify-sym &env s)] 659 | (if (contains? @registry qsym) 660 | (get-in @registry [qsym :rules]) 661 | [s])) 662 | [s]))) 663 | 664 | #?(:clj 665 | (defn prop->lvalue [&env s] 666 | (if (symbol? s) 667 | (let [qsym (qualify-sym &env s)] 668 | (if (contains? @props-registry qsym) 669 | (str "--" (get-in @props-registry [qsym :propname])) 670 | s)) 671 | s))) 672 | 673 | #?(:clj 674 | (defn prop->rvalue [&env s] 675 | (if (symbol? s) 676 | (let [qsym (qualify-sym &env s)] 677 | (if (contains? @props-registry qsym) 678 | (str "var(--" (get-in @props-registry [qsym :propname]) ")") 679 | s)) 680 | s))) 681 | 682 | #?(:clj 683 | (defn eval-rules [&env rules] 684 | ;; For ClojureScript support (but also used in Clojure-only), add the 685 | ;; Clojure-version of the styled component to the registry directly 686 | ;; during macroexpansion, so that even in a ClojureScript-only world 687 | ;; we can access it later to compile the styles, even though the 688 | ;; styles themselves are never part of a ClojureScript build. 689 | ;; 690 | ;; To allow using previously defined styled components as selectors 691 | ;; we do our own resolution of these symbols, if we recognize them. 692 | ;; This is necessary since in ClojureScript rules are fully handled 693 | ;; on the Clojure side (we don't want any of the CSS overhead in the 694 | ;; build output), and when defined defstyled in cljs files there are 695 | ;; no Clojure vars that we can resolve, so we need to resolve this 696 | ;; ourselves via the registry. 697 | (let [component->selector (partial component->selector &env) 698 | prop->rvalue (partial prop->rvalue &env) 699 | prop->lvalue (partial prop->lvalue &env) 700 | component->rules (partial component->rules &env)] 701 | (eval `(do 702 | (in-ns '~(ns-name *ns*)) 703 | ~(walk/postwalk 704 | (fn [o] 705 | (cond 706 | (vector? o) 707 | (into [(if (set? (first o)) 708 | (into #{} (map component->selector (first o))) 709 | (component->selector (first o)))] 710 | (mapcat component->rules) 711 | (next o)) 712 | (map? o) 713 | (-> o 714 | (update-keys prop->lvalue) 715 | (update-vals prop->rvalue)) 716 | :else 717 | o)) 718 | (vec 719 | (mapcat component->rules rules)))))))) 720 | 721 | #?(:clj 722 | (defmacro defstyled [sym tagname & styles] 723 | (let [varsym (symbol (name (ns-name *ns*)) (name sym)) 724 | css-class (classname-for varsym) 725 | [docstring & styles] (if (string? (first styles)) styles (cons nil styles)) 726 | [styles fn-tails] (split-with (complement fn-tail?) styles) 727 | tag (if (keyword? tagname) 728 | tagname 729 | (get-in @registry [(qualify-sym &env tagname) :tag])) 730 | rules (cond 731 | (keyword? tagname) 732 | (vec styles) 733 | (symbol? tagname) 734 | (into (or (:rules (get @registry (qualify-sym &env tagname))) []) 735 | styles)) 736 | fn-tails (if (seq fn-tails) 737 | fn-tails 738 | (when (symbol? tagname) 739 | (:fn-tails (get @registry (qualify-sym &env tagname))))) 740 | 741 | fn-tails (when (seq fn-tails) 742 | (if (and (= 1 (count fn-tails)) 743 | (= 0 (count (ffirst fn-tails)))) 744 | `(([] ~@(rest (first fn-tails))) 745 | ([attrs#] [:<> attrs# (do ~@(rest (first fn-tails)))])) 746 | fn-tails)) 747 | rules (eval-rules &env rules)] 748 | (register! registry 749 | varsym 750 | {:var varsym 751 | :tag tag 752 | :rules rules 753 | :classname css-class 754 | :fn-tails fn-tails 755 | :component (styled varsym 756 | css-class 757 | tag 758 | rules 759 | nil)}) 760 | 761 | ;; Actual output of the macro, this creates a styled component as a var, 762 | ;; so that it can be used in Hiccup. This `styled` invocation in turn is 763 | ;; platform-specific, the ClojureScript version only knows how to render 764 | ;; the component with the appropriate classes, it has no knowledge of the 765 | ;; actual styles, which are expected to be rendered on the backend or 766 | ;; during compilation. 767 | `(def ~(with-meta sym 768 | {::css true 769 | :ornament (dissoc (get @registry varsym) :component :fn-tails) 770 | :arglists (if (seq fn-tails) 771 | `'~(map first fn-tails) 772 | ''([] [& children] [attrs & children])) 773 | :doc (render-docstring docstring [(into [(str "." css-class)] rules)])}) 774 | (styled '~varsym 775 | ~css-class 776 | ~tag 777 | ~(when-not (:ns &env) rules) 778 | ~(when (seq fn-tails) 779 | `(fn ~@fn-tails))))))) 780 | 781 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 782 | ;; Rules 783 | 784 | #?(:clj 785 | (defmacro defrules 786 | "Define plain garden rules. Takes an optional docstring, and any number of 787 | Garden rules (vectors of selector + styles, possibly nested, at-rules, etc). 788 | 789 | Defines a var just so that you can inspect what's been evaluated, but the main 790 | action is the side-effect of registering the rules in a registry, which gets 791 | prepended to the rest of your Ornament CSS." 792 | [rules-name & rules] 793 | (let [[docstring & rules] (if (string? (first rules)) 794 | rules 795 | (cons nil rules)) 796 | varsym (qualify-sym &env rules-name) 797 | rules (process-rules 798 | (eval-rules &env rules))] 799 | (register! rules-registry varsym {:rules rules}) 800 | (when-not (:ns &env) 801 | `(def ~rules-name ~(render-docstring docstring rules) '~rules))))) 802 | 803 | #?(:clj 804 | (defmacro defutil 805 | "Define utility class, takes a name for the class, optionally a docstring, and a 806 | style map. Use the util var in your styles or as as class in hiccup." 807 | ([util-name styles] 808 | `(defutil ~util-name ~nil ~styles)) 809 | ([util-name docstring styles] 810 | (let [varsym (qualify-sym &env util-name) 811 | klzname (classname-for varsym) 812 | rules (list [(str "." klzname) 813 | (eval `(do 814 | (in-ns '~(ns-name *ns*)) 815 | ~styles))]) 816 | docstring (render-docstring docstring rules)] 817 | (register! rules-registry varsym {:rules rules}) 818 | `(def ~util-name 819 | ~docstring 820 | (with-meta 821 | (reify 822 | Object 823 | (toString [_] ~klzname) 824 | gc/IExpandable 825 | (expand [_] 826 | (gc/expand 827 | [:& ~styles]))) 828 | {:type ::util})))))) 829 | 830 | #?(:clj 831 | (defmethod print-method ::util [u writer] 832 | (.write writer (str u)))) 833 | 834 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 835 | ;; Props 836 | 837 | (defprotocol CSSProp 838 | (lvalue [p]) 839 | (rvalue [p])) 840 | 841 | (defn css-prop [prop-name default] 842 | #?(:clj 843 | (with-meta 844 | (reify 845 | CSSProp 846 | (lvalue [_] (str "--" (name prop-name))) 847 | (rvalue [_] (str "var(--" (name prop-name) ")")) 848 | gu/ToString 849 | (to-str [this] 850 | (str "--" (name prop-name))) 851 | Object 852 | (toString [_] (str "var(--" (name prop-name) ")")) 853 | clojure.lang.ILookup 854 | (valAt [this kw] (when (= :default kw) default)) 855 | (valAt [this kw fallback] (if (= :default kw) default fallback))) 856 | {:type ::prop}) 857 | :cljs 858 | (with-meta 859 | (reify 860 | CSSProp 861 | (lvalue [_] (str "--" (name prop-name))) 862 | (rvalue [_] (str "var(--" (name prop-name) ")")) 863 | ILookup 864 | (-lookup [this kw] (when (= :default kw) default)) 865 | (-lookup [this kw fallback] (if (= :default kw) default fallback)) 866 | Object 867 | (toString [_] 868 | (str "--" (name prop-name)))) 869 | {:type ::prop}))) 870 | 871 | #?(:clj 872 | (defmethod print-method ::prop [p writer] 873 | (.write writer (lvalue p)))) 874 | 875 | #?(:clj 876 | (defn propname-for 877 | [propsym] 878 | (let [prefix (or (:ornament/prefix (meta (the-ns (symbol (namespace propsym))))) 879 | (-> propsym 880 | namespace 881 | (str/replace #"\." "-") 882 | (str "--")))] 883 | (str prefix (munge-str (str/replace (name propsym) 884 | #"^--" "") (dissoc munge-map \-)))))) 885 | 886 | #?(:clj 887 | (defmacro defprop 888 | "Define a custom CSS property (variable). Use the resulting var either where a 889 | value is expected (will expand to `var(--var-name)`), or where a name is 890 | expected (e.g. to assign it in a context)." 891 | ([prop-name] 892 | `(defprop ~prop-name nil)) 893 | ([prop-name value] 894 | `(defprop ~prop-name nil ~value)) 895 | ([prop-name docstring value] 896 | (let [varsym (qualify-sym &env prop-name) 897 | propname (propname-for varsym) 898 | value (eval value)] 899 | (register! props-registry varsym {:propname propname :value value}) 900 | `(def ~prop-name 901 | ~(str 902 | (when docstring 903 | (str docstring "\n\n")) 904 | "Default: " value) 905 | (css-prop '~propname ~value)))))) 906 | 907 | #?(:clj 908 | (defn import-tokens*! 909 | ([tokens {:keys [include-values? prefix] 910 | :or {include-values? true 911 | prefix ""}}] 912 | (mapcat 913 | identity 914 | (for [[tname tdef] tokens] 915 | (let [tname (str prefix tname) 916 | {:strs [$description $value $type]} tdef 917 | more (into {} (remove (fn [[k v]] (= (first k) \$))) tdef)] 918 | (cond-> [`(defprop ~(symbol tname) 919 | ~@(when $description [(str $description "\n\nDefault: " $value)]) 920 | ~@(when (and $value include-values?) 921 | [$value]))] 922 | (seq more) 923 | (into (import-tokens*! (str tname "-") more))))))))) 924 | 925 | #?(:clj 926 | (defmacro import-tokens! 927 | "Import a standard design tokens JSON file. 928 | Emits a sequence of `defprop`, i.e. it defines custom CSS properties (aka 929 | variables). See https://design-tokens.github.io/community-group/format/ 930 | - tokens: parsed JSON, we don't bundle a parser, you have to do that yourself 931 | - opts: options map, supports `:prefix` and `:include-values?`. Has to be 932 | literal (used by the macro itself) 933 | - prefix: string prefix to add to the (clojure and CSS) var names 934 | - :include-values? false: only create the Clojure vars to access the props, 935 | don't include their definitions/values in the CSS. Presumably because you are 936 | loading CSS separately that already defines these. 937 | " 938 | ([tokens & [opts]] 939 | `(do ~@(import-tokens*! (eval tokens) opts))))) 940 | 941 | #?(:clj 942 | (defn defined-garden 943 | "All CSS defined through the different Ornament facilities (defprop, defstyled, 944 | defrules), in Garden syntax. Run this through `garden.compiler/compile-css`." 945 | [] 946 | (concat 947 | (let [props (->> @props-registry 948 | vals 949 | (filter (comp some? :value)))] 950 | (when (seq props) 951 | [[":where(html)" (into {} 952 | (map (juxt (comp (partial str "--") :propname) 953 | :value)) 954 | props)]])) 955 | (->> @rules-registry 956 | vals 957 | (sort-by :index) 958 | (mapcat :rules)) 959 | (->> @registry 960 | vals 961 | (sort-by :index) 962 | (map (fn [{:keys [var tag rules classname]}] 963 | (as-garden (styled var classname tag rules nil)))))))) 964 | 965 | #?(:clj 966 | (defn defined-styles 967 | "Collect all styles that have been defined, and compile them down to CSS. Use 968 | this to either spit out or inline a stylesheet with all your Ornament styles. 969 | Optionally the Tailwind preflight (reset) stylesheet can be prepended using 970 | `:preflight? true`. This defaults to Tailwind v2 (as provided by Girouette). 971 | Version 3 is available with `:tw-version 3`" 972 | [& [{:keys [preflight? tw-version compress?] 973 | :or {preflight? false 974 | tw-version 2 975 | compress? true}}]] 976 | (gc/compile-css 977 | {:pretty-print? (not compress?)} 978 | (cond->> (defined-garden) 979 | preflight? (concat (case tw-version 980 | 2 girouette-preflight/preflight-v2_0_3 981 | 3 girouette-preflight/preflight-v3_0_24)))))) 982 | 983 | #?(:clj 984 | (defn cljs-restore-registry 985 | "Restore the Ornament registry based on a ClojureScript compiler env 986 | 987 | Due to caching some defstyled macros may not get recompiled, causing gaps in 988 | the CSS. To work around this we add Ornament data to the cljs analyzer var 989 | metadata, so it gets cached and restored with the rest of the analyzer state." 990 | [compiler-env] 991 | (when (empty? @registry) 992 | (reset! registry 993 | (into {} 994 | (for [[_ {:keys [defs]}] (:cljs.analyzer/namespaces compiler-env) 995 | [_ {{:keys [ornament]} :meta}] defs 996 | :when ornament] 997 | [(:var ornament) ornament])))))) 998 | 999 | (comment 1000 | (spit "/tmp/ornament.css" (defined-styles)) 1001 | 1002 | (->> @rules-registry 1003 | vals 1004 | (sort-by :index) 1005 | (mapcat :rules) 1006 | process-rules)) 1007 | -------------------------------------------------------------------------------- /src/lambdaisland/ornament/clerk_util.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.ornament.clerk-util 2 | (:require [lambdaisland.hiccup :as hiccup] 3 | [lambdaisland.ornament :as o] 4 | [nextjournal.clerk :as clerk])) 5 | 6 | (defn render 7 | "Render hiccup containing Ornament component references inside a Clerk 8 | notebook." 9 | [h] 10 | (clerk/html (hiccup/render h {:doctype? false}))) 11 | 12 | (defn inline-styles 13 | "Inject our CSS styles into the Clerk document, so components render correctly. 14 | Add this at the end of your notebook, and add a 'no-cache' marker. 15 | 16 | ``` 17 | ^{::clerk/no-cache true} 18 | (util/inline-styles) 19 | ``` 20 | " 21 | [] 22 | (render [:style (o/defined-styles)])) 23 | 24 | (defn expand 25 | "Expand a hiccup form with an ornament component to plain hiccup elements. Does not recurse." 26 | [[component & args]] 27 | (o/as-hiccup component args)) 28 | -------------------------------------------------------------------------------- /src/lambdaisland/ornament/watcher.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.ornament.watcher 2 | "Watch the filesystem for changes, and regenerate the Ornament CSS file 3 | 4 | We generally combine this with Figwheel, and let figwheel handle reloading the 5 | CLJ files, as well as hot-loading the new CSS in the browser. 6 | 7 | For shadow-cljs use build-hooks, see the Ornament README for an example. 8 | 9 | Hawk and Glögi are BYO (you need to declare the dependencies yourself.)" 10 | (:require [clojure.java.io :as io] 11 | [hawk.core :as hawk] 12 | [lambdaisland.glogc :as log] 13 | [lambdaisland.ornament :as ornament]) 14 | (:import [java.util Timer TimerTask])) 15 | 16 | (defn debounced 17 | "Debounce a function, it will be called at most once every delay-ms 18 | milliseconds." 19 | [f delay-ms] 20 | (let [timer (Timer.) 21 | last-task (atom nil)] 22 | (fn [& args] 23 | (let [task (proxy [TimerTask] [] (run [] (apply f args)))] 24 | (swap! last-task 25 | (fn [prev] 26 | (when prev (.cancel ^TimerTask prev)) 27 | (.schedule ^Timer timer ^TimerTask task delay-ms) 28 | task))) 29 | nil))) 30 | 31 | (defn requires-ornament? 32 | "Does this Clojure file require the Ornament namespace?" 33 | [f] 34 | (try 35 | (with-open [rdr (-> f io/file io/reader java.io.PushbackReader.)] 36 | (->> rdr 37 | (read {:features #{:clj} :read-cond :allow}) 38 | flatten 39 | (some '#{lambdaisland.ornament}))) 40 | (catch Exception _ 41 | false))) 42 | 43 | (defn make-output-fn [{:keys [outfile] 44 | :or {outfile "resources/public/css/compiled/ornament.css"}}] 45 | (debounced 46 | (fn [] 47 | (log/debug :ornament-watcher/writing outfile) 48 | (io/make-parents outfile) 49 | (spit outfile (ornament/defined-styles))) 50 | 1000)) 51 | 52 | (defn make-hawk-handler [opts] 53 | (let [write-ornament-css! (make-output-fn opts)] 54 | (fn [ctx {:keys [kind file]}] 55 | (when (requires-ornament? file) 56 | (write-ornament-css!) 57 | (when-let [cb (:callback opts)] 58 | (cb)))))) 59 | 60 | (defn start-watcher! 61 | "Start a watcher which recreates the ornament CSS output file when source 62 | namespaces change. 63 | 64 | - `:watch-paths` The source directories to watch 65 | - `:outfile` The CSS file to write to 66 | - `:callback` Optional function to call after the CSS updates" 67 | [{:keys [watch-paths] 68 | :or {watch-paths ["src"]} 69 | :as opts}] 70 | (hawk/watch! [{:paths ["src"] 71 | :handler (make-hawk-handler opts)}])) 72 | 73 | (defn stop-watcher! [hawk] 74 | (hawk/stop! hawk)) 75 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaisland/ornament/ca7862fdb54f26a2139460b6138284c70ea240d7/test/.gitkeep -------------------------------------------------------------------------------- /test/lambdaisland/ornament_test.cljc: -------------------------------------------------------------------------------- 1 | (ns ^{:ornament/prefix "ot__"} 2 | lambdaisland.ornament-test 3 | (:require 4 | [lambdaisland.ornament :as o] 5 | [clojure.string :as str] 6 | [clojure.test :refer [deftest testing is are use-fixtures run-tests join-fixtures]] 7 | #?(:clj [lambdaisland.hiccup :as hiccup] 8 | :cljs [lambdaisland.thicc :as thicc])) 9 | #?(:cljs 10 | (:require-macros lambdaisland.ornament-test))) 11 | 12 | (defn render [h] 13 | #?(:clj (hiccup/render h {:doctype? false}) 14 | :cljs (.-outerHTML (thicc/dom h)))) 15 | 16 | (o/defstyled simple :span 17 | {:color "#ffffff"}) 18 | 19 | (o/defstyled tokens :span 20 | :px-5 :py-3 :rounded-xl) 21 | 22 | (o/defstyled child-selector-tokens :div 23 | :pt-4 24 | :space-y-2) 25 | 26 | (o/defstyled combined :span 27 | :px-5 :py-3 :rounded-xl 28 | {:color "azure"}) 29 | 30 | (o/defstyled nested :ul 31 | :px-3 32 | [:li {:list-style :square}]) 33 | 34 | (o/defstyled with-body :p 35 | :px-5 :py-3 :rounded-xl 36 | {:color "azure"} 37 | ([& children] 38 | (into [:strong] children))) 39 | 40 | (o/defstyled with-body-derived with-body 41 | :font-bold) 42 | 43 | (o/defstyled timed :time 44 | :border 45 | :border-black 46 | ([{:keys [date time]}] 47 | ^{:datetime (str date " " time)} 48 | [:<> date " " time])) 49 | 50 | (o/defstyled ornament-in-ornament :div 51 | {:color "blue"} 52 | [simple {:color "red"}]) 53 | 54 | (o/defstyled base :span 55 | {:color "blue" 56 | :background-color "red"}) 57 | 58 | (o/defstyled inherited base 59 | {:color "green" 60 | :list-style :square}) 61 | 62 | (def my-tokens {:main-color "green"}) 63 | 64 | ;; Referencing non-defstyled variables in rules is only possible in Clojure 65 | (o/defstyled with-code :div 66 | {:background-color (-> my-tokens :main-color)}) 67 | 68 | ;; TODO add assertions for these 69 | (o/defstyled with-media :div 70 | {:padding "0 1rem 1rem"} 71 | [:at-media {:min-width "1rem"} 72 | {:grid-gap "1rem" 73 | :padding "0 2rem 2rem"}]) 74 | 75 | (o/defstyled with-css-fn :a 76 | [:&:after {:content [:str " (" [:cssfn :attr "href"] ")"]}]) 77 | 78 | (o/defstyled feature-check :div 79 | [:at-supports {:display "grid"} 80 | {:display "grid"}]) 81 | 82 | (o/defstyled color-fns :div 83 | {:color [:rgb 150 30 75] 84 | :background-color [:hsla 235 100 50 0.5]}) 85 | 86 | (o/defstyled nav-link :a 87 | ([{:keys [id]}] 88 | (let [{:keys [url title description]} {:url "/videos" :title "Videos" :description "Watch amazing videos"}] 89 | ^{:href url :title description} 90 | [:<> title]))) 91 | 92 | (o/defstyled referenced :div 93 | {:color :blue}) 94 | 95 | (o/defstyled referer :p 96 | [referenced {:color :red}] ;; use as classname 97 | [:.foo referenced]) ;; use as style rule 98 | 99 | (o/defstyled siblings :p 100 | ;; Because of Tailwind/Girouette we can't always use the [:foo :bar {styles}] 101 | ;; garden syntax, instead we support using a set for this. 102 | [#{:span :div} {:color "red"}]) 103 | 104 | (o/defstyled siblings-plain :div 105 | ;; Plain garden version still works *if* it doesn't clash with anything 106 | ;; Girouette recognizes. 107 | [:ul :ol {:background-color "blue"}]) 108 | 109 | (o/defstyled attrs-in-fragment :div 110 | ([children] 111 | [:<> {:lang "nl"} 112 | children])) 113 | 114 | (o/defstyled attrs-in-fragment-props :div 115 | ([{:keys [person]}] 116 | [:<> {:lang "nl"} 117 | "hello, " person])) 118 | 119 | (o/defstyled attrs-in-fragment-styled :div 120 | ([{:keys [person]}] 121 | [:<> {:class "extra-class" 122 | :style {:color "blue"}} 123 | "hello, " person])) 124 | 125 | (o/defstyled attrs-legacy :div 126 | ([{:keys [person]}] 127 | ^{:class "extra-class" 128 | :style {:color "blue"}} 129 | [:<> "hello, " person])) 130 | 131 | ;; Example from the README 132 | (o/defstyled freebies-link :a 133 | {:font-size "1rem" 134 | :color "#cff9cf" 135 | :text-decoration "underline"}) 136 | 137 | ;; For use in reagent, `::o/attrs` are still propagated to the element 138 | (o/defstyled form-2 :div 139 | ([a] 140 | (fn [b] 141 | [:<> "hello"]))) 142 | 143 | ;; Will fail to compile on cljs if the :require-macros line is missing 144 | (o/defstyled with-str :div 145 | {:border (str "1px solid red")} 146 | ([props] 147 | [:<> "foo"])) 148 | 149 | 150 | ;; More ways to reuse styles across components 151 | (o/defstyled bold :span 152 | :font-medium) 153 | 154 | ;; Referencing another component at the top level like this inherits its styles 155 | (o/defstyled heading-1-top :h1 156 | bold :text-3xl) 157 | 158 | ;; Of course we can chain these 159 | (o/defstyled heading-2-top :h2 160 | heading-1-top :text-2xl) 161 | 162 | ;; Doing this inside a `:&` is equivalent 163 | (o/defstyled heading-1-nest :h1 164 | [:& bold :text-3xl]) 165 | 166 | (o/defstyled heading-2-nest :h2 167 | [:& heading-1-nest :text-2xl]) 168 | 169 | (o/defstyled with-doc :div 170 | "A documented component") 171 | 172 | (o/defstyled with-doc2 :div 173 | "A documented component" 174 | :mx-2 {:color "blue"}) 175 | 176 | (o/defstyled with-doc3 :div 177 | "A documented component" 178 | ([] 179 | [:<> "hello"])) 180 | 181 | #?(:clj 182 | (deftest css-test 183 | (is (= ".ot__simple{color:#fff}" 184 | (o/css simple))) 185 | 186 | (is (= ".ot__tokens{padding-left:1.25rem;padding-right:1.25rem;padding-top:.75rem;padding-bottom:.75rem;border-radius:.75rem}" 187 | (o/css tokens))) 188 | 189 | (is (= ".ot__child_selector_tokens{padding-top:1rem}.ot__child_selector_tokens>:not([hidden])~:not([hidden]){margin-top:.5rem}" 190 | ;; This is what the Tailwind docs say should be output, but that's 191 | ;; not what Tailwind actually does, and so Girouette changed its 192 | ;; behavior to match Tailwind, see 193 | ;; https://github.com/green-coder/girouette/issues/84 194 | ;; ".ot__child_selector_tokens{padding-top:1rem}.ot__child_selector_tokens>*+*{margin-top:.5rem}" 195 | (o/css child-selector-tokens))) 196 | 197 | (is (= ".ot__combined{padding-left:1.25rem;padding-right:1.25rem;padding-top:.75rem;padding-bottom:.75rem;border-radius:.75rem;color:azure}" 198 | (o/css combined))) 199 | 200 | (is (= ".ot__nested{padding-left:.75rem;padding-right:.75rem}.ot__nested li{list-style:square}" 201 | (o/css nested))) 202 | 203 | (is (= ".ot__with_body{padding-left:1.25rem;padding-right:1.25rem;padding-top:.75rem;padding-bottom:.75rem;border-radius:.75rem;color:azure}" 204 | (o/css with-body))) 205 | 206 | (is (= ".ot__with_body_derived{padding-left:1.25rem;padding-right:1.25rem;padding-top:.75rem;padding-bottom:.75rem;border-radius:.75rem;color:azure;font-weight:700}" 207 | (o/css with-body-derived))) 208 | 209 | (is (= ".ot__ornament_in_ornament{color:blue}.ot__ornament_in_ornament .ot__simple{color:red}" 210 | (o/css ornament-in-ornament))) 211 | 212 | (is (= ".ot__inherited{color:green;background-color:red;list-style:square}" 213 | (o/css inherited))) 214 | 215 | #?(:clj 216 | (is (= ".ot__with_code{background-color:green}" 217 | (o/css with-code)))) 218 | 219 | (is (= ".ot__with_media{padding:0 1rem 1rem}@media(min-width:1rem){.ot__with_media{grid-gap:1rem;padding:0 2rem 2rem}}" 220 | (o/css with-media))) 221 | 222 | (is (= ".ot__referer .ot__referenced{color:red}.ot__referer .foo{color:blue}" 223 | (o/css referer))) 224 | 225 | (is (= (o/css siblings) 226 | ".ot__siblings div,.ot__siblings span{color:red}")) 227 | 228 | (is (= ".ot__siblings_plain ul,.ot__siblings_plain ol{background-color:blue}" 229 | (o/css siblings-plain))) 230 | 231 | 232 | (is (= ".ot__heading_1_top{font-weight:500;font-size:1.875rem;line-height:2.25rem}" 233 | (o/css heading-1-top))) 234 | 235 | (is (= ".ot__heading_2_top{font-weight:500;font-size:1.5rem;line-height:2rem}" 236 | (o/css heading-2-top))) 237 | 238 | (is (= ".ot__heading_1_nest{font-weight:500;font-size:1.875rem;line-height:2.25rem}" 239 | (o/css heading-1-nest))) 240 | 241 | (is (= ".ot__heading_2_nest{font-weight:500;font-size:1.5rem;line-height:2rem}" 242 | (o/css heading-2-nest))))) 243 | 244 | (deftest rendering-test 245 | (are [hiccup html] (= html (render hiccup)) 246 | [simple] 247 | "" 248 | 249 | [simple {:class "xxx"}] 250 | "" 251 | 252 | [simple {:class "xxx"} [:strong "child"]] 253 | "child" 254 | 255 | [simple {:class "xxx" :style {:border-bottom "1px solid black"}} [:strong "child"]] 256 | "child" 257 | 258 | [timed {:date "2021-06-25" :time "10:11:12"}] 259 | "" 260 | 261 | [simple {:class timed}] 262 | "" 263 | 264 | [simple {:class [timed]}] 265 | "" 266 | 267 | [with-body "hello"] 268 | "

hello

" 269 | 270 | [with-body-derived "hello"] 271 | "

hello

" 272 | 273 | ;; we're getting inconsistent but equivalent rendering here between clj and 274 | ;; cljs. Not ideal, but not a big deal either. Working around with reader 275 | ;; conditionals. 276 | ;; FIXME: write this in a more robust way, maintaining this is becoming a PITA 277 | [attrs-in-fragment "hello"] 278 | "
hello
" 279 | 280 | [attrs-in-fragment-props 281 | {:person "Arne" 282 | ::o/attrs {:lang "en" :title "greeting"}}] 283 | "
hello, Arne
" 284 | 285 | [attrs-in-fragment-props {:person "Jake"}] 286 | "
hello, Jake
" 287 | 288 | [attrs-in-fragment-styled {:person "Finn"}] 289 | "
hello, Finn
" 290 | 291 | [attrs-in-fragment-styled {:person "Finn" 292 | ::o/attrs {:class "extra2" 293 | :style {:background-color "rebeccapurple"}}}] 294 | #?(:clj "
hello, Finn
" 295 | :cljs "
hello, Finn
") 296 | 297 | [attrs-legacy {:person "Arne"}] 298 | "
hello, Arne
" 299 | 300 | ;; ClojureScript bug, this does not currently work: 301 | ;; https://ask.clojure.org/index.php/11514/functions-with-metadata-can-not-take-more-than-20-arguments 302 | #_#_ 303 | [simple 304 | [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] 305 | [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] [:a] 306 | [:a] [:a] [:a] [:a]] 307 | "" 308 | [(form-2 7) {::o/attrs {:data-a 11}}] 309 | "
hello
" 310 | 311 | )) 312 | 313 | (deftest direct-invocation-test 314 | (is (= [:a {:class ["ot__freebies_link"] :href "/episodes/interceptors-concepts"} "hello"] 315 | (freebies-link {:href "/episodes/interceptors-concepts"} "hello"))) 316 | 317 | (is (= [:a {:class ["ot__freebies_link"]} "hello"] 318 | (freebies-link "hello")))) 319 | 320 | (o/defstyled custok1 :div 321 | :bg-primary) 322 | 323 | (o/defstyled custok2 :div 324 | :font-system) 325 | 326 | (o/defstyled custok3 :div 327 | :full-center) 328 | 329 | (o/defstyled custok4 :div 330 | :full-center-bis) 331 | 332 | (o/defstyled custok5 :ul 333 | :bullets-🐻) 334 | 335 | #?(:clj 336 | (deftest custom-tokens-test 337 | (o/set-tokens! {:colors {:primary "001122"} 338 | :fonts {:system "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"} 339 | :components [{:id :full-center 340 | :garden {:display "inline-flex" 341 | :align-items "center"}} 342 | {:id :full-center-bis 343 | :garden [:& :inline-flex :items-center]} 344 | {:id :custom-bullets 345 | :rules "custom-bullets = <'bullets-'> bullet-char 346 | = #\".\"" 347 | :garden (fn [{[bullet-char] :component-data}] 348 | [:& 349 | {:list-style "none" 350 | :padding 0 351 | :margin 0} 352 | [:li 353 | {:padding-left "1rem" 354 | :text-indent "-0.7rem"}] 355 | ["li::before" 356 | {:content bullet-char}]])}]}) 357 | 358 | 359 | (is (= ".ot__custok1{--gi-bg-opacity:1;background-color:rgba(0,17,34,var(--gi-bg-opacity))}" 360 | (o/css custok1))) 361 | 362 | (is (= ".ot__custok2{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}" 363 | (o/css custok2))) 364 | 365 | (is (= ".ot__custok3{display:inline-flex;align-items:center}" 366 | (o/css custok3))) 367 | 368 | (is (= ".ot__custok4{display:inline-flex;align-items:center}" 369 | (o/css custok4))) 370 | 371 | (is (= ".ot__custok5{list-style:none;padding:0;margin:0}.ot__custok5 li{padding-left:1rem;text-indent:-0.7rem}.ot__custok5 li::before{content:🐻}" 372 | (o/css custok5))) 373 | 374 | (o/set-tokens! {}))) 375 | 376 | #?(:clj 377 | (deftest meta-merge-tokens-test 378 | ;; establish baseline 379 | (is (= {:--gi-bg-opacity 1, :background-color "rgba(239,68,68,var(--gi-bg-opacity))"} 380 | (o/process-rule :bg-red-500))) 381 | 382 | (is (= {:font-family "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"} 383 | (o/process-rule :font-mono))) 384 | 385 | (is (= {:border-radius "0.75rem"} 386 | (o/process-rule :rounded-xl))) 387 | 388 | ;; Replace the default colors/fonts, leave the components so we can still do bg-* or font-* 389 | (o/set-tokens! {:colors ^:replace {:primary "001122"} 390 | :fonts ^:replace {:system "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"}}) 391 | 392 | ;; The built-in ones are all gone, they expand to selectors now 393 | (is (= :bg-red-500 (o/process-rule :bg-red-500))) 394 | (is (= :font-mono (o/process-rule :font-mono))) 395 | 396 | (is {:--gi-bg-opacity 1, :background-color "rgba(0,17,34,var(--gi-bg-opacity))"} 397 | (o/process-rule :bg-primary)) 398 | 399 | (is {:font-family "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"} 400 | (o/process-rule :font-system)) 401 | 402 | 403 | ;; Replace the components 404 | (o/set-tokens! {:components ^:replace [{:id :full-center 405 | :garden {:display "inline-flex" 406 | :align-items "center"}}]}) 407 | 408 | (is (= {:display "inline-flex", :align-items "center"} 409 | (o/process-rule :full-center))) 410 | 411 | (is (= :rounded-xl (o/process-rule :rounded-xl))) 412 | 413 | ;; Reset to defaults 414 | (o/set-tokens! {}))) 415 | 416 | #?(:clj 417 | (deftest defined-styles-test 418 | (let [reg @o/registry] 419 | (reset! o/registry {}) 420 | 421 | ;; Deal with the fact that the registry is populated at compile time 422 | (eval 423 | `(do 424 | (in-ns '~(symbol (namespace `_))) 425 | (o/defstyled ~'my-styles :div 426 | {:color "red"}) 427 | (o/defstyled ~'more-styles :span 428 | :rounded-xl))) 429 | 430 | (is (= ".ot__my_styles{color:red}.ot__more_styles{border-radius:.75rem}" 431 | (o/defined-styles))) 432 | 433 | (reset! o/registry reg)))) 434 | 435 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 436 | ;; Component resolution 437 | 438 | (o/defstyled child-1 :div 439 | :bg-red-100 440 | ([] 441 | [:p "hello"])) 442 | 443 | (o/defstyled child-2 :div 444 | :bg-blue-100 445 | ([] 446 | [:p "world"])) 447 | 448 | (o/defstyled parent-set :div 449 | [#{child-1 child-2} :bg-green-700] 450 | ([] 451 | [:<> 452 | [child-1] 453 | [child-2]])) 454 | 455 | #?(:clj 456 | (deftest component-resolution-inside-set 457 | (is (= ".ot__parent_set .ot__child_2,.ot__parent_set .ot__child_1{--gi-bg-opacity:1;background-color:rgba(21,128,61,var(--gi-bg-opacity))}" 458 | (o/css parent-set))))) 459 | 460 | (deftest docstring-test 461 | (is (= "A documented component" (:doc (meta #'with-doc)))) 462 | (is (str/starts-with? (:doc (meta #'with-doc2)) "A documented component")) 463 | (is (str/starts-with? (:doc (meta #'with-doc3)) "A documented component")) 464 | 465 | #?(:clj 466 | (is (= '([] [& children] [attrs & children]) 467 | (:arglists (meta #'combined))))) 468 | 469 | #?(:clj 470 | (is (= '([{:keys [date time]}]) 471 | (:arglists (meta #'timed)))))) 472 | 473 | (comment 474 | (require 'kaocha.repl) 475 | (kaocha.repl/run) 476 | ) 477 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:plugins [#_:notifier 3 | :print-invocations 4 | :profiling] 5 | :tests [{:id :clj} 6 | {:id :cljs 7 | :type :kaocha.type/cljs 8 | :cljs/repl-env cljs.repl.browser/repl-env 9 | :cljs/timeout 20000}] 10 | :bindings {#_#_kaocha.type.cljs/*debug* true 11 | kaocha.stacktrace/*stacktrace-filters* []}} 12 | --------------------------------------------------------------------------------