├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── __tests__ ├── common.index.scss ├── css-helpers.spec.ts ├── fallback-unmatch-palette │ ├── 1.input.spec.scss │ ├── 1.output-css.spec.scss │ └── 1.output-json.spec.json ├── fallback.spec.ts ├── fallback │ ├── 1.input.spec.scss │ ├── 1.output-css.spec.scss │ └── 1.output-json.spec.json ├── invalid-css.spec.ts ├── invalid-css │ ├── themify-empty-list.input.spec.scss │ ├── themify-empty-list.output.spec.scss │ ├── themify-empty-value.input.spec.scss │ ├── themify-empty-value.output.spec.scss │ ├── themify-not-exists.input.spec.scss │ └── themify-not-exists.output.spec.scss ├── palettes │ ├── palette.ts │ ├── tiny-palette.ts │ └── unmatch-palette.ts ├── test.util.ts ├── theme-helpers │ ├── color-helper.input.spec.scss │ └── color-helper.output.spec.scss ├── unmatch-palette.spec.ts ├── unmatch-palette │ ├── unmatch-palette.input.spec.scss │ └── unmatch-palette.output.spec.scss ├── utils.spec.ts ├── valid-css.spec.ts └── valid-css │ ├── different-color-alpha.input.spec.scss │ ├── different-color-alpha.output.spec.scss │ ├── empty-class.input.spec.scss │ ├── empty-class.output.spec.scss │ ├── multiple-selectors.input.spec.scss │ ├── multiple-selectors.output.spec.scss │ ├── multiple-themify-same-line.input.spec.scss │ ├── multiple-themify-same-line.output.spec.scss │ ├── no-themify.input.spec.scss │ ├── no-themify.output.spec.scss │ ├── pseudo-tests.input.spec.scss │ ├── pseudo-tests.output.spec.scss │ ├── selectors-hell-one-variation.input.spec.scss │ ├── selectors-hell-one-variation.output.spec.scss │ ├── selectors-hell-two-variations.input.spec.scss │ ├── selectors-hell-two-variations.output.spec.scss │ ├── themify-keyframes.input.spec.scss │ ├── themify-keyframes.output.spec.scss │ ├── themify-with-mix-decl.input.spec.scss │ └── themify-with-mix-decl.output.spec.scss ├── commitlint.config.js ├── package-lock.json ├── package.json ├── playground ├── bundle.css ├── gulpfile.js ├── helpers.js ├── index.html ├── package.json ├── palette.js ├── scss │ ├── button.theme.scss │ ├── index.theme.scss │ └── input.theme.scss ├── theme_fallback.css ├── theme_fallback.json └── whitelabel.json ├── src ├── helpers │ ├── css.util.ts │ ├── js-sass.ts │ └── json.util.ts ├── index.ts ├── rule-processor.ts ├── sass │ ├── _internal.scss │ ├── encode │ │ ├── encode │ │ │ ├── api │ │ │ │ └── _json.scss │ │ │ ├── encode.scss │ │ │ ├── helpers │ │ │ │ └── _quote.scss │ │ │ ├── mixins │ │ │ │ └── _json.scss │ │ │ └── types │ │ │ │ ├── _bool.scss │ │ │ │ ├── _color.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _map.scss │ │ │ │ ├── _null.scss │ │ │ │ ├── _number.scss │ │ │ │ └── _string.scss │ │ └── sass-json-export.scss │ └── themify.scss └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .vscode 4 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "printWidth": 5000, 6 | "parser": "typescript" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: true 7 | node_js: 8 | - '9' 9 | after_success: 10 | - npm run build 11 | - npm run travis-deploy-once "npm run semantic-release" 12 | branches: 13 | except: 14 | - /^v\d+\.\d+\.\d+$/ 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [1.0.5](https://github.com/datorama/themify/compare/v1.0.4...v1.0.5) (2018-05-24) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **package:** remove hextorgba package ([9da3fce](https://github.com/datorama/themify/commit/9da3fce)) 8 | 9 | 10 | ## [1.0.4](https://github.com/datorama/themify/compare/v1.0.3...v1.0.4) (2018-05-23) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **npm:** add readme file ([cb3aa35](https://github.com/datorama/themify/commit/cb3aa35)) 16 | 17 | 18 | ## [1.0.3](https://github.com/datorama/themify/compare/v1.0.2...v1.0.3) (2018-05-23) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * **npm:** add missing details ([491ed9f](https://github.com/datorama/themify/commit/491ed9f)) 24 | 25 | 26 | ## [1.0.2](https://github.com/datorama/themify/compare/v1.0.1...v1.0.2) (2018-05-13) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **tests:** upgrade node-sass to latest version ([b4e66f5](https://github.com/datorama/themify/commit/b4e66f5)) 32 | 33 | 34 | ## [1.0.1](https://github.com/datorama/themify/compare/v1.0.0...v1.0.1) (2018-05-10) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * **playground:** fix native variables assertion ([1505b58](https://github.com/datorama/themify/commit/1505b58)) 40 | * **themify:** css fallback - minification and at-rule fix ([c0b06a8](https://github.com/datorama/themify/commit/c0b06a8)) 41 | 42 | 43 | # 1.0.0 (2018-05-07) 44 | 45 | 46 | ### Features 47 | 48 | * **themify:** init ([728c429](https://github.com/datorama/themify/commit/728c429)) 49 | * **themify:** init ([eeb947d](https://github.com/datorama/themify/commit/eeb947d)) 50 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2013] [Datorama] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![themify](https://i.imgur.com/JZyjWm6.png) 2 | [![Build Status](https://img.shields.io/travis/datorama/themify.svg?style=flat-square)](https://travis-ci.org/datorama/themify) 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors) 4 | [![commitizen](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)]() 5 | [![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)]() 6 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) 8 | 9 | > *CSS Themes Made Easy* 10 | 11 | Themify lets you manage your application’s themes in realtime, using a robust solution that’s easily configurable. 12 | 13 | Themify is a PostCSS plugin that generates your theme during the build phase. 14 | The main concept behind it is to provide two palettes, one light and one dark (resembles the inverse of the light palette). 15 | 16 | Under the hood, `themify` will replace your CSS colors with CSS variables, and also take care to provide a fallback for unsupported browsers (such as IE11). 17 | 18 | [Introducing Themify: CSS Themes Made Easy](https://engineering.datorama.com/introducing-themify-css-themes-made-easy-669b7ecb8720) 19 | 20 | ## 🤓 Features 21 | 22 | * 🖌 **Light & Dark palettes** - define your theme using a simple JSON format 23 | 24 | * 🎨 **Replace your colors in runtime** - provide your clients with **white-labeling** capabilities. Let them choose their own colors and replace them instantly 25 | 26 | * :pencil2: **Use it inside your CSS** - use your theme directly in your SASS files, no JavaScript is required 27 | 28 | * 🏃 **Runtime replacement** - change the active palette at runtime, either for the entire application or for a specific HTML container 29 | 30 | * 🔥 **Legacy Browser Support** - support for all major browsers including IE11 31 | 32 | ## Installation 33 | `npm install @datorama/themify --save` 34 | 35 | ## Usage 36 | 37 | #### Options 38 | 39 | |Input|Type|Default|Description| 40 | |---|---|---|---| 41 | |createVars|boolean|`true`|Determines whether CSS variables are automatically generated. This should kept as true, unless you want to inject them yourself.| 42 | |palette|`{light: [key: string]: string, dark: [key: string]: string}`|`{}`|Palette colors| 43 | |classPrefix|string|`''`|A class prefix to append to each generated theme class.| 44 | |screwIE11|boolean|`true`|Whether to generate a fallback for legacy browsers that do not supports CSS Variables.| 45 | |fallback|`{cssPath: string \| null, dynamicPath: string \| null}`|`{}`|`cssPath`: An absolute path to the fallback CSS.
`dynamicPath`: An absolute path to the fallback JSON.| 46 | 47 | #### Add themify to your build pipe: 48 | 49 | ```js 50 | const themifyOptions = { 51 | palette : { 52 | light: { 53 | 'primary-100': '#f2f2f4', 54 | 'primary-200': '#cccece', 55 | 'accent-100': '#e6f9fc', 56 | 'accent-200': '#96e1ed' 57 | }, 58 | dark: { 59 | 'primary-100': '#505050', 60 | 'primary-200': '#666a6b', 61 | 'accent-100': '#096796', 62 | 'accent-200': '#0a87c6' 63 | } 64 | }, 65 | screwIE11 : false, 66 | fallback : { 67 | cssPath : './dist/theme_fallback.css', // use checksum 68 | dynamicPath: './dist/theme_fallback.json' 69 | } 70 | }; 71 | ``` 72 | 73 | ##### Gulp 74 | 75 | ```js 76 | gulp.src('./main.scss') 77 | .pipe(postcss([ 78 | initThemify(themifyOptions), 79 | sass(), 80 | themify(themifyOptions) 81 | ])) 82 | .pipe(rename("bundle.css")) 83 | .pipe(gulp.dest('dist')); 84 | ``` 85 | 86 | ##### Webpack 87 | 88 | ```js 89 | const isProd = process.env.ENV === 'production'; 90 | const basePath = isProd ? './dist' : './src'; 91 | const cssPath = `${basePath}/theme_fallback.css`; 92 | const dynamicPath = `${basePath}/theme_fallback.json`; 93 | 94 | { 95 | test: /\.scss$/, 96 | use: [{loader: "style-loader"}].concat(getLoaders()) 97 | } 98 | 99 | const getLoaders = () => [{ 100 | loader: "css-loader" 101 | }, 102 | { 103 | loader: 'postcss-loader', 104 | options: { 105 | ident: 'postcss2', 106 | plugins: () => [ 107 | require('@datorama/themify').themify(themifyOptions) 108 | ] 109 | } 110 | }, 111 | { 112 | loader: "sass-loader" 113 | }, 114 | { 115 | loader: 'postcss-loader', 116 | options: { 117 | ident: 'postcss1', 118 | plugins: () => [ 119 | require('@datorama/themify').initThemify(themifyOptions) 120 | ] 121 | } 122 | } 123 | ] 124 | ``` 125 | 126 | 127 | #### Add themify to SASS 128 | 129 | In order to use the `themify` function and other SASS helpers, you need to import the `themify` library from your main SASS file: 130 | 131 | ```sass 132 | @import 'node_modules/datorama/themify/themify'; 133 | ``` 134 | 135 | The themify function receives as parameters the name of the color defined in the palette map and an optional opacity parameter. Themify will generate CSS selectors for each palette — one for the light and one for the dark. 136 | 137 | ```scss 138 | .my-awesome-selector { 139 | // color-key: a mandatory key from your palette. For example: primary-100 140 | // opacity: an optional opacity. Valid values between 0 - 1. Defaults 1. 141 | background-color: themify(color-key, opacity); 142 | 143 | // Define a different color for dark and light. 144 | color: themify((dark: color-key-1, light: color-key-2)); 145 | } 146 | ``` 147 | 148 | #### Basic usage 149 | 150 | ```scss 151 | button { 152 | background-color: themify(primary-100); 153 | color: themify(accent-200); 154 | &:hover { 155 | background-color: themify(primary-100, 0.5); 156 | } 157 | } 158 | ``` 159 | 160 | The above example will produce the following CSS: 161 | 162 | ```css 163 | .dark button, button { 164 | background-color: rgba(var(--primary-100), 1); 165 | color: rgba(var(--accent-200), 1); 166 | } 167 | .dark button:hover, button:hover { 168 | background-color: rgba(var(--primary-100), 0.5); 169 | } 170 | ``` 171 | 172 | And the following fallback for IE11: 173 | 174 | ```css 175 | button { 176 | background-color: #f2f2f4; 177 | color: #666a6b; 178 | } 179 | 180 | .dark button { 181 | background-color: #505050; 182 | color: #0a87c6; 183 | } 184 | 185 | button:hover { 186 | background-color: rgba(242, 242, 244, 0.5); 187 | } 188 | 189 | .dark button:hover { 190 | background-color: rgba(80, 80, 80, 0.5); 191 | } 192 | ``` 193 | 194 | #### A different color for each palette 195 | 196 | Sometimes we need more control over the colors so it's possible to specify explicitly one color for **light** and another color for **dark**: 197 | 198 | ```scss 199 | button { 200 | background-color: themify((dark: primary-100, light: accent-200)); 201 | } 202 | ``` 203 | 204 | The above example will produce the following CSS: 205 | 206 | ```css 207 | .button { 208 | background-color: rgba(var(--accent-200), 1); 209 | } 210 | 211 | .dark button { 212 | background-color: rgba(var(--primary-100), 1); 213 | } 214 | ``` 215 | 216 | #### Advanced usage 217 | 218 | `themify` can be combined with every valid CSS: 219 | 220 | ```scss 221 | button { 222 | border: 1px solid themify(primary-100); 223 | background: linear-gradient(themify(accent-200), themify(accent-100)); 224 | } 225 | ``` 226 | 227 | Even in your animations: 228 | 229 | ```scss 230 | .element { 231 | animation: pulse 5s infinite; 232 | } 233 | 234 | @keyframes pulse { 235 | 0% { 236 | background-color: themify(accent-100); 237 | } 238 | 100% { 239 | background-color: themify(accent-200); 240 | } 241 | } 242 | ``` 243 | 244 | #### Runtime replacement 245 | 246 | First, we'll create our own theme service. 247 | 248 | ```ts 249 | import {loadCSSVariablesFallback, replaceColors, Theme} from '@datorama/themify/utils'; 250 | const palette = require('path_to_my_json_pallete'); 251 | 252 | /** fallback for CSS variables support */ 253 | const themeCSSFallback = 'path/theme_fallback.css'; 254 | const themeJSONFallback = 'path/theme_fallback.json'; 255 | 256 | export class MyThemeService { 257 | 258 | constructor(){ 259 | /** 260 | * load the CSS fallback file, in case the browser do not support CSS Variables. 261 |    * Required only if you set screwIE11 option to false. 262 | * 263 | * callback - load event for the CSS file 264 |    */ 265 | loadCSSVariablesFallback(themeCSSFallback, callback); 266 | } 267 | 268 | /** 269 | * Replace the theme colors at runtime 270 | * @param partialTheme a partial theme configuration. 271 | */ 272 | setColors(partialTheme: Theme){ 273 | replaceColors(themeJSONFallback, partialTheme, palette); 274 | } 275 | 276 | } 277 | ``` 278 | 279 | Now let's use this service in our web application: 280 | 281 | ```ts 282 | const themeService = new MyThemeService(); 283 | 284 | /** replace the colors at runtime **/ 285 | themeService.setColors({ 286 | light: { 287 | 'primary-100': '#0c93e4' 288 | } 289 | }); 290 | 291 | ``` 292 | 293 | 294 | #### Changing the active palette 295 | 296 | In order to switch between the dark and light palettes, simply add the appropriate class to the desired HTML element. 297 | 298 | ```scss 299 | p { 300 | /** #96e1ed in light and #0a87c6 in dark */ 301 | color: themify(accent-200); 302 | } 303 | ``` 304 | 305 | ```html 306 |

I'm from the light palette

307 |
308 |

I'm from the dark palette

309 |
310 | ``` 311 | ### Theme class helpers 312 | You can take advantage of your themes not just in your CSS, but also directly in your HTML, by generating a CSS class for each color you define. 313 | 314 | In order to achieve this, use the `generateThemeHelpers` mixin, and pass the CSS properties you want to generate. For example: 315 | 316 | ```scss 317 | // generates the following predefined classes, for each color 318 | $themeRules: ( 319 | 'color', 320 | 'border-top-color', 321 | 'border-bottom-color', 322 | 'border-right-color', 323 | 'border-left-color', 324 | 'background-color', 325 | 'fill', 326 | 'stroke', 327 | // PSEUDO_CLASSES 328 | 'color:h:f:a:vi' 329 | ); 330 | @include generateThemeHelpers($themeRules); 331 | ``` 332 | 333 | This will generate the following CSS: 334 | 335 | ```css 336 | .dark .primary-100-color, .primary-100-color { 337 | color: rgba(var(--primary-100), 1) 338 | } 339 | 340 | .dark .primary-200-color, .primary-200-color { 341 | color: rgba(var(--primary-100), 1) 342 | } 343 | 344 | .dark .primary-100-color\:vi:visited, .primary-100-color\:vi:visited { 345 | color: rgba(var(--primary-100), 1) 346 | } 347 | ``` 348 | and so on.. 349 | 350 | As you see, you can pass any CSS property, including pseudo classes. 351 | The following SASS map details the pseudo class keys and their values: 352 | 353 | ```sass 354 | $PSEUDO_CLASSES: ( 355 | ':a': ':active', 356 | ':c': ':checked', 357 | ':d': ':default', 358 | ':di': ':disabled', 359 | ':e': ':empty', 360 | ':en': ':enabled', 361 | ':fi': ':first', 362 | ':fc': ':first-child', 363 | ':fot': ':first-of-type', 364 | ':fs': ':fullscreen', 365 | ':f': ':focus', 366 | ':h': ':hover', 367 | ':ind': ':indeterminate', 368 | ':ir': ':in-range', 369 | ':inv': ':invalid', 370 | ':lc': ':last-child', 371 | ':lot': ':last-of-type', 372 | ':l': ':left', 373 | ':li': ':link', 374 | ':oc': ':only-child', 375 | ':oot': ':only-of-type', 376 | ':o': ':optional', 377 | ':oor': ':out-of-range', 378 | ':ro': ':read-only', 379 | ':rw' : ':read-write', 380 | ':req': ':required', 381 | ':r': ':right', 382 | ':rt' : ':root', 383 | ':s': ':scope', 384 | ':t' : ':target', 385 | ':va': ':valid', 386 | ':vi': ':visited' 387 | ); 388 | ``` 389 | 390 |
391 | Now you can use the generated CSS classes directly in your HTML: 392 | 393 | ```html 394 | 395 | The default color is primary-100 396 | The active color will be primary-200 397 | 398 | ``` 399 | 400 | ## Known issues 401 | - We discovered that Safari doesn't support the following syntax when it comes to borders with CSS variables: 402 | ```css 403 | /** This will NOT work */ 404 | border: 1px solid themify(primary-100); 405 | 406 | /** This will work */ 407 | border-color: themify(primary-100); 408 | 409 | /** This will work */ 410 | border: themify(primary-100) 1px solid; 411 | ``` 412 | 413 | - Safari doesn't support box-shadow. 414 | 415 | ## Contributors 416 | 417 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 418 | 419 | 420 | 421 | | [
Netanel Basal](https://www.netbasal.com)
[📖](https://github.com/datorama/themify/commits?author=NetanelBasal "Documentation") [💻](https://github.com/datorama/themify/commits?author=NetanelBasal "Code") [🤔](#ideas-NetanelBasal "Ideas, Planning, & Feedback") | [
bh86](https://github.com/bh86)
[📖](https://github.com/datorama/themify/commits?author=bh86 "Documentation") [💻](https://github.com/datorama/themify/commits?author=bh86 "Code") [🤔](#ideas-bh86 "Ideas, Planning, & Feedback") | 422 | | :---: | :---: | 423 | 424 | 425 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 426 | 427 | ## License 428 | 429 | Apache © [datorama](https://github.com/datorama) 430 | -------------------------------------------------------------------------------- /__tests__/common.index.scss: -------------------------------------------------------------------------------- 1 | @import "../src/sass/themify"; -------------------------------------------------------------------------------- /__tests__/css-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestUtils} from "./test.util"; 2 | 3 | const options = { 4 | palette: TestUtils.tinyPalette 5 | }; 6 | const plugins = [TestUtils.plugin.initThemify(options), TestUtils.plugin.sass(), TestUtils.plugin.themify(options)]; 7 | TestUtils.run('Themify - CSS Helpers', '__tests__/theme-helpers/*.input.spec.scss', plugins); -------------------------------------------------------------------------------- /__tests__/fallback-unmatch-palette/1.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | // when the palette contains a variable only for the default variation, 4 | // we should generate the color only for it, and not for the dark 5 | .selector-mix { 6 | color: themify(primary-700, 0.8); 7 | background-color: themify(primary-600); 8 | } 9 | .selector-with-one-variation { 10 | color: themify(primary-700, 0.8); 11 | } -------------------------------------------------------------------------------- /__tests__/fallback-unmatch-palette/1.output-css.spec.scss: -------------------------------------------------------------------------------- 1 | .dark .selector-mix { 2 | background-color: #000 3 | } 4 | 5 | .selector-mix { 6 | color: rgba(48, 48, 48, .8); 7 | background-color: #fff 8 | } 9 | 10 | .selector-with-one-variation { 11 | color: rgba(48, 48, 48, .8) 12 | } -------------------------------------------------------------------------------- /__tests__/fallback-unmatch-palette/1.output-json.spec.json: -------------------------------------------------------------------------------- 1 | {"dark":".dark .selector-mix{background-color:%[dark,primary-600,1]%}","light":".selector-mix{color:%[light,primary-700,.8]%;background-color:%[light,primary-600,1]%}.selector-with-one-variation{color:%[light,primary-700,.8]%}"} -------------------------------------------------------------------------------- /__tests__/fallback.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestUtils} from "./test.util"; 2 | import {ThemifyOptions} from "../src/index"; 3 | import {minifyJSON} from "../src/helpers/json.util"; 4 | 5 | const glob = require("glob"); 6 | const tmp = require('tmp'); 7 | 8 | const options: Partial = { 9 | palette: TestUtils.tinyPalette, 10 | screwIE11: false, 11 | fallback: { 12 | cssPath: '', 13 | dynamicPath: '' 14 | } 15 | }; 16 | const unmatchPaletteOptions: Partial = {...options, 17 | palette: TestUtils.unmatchPalette 18 | }; 19 | 20 | const inputFiles = glob.sync('__tests__/fallback/*.input.spec.scss'); 21 | const inputUnmatchPaletteFiles = glob.sync('__tests__/fallback-unmatch-palette/*.input.spec.scss'); 22 | describe('Themify - Fallback', () => { 23 | inputFiles.forEach((inputFile) => { 24 | const testName = TestUtils.getTestName(inputFile); 25 | it(testName, (done) => { 26 | test(inputFile, options, done); 27 | }); 28 | }); 29 | 30 | inputUnmatchPaletteFiles.forEach((inputFile) => { 31 | const testName = TestUtils.getTestName(inputFile); 32 | it(`${testName}-unmatch-palette`, (done) => { 33 | test(inputFile, unmatchPaletteOptions, done); 34 | }); 35 | }); 36 | }); 37 | 38 | function test(inputFile, options, done) { 39 | // creating temp files for the CSS & JSON files 40 | const {cssTmp, dynamicTmp} = { 41 | cssTmp: tmp.fileSync(), 42 | dynamicTmp: tmp.fileSync() 43 | }; 44 | 45 | const myOptions = {...options, ...{ 46 | fallback: { 47 | cssPath: cssTmp.name, 48 | dynamicPath: dynamicTmp.name 49 | } 50 | } 51 | }; 52 | 53 | return TestUtils.processFile(inputFile, getPlugins(myOptions)).then(() => { 54 | setTimeout(() => { 55 | const cssTempContent = TestUtils.readFile(cssTmp.name); 56 | const jsonTempContent = minifyJSON(TestUtils.readFile(dynamicTmp.name)); 57 | 58 | const cssTempExpectedContent = TestUtils.minify(TestUtils.readFile(inputFile.replace("input", "output-css"))); 59 | const jsonTempExpectedContent = minifyJSON(TestUtils.readFile(inputFile.replace("input.spec.scss", "output-json.spec.json"))); 60 | 61 | // expect the fallback CSS file will be equal to the generated one 62 | expect(cssTempContent).toEqual(cssTempExpectedContent); 63 | // expect the fallback JSON file will be equal 64 | expect(jsonTempContent).toEqual(jsonTempExpectedContent); 65 | 66 | // cleanup 67 | cssTmp.removeCallback(); 68 | dynamicTmp.removeCallback(); 69 | 70 | done(); 71 | }, 100); 72 | 73 | }, (err) => { 74 | console.log(err); 75 | throw err; 76 | }); 77 | 78 | } 79 | 80 | function getPlugins(pluginOptions) { 81 | return [TestUtils.plugin.initThemify(pluginOptions), TestUtils.plugin.sass(), TestUtils.plugin.themify(pluginOptions)]; 82 | } 83 | -------------------------------------------------------------------------------- /__tests__/fallback/1.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .selector a { 4 | color: themify(primary-700, 0.8); 5 | } 6 | .selector a:hover { 7 | color: themify(primary-700, 0.2); 8 | } 9 | .selector a:focus { 10 | color: themify(primary-700, 1); 11 | } 12 | 13 | .element-should-not-be-included { 14 | animation: pulse 5s infinite; 15 | } 16 | 17 | @keyframes shouldBeIncluded { 18 | 0% { 19 | background-color: themify(primary-700); 20 | } 21 | 100% { 22 | background-color: themify(primary-700, 0.5); 23 | } 24 | } 25 | 26 | @keyframes shouldBeIncludedAsWell { 27 | 0% { 28 | background-color: themify(primary-700); 29 | } 30 | 100% { 31 | background-color: #00bee8; 32 | } 33 | } 34 | 35 | @keyframes shouldNOTBeIncluded { 36 | 0% { 37 | background-color: #fff; 38 | } 39 | 100% { 40 | background-color: #00bee8; 41 | } 42 | } -------------------------------------------------------------------------------- /__tests__/fallback/1.output-css.spec.scss: -------------------------------------------------------------------------------- 1 | @keyframes shouldBeIncluded { 2 | 0% { 3 | background-color: #303030 4 | } 5 | 100% { 6 | background-color: rgba(48, 48, 48, .5) 7 | } 8 | } 9 | 10 | @keyframes shouldBeIncludedAsWell { 11 | 0% { 12 | background-color: #303030 13 | } 14 | 100% { 15 | background-color: #00bee8 16 | } 17 | } 18 | 19 | .dark .selector a { 20 | color: rgba(255, 255, 255, 0.8) 21 | } 22 | 23 | .selector a { 24 | color: rgba(48, 48, 48, 0.8); 25 | } 26 | 27 | .dark .selector a:hover { 28 | color: rgba(255, 255, 255, 0.2) 29 | } 30 | 31 | .selector a:hover { 32 | color: rgba(48, 48, 48, 0.2); 33 | } 34 | 35 | .dark .selector a:focus { 36 | color: #ffffff 37 | } 38 | 39 | .selector a:focus { 40 | color: #303030; 41 | } 42 | -------------------------------------------------------------------------------- /__tests__/fallback/1.output-json.spec.json: -------------------------------------------------------------------------------- 1 | {"dark":".dark .selector a{color:%[dark,primary-700,.8]%}.dark .selector a:hover{color:%[dark,primary-700,.2]%}.dark .selector a:focus{color:%[dark,primary-700,1]%}","light":"@keyframes shouldBeIncluded{0%{background-color:%[light,primary-700,1]%}100%{background-color:%[light,primary-700,.5]%}}@keyframes shouldBeIncludedAsWell{0%{background-color:%[light,primary-700,1]%}100%{background-color:#00bee8}}.selector a{color:%[light,primary-700,.8]%}.selector a:hover{color:%[light,primary-700,.2]%}.selector a:focus{color:%[light,primary-700,1]%}"} -------------------------------------------------------------------------------- /__tests__/invalid-css.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestUtils} from "./test.util"; 2 | 3 | const options = { 4 | palette: TestUtils.defaultPalette 5 | }; 6 | const plugins = [TestUtils.plugin.sass(), TestUtils.plugin.themify(options)]; 7 | TestUtils.run('Themify - Invalid CSS', '__tests__/invalid-css/*.input.spec.scss', plugins, true); -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-empty-list.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | :root { 4 | color: themify(()); 5 | } -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-empty-list.output.spec.scss: -------------------------------------------------------------------------------- 1 | Oops. Received an empty color! -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-empty-value.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | :root { 4 | color: themify(); 5 | } -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-empty-value.output.spec.scss: -------------------------------------------------------------------------------- 1 | Oops. Received an empty color! -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-not-exists.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | :root { 4 | color: themify((primary-2000)); 5 | } -------------------------------------------------------------------------------- /__tests__/invalid-css/themify-not-exists.output.spec.scss: -------------------------------------------------------------------------------- 1 | The variable name 'primary-2000' doesn't exists in your palette. -------------------------------------------------------------------------------- /__tests__/palettes/palette.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | light: { 3 | 'primary-700': '#303030', 4 | 'primary-600': '#383838', 5 | 'primary-500': '#505050', 6 | 'primary-400': '#666a6b', 7 | 'primary-300': '#9ca0a0', 8 | 'primary-200': '#cccece', 9 | 'primary-100': '#f2f2f4', 10 | 'primary-50': '#f8f8f9', 11 | 'primary-0': '#ffffff', 12 | 'accent-700': '#096796', 13 | 'accent-600': '#0a87c6', 14 | 'accent-500': '#04a2d6', 15 | 'accent-400': '#00bee8', 16 | 'accent-300': '#4cd1ef', 17 | 'accent-200': '#96e1ed', 18 | 'accent-100': '#e6f9fc', 19 | }, 20 | dark: { 21 | 'primary-700': '#ffffff', 22 | 'primary-600': '#f8f8f9', 23 | 'primary-500': '#f2f2f4', 24 | 'primary-400': '#cccece', 25 | 'primary-300': '#9ca0a0', 26 | 'primary-200': '#666a6b', 27 | 'primary-100': '#505050', 28 | 'primary-50': '#383838', 29 | 'primary-0': '#303030', 30 | 'accent-700': '#e6f9fc', 31 | 'accent-600': '#96e1ed', 32 | 'accent-500': '#4cd1ef', 33 | 'accent-400': '#00bee8', 34 | 'accent-300': '#04a2d6', 35 | 'accent-200': '#0a87c6', 36 | 'accent-100': '#096796', 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /__tests__/palettes/tiny-palette.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | light: { 3 | 'primary-700': '#303030' 4 | }, 5 | dark: { 6 | 'primary-700': '#ffffff' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /__tests__/palettes/unmatch-palette.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | light: { 3 | 'primary-700': '#303030', 4 | 'primary-600': '#ffffff' 5 | }, 6 | dark: { 7 | 'primary-100': '#ffffff', 8 | 'primary-600': '#000000' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /__tests__/test.util.ts: -------------------------------------------------------------------------------- 1 | import {minifyCSS} from "../src/helpers/css.util"; 2 | const glob = require("glob"); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const postcss = require('postcss'); 6 | const sass = require('@datorama/postcss-node-sass'); 7 | 8 | import {ProcessOptions} from "postcss"; 9 | import palette from './palettes/palette'; 10 | import tinyPalette from './palettes/tiny-palette'; 11 | import unmatchPalette from './palettes/unmatch-palette'; 12 | 13 | const {initThemify, themify} = require('../src/index'); 14 | 15 | export class TestUtils { 16 | 17 | static plugin = { 18 | initThemify, 19 | sass, 20 | themify 21 | }; 22 | 23 | static get defaultPalette() { 24 | return palette; 25 | } 26 | static get tinyPalette() { 27 | return tinyPalette; 28 | } 29 | static get unmatchPalette() { 30 | return unmatchPalette; 31 | } 32 | 33 | static run(suite: string, filesGlob: string, plugins: any[], withError = false) { 34 | const inputFiles = glob.sync(filesGlob); 35 | describe(suite, () => { 36 | inputFiles.forEach((inputFile) => { 37 | const testName = this.getTestName(inputFile); 38 | it(testName, () => { 39 | return this.testFile(inputFile, plugins, withError); 40 | }); 41 | }); 42 | }); 43 | } 44 | 45 | static testFile(inputFilename: string, plugins: any[], withError = false): any { 46 | 47 | const outputFile = `./${inputFilename.replace('input', 'output')}`; 48 | const expected = this.readFile(outputFile); 49 | 50 | const exp = expect(this.processFile(inputFilename, plugins)); 51 | if (withError) { 52 | return exp.rejects.toMatch(expected); 53 | } 54 | return exp.resolves.toEqual(this.minify(expected)); 55 | } 56 | 57 | static processFile(inputFilename: string, plugins: any[]) { 58 | const inputFile = `./${inputFilename}`; 59 | const outputFile = `./${inputFilename.replace('input', 'output')}`; 60 | const input = this.readFile(inputFile); 61 | 62 | const options: ProcessOptions = {}; 63 | options.from = inputFile; 64 | options.to = outputFile; 65 | 66 | return postcss(plugins) 67 | .process(input, options) 68 | .then(result => { 69 | return this.minify(result.css); 70 | }, error => { 71 | throw error.message; 72 | }); 73 | } 74 | 75 | static minify(css) { 76 | return minifyCSS(css); 77 | } 78 | 79 | static readFile(fileName) { 80 | return fs.readFileSync(fileName, 'utf8').toString(); 81 | } 82 | 83 | static getTestName(filePath) { 84 | const fileName = path.basename(filePath); 85 | const inputPos = fileName.indexOf(".input"); 86 | return fileName.substring(0, inputPos); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /__tests__/theme-helpers/color-helper.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../../src/sass/themify"; 2 | 3 | // generates the following predefined classes, for each color 4 | $themeRules: ( 5 | 'color', 6 | 'border-top-color', 7 | 'border-bottom-color', 8 | 'border-right-color', 9 | 'border-left-color', 10 | 'background-color', 11 | 'fill', 12 | 'stroke', 13 | // PSEUDO_CLASSES 14 | 'color:h:f:a:vi' 15 | ); 16 | @include generateThemeHelpers($themeRules); 17 | -------------------------------------------------------------------------------- /__tests__/theme-helpers/color-helper.output.spec.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-700: 48, 48, 48 3 | } 4 | 5 | .dark { 6 | --primary-700: 255, 255, 255 7 | } 8 | 9 | .dark .primary-700-color, .primary-700-color { 10 | color: rgba(var(--primary-700), 1) 11 | } 12 | 13 | .dark .primary-700-color, .primary-700-color { 14 | color: rgba(var(--primary-700), 1) 15 | } 16 | 17 | .dark .primary-700-border-top-color, .primary-700-border-top-color { 18 | border-top-color: rgba(var(--primary-700), 1) 19 | } 20 | 21 | .dark .primary-700-border-top-color, .primary-700-border-top-color { 22 | border-top-color: rgba(var(--primary-700), 1) 23 | } 24 | 25 | .dark .primary-700-border-bottom-color, .primary-700-border-bottom-color { 26 | border-bottom-color: rgba(var(--primary-700), 1) 27 | } 28 | 29 | .dark .primary-700-border-bottom-color, .primary-700-border-bottom-color { 30 | border-bottom-color: rgba(var(--primary-700), 1) 31 | } 32 | 33 | .dark .primary-700-border-right-color, .primary-700-border-right-color { 34 | border-right-color: rgba(var(--primary-700), 1) 35 | } 36 | 37 | .dark .primary-700-border-right-color, .primary-700-border-right-color { 38 | border-right-color: rgba(var(--primary-700), 1) 39 | } 40 | 41 | .dark .primary-700-border-left-color, .primary-700-border-left-color { 42 | border-left-color: rgba(var(--primary-700), 1) 43 | } 44 | 45 | .dark .primary-700-border-left-color, .primary-700-border-left-color { 46 | border-left-color: rgba(var(--primary-700), 1) 47 | } 48 | 49 | .dark .primary-700-background-color, .primary-700-background-color { 50 | background-color: rgba(var(--primary-700), 1) 51 | } 52 | 53 | .dark .primary-700-background-color, .primary-700-background-color { 54 | background-color: rgba(var(--primary-700), 1) 55 | } 56 | 57 | .dark .primary-700-fill, .primary-700-fill { 58 | fill: rgba(var(--primary-700), 1) 59 | } 60 | 61 | .dark .primary-700-fill, .primary-700-fill { 62 | fill: rgba(var(--primary-700), 1) 63 | } 64 | 65 | .dark .primary-700-stroke, .primary-700-stroke { 66 | stroke: rgba(var(--primary-700), 1) 67 | } 68 | 69 | .dark .primary-700-stroke, .primary-700-stroke { 70 | stroke: rgba(var(--primary-700), 1) 71 | } 72 | 73 | .dark .primary-700-color, .primary-700-color { 74 | color: rgba(var(--primary-700), 1) 75 | } 76 | 77 | .dark .primary-700-color\:h:hover, .primary-700-color\:h:hover { 78 | color: rgba(var(--primary-700), 1) 79 | } 80 | 81 | .dark .primary-700-color\:f:focus, .primary-700-color\:f:focus { 82 | color: rgba(var(--primary-700), 1) 83 | } 84 | 85 | .dark .primary-700-color\:a:active, .primary-700-color\:a:active { 86 | color: rgba(var(--primary-700), 1) 87 | } 88 | 89 | .dark .primary-700-color\:vi:visited, .primary-700-color\:vi:visited { 90 | color: rgba(var(--primary-700), 1) 91 | } 92 | 93 | .dark .primary-700-color, .primary-700-color { 94 | color: rgba(var(--primary-700), 1) 95 | } 96 | 97 | .dark .primary-700-color\:h:hover, .primary-700-color\:h:hover { 98 | color: rgba(var(--primary-700), 1) 99 | } 100 | 101 | .dark .primary-700-color\:f:focus, .primary-700-color\:f:focus { 102 | color: rgba(var(--primary-700), 1) 103 | } 104 | 105 | .dark .primary-700-color\:a:active, .primary-700-color\:a:active { 106 | color: rgba(var(--primary-700), 1) 107 | } 108 | 109 | .dark .primary-700-color\:vi:visited, .primary-700-color\:vi:visited { 110 | color: rgba(var(--primary-700), 1) 111 | } -------------------------------------------------------------------------------- /__tests__/unmatch-palette.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestUtils} from "./test.util"; 2 | 3 | const options = { 4 | palette: TestUtils.unmatchPalette 5 | }; 6 | const plugins = [TestUtils.plugin.sass(), TestUtils.plugin.themify(options)]; 7 | TestUtils.run('Themify - Valid CSS with Unmatch Palette', '__tests__/unmatch-palette/*.input.spec.scss', plugins); -------------------------------------------------------------------------------- /__tests__/unmatch-palette/unmatch-palette.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | // when the palette contains a variable only for the default variation, 4 | // we should generate the color only for it, and not for the dark 5 | .selector-mix { 6 | color: themify(primary-700, 0.8); 7 | background-color: themify(primary-600); 8 | } 9 | .selector-with-one-variation { 10 | color: themify(primary-700, 0.8); 11 | } -------------------------------------------------------------------------------- /__tests__/unmatch-palette/unmatch-palette.output.spec.scss: -------------------------------------------------------------------------------- 1 | .dark .selector-mix, .selector-mix { 2 | color: rgba(var(--primary-700), .8); 3 | background-color: rgba(var(--primary-600), 1) 4 | } 5 | 6 | .selector-with-one-variation { 7 | color: rgba(var(--primary-700), .8) 8 | } -------------------------------------------------------------------------------- /__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import {_handleUnSupportedBrowsers, _generateNewVariables} from '../src/utils'; 2 | import palette from './palettes/palette'; 3 | 4 | const whitelabel = { 5 | "dark": { 6 | "primary-100": "#c333d3", 7 | "primary-200": "#c333d3" 8 | }, 9 | "light": { 10 | "accent-300": "#ff0000" 11 | } 12 | } 13 | 14 | const whitelabelOnlyDark = { 15 | "dark": { 16 | "primary-100": "#c333d3", 17 | "primary-200": "#c333d3" 18 | } 19 | } 20 | 21 | const fallbackJSON = { 22 | "dark": ".dark button {color: %[dark, primary-100, 1]%;background-color: %[dark, primary-200, 0.5]%;border: 1px solid %[dark, primary-300, 0.5]%}.dark h1 {color: %[dark, accent-300, 1]%;background: linear-gradient(to right, %[dark, primary-100, 1]%, %[dark, accent-100, 1]%)}.dark p {color: %[dark, primary-100, 1]%}", 23 | "light": "button {color: %[light, primary-100, 1]%;background-color: %[light, primary-200, 0.5]%;border: 1px solid %[light, primary-300, 0.5]%;}h1 {color: %[light, accent-300, 1]%;background: linear-gradient(to right, %[light, primary-100, 1]%, %[light, accent-100, 1]%);}p {color: %[light, accent-300, 1]%;}" 24 | }; 25 | 26 | describe('Utils', () => { 27 | 28 | describe('Supported Browsers', () => { 29 | it('should generate the correct scheme', () => { 30 | const output = `.dark{--primary-100: 195,51,211;--primary-200: 195,51,211;}:root{--accent-300: 255,0,0;}`; 31 | expect(_generateNewVariables(whitelabel)).toEqual(output); 32 | }); 33 | 34 | it('should work with one variation', () => { 35 | const output = `.dark{--primary-100: 195,51,211;--primary-200: 195,51,211;}`; 36 | expect(_generateNewVariables(whitelabelOnlyDark)).toEqual(output); 37 | }); 38 | }); 39 | 40 | describe('UnSupported Browsers', () => { 41 | it('should generate the correct scheme', () => { 42 | const output = ".darkbutton{color:rgba(195,51,211,1);background-color:rgba(195,51,211,0.5);border:1pxsolidrgba(156,160,160,0.5)}.darkh1{color:rgba(4,162,214,1);background:linear-gradient(toright,rgba(195,51,211,1),rgba(9,103,150,1))}.darkp{color:rgba(195,51,211,1)}button{color:rgba(242,242,244,1);background-color:rgba(204,206,206,0.5);border:1pxsolidrgba(156,160,160,0.5);}h1{color:rgba(255,0,0,1);background:linear-gradient(toright,rgba(242,242,244,1),rgba(230,249,252,1));}p{color:rgba(255,0,0,1);}" 43 | expect(_handleUnSupportedBrowsers(whitelabel, palette, fallbackJSON).replace(/\s/g, '')).toEqual(output.replace(/\s/g, '')); 44 | }); 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /__tests__/valid-css.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestUtils} from "./test.util"; 2 | 3 | const options = { 4 | palette: TestUtils.defaultPalette 5 | }; 6 | const plugins = [TestUtils.plugin.sass(), TestUtils.plugin.themify(options)]; 7 | TestUtils.run('Themify - Valid CSS', '__tests__/valid-css/*.input.spec.scss', plugins); -------------------------------------------------------------------------------- /__tests__/valid-css/different-color-alpha.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .selector a { 4 | color: themify(accent-200, 0.8); 5 | } 6 | .selector a:hover { 7 | color: themify(accent-200, 0.2); 8 | } 9 | .selector a:focus { 10 | color: themify(accent-200, 1); 11 | } -------------------------------------------------------------------------------- /__tests__/valid-css/different-color-alpha.output.spec.scss: -------------------------------------------------------------------------------- 1 | .dark .selector a, .selector a { 2 | color: rgba(var(--accent-200), 0.8) 3 | } 4 | 5 | .dark .selector a:hover, .selector a:hover { 6 | color: rgba(var(--accent-200), 0.2) 7 | } 8 | 9 | .dark .selector a:focus, .selector a:focus { 10 | color: rgba(var(--accent-200), 1) 11 | } -------------------------------------------------------------------------------- /__tests__/valid-css/empty-class.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .an-empty-class { 4 | 5 | } -------------------------------------------------------------------------------- /__tests__/valid-css/empty-class.output.spec.scss: -------------------------------------------------------------------------------- 1 | .an-empty-class { 2 | 3 | } -------------------------------------------------------------------------------- /__tests__/valid-css/multiple-selectors.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | dato-button, 4 | dato-button:hover { 5 | color: themify(primary-100); 6 | background-color: themify(primary-200, 0.5); 7 | border: 1px solid themify((dark: primary-100, light: accent-300)); 8 | } -------------------------------------------------------------------------------- /__tests__/valid-css/multiple-selectors.output.spec.scss: -------------------------------------------------------------------------------- 1 | .dark dato-button, .dark dato-button:hover, dato-button, dato-button:hover { 2 | color: rgba(var(--primary-100), 1); 3 | background-color: rgba(var(--primary-200), .5); 4 | border: 1px solid rgba(var(--accent-300), 1) 5 | } 6 | 7 | /* Only the border should be extracted, in the .dark selector */ 8 | .dark dato-button, .dark dato-button:hover { 9 | border: 1px solid rgba(var(--primary-100), 1) 10 | } -------------------------------------------------------------------------------- /__tests__/valid-css/multiple-themify-same-line.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | div { 4 | background: linear-gradient(themify(accent-200), themify(accent-700)); 5 | } -------------------------------------------------------------------------------- /__tests__/valid-css/multiple-themify-same-line.output.spec.scss: -------------------------------------------------------------------------------- 1 | 2 | /* Should combined the .dark selector */ 3 | .dark div, div { 4 | background: linear-gradient(rgba(var(--accent-200), 1), rgba(var(--accent-700), 1)) 5 | } -------------------------------------------------------------------------------- /__tests__/valid-css/no-themify.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .a-simple-selector { 4 | color: red; 5 | } -------------------------------------------------------------------------------- /__tests__/valid-css/no-themify.output.spec.scss: -------------------------------------------------------------------------------- 1 | .a-simple-selector { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /__tests__/valid-css/pseudo-tests.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | a:hover { 4 | color: themify(accent-200); 5 | } 6 | 7 | .selector::before { 8 | content: ' '; 9 | display: inline-block; 10 | color: themify(accent-200); 11 | } 12 | .selector::after { 13 | content: ' '; 14 | display: inline-block; 15 | color: themify((dark: primary-0, light: accent-300)); 16 | } -------------------------------------------------------------------------------- /__tests__/valid-css/pseudo-tests.output.spec.scss: -------------------------------------------------------------------------------- 1 | .dark a:hover, a:hover { 2 | color: rgba(var(--accent-200), 1) 3 | } 4 | 5 | .dark .selector::before, .selector::before { 6 | content: ' '; 7 | display: inline-block; 8 | color: rgba(var(--accent-200), 1) 9 | } 10 | 11 | .selector::after { 12 | content: ' '; 13 | display: inline-block; 14 | color: rgba(var(--accent-300), 1) 15 | } 16 | 17 | .dark .selector::after { 18 | color: rgba(var(--primary-0), 1) 19 | } -------------------------------------------------------------------------------- /__tests__/valid-css/selectors-hell-one-variation.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .selector, 4 | .selector .a, 5 | .selector .a .b, 6 | .selector .a .b .c, 7 | .selector .a .b .c .d, 8 | .selector .a .b .c .d .e, 9 | .selector .a .b .c .d .e .f, 10 | .selector .a .b .c .d .e .f .g, 11 | .selector .a .b .c .d .e .f .g .h, 12 | .selector .a .b .c .d .e .f .g .h .i, 13 | .selector .a .b .c .d .e .f .g .h .i .j, 14 | .selector .a .b .c .d .e .f .g .h .i .j .k{ 15 | color: themify(primary-600); 16 | } -------------------------------------------------------------------------------- /__tests__/valid-css/selectors-hell-one-variation.output.spec.scss: -------------------------------------------------------------------------------- 1 | .dark .selector, 2 | .dark .selector .a, 3 | .dark .selector .a .b, 4 | .dark .selector .a .b .c, 5 | .dark .selector .a .b .c .d, 6 | .dark .selector .a .b .c .d .e, 7 | .dark .selector .a .b .c .d .e .f, 8 | .dark .selector .a .b .c .d .e .f .g, 9 | .dark .selector .a .b .c .d .e .f .g .h, 10 | .dark .selector .a .b .c .d .e .f .g .h .i, 11 | .dark .selector .a .b .c .d .e .f .g .h .i .j, 12 | .dark .selector .a .b .c .d .e .f .g .h .i .j .k, 13 | .selector, 14 | .selector .a, 15 | .selector .a .b, 16 | .selector .a .b .c, 17 | .selector .a .b .c .d, 18 | .selector .a .b .c .d .e, 19 | .selector .a .b .c .d .e .f, 20 | .selector .a .b .c .d .e .f .g, 21 | .selector .a .b .c .d .e .f .g .h, 22 | .selector .a .b .c .d .e .f .g .h .i, 23 | .selector .a .b .c .d .e .f .g .h .i .j, 24 | .selector .a .b .c .d .e .f .g .h .i .j .k { 25 | color: rgba(var(--primary-600), 1) 26 | } -------------------------------------------------------------------------------- /__tests__/valid-css/selectors-hell-two-variations.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .selector, 4 | .selector .a, 5 | .selector .a .b, 6 | .selector .a .b .c, 7 | .selector .a .b .c .d, 8 | .selector .a .b .c .d .e, 9 | .selector .a .b .c .d .e .f, 10 | .selector .a .b .c .d .e .f .g, 11 | .selector .a .b .c .d .e .f .g .h, 12 | .selector .a .b .c .d .e .f .g .h .i, 13 | .selector .a .b .c .d .e .f .g .h .i .j, 14 | .selector .a .b .c .d .e .f .g .h .i .j .k{ 15 | color: themify((dark: primary-100, light: accent-300)); 16 | } -------------------------------------------------------------------------------- /__tests__/valid-css/selectors-hell-two-variations.output.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | /* Should be splitted to two different classes */ 4 | 5 | 6 | .selector, 7 | .selector .a, 8 | .selector .a .b, 9 | .selector .a .b .c, 10 | .selector .a .b .c .d, 11 | .selector .a .b .c .d .e, 12 | .selector .a .b .c .d .e .f, 13 | .selector .a .b .c .d .e .f .g, 14 | .selector .a .b .c .d .e .f .g .h, 15 | .selector .a .b .c .d .e .f .g .h .i, 16 | .selector .a .b .c .d .e .f .g .h .i .j, 17 | .selector .a .b .c .d .e .f .g .h .i .j .k { 18 | color: rgba(var(--accent-300), 1) 19 | } 20 | 21 | .dark .selector, 22 | .dark .selector .a, 23 | .dark .selector .a .b, 24 | .dark .selector .a .b .c, 25 | .dark .selector .a .b .c .d, 26 | .dark .selector .a .b .c .d .e, 27 | .dark .selector .a .b .c .d .e .f, 28 | .dark .selector .a .b .c .d .e .f .g, 29 | .dark .selector .a .b .c .d .e .f .g .h, 30 | .dark .selector .a .b .c .d .e .f .g .h .i, 31 | .dark .selector .a .b .c .d .e .f .g .h .i .j, 32 | .dark .selector .a .b .c .d .e .f .g .h .i .j .k { 33 | color: rgba(var(--primary-100), 1) 34 | } -------------------------------------------------------------------------------- /__tests__/valid-css/themify-keyframes.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .element { 4 | animation: pulse 5s infinite; 5 | } 6 | 7 | @keyframes pulse { 8 | 0% { 9 | background-color: themify(accent-100); 10 | } 11 | 100% { 12 | background-color: themify(accent-600); 13 | } 14 | } -------------------------------------------------------------------------------- /__tests__/valid-css/themify-keyframes.output.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | .element { 4 | animation: pulse 5s infinite 5 | } 6 | 7 | /* since @keyframes cannot be nested under a class, we get only the default variation */ 8 | 9 | @keyframes pulse { 10 | 0% { 11 | background-color: rgba(var(--accent-100), 1) 12 | } 13 | 100% { 14 | background-color: rgba(var(--accent-600), 1) 15 | } 16 | } -------------------------------------------------------------------------------- /__tests__/valid-css/themify-with-mix-decl.input.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | div { 4 | margin: 20px; 5 | background: themify(accent-200); 6 | padding: 10px; 7 | } -------------------------------------------------------------------------------- /__tests__/valid-css/themify-with-mix-decl.output.spec.scss: -------------------------------------------------------------------------------- 1 | @import "../common.index"; 2 | 3 | /* Should leave the margin and padding in place */ 4 | .dark div, div { 5 | margin: 20px; 6 | background: rgba(var(--accent-200), 1); 7 | padding: 10px 8 | } -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | 2 | const types = [ 3 | 'build', 4 | 'ci', 5 | 'docs', 6 | 'feat', 7 | 'fix', 8 | 'perf', 9 | 'refactor', 10 | 'revert', 11 | 'style', 12 | 'test', 13 | 'release' 14 | ]; 15 | 16 | module.exports = { 17 | extends: ['@commitlint/config-angular'], 18 | rules: { 19 | 'type-enum': [2, 'always', types] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datorama/themify", 3 | "version": "0.0.0-development", 4 | "description": "CSS themes made easy. A robust, opinionated solution to manage themes in your web application", 5 | "main": "index.js", 6 | "gh-pages-deploy": { 7 | "staticpath": "playground", 8 | "commit": "pages" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Netanel Basal", 13 | "url": "https://netbasal.com" 14 | }, 15 | { 16 | "name": "Amit Bar Hanin" 17 | } 18 | ], 19 | "bugs": { 20 | "url": "https://github.com/datorama/themify/issues" 21 | }, 22 | "license": "Apache License 2.0", 23 | "homepage": "https://github.com/datorama/themify#readme", 24 | "scripts": { 25 | "setup": "semantic-release-cli setup", 26 | "github-pages": "gh-pages-deploy", 27 | "contributors:add": "all-contributors add", 28 | "contributors:generate": "all-contributors generate", 29 | "play": "gulp --gulpfile ./playground/gulpfile.js", 30 | "commit": "git-cz", 31 | "build": "npm run clean:dist && tsc && npm run copy:dist", 32 | "copy:dist": "cp -R src/sass/. package.json README.md dist", 33 | "clean:dist": "rimraf dist", 34 | "format": "prettier --write --config .prettierrc src/*.ts src/**/*.ts playground/*.js", 35 | "test": "jest", 36 | "commitmsg": "commitlint -e $GIT_PARAMS", 37 | "precommit": "lint-staged", 38 | "travis-deploy-once": "travis-deploy-once", 39 | "semantic-release": "semantic-release" 40 | }, 41 | "jest": { 42 | "moduleFileExtensions": [ 43 | "ts", 44 | "js" 45 | ], 46 | "transform": { 47 | "\\.(ts)$": "/node_modules/ts-jest/preprocessor.js" 48 | }, 49 | "testRegex": "/__tests__/.*spec\\.(ts|js)$" 50 | }, 51 | "lint-staged": { 52 | "src/**/*.ts": [ 53 | "npm run format", 54 | "git add" 55 | ] 56 | }, 57 | "keywords": [ 58 | "CSS", 59 | "CSS themes", 60 | "CSS variables", 61 | "themes", 62 | "css vars", 63 | "sass themes" 64 | ], 65 | "author": "Datorama", 66 | "dependencies": { 67 | "fs-extra": "^5.0.0" 68 | }, 69 | "config": { 70 | "github_deploy_source": "playground", 71 | "commitizen": { 72 | "path": "./node_modules/cz-conventional-changelog" 73 | } 74 | }, 75 | "devDependencies": { 76 | "@commitlint/cli": "^8.1.0", 77 | "@commitlint/config-angular": "^6.1.3", 78 | "@datorama/postcss-node-sass": "^1.0.0", 79 | "@semantic-release/changelog": "^2.0.1", 80 | "@semantic-release/git": "^4.0.1", 81 | "@semantic-release/npm": "^3.2.2", 82 | "@types/jest": "^22.2.0", 83 | "@types/node": "^9.4.6", 84 | "all-contributors-cli": "^4.11.1", 85 | "browser-sync": "^2.26.7", 86 | "clean-css": "^4.1.11", 87 | "commitizen": "^3.1.1", 88 | "cross-env": "^5.1.4", 89 | "cz-conventional-changelog": "^2.1.0", 90 | "gh-pages": "^2.0.1", 91 | "gh-pages-deploy": "^0.5.1", 92 | "glob": "^7.1.2", 93 | "gulp": "^4.0.2", 94 | "gulp-postcss": "^7.0.1", 95 | "gulp-rename": "^1.2.2", 96 | "gulp-sass": "^4.0.1", 97 | "husky": "^0.14.3", 98 | "jest": "^24.8.0", 99 | "lint-staged": "^9.2.0", 100 | "node-sass": "^4.12.0", 101 | "postcss": "^6.0.19", 102 | "prettier": "^1.11.1", 103 | "rimraf": "^2.6.2", 104 | "semantic-release": "^15.13.19", 105 | "semantic-release-cli": "^5.1.1", 106 | "tmp": "0.0.33", 107 | "travis-deploy-once": "^5.0.0", 108 | "ts-jest": "^24.0.2", 109 | "typescript": "^2.7.2" 110 | }, 111 | "release": { 112 | "verifyConditions": [ 113 | "@semantic-release/changelog", 114 | "@semantic-release/npm", 115 | "@semantic-release/git" 116 | ], 117 | "prepare": [ 118 | "@semantic-release/changelog", 119 | { 120 | "path": "@semantic-release/npm", 121 | "pkgRoot": "dist" 122 | }, 123 | "@semantic-release/git" 124 | ], 125 | "publish": [ 126 | { 127 | "path": "@semantic-release/npm", 128 | "pkgRoot": "dist" 129 | } 130 | ] 131 | }, 132 | "repository": { 133 | "type": "git", 134 | "url": "https://github.com/datorama/themify.git" 135 | } 136 | } -------------------------------------------------------------------------------- /playground/bundle.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-700: 48, 48, 48; 3 | --primary-600: 56, 56, 56; 4 | --primary-500: 80, 80, 80; 5 | --primary-400: 102, 106, 107; 6 | --primary-300: 156, 160, 160; 7 | --primary-200: 204, 206, 206; 8 | --primary-100: 242, 242, 244; 9 | --primary-50: 248, 248, 249; 10 | --primary-0: 255, 255, 255; 11 | --accent-700: 9, 103, 150; 12 | --accent-600: 10, 135, 198; 13 | --accent-500: 4, 162, 214; 14 | --accent-400: 0, 190, 232; 15 | --accent-300: 76, 209, 239; 16 | --accent-200: 150, 225, 237; 17 | --accent-100: 230, 249, 252; 18 | } 19 | 20 | .dark { 21 | --primary-700: 255, 255, 255; 22 | --primary-600: 248, 248, 249; 23 | --primary-500: 242, 242, 244; 24 | --primary-400: 204, 206, 206; 25 | --primary-300: 156, 160, 160; 26 | --primary-200: 102, 106, 107; 27 | --primary-100: 80, 80, 80; 28 | --primary-50: 56, 56, 56; 29 | --primary-0: 48, 48, 48; 30 | --accent-700: 230, 249, 252; 31 | --accent-600: 150, 225, 237; 32 | --accent-500: 76, 209, 239; 33 | --accent-400: 0, 190, 232; 34 | --accent-300: 4, 162, 214; 35 | --accent-200: 10, 135, 198; 36 | --accent-100: 9, 103, 150; 37 | } 38 | 39 | /** 40 | * Credits to https://github.com/acss-io/atomizer/blob/master/src/lib/grammar.js 41 | */ 42 | .primary-700-background-color, .dark .primary-700-background-color { 43 | background-color: rgba(var(--primary-700), 1); 44 | } 45 | 46 | .primary-600-background-color, .dark .primary-600-background-color { 47 | background-color: rgba(var(--primary-600), 1); 48 | } 49 | 50 | .primary-500-background-color, .dark .primary-500-background-color { 51 | background-color: rgba(var(--primary-500), 1); 52 | } 53 | 54 | .primary-400-background-color, .dark .primary-400-background-color { 55 | background-color: rgba(var(--primary-400), 1); 56 | } 57 | 58 | .primary-300-background-color, .dark .primary-300-background-color { 59 | background-color: rgba(var(--primary-300), 1); 60 | } 61 | 62 | .primary-200-background-color, .dark .primary-200-background-color { 63 | background-color: rgba(var(--primary-200), 1); 64 | } 65 | 66 | .primary-100-background-color, .dark .primary-100-background-color { 67 | background-color: rgba(var(--primary-100), 1); 68 | } 69 | 70 | .primary-50-background-color, .dark .primary-50-background-color { 71 | background-color: rgba(var(--primary-50), 1); 72 | } 73 | 74 | .primary-0-background-color, .dark .primary-0-background-color { 75 | background-color: rgba(var(--primary-0), 1); 76 | } 77 | 78 | .accent-700-background-color, .dark .accent-700-background-color { 79 | background-color: rgba(var(--accent-700), 1); 80 | } 81 | 82 | .accent-600-background-color, .dark .accent-600-background-color { 83 | background-color: rgba(var(--accent-600), 1); 84 | } 85 | 86 | .accent-500-background-color, .dark .accent-500-background-color { 87 | background-color: rgba(var(--accent-500), 1); 88 | } 89 | 90 | .accent-400-background-color, .dark .accent-400-background-color { 91 | background-color: rgba(var(--accent-400), 1); 92 | } 93 | 94 | .accent-300-background-color, .dark .accent-300-background-color { 95 | background-color: rgba(var(--accent-300), 1); 96 | } 97 | 98 | .accent-200-background-color, .dark .accent-200-background-color { 99 | background-color: rgba(var(--accent-200), 1); 100 | } 101 | 102 | .accent-100-background-color, .dark .accent-100-background-color { 103 | background-color: rgba(var(--accent-100), 1); 104 | } 105 | 106 | .primary-700-background-color, .dark .primary-700-background-color { 107 | background-color: rgba(var(--primary-700), 1); 108 | } 109 | 110 | .primary-600-background-color, .dark .primary-600-background-color { 111 | background-color: rgba(var(--primary-600), 1); 112 | } 113 | 114 | .primary-500-background-color, .dark .primary-500-background-color { 115 | background-color: rgba(var(--primary-500), 1); 116 | } 117 | 118 | .primary-400-background-color, .dark .primary-400-background-color { 119 | background-color: rgba(var(--primary-400), 1); 120 | } 121 | 122 | .primary-300-background-color, .dark .primary-300-background-color { 123 | background-color: rgba(var(--primary-300), 1); 124 | } 125 | 126 | .primary-200-background-color, .dark .primary-200-background-color { 127 | background-color: rgba(var(--primary-200), 1); 128 | } 129 | 130 | .primary-100-background-color, .dark .primary-100-background-color { 131 | background-color: rgba(var(--primary-100), 1); 132 | } 133 | 134 | .primary-50-background-color, .dark .primary-50-background-color { 135 | background-color: rgba(var(--primary-50), 1); 136 | } 137 | 138 | .primary-0-background-color, .dark .primary-0-background-color { 139 | background-color: rgba(var(--primary-0), 1); 140 | } 141 | 142 | .accent-700-background-color, .dark .accent-700-background-color { 143 | background-color: rgba(var(--accent-700), 1); 144 | } 145 | 146 | .accent-600-background-color, .dark .accent-600-background-color { 147 | background-color: rgba(var(--accent-600), 1); 148 | } 149 | 150 | .accent-500-background-color, .dark .accent-500-background-color { 151 | background-color: rgba(var(--accent-500), 1); 152 | } 153 | 154 | .accent-400-background-color, .dark .accent-400-background-color { 155 | background-color: rgba(var(--accent-400), 1); 156 | } 157 | 158 | .accent-300-background-color, .dark .accent-300-background-color { 159 | background-color: rgba(var(--accent-300), 1); 160 | } 161 | 162 | .accent-200-background-color, .dark .accent-200-background-color { 163 | background-color: rgba(var(--accent-200), 1); 164 | } 165 | 166 | .accent-100-background-color, .dark .accent-100-background-color { 167 | background-color: rgba(var(--accent-100), 1); 168 | } 169 | 170 | .primary-700-fill, .dark .primary-700-fill { 171 | fill: rgba(var(--primary-700), 1); 172 | } 173 | 174 | .primary-600-fill, .dark .primary-600-fill { 175 | fill: rgba(var(--primary-600), 1); 176 | } 177 | 178 | .primary-500-fill, .dark .primary-500-fill { 179 | fill: rgba(var(--primary-500), 1); 180 | } 181 | 182 | .primary-400-fill, .dark .primary-400-fill { 183 | fill: rgba(var(--primary-400), 1); 184 | } 185 | 186 | .primary-300-fill, .dark .primary-300-fill { 187 | fill: rgba(var(--primary-300), 1); 188 | } 189 | 190 | .primary-200-fill, .dark .primary-200-fill { 191 | fill: rgba(var(--primary-200), 1); 192 | } 193 | 194 | .primary-100-fill, .dark .primary-100-fill { 195 | fill: rgba(var(--primary-100), 1); 196 | } 197 | 198 | .primary-50-fill, .dark .primary-50-fill { 199 | fill: rgba(var(--primary-50), 1); 200 | } 201 | 202 | .primary-0-fill, .dark .primary-0-fill { 203 | fill: rgba(var(--primary-0), 1); 204 | } 205 | 206 | .accent-700-fill, .dark .accent-700-fill { 207 | fill: rgba(var(--accent-700), 1); 208 | } 209 | 210 | .accent-600-fill, .dark .accent-600-fill { 211 | fill: rgba(var(--accent-600), 1); 212 | } 213 | 214 | .accent-500-fill, .dark .accent-500-fill { 215 | fill: rgba(var(--accent-500), 1); 216 | } 217 | 218 | .accent-400-fill, .dark .accent-400-fill { 219 | fill: rgba(var(--accent-400), 1); 220 | } 221 | 222 | .accent-300-fill, .dark .accent-300-fill { 223 | fill: rgba(var(--accent-300), 1); 224 | } 225 | 226 | .accent-200-fill, .dark .accent-200-fill { 227 | fill: rgba(var(--accent-200), 1); 228 | } 229 | 230 | .accent-100-fill, .dark .accent-100-fill { 231 | fill: rgba(var(--accent-100), 1); 232 | } 233 | 234 | .primary-700-fill, .dark .primary-700-fill { 235 | fill: rgba(var(--primary-700), 1); 236 | } 237 | 238 | .primary-600-fill, .dark .primary-600-fill { 239 | fill: rgba(var(--primary-600), 1); 240 | } 241 | 242 | .primary-500-fill, .dark .primary-500-fill { 243 | fill: rgba(var(--primary-500), 1); 244 | } 245 | 246 | .primary-400-fill, .dark .primary-400-fill { 247 | fill: rgba(var(--primary-400), 1); 248 | } 249 | 250 | .primary-300-fill, .dark .primary-300-fill { 251 | fill: rgba(var(--primary-300), 1); 252 | } 253 | 254 | .primary-200-fill, .dark .primary-200-fill { 255 | fill: rgba(var(--primary-200), 1); 256 | } 257 | 258 | .primary-100-fill, .dark .primary-100-fill { 259 | fill: rgba(var(--primary-100), 1); 260 | } 261 | 262 | .primary-50-fill, .dark .primary-50-fill { 263 | fill: rgba(var(--primary-50), 1); 264 | } 265 | 266 | .primary-0-fill, .dark .primary-0-fill { 267 | fill: rgba(var(--primary-0), 1); 268 | } 269 | 270 | .accent-700-fill, .dark .accent-700-fill { 271 | fill: rgba(var(--accent-700), 1); 272 | } 273 | 274 | .accent-600-fill, .dark .accent-600-fill { 275 | fill: rgba(var(--accent-600), 1); 276 | } 277 | 278 | .accent-500-fill, .dark .accent-500-fill { 279 | fill: rgba(var(--accent-500), 1); 280 | } 281 | 282 | .accent-400-fill, .dark .accent-400-fill { 283 | fill: rgba(var(--accent-400), 1); 284 | } 285 | 286 | .accent-300-fill, .dark .accent-300-fill { 287 | fill: rgba(var(--accent-300), 1); 288 | } 289 | 290 | .accent-200-fill, .dark .accent-200-fill { 291 | fill: rgba(var(--accent-200), 1); 292 | } 293 | 294 | .accent-100-fill, .dark .accent-100-fill { 295 | fill: rgba(var(--accent-100), 1); 296 | } 297 | 298 | .primary-700-color, .dark .primary-700-color { 299 | color: rgba(var(--primary-700), 1); 300 | } 301 | 302 | .primary-600-color, .dark .primary-600-color { 303 | color: rgba(var(--primary-600), 1); 304 | } 305 | 306 | .primary-500-color, .dark .primary-500-color { 307 | color: rgba(var(--primary-500), 1); 308 | } 309 | 310 | .primary-400-color, .dark .primary-400-color { 311 | color: rgba(var(--primary-400), 1); 312 | } 313 | 314 | .primary-300-color, .dark .primary-300-color { 315 | color: rgba(var(--primary-300), 1); 316 | } 317 | 318 | .primary-200-color, .dark .primary-200-color { 319 | color: rgba(var(--primary-200), 1); 320 | } 321 | 322 | .primary-100-color, .dark .primary-100-color { 323 | color: rgba(var(--primary-100), 1); 324 | } 325 | 326 | .primary-50-color, .dark .primary-50-color { 327 | color: rgba(var(--primary-50), 1); 328 | } 329 | 330 | .primary-0-color, .dark .primary-0-color { 331 | color: rgba(var(--primary-0), 1); 332 | } 333 | 334 | .accent-700-color, .dark .accent-700-color { 335 | color: rgba(var(--accent-700), 1); 336 | } 337 | 338 | .accent-600-color, .dark .accent-600-color { 339 | color: rgba(var(--accent-600), 1); 340 | } 341 | 342 | .accent-500-color, .dark .accent-500-color { 343 | color: rgba(var(--accent-500), 1); 344 | } 345 | 346 | .accent-400-color, .dark .accent-400-color { 347 | color: rgba(var(--accent-400), 1); 348 | } 349 | 350 | .accent-300-color, .dark .accent-300-color { 351 | color: rgba(var(--accent-300), 1); 352 | } 353 | 354 | .accent-200-color, .dark .accent-200-color { 355 | color: rgba(var(--accent-200), 1); 356 | } 357 | 358 | .accent-100-color, .dark .accent-100-color { 359 | color: rgba(var(--accent-100), 1); 360 | } 361 | 362 | .primary-700-color, .dark .primary-700-color { 363 | color: rgba(var(--primary-700), 1); 364 | } 365 | 366 | .primary-600-color, .dark .primary-600-color { 367 | color: rgba(var(--primary-600), 1); 368 | } 369 | 370 | .primary-500-color, .dark .primary-500-color { 371 | color: rgba(var(--primary-500), 1); 372 | } 373 | 374 | .primary-400-color, .dark .primary-400-color { 375 | color: rgba(var(--primary-400), 1); 376 | } 377 | 378 | .primary-300-color, .dark .primary-300-color { 379 | color: rgba(var(--primary-300), 1); 380 | } 381 | 382 | .primary-200-color, .dark .primary-200-color { 383 | color: rgba(var(--primary-200), 1); 384 | } 385 | 386 | .primary-100-color, .dark .primary-100-color { 387 | color: rgba(var(--primary-100), 1); 388 | } 389 | 390 | .primary-50-color, .dark .primary-50-color { 391 | color: rgba(var(--primary-50), 1); 392 | } 393 | 394 | .primary-0-color, .dark .primary-0-color { 395 | color: rgba(var(--primary-0), 1); 396 | } 397 | 398 | .accent-700-color, .dark .accent-700-color { 399 | color: rgba(var(--accent-700), 1); 400 | } 401 | 402 | .accent-600-color, .dark .accent-600-color { 403 | color: rgba(var(--accent-600), 1); 404 | } 405 | 406 | .accent-500-color, .dark .accent-500-color { 407 | color: rgba(var(--accent-500), 1); 408 | } 409 | 410 | .accent-400-color, .dark .accent-400-color { 411 | color: rgba(var(--accent-400), 1); 412 | } 413 | 414 | .accent-300-color, .dark .accent-300-color { 415 | color: rgba(var(--accent-300), 1); 416 | } 417 | 418 | .accent-200-color, .dark .accent-200-color { 419 | color: rgba(var(--accent-200), 1); 420 | } 421 | 422 | .accent-100-color, .dark .accent-100-color { 423 | color: rgba(var(--accent-100), 1); 424 | } 425 | 426 | .primary-700-stroke, .dark .primary-700-stroke { 427 | stroke: rgba(var(--primary-700), 1); 428 | } 429 | 430 | .primary-600-stroke, .dark .primary-600-stroke { 431 | stroke: rgba(var(--primary-600), 1); 432 | } 433 | 434 | .primary-500-stroke, .dark .primary-500-stroke { 435 | stroke: rgba(var(--primary-500), 1); 436 | } 437 | 438 | .primary-400-stroke, .dark .primary-400-stroke { 439 | stroke: rgba(var(--primary-400), 1); 440 | } 441 | 442 | .primary-300-stroke, .dark .primary-300-stroke { 443 | stroke: rgba(var(--primary-300), 1); 444 | } 445 | 446 | .primary-200-stroke, .dark .primary-200-stroke { 447 | stroke: rgba(var(--primary-200), 1); 448 | } 449 | 450 | .primary-100-stroke, .dark .primary-100-stroke { 451 | stroke: rgba(var(--primary-100), 1); 452 | } 453 | 454 | .primary-50-stroke, .dark .primary-50-stroke { 455 | stroke: rgba(var(--primary-50), 1); 456 | } 457 | 458 | .primary-0-stroke, .dark .primary-0-stroke { 459 | stroke: rgba(var(--primary-0), 1); 460 | } 461 | 462 | .accent-700-stroke, .dark .accent-700-stroke { 463 | stroke: rgba(var(--accent-700), 1); 464 | } 465 | 466 | .accent-600-stroke, .dark .accent-600-stroke { 467 | stroke: rgba(var(--accent-600), 1); 468 | } 469 | 470 | .accent-500-stroke, .dark .accent-500-stroke { 471 | stroke: rgba(var(--accent-500), 1); 472 | } 473 | 474 | .accent-400-stroke, .dark .accent-400-stroke { 475 | stroke: rgba(var(--accent-400), 1); 476 | } 477 | 478 | .accent-300-stroke, .dark .accent-300-stroke { 479 | stroke: rgba(var(--accent-300), 1); 480 | } 481 | 482 | .accent-200-stroke, .dark .accent-200-stroke { 483 | stroke: rgba(var(--accent-200), 1); 484 | } 485 | 486 | .accent-100-stroke, .dark .accent-100-stroke { 487 | stroke: rgba(var(--accent-100), 1); 488 | } 489 | 490 | .primary-700-stroke, .dark .primary-700-stroke { 491 | stroke: rgba(var(--primary-700), 1); 492 | } 493 | 494 | .primary-600-stroke, .dark .primary-600-stroke { 495 | stroke: rgba(var(--primary-600), 1); 496 | } 497 | 498 | .primary-500-stroke, .dark .primary-500-stroke { 499 | stroke: rgba(var(--primary-500), 1); 500 | } 501 | 502 | .primary-400-stroke, .dark .primary-400-stroke { 503 | stroke: rgba(var(--primary-400), 1); 504 | } 505 | 506 | .primary-300-stroke, .dark .primary-300-stroke { 507 | stroke: rgba(var(--primary-300), 1); 508 | } 509 | 510 | .primary-200-stroke, .dark .primary-200-stroke { 511 | stroke: rgba(var(--primary-200), 1); 512 | } 513 | 514 | .primary-100-stroke, .dark .primary-100-stroke { 515 | stroke: rgba(var(--primary-100), 1); 516 | } 517 | 518 | .primary-50-stroke, .dark .primary-50-stroke { 519 | stroke: rgba(var(--primary-50), 1); 520 | } 521 | 522 | .primary-0-stroke, .dark .primary-0-stroke { 523 | stroke: rgba(var(--primary-0), 1); 524 | } 525 | 526 | .accent-700-stroke, .dark .accent-700-stroke { 527 | stroke: rgba(var(--accent-700), 1); 528 | } 529 | 530 | .accent-600-stroke, .dark .accent-600-stroke { 531 | stroke: rgba(var(--accent-600), 1); 532 | } 533 | 534 | .accent-500-stroke, .dark .accent-500-stroke { 535 | stroke: rgba(var(--accent-500), 1); 536 | } 537 | 538 | .accent-400-stroke, .dark .accent-400-stroke { 539 | stroke: rgba(var(--accent-400), 1); 540 | } 541 | 542 | .accent-300-stroke, .dark .accent-300-stroke { 543 | stroke: rgba(var(--accent-300), 1); 544 | } 545 | 546 | .accent-200-stroke, .dark .accent-200-stroke { 547 | stroke: rgba(var(--accent-200), 1); 548 | } 549 | 550 | .accent-100-stroke, .dark .accent-100-stroke { 551 | stroke: rgba(var(--accent-100), 1); 552 | } 553 | 554 | input[type="text"], .dark input[type="text"] { 555 | color: rgba(var(--accent-400), 1); 556 | background-color: rgba(var(--primary-500), 0.5); 557 | border: 1px solid rgba(var(--primary-300), 0.5); 558 | } 559 | 560 | h1, .dark h1 { 561 | color: rgba(var(--primary-600), 1); 562 | background: linear-gradient(to right, rgba(var(--primary-100), 1), rgba(var(--accent-100), 1)); 563 | } 564 | -------------------------------------------------------------------------------- /playground/gulpfile.js: -------------------------------------------------------------------------------- 1 | const rename = require('gulp-rename'); 2 | const sass = require('@datorama/postcss-node-sass'); 3 | const postcss = require('gulp-postcss'); 4 | const gulp = require('gulp'); 5 | const { initThemify, themify } = require('../dist'); 6 | const browserSync = require('browser-sync').create(); 7 | const palette = require('./palette'); 8 | 9 | const themifyOptions = { 10 | palette, 11 | screwIE11: false, 12 | fallback: { 13 | cssPath: './dist/theme_fallback.css', 14 | dynamicPath: './dist/theme_fallback.json' 15 | } 16 | }; 17 | 18 | gulp.task('sass', () => { 19 | return gulp 20 | .src('./scss/index.theme.scss') 21 | .pipe(postcss([initThemify(themifyOptions), sass(), themify(themifyOptions)])) 22 | .pipe(rename('bundle.css')) 23 | .pipe(gulp.dest('dist')) 24 | .pipe(browserSync.stream()); 25 | }); 26 | 27 | gulp.task('serve', ['sass'], () => { 28 | browserSync.init({ 29 | server: '.', 30 | port: 8080 31 | }); 32 | 33 | gulp.watch('./scss/*.scss', ['sass']); 34 | gulp.watch('./*.html').on('change', browserSync.reload); 35 | }); 36 | 37 | gulp.task('default', ['serve']); 38 | -------------------------------------------------------------------------------- /playground/helpers.js: -------------------------------------------------------------------------------- 1 | let JSONFallbackCache; 2 | 3 | /** 4 | * 5 | * @param path 6 | */ 7 | function loadJSON(url, cb) { 8 | const req = new XMLHttpRequest(); 9 | req.overrideMimeType('application/json'); 10 | req.open('GET', url, true); 11 | req.onload = function() { 12 | cb(JSON.parse(req.responseText)); 13 | }; 14 | req.send(null); 15 | } 16 | 17 | /** 18 | * 19 | * @param path 20 | */ 21 | function loadCSS(path) { 22 | const head = document.getElementsByTagName('head')[0]; 23 | const style = document.createElement('link'); 24 | style.href = path; 25 | style.id = 'themify-ie'; 26 | style.type = 'text/css'; 27 | style.rel = 'stylesheet'; 28 | head.appendChild(style); 29 | } 30 | 31 | /** 32 | * 33 | * @param style 34 | */ 35 | function injectStyle(style) { 36 | var node = document.createElement('style'); 37 | node.id = 'themify'; 38 | node.innerHTML = style; 39 | document.head.appendChild(node); 40 | } 41 | 42 | /** 43 | * 44 | * .dark { 45 | * --primary-100: 30, 24, 33; 46 | * } 47 | * 48 | * :root { 49 | * --primary-100: 22, 21, 22; 50 | * } 51 | * 52 | * @param customTheme 53 | * @returns {string} 54 | */ 55 | function generateNewVariables(customTheme) { 56 | // First, we need the variations [dark, light] 57 | const variations = Object.keys(customTheme); 58 | return variations.reduce((finalOutput, variation) => { 59 | // Next, we need the variation keys [primary-100, accent-100] 60 | const variationKeys = Object.keys(customTheme[variation]); 61 | 62 | const variationOutput = variationKeys.reduce((acc, variable) => { 63 | const value = normalizeColor(customTheme[variation][variable]); 64 | return (acc += `--${variable}: ${value};`); 65 | }, ''); 66 | 67 | return (finalOutput += `${variation === 'light' ? ':root' : '.' + variation}{${variationOutput}}`); 68 | }, ''); 69 | } 70 | 71 | /** 72 | * 73 | * @returns {boolean} 74 | */ 75 | function hasNativeCSSProperties() { 76 | return window.CSS && window.CSS.supports && window.CSS.supports('--fake-var', 0); 77 | } 78 | 79 | /** 80 | * Load the CSS fallback file on load 81 | */ 82 | function loadCSSVariablesFallback(fallbackPath) { 83 | if (!hasNativeCSSProperties()) { 84 | loadCSS(fallbackPath); 85 | } 86 | } 87 | 88 | /** 89 | * 90 | * @param customTheme 91 | */ 92 | function replaceColors(fallbackJSONPath, customTheme) { 93 | if (customTheme) { 94 | if (hasNativeCSSProperties()) { 95 | const newColors = generateNewVariables(customTheme); 96 | injectStyle(newColors); 97 | } else { 98 | const replace = JSONFallback => { 99 | JSONFallbackCache = JSONFallback; 100 | handleUnSupportedBrowsers(customTheme, JSONFallbackCache); 101 | }; 102 | if (JSONFallbackCache) { 103 | replace(JSONFallbackCache); 104 | } else { 105 | loadJSON(fallbackJSONPath, replace); 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * 113 | * @param customTheme 114 | */ 115 | function handleUnSupportedBrowsers(customTheme, JSONFallback) { 116 | const themifyRegExp = /%\[(.*?)\]%/gi; 117 | const merged = mergeDeep(palette, customTheme); 118 | 119 | let finalOutput = Object.keys(customTheme).reduce((acc, variation) => { 120 | let value = JSONFallback[variation].replace(themifyRegExp, (occurrence, value) => { 121 | const [variation, variable, opacity] = value.replace(/\s/g, '').split(','); 122 | const color = merged[variation][variable]; 123 | const normalized = hexToRGB(color, opacity); 124 | return normalized; 125 | }); 126 | 127 | return (acc += value); 128 | }, ''); 129 | 130 | injectStyle(finalOutput); 131 | } 132 | 133 | /** 134 | * Omit the rgb and braces from rgb 135 | * rgb(235, 246, 244) => 235, 246, 244 136 | * @param rgb 137 | * @returns {string} 138 | */ 139 | function normalizeRgb(rgb) { 140 | return rgb.replace('rgb(', '').replace(')', ''); 141 | } 142 | 143 | /** 144 | * 145 | * @param color 146 | * @returns {*} 147 | */ 148 | function normalizeColor(color) { 149 | if (isHex(color)) { 150 | return normalizeRgb(hexToRGB(color)); 151 | } 152 | 153 | if (isRgb(color)) { 154 | return normalizeRgb(color); 155 | } 156 | 157 | return color; 158 | } 159 | 160 | /** 161 | * 162 | * @param color 163 | * @returns {boolean} 164 | */ 165 | function isHex(color) { 166 | return color.indexOf('#') > -1; 167 | } 168 | 169 | /** 170 | * 171 | * @param color 172 | * @returns {boolean} 173 | */ 174 | function isRgb(color) { 175 | return color.indexOf('rgb') > -1; 176 | } 177 | 178 | /** 179 | * 180 | * @param hex 181 | * @param alpha 182 | * @returns {string} 183 | */ 184 | function hexToRGB(hex, alpha = false) { 185 | const r = parseInt(hex.slice(1, 3), 16); 186 | const g = parseInt(hex.slice(3, 5), 16); 187 | const b = parseInt(hex.slice(5, 7), 16); 188 | if (alpha) { 189 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 190 | } 191 | return `rgb(${r}, ${g}, ${b})`; 192 | } 193 | 194 | var palette = { 195 | light: { 196 | 'primary-700': '#303030', 197 | 'primary-600': '#383838', 198 | 'primary-500': '#505050', 199 | 'primary-400': '#666a6b', 200 | 'primary-300': '#9ca0a0', 201 | 'primary-200': '#cccece', 202 | 'primary-100': '#f2f2f4', 203 | 'primary-50': '#f8f8f9', 204 | 'primary-0': '#ffffff', 205 | 'accent-700': '#096796', 206 | 'accent-600': '#0a87c6', 207 | 'accent-500': '#04a2d6', 208 | 'accent-400': '#00bee8', 209 | 'accent-300': '#4cd1ef', 210 | 'accent-200': '#96e1ed', 211 | 'accent-100': '#e6f9fc' 212 | }, 213 | dark: { 214 | 'primary-700': '#ffffff', 215 | 'primary-600': '#f8f8f9', 216 | 'primary-500': '#f2f2f4', 217 | 'primary-400': '#cccece', 218 | 'primary-300': '#9ca0a0', 219 | 'primary-200': '#666a6b', 220 | 'primary-100': '#505050', 221 | 'primary-50': '#383838', 222 | 'primary-0': '#303030', 223 | 'accent-700': '#e6f9fc', 224 | 'accent-600': '#96e1ed', 225 | 'accent-500': '#4cd1ef', 226 | 'accent-400': '#00bee8', 227 | 'accent-300': '#04a2d6', 228 | 'accent-200': '#0a87c6', 229 | 'accent-100': '#096796' 230 | } 231 | }; 232 | 233 | /** 234 | * 235 | * @param target 236 | * @param sources 237 | * @returns {*} 238 | */ 239 | function mergeDeep(target, ...sources) { 240 | if (!sources.length) return target; 241 | const source = sources.shift(); 242 | 243 | if (isObject(target) && isObject(source)) { 244 | for (const key in source) { 245 | if (isObject(source[key])) { 246 | if (!target[key]) Object.assign(target, { [key]: {} }); 247 | mergeDeep(target[key], source[key]); 248 | } else { 249 | Object.assign(target, { [key]: source[key] }); 250 | } 251 | } 252 | } 253 | 254 | return mergeDeep(target, ...sources); 255 | } 256 | 257 | /** 258 | * 259 | * @param item 260 | * @returns {*|boolean} 261 | */ 262 | function isObject(value) { 263 | return Object.prototype.toString.call(value) === '[object Object]'; 264 | } -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Themify playground 9 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 71 | 72 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /playground/palette.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | light: { 3 | 'primary-700': '#303030', 4 | 'primary-600': '#383838', 5 | 'primary-500': '#505050', 6 | 'primary-400': '#666a6b', 7 | 'primary-300': '#9ca0a0', 8 | 'primary-200': '#cccece', 9 | 'primary-100': '#f2f2f4', 10 | 'primary-50': '#f8f8f9', 11 | 'primary-0': '#ffffff', 12 | 'accent-700': '#096796', 13 | 'accent-600': '#0a87c6', 14 | 'accent-500': '#04a2d6', 15 | 'accent-400': '#00bee8', 16 | 'accent-300': '#4cd1ef', 17 | 'accent-200': '#96e1ed', 18 | 'accent-100': '#e6f9fc' 19 | }, 20 | dark: { 21 | 'primary-700': '#ffffff', 22 | 'primary-600': '#f8f8f9', 23 | 'primary-500': '#f2f2f4', 24 | 'primary-400': '#cccece', 25 | 'primary-300': '#9ca0a0', 26 | 'primary-200': '#666a6b', 27 | 'primary-100': '#505050', 28 | 'primary-50': '#383838', 29 | 'primary-0': '#303030', 30 | 'accent-700': '#e6f9fc', 31 | 'accent-600': '#96e1ed', 32 | 'accent-500': '#4cd1ef', 33 | 'accent-400': '#00bee8', 34 | 'accent-300': '#04a2d6', 35 | 'accent-200': '#0a87c6', 36 | 'accent-100': '#096796' 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /playground/scss/button.theme.scss: -------------------------------------------------------------------------------- 1 | input[type="text"] { 2 | color: themify(accent-400); 3 | background-color: themify(primary-500, 0.5); 4 | border: 1px solid themify(primary-300, 0.5); 5 | } -------------------------------------------------------------------------------- /playground/scss/index.theme.scss: -------------------------------------------------------------------------------- 1 | @import '../../dist/themify'; 2 | 3 | $themeRules: ( 4 | 'background-color', 5 | 'fill', 6 | 'color', 7 | 'stroke' 8 | ); 9 | @include generateThemeHelpers($themeRules); 10 | 11 | @import 'button.theme'; 12 | @import 'input.theme'; 13 | -------------------------------------------------------------------------------- /playground/scss/input.theme.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: themify(primary-600); 3 | background: linear-gradient(to right, themify(primary-100), themify(accent-100)); 4 | } 5 | 6 | //p { 7 | // color: themify((dark: primary-100, light: accent-300)); 8 | //} -------------------------------------------------------------------------------- /playground/theme_fallback.css: -------------------------------------------------------------------------------- 1 | .dark .primary-700-background-color { 2 | background-color: #ffffff 3 | }.primary-700-background-color { 4 | background-color: #303030; 5 | }.dark .primary-600-background-color { 6 | background-color: #f8f8f9 7 | }.primary-600-background-color { 8 | background-color: #383838; 9 | }.dark .primary-500-background-color { 10 | background-color: #f2f2f4 11 | }.primary-500-background-color { 12 | background-color: #505050; 13 | }.dark .primary-400-background-color { 14 | background-color: #cccece 15 | }.primary-400-background-color { 16 | background-color: #666a6b; 17 | }.dark .primary-300-background-color { 18 | background-color: #9ca0a0 19 | }.primary-300-background-color { 20 | background-color: #9ca0a0; 21 | }.dark .primary-200-background-color { 22 | background-color: #666a6b 23 | }.primary-200-background-color { 24 | background-color: #cccece; 25 | }.dark .primary-100-background-color { 26 | background-color: #505050 27 | }.primary-100-background-color { 28 | background-color: #f2f2f4; 29 | }.dark .primary-50-background-color { 30 | background-color: #383838 31 | }.primary-50-background-color { 32 | background-color: #f8f8f9; 33 | }.dark .primary-0-background-color { 34 | background-color: #303030 35 | }.primary-0-background-color { 36 | background-color: #ffffff; 37 | }.dark .accent-700-background-color { 38 | background-color: #e6f9fc 39 | }.accent-700-background-color { 40 | background-color: #096796; 41 | }.dark .accent-600-background-color { 42 | background-color: #96e1ed 43 | }.accent-600-background-color { 44 | background-color: #0a87c6; 45 | }.dark .accent-500-background-color { 46 | background-color: #4cd1ef 47 | }.accent-500-background-color { 48 | background-color: #04a2d6; 49 | }.dark .accent-400-background-color { 50 | background-color: #00bee8 51 | }.accent-400-background-color { 52 | background-color: #00bee8; 53 | }.dark .accent-300-background-color { 54 | background-color: #04a2d6 55 | }.accent-300-background-color { 56 | background-color: #4cd1ef; 57 | }.dark .accent-200-background-color { 58 | background-color: #0a87c6 59 | }.accent-200-background-color { 60 | background-color: #96e1ed; 61 | }.dark .accent-100-background-color { 62 | background-color: #096796 63 | }.accent-100-background-color { 64 | background-color: #e6f9fc; 65 | }.dark .primary-700-background-color { 66 | background-color: #ffffff 67 | }.primary-700-background-color { 68 | background-color: #303030; 69 | }.dark .primary-600-background-color { 70 | background-color: #f8f8f9 71 | }.primary-600-background-color { 72 | background-color: #383838; 73 | }.dark .primary-500-background-color { 74 | background-color: #f2f2f4 75 | }.primary-500-background-color { 76 | background-color: #505050; 77 | }.dark .primary-400-background-color { 78 | background-color: #cccece 79 | }.primary-400-background-color { 80 | background-color: #666a6b; 81 | }.dark .primary-300-background-color { 82 | background-color: #9ca0a0 83 | }.primary-300-background-color { 84 | background-color: #9ca0a0; 85 | }.dark .primary-200-background-color { 86 | background-color: #666a6b 87 | }.primary-200-background-color { 88 | background-color: #cccece; 89 | }.dark .primary-100-background-color { 90 | background-color: #505050 91 | }.primary-100-background-color { 92 | background-color: #f2f2f4; 93 | }.dark .primary-50-background-color { 94 | background-color: #383838 95 | }.primary-50-background-color { 96 | background-color: #f8f8f9; 97 | }.dark .primary-0-background-color { 98 | background-color: #303030 99 | }.primary-0-background-color { 100 | background-color: #ffffff; 101 | }.dark .accent-700-background-color { 102 | background-color: #e6f9fc 103 | }.accent-700-background-color { 104 | background-color: #096796; 105 | }.dark .accent-600-background-color { 106 | background-color: #96e1ed 107 | }.accent-600-background-color { 108 | background-color: #0a87c6; 109 | }.dark .accent-500-background-color { 110 | background-color: #4cd1ef 111 | }.accent-500-background-color { 112 | background-color: #04a2d6; 113 | }.dark .accent-400-background-color { 114 | background-color: #00bee8 115 | }.accent-400-background-color { 116 | background-color: #00bee8; 117 | }.dark .accent-300-background-color { 118 | background-color: #04a2d6 119 | }.accent-300-background-color { 120 | background-color: #4cd1ef; 121 | }.dark .accent-200-background-color { 122 | background-color: #0a87c6 123 | }.accent-200-background-color { 124 | background-color: #96e1ed; 125 | }.dark .accent-100-background-color { 126 | background-color: #096796 127 | }.accent-100-background-color { 128 | background-color: #e6f9fc; 129 | }.dark .primary-700-fill { 130 | fill: #ffffff 131 | }.primary-700-fill { 132 | fill: #303030; 133 | }.dark .primary-600-fill { 134 | fill: #f8f8f9 135 | }.primary-600-fill { 136 | fill: #383838; 137 | }.dark .primary-500-fill { 138 | fill: #f2f2f4 139 | }.primary-500-fill { 140 | fill: #505050; 141 | }.dark .primary-400-fill { 142 | fill: #cccece 143 | }.primary-400-fill { 144 | fill: #666a6b; 145 | }.dark .primary-300-fill { 146 | fill: #9ca0a0 147 | }.primary-300-fill { 148 | fill: #9ca0a0; 149 | }.dark .primary-200-fill { 150 | fill: #666a6b 151 | }.primary-200-fill { 152 | fill: #cccece; 153 | }.dark .primary-100-fill { 154 | fill: #505050 155 | }.primary-100-fill { 156 | fill: #f2f2f4; 157 | }.dark .primary-50-fill { 158 | fill: #383838 159 | }.primary-50-fill { 160 | fill: #f8f8f9; 161 | }.dark .primary-0-fill { 162 | fill: #303030 163 | }.primary-0-fill { 164 | fill: #ffffff; 165 | }.dark .accent-700-fill { 166 | fill: #e6f9fc 167 | }.accent-700-fill { 168 | fill: #096796; 169 | }.dark .accent-600-fill { 170 | fill: #96e1ed 171 | }.accent-600-fill { 172 | fill: #0a87c6; 173 | }.dark .accent-500-fill { 174 | fill: #4cd1ef 175 | }.accent-500-fill { 176 | fill: #04a2d6; 177 | }.dark .accent-400-fill { 178 | fill: #00bee8 179 | }.accent-400-fill { 180 | fill: #00bee8; 181 | }.dark .accent-300-fill { 182 | fill: #04a2d6 183 | }.accent-300-fill { 184 | fill: #4cd1ef; 185 | }.dark .accent-200-fill { 186 | fill: #0a87c6 187 | }.accent-200-fill { 188 | fill: #96e1ed; 189 | }.dark .accent-100-fill { 190 | fill: #096796 191 | }.accent-100-fill { 192 | fill: #e6f9fc; 193 | }.dark .primary-700-fill { 194 | fill: #ffffff 195 | }.primary-700-fill { 196 | fill: #303030; 197 | }.dark .primary-600-fill { 198 | fill: #f8f8f9 199 | }.primary-600-fill { 200 | fill: #383838; 201 | }.dark .primary-500-fill { 202 | fill: #f2f2f4 203 | }.primary-500-fill { 204 | fill: #505050; 205 | }.dark .primary-400-fill { 206 | fill: #cccece 207 | }.primary-400-fill { 208 | fill: #666a6b; 209 | }.dark .primary-300-fill { 210 | fill: #9ca0a0 211 | }.primary-300-fill { 212 | fill: #9ca0a0; 213 | }.dark .primary-200-fill { 214 | fill: #666a6b 215 | }.primary-200-fill { 216 | fill: #cccece; 217 | }.dark .primary-100-fill { 218 | fill: #505050 219 | }.primary-100-fill { 220 | fill: #f2f2f4; 221 | }.dark .primary-50-fill { 222 | fill: #383838 223 | }.primary-50-fill { 224 | fill: #f8f8f9; 225 | }.dark .primary-0-fill { 226 | fill: #303030 227 | }.primary-0-fill { 228 | fill: #ffffff; 229 | }.dark .accent-700-fill { 230 | fill: #e6f9fc 231 | }.accent-700-fill { 232 | fill: #096796; 233 | }.dark .accent-600-fill { 234 | fill: #96e1ed 235 | }.accent-600-fill { 236 | fill: #0a87c6; 237 | }.dark .accent-500-fill { 238 | fill: #4cd1ef 239 | }.accent-500-fill { 240 | fill: #04a2d6; 241 | }.dark .accent-400-fill { 242 | fill: #00bee8 243 | }.accent-400-fill { 244 | fill: #00bee8; 245 | }.dark .accent-300-fill { 246 | fill: #04a2d6 247 | }.accent-300-fill { 248 | fill: #4cd1ef; 249 | }.dark .accent-200-fill { 250 | fill: #0a87c6 251 | }.accent-200-fill { 252 | fill: #96e1ed; 253 | }.dark .accent-100-fill { 254 | fill: #096796 255 | }.accent-100-fill { 256 | fill: #e6f9fc; 257 | }.dark .primary-700-color { 258 | color: #ffffff 259 | }.primary-700-color { 260 | color: #303030; 261 | }.dark .primary-600-color { 262 | color: #f8f8f9 263 | }.primary-600-color { 264 | color: #383838; 265 | }.dark .primary-500-color { 266 | color: #f2f2f4 267 | }.primary-500-color { 268 | color: #505050; 269 | }.dark .primary-400-color { 270 | color: #cccece 271 | }.primary-400-color { 272 | color: #666a6b; 273 | }.dark .primary-300-color { 274 | color: #9ca0a0 275 | }.primary-300-color { 276 | color: #9ca0a0; 277 | }.dark .primary-200-color { 278 | color: #666a6b 279 | }.primary-200-color { 280 | color: #cccece; 281 | }.dark .primary-100-color { 282 | color: #505050 283 | }.primary-100-color { 284 | color: #f2f2f4; 285 | }.dark .primary-50-color { 286 | color: #383838 287 | }.primary-50-color { 288 | color: #f8f8f9; 289 | }.dark .primary-0-color { 290 | color: #303030 291 | }.primary-0-color { 292 | color: #ffffff; 293 | }.dark .accent-700-color { 294 | color: #e6f9fc 295 | }.accent-700-color { 296 | color: #096796; 297 | }.dark .accent-600-color { 298 | color: #96e1ed 299 | }.accent-600-color { 300 | color: #0a87c6; 301 | }.dark .accent-500-color { 302 | color: #4cd1ef 303 | }.accent-500-color { 304 | color: #04a2d6; 305 | }.dark .accent-400-color { 306 | color: #00bee8 307 | }.accent-400-color { 308 | color: #00bee8; 309 | }.dark .accent-300-color { 310 | color: #04a2d6 311 | }.accent-300-color { 312 | color: #4cd1ef; 313 | }.dark .accent-200-color { 314 | color: #0a87c6 315 | }.accent-200-color { 316 | color: #96e1ed; 317 | }.dark .accent-100-color { 318 | color: #096796 319 | }.accent-100-color { 320 | color: #e6f9fc; 321 | }.dark .primary-700-color { 322 | color: #ffffff 323 | }.primary-700-color { 324 | color: #303030; 325 | }.dark .primary-600-color { 326 | color: #f8f8f9 327 | }.primary-600-color { 328 | color: #383838; 329 | }.dark .primary-500-color { 330 | color: #f2f2f4 331 | }.primary-500-color { 332 | color: #505050; 333 | }.dark .primary-400-color { 334 | color: #cccece 335 | }.primary-400-color { 336 | color: #666a6b; 337 | }.dark .primary-300-color { 338 | color: #9ca0a0 339 | }.primary-300-color { 340 | color: #9ca0a0; 341 | }.dark .primary-200-color { 342 | color: #666a6b 343 | }.primary-200-color { 344 | color: #cccece; 345 | }.dark .primary-100-color { 346 | color: #505050 347 | }.primary-100-color { 348 | color: #f2f2f4; 349 | }.dark .primary-50-color { 350 | color: #383838 351 | }.primary-50-color { 352 | color: #f8f8f9; 353 | }.dark .primary-0-color { 354 | color: #303030 355 | }.primary-0-color { 356 | color: #ffffff; 357 | }.dark .accent-700-color { 358 | color: #e6f9fc 359 | }.accent-700-color { 360 | color: #096796; 361 | }.dark .accent-600-color { 362 | color: #96e1ed 363 | }.accent-600-color { 364 | color: #0a87c6; 365 | }.dark .accent-500-color { 366 | color: #4cd1ef 367 | }.accent-500-color { 368 | color: #04a2d6; 369 | }.dark .accent-400-color { 370 | color: #00bee8 371 | }.accent-400-color { 372 | color: #00bee8; 373 | }.dark .accent-300-color { 374 | color: #04a2d6 375 | }.accent-300-color { 376 | color: #4cd1ef; 377 | }.dark .accent-200-color { 378 | color: #0a87c6 379 | }.accent-200-color { 380 | color: #96e1ed; 381 | }.dark .accent-100-color { 382 | color: #096796 383 | }.accent-100-color { 384 | color: #e6f9fc; 385 | }.dark .primary-700-stroke { 386 | stroke: #ffffff 387 | }.primary-700-stroke { 388 | stroke: #303030; 389 | }.dark .primary-600-stroke { 390 | stroke: #f8f8f9 391 | }.primary-600-stroke { 392 | stroke: #383838; 393 | }.dark .primary-500-stroke { 394 | stroke: #f2f2f4 395 | }.primary-500-stroke { 396 | stroke: #505050; 397 | }.dark .primary-400-stroke { 398 | stroke: #cccece 399 | }.primary-400-stroke { 400 | stroke: #666a6b; 401 | }.dark .primary-300-stroke { 402 | stroke: #9ca0a0 403 | }.primary-300-stroke { 404 | stroke: #9ca0a0; 405 | }.dark .primary-200-stroke { 406 | stroke: #666a6b 407 | }.primary-200-stroke { 408 | stroke: #cccece; 409 | }.dark .primary-100-stroke { 410 | stroke: #505050 411 | }.primary-100-stroke { 412 | stroke: #f2f2f4; 413 | }.dark .primary-50-stroke { 414 | stroke: #383838 415 | }.primary-50-stroke { 416 | stroke: #f8f8f9; 417 | }.dark .primary-0-stroke { 418 | stroke: #303030 419 | }.primary-0-stroke { 420 | stroke: #ffffff; 421 | }.dark .accent-700-stroke { 422 | stroke: #e6f9fc 423 | }.accent-700-stroke { 424 | stroke: #096796; 425 | }.dark .accent-600-stroke { 426 | stroke: #96e1ed 427 | }.accent-600-stroke { 428 | stroke: #0a87c6; 429 | }.dark .accent-500-stroke { 430 | stroke: #4cd1ef 431 | }.accent-500-stroke { 432 | stroke: #04a2d6; 433 | }.dark .accent-400-stroke { 434 | stroke: #00bee8 435 | }.accent-400-stroke { 436 | stroke: #00bee8; 437 | }.dark .accent-300-stroke { 438 | stroke: #04a2d6 439 | }.accent-300-stroke { 440 | stroke: #4cd1ef; 441 | }.dark .accent-200-stroke { 442 | stroke: #0a87c6 443 | }.accent-200-stroke { 444 | stroke: #96e1ed; 445 | }.dark .accent-100-stroke { 446 | stroke: #096796 447 | }.accent-100-stroke { 448 | stroke: #e6f9fc; 449 | }.dark .primary-700-stroke { 450 | stroke: #ffffff 451 | }.primary-700-stroke { 452 | stroke: #303030; 453 | }.dark .primary-600-stroke { 454 | stroke: #f8f8f9 455 | }.primary-600-stroke { 456 | stroke: #383838; 457 | }.dark .primary-500-stroke { 458 | stroke: #f2f2f4 459 | }.primary-500-stroke { 460 | stroke: #505050; 461 | }.dark .primary-400-stroke { 462 | stroke: #cccece 463 | }.primary-400-stroke { 464 | stroke: #666a6b; 465 | }.dark .primary-300-stroke { 466 | stroke: #9ca0a0 467 | }.primary-300-stroke { 468 | stroke: #9ca0a0; 469 | }.dark .primary-200-stroke { 470 | stroke: #666a6b 471 | }.primary-200-stroke { 472 | stroke: #cccece; 473 | }.dark .primary-100-stroke { 474 | stroke: #505050 475 | }.primary-100-stroke { 476 | stroke: #f2f2f4; 477 | }.dark .primary-50-stroke { 478 | stroke: #383838 479 | }.primary-50-stroke { 480 | stroke: #f8f8f9; 481 | }.dark .primary-0-stroke { 482 | stroke: #303030 483 | }.primary-0-stroke { 484 | stroke: #ffffff; 485 | }.dark .accent-700-stroke { 486 | stroke: #e6f9fc 487 | }.accent-700-stroke { 488 | stroke: #096796; 489 | }.dark .accent-600-stroke { 490 | stroke: #96e1ed 491 | }.accent-600-stroke { 492 | stroke: #0a87c6; 493 | }.dark .accent-500-stroke { 494 | stroke: #4cd1ef 495 | }.accent-500-stroke { 496 | stroke: #04a2d6; 497 | }.dark .accent-400-stroke { 498 | stroke: #00bee8 499 | }.accent-400-stroke { 500 | stroke: #00bee8; 501 | }.dark .accent-300-stroke { 502 | stroke: #04a2d6 503 | }.accent-300-stroke { 504 | stroke: #4cd1ef; 505 | }.dark .accent-200-stroke { 506 | stroke: #0a87c6 507 | }.accent-200-stroke { 508 | stroke: #96e1ed; 509 | }.dark .accent-100-stroke { 510 | stroke: #096796 511 | }.accent-100-stroke { 512 | stroke: #e6f9fc; 513 | }.dark input[type="text"] { 514 | color: #00bee8; 515 | background-color: rgba(242, 242, 244, 0.5); 516 | border: 1px solid rgba(156, 160, 160, 0.5) 517 | }input[type="text"] { 518 | color: #00bee8; 519 | background-color: rgba(80, 80, 80, 0.5); 520 | border: 1px solid rgba(156, 160, 160, 0.5); 521 | }.dark h1 { 522 | color: #f8f8f9; 523 | background: linear-gradient(to right, #505050, #096796) 524 | }h1 { 525 | color: #383838; 526 | background: linear-gradient(to right, #f2f2f4, #e6f9fc); 527 | } -------------------------------------------------------------------------------- /playground/theme_fallback.json: -------------------------------------------------------------------------------- 1 | {"dark":".dark .primary-700-background-color{background-color:%[dark,primary-700,1]%}.dark .primary-600-background-color{background-color:%[dark,primary-600,1]%}.dark .primary-500-background-color{background-color:%[dark,primary-500,1]%}.dark .primary-400-background-color{background-color:%[dark,primary-400,1]%}.dark .primary-300-background-color{background-color:%[dark,primary-300,1]%}.dark .primary-200-background-color{background-color:%[dark,primary-200,1]%}.dark .primary-100-background-color{background-color:%[dark,primary-100,1]%}.dark .primary-50-background-color{background-color:%[dark,primary-50,1]%}.dark .primary-0-background-color{background-color:%[dark,primary-0,1]%}.dark .accent-700-background-color{background-color:%[dark,accent-700,1]%}.dark .accent-600-background-color{background-color:%[dark,accent-600,1]%}.dark .accent-500-background-color{background-color:%[dark,accent-500,1]%}.dark .accent-400-background-color{background-color:%[dark,accent-400,1]%}.dark .accent-300-background-color{background-color:%[dark,accent-300,1]%}.dark .accent-200-background-color{background-color:%[dark,accent-200,1]%}.dark .accent-100-background-color{background-color:%[dark,accent-100,1]%}.dark .primary-700-background-color{background-color:%[dark,primary-700,1]%}.dark .primary-600-background-color{background-color:%[dark,primary-600,1]%}.dark .primary-500-background-color{background-color:%[dark,primary-500,1]%}.dark .primary-400-background-color{background-color:%[dark,primary-400,1]%}.dark .primary-300-background-color{background-color:%[dark,primary-300,1]%}.dark .primary-200-background-color{background-color:%[dark,primary-200,1]%}.dark .primary-100-background-color{background-color:%[dark,primary-100,1]%}.dark .primary-50-background-color{background-color:%[dark,primary-50,1]%}.dark .primary-0-background-color{background-color:%[dark,primary-0,1]%}.dark .accent-700-background-color{background-color:%[dark,accent-700,1]%}.dark .accent-600-background-color{background-color:%[dark,accent-600,1]%}.dark .accent-500-background-color{background-color:%[dark,accent-500,1]%}.dark .accent-400-background-color{background-color:%[dark,accent-400,1]%}.dark .accent-300-background-color{background-color:%[dark,accent-300,1]%}.dark .accent-200-background-color{background-color:%[dark,accent-200,1]%}.dark .accent-100-background-color{background-color:%[dark,accent-100,1]%}.dark .primary-700-fill{fill:%[dark,primary-700,1]%}.dark .primary-600-fill{fill:%[dark,primary-600,1]%}.dark .primary-500-fill{fill:%[dark,primary-500,1]%}.dark .primary-400-fill{fill:%[dark,primary-400,1]%}.dark .primary-300-fill{fill:%[dark,primary-300,1]%}.dark .primary-200-fill{fill:%[dark,primary-200,1]%}.dark .primary-100-fill{fill:%[dark,primary-100,1]%}.dark .primary-50-fill{fill:%[dark,primary-50,1]%}.dark .primary-0-fill{fill:%[dark,primary-0,1]%}.dark .accent-700-fill{fill:%[dark,accent-700,1]%}.dark .accent-600-fill{fill:%[dark,accent-600,1]%}.dark .accent-500-fill{fill:%[dark,accent-500,1]%}.dark .accent-400-fill{fill:%[dark,accent-400,1]%}.dark .accent-300-fill{fill:%[dark,accent-300,1]%}.dark .accent-200-fill{fill:%[dark,accent-200,1]%}.dark .accent-100-fill{fill:%[dark,accent-100,1]%}.dark .primary-700-fill{fill:%[dark,primary-700,1]%}.dark .primary-600-fill{fill:%[dark,primary-600,1]%}.dark .primary-500-fill{fill:%[dark,primary-500,1]%}.dark .primary-400-fill{fill:%[dark,primary-400,1]%}.dark .primary-300-fill{fill:%[dark,primary-300,1]%}.dark .primary-200-fill{fill:%[dark,primary-200,1]%}.dark .primary-100-fill{fill:%[dark,primary-100,1]%}.dark .primary-50-fill{fill:%[dark,primary-50,1]%}.dark .primary-0-fill{fill:%[dark,primary-0,1]%}.dark .accent-700-fill{fill:%[dark,accent-700,1]%}.dark .accent-600-fill{fill:%[dark,accent-600,1]%}.dark .accent-500-fill{fill:%[dark,accent-500,1]%}.dark .accent-400-fill{fill:%[dark,accent-400,1]%}.dark .accent-300-fill{fill:%[dark,accent-300,1]%}.dark .accent-200-fill{fill:%[dark,accent-200,1]%}.dark .accent-100-fill{fill:%[dark,accent-100,1]%}.dark .primary-700-color{color:%[dark,primary-700,1]%}.dark .primary-600-color{color:%[dark,primary-600,1]%}.dark .primary-500-color{color:%[dark,primary-500,1]%}.dark .primary-400-color{color:%[dark,primary-400,1]%}.dark .primary-300-color{color:%[dark,primary-300,1]%}.dark .primary-200-color{color:%[dark,primary-200,1]%}.dark .primary-100-color{color:%[dark,primary-100,1]%}.dark .primary-50-color{color:%[dark,primary-50,1]%}.dark .primary-0-color{color:%[dark,primary-0,1]%}.dark .accent-700-color{color:%[dark,accent-700,1]%}.dark .accent-600-color{color:%[dark,accent-600,1]%}.dark .accent-500-color{color:%[dark,accent-500,1]%}.dark .accent-400-color{color:%[dark,accent-400,1]%}.dark .accent-300-color{color:%[dark,accent-300,1]%}.dark .accent-200-color{color:%[dark,accent-200,1]%}.dark .accent-100-color{color:%[dark,accent-100,1]%}.dark .primary-700-color{color:%[dark,primary-700,1]%}.dark .primary-600-color{color:%[dark,primary-600,1]%}.dark .primary-500-color{color:%[dark,primary-500,1]%}.dark .primary-400-color{color:%[dark,primary-400,1]%}.dark .primary-300-color{color:%[dark,primary-300,1]%}.dark .primary-200-color{color:%[dark,primary-200,1]%}.dark .primary-100-color{color:%[dark,primary-100,1]%}.dark .primary-50-color{color:%[dark,primary-50,1]%}.dark .primary-0-color{color:%[dark,primary-0,1]%}.dark .accent-700-color{color:%[dark,accent-700,1]%}.dark .accent-600-color{color:%[dark,accent-600,1]%}.dark .accent-500-color{color:%[dark,accent-500,1]%}.dark .accent-400-color{color:%[dark,accent-400,1]%}.dark .accent-300-color{color:%[dark,accent-300,1]%}.dark .accent-200-color{color:%[dark,accent-200,1]%}.dark .accent-100-color{color:%[dark,accent-100,1]%}.dark .primary-700-stroke{stroke:%[dark,primary-700,1]%}.dark .primary-600-stroke{stroke:%[dark,primary-600,1]%}.dark .primary-500-stroke{stroke:%[dark,primary-500,1]%}.dark .primary-400-stroke{stroke:%[dark,primary-400,1]%}.dark .primary-300-stroke{stroke:%[dark,primary-300,1]%}.dark .primary-200-stroke{stroke:%[dark,primary-200,1]%}.dark .primary-100-stroke{stroke:%[dark,primary-100,1]%}.dark .primary-50-stroke{stroke:%[dark,primary-50,1]%}.dark .primary-0-stroke{stroke:%[dark,primary-0,1]%}.dark .accent-700-stroke{stroke:%[dark,accent-700,1]%}.dark .accent-600-stroke{stroke:%[dark,accent-600,1]%}.dark .accent-500-stroke{stroke:%[dark,accent-500,1]%}.dark .accent-400-stroke{stroke:%[dark,accent-400,1]%}.dark .accent-300-stroke{stroke:%[dark,accent-300,1]%}.dark .accent-200-stroke{stroke:%[dark,accent-200,1]%}.dark .accent-100-stroke{stroke:%[dark,accent-100,1]%}.dark .primary-700-stroke{stroke:%[dark,primary-700,1]%}.dark .primary-600-stroke{stroke:%[dark,primary-600,1]%}.dark .primary-500-stroke{stroke:%[dark,primary-500,1]%}.dark .primary-400-stroke{stroke:%[dark,primary-400,1]%}.dark .primary-300-stroke{stroke:%[dark,primary-300,1]%}.dark .primary-200-stroke{stroke:%[dark,primary-200,1]%}.dark .primary-100-stroke{stroke:%[dark,primary-100,1]%}.dark .primary-50-stroke{stroke:%[dark,primary-50,1]%}.dark .primary-0-stroke{stroke:%[dark,primary-0,1]%}.dark .accent-700-stroke{stroke:%[dark,accent-700,1]%}.dark .accent-600-stroke{stroke:%[dark,accent-600,1]%}.dark .accent-500-stroke{stroke:%[dark,accent-500,1]%}.dark .accent-400-stroke{stroke:%[dark,accent-400,1]%}.dark .accent-300-stroke{stroke:%[dark,accent-300,1]%}.dark .accent-200-stroke{stroke:%[dark,accent-200,1]%}.dark .accent-100-stroke{stroke:%[dark,accent-100,1]%}.dark input[type=text]{color:%[dark,accent-400,1]%;background-color:%[dark,primary-500,.5]%;border:1px solid %[dark,primary-300,.5]%}.dark h1{color:%[dark,primary-600,1]%;background:linear-gradient(to right,%[dark,primary-100,1]%,%[dark,accent-100,1]%)}","light":".primary-700-background-color{background-color:%[light,primary-700,1]%}.primary-600-background-color{background-color:%[light,primary-600,1]%}.primary-500-background-color{background-color:%[light,primary-500,1]%}.primary-400-background-color{background-color:%[light,primary-400,1]%}.primary-300-background-color{background-color:%[light,primary-300,1]%}.primary-200-background-color{background-color:%[light,primary-200,1]%}.primary-100-background-color{background-color:%[light,primary-100,1]%}.primary-50-background-color{background-color:%[light,primary-50,1]%}.primary-0-background-color{background-color:%[light,primary-0,1]%}.accent-700-background-color{background-color:%[light,accent-700,1]%}.accent-600-background-color{background-color:%[light,accent-600,1]%}.accent-500-background-color{background-color:%[light,accent-500,1]%}.accent-400-background-color{background-color:%[light,accent-400,1]%}.accent-300-background-color{background-color:%[light,accent-300,1]%}.accent-200-background-color{background-color:%[light,accent-200,1]%}.accent-100-background-color{background-color:%[light,accent-100,1]%}.primary-700-background-color{background-color:%[light,primary-700,1]%}.primary-600-background-color{background-color:%[light,primary-600,1]%}.primary-500-background-color{background-color:%[light,primary-500,1]%}.primary-400-background-color{background-color:%[light,primary-400,1]%}.primary-300-background-color{background-color:%[light,primary-300,1]%}.primary-200-background-color{background-color:%[light,primary-200,1]%}.primary-100-background-color{background-color:%[light,primary-100,1]%}.primary-50-background-color{background-color:%[light,primary-50,1]%}.primary-0-background-color{background-color:%[light,primary-0,1]%}.accent-700-background-color{background-color:%[light,accent-700,1]%}.accent-600-background-color{background-color:%[light,accent-600,1]%}.accent-500-background-color{background-color:%[light,accent-500,1]%}.accent-400-background-color{background-color:%[light,accent-400,1]%}.accent-300-background-color{background-color:%[light,accent-300,1]%}.accent-200-background-color{background-color:%[light,accent-200,1]%}.accent-100-background-color{background-color:%[light,accent-100,1]%}.primary-700-fill{fill:%[light,primary-700,1]%}.primary-600-fill{fill:%[light,primary-600,1]%}.primary-500-fill{fill:%[light,primary-500,1]%}.primary-400-fill{fill:%[light,primary-400,1]%}.primary-300-fill{fill:%[light,primary-300,1]%}.primary-200-fill{fill:%[light,primary-200,1]%}.primary-100-fill{fill:%[light,primary-100,1]%}.primary-50-fill{fill:%[light,primary-50,1]%}.primary-0-fill{fill:%[light,primary-0,1]%}.accent-700-fill{fill:%[light,accent-700,1]%}.accent-600-fill{fill:%[light,accent-600,1]%}.accent-500-fill{fill:%[light,accent-500,1]%}.accent-400-fill{fill:%[light,accent-400,1]%}.accent-300-fill{fill:%[light,accent-300,1]%}.accent-200-fill{fill:%[light,accent-200,1]%}.accent-100-fill{fill:%[light,accent-100,1]%}.primary-700-fill{fill:%[light,primary-700,1]%}.primary-600-fill{fill:%[light,primary-600,1]%}.primary-500-fill{fill:%[light,primary-500,1]%}.primary-400-fill{fill:%[light,primary-400,1]%}.primary-300-fill{fill:%[light,primary-300,1]%}.primary-200-fill{fill:%[light,primary-200,1]%}.primary-100-fill{fill:%[light,primary-100,1]%}.primary-50-fill{fill:%[light,primary-50,1]%}.primary-0-fill{fill:%[light,primary-0,1]%}.accent-700-fill{fill:%[light,accent-700,1]%}.accent-600-fill{fill:%[light,accent-600,1]%}.accent-500-fill{fill:%[light,accent-500,1]%}.accent-400-fill{fill:%[light,accent-400,1]%}.accent-300-fill{fill:%[light,accent-300,1]%}.accent-200-fill{fill:%[light,accent-200,1]%}.accent-100-fill{fill:%[light,accent-100,1]%}.primary-700-color{color:%[light,primary-700,1]%}.primary-600-color{color:%[light,primary-600,1]%}.primary-500-color{color:%[light,primary-500,1]%}.primary-400-color{color:%[light,primary-400,1]%}.primary-300-color{color:%[light,primary-300,1]%}.primary-200-color{color:%[light,primary-200,1]%}.primary-100-color{color:%[light,primary-100,1]%}.primary-50-color{color:%[light,primary-50,1]%}.primary-0-color{color:%[light,primary-0,1]%}.accent-700-color{color:%[light,accent-700,1]%}.accent-600-color{color:%[light,accent-600,1]%}.accent-500-color{color:%[light,accent-500,1]%}.accent-400-color{color:%[light,accent-400,1]%}.accent-300-color{color:%[light,accent-300,1]%}.accent-200-color{color:%[light,accent-200,1]%}.accent-100-color{color:%[light,accent-100,1]%}.primary-700-color{color:%[light,primary-700,1]%}.primary-600-color{color:%[light,primary-600,1]%}.primary-500-color{color:%[light,primary-500,1]%}.primary-400-color{color:%[light,primary-400,1]%}.primary-300-color{color:%[light,primary-300,1]%}.primary-200-color{color:%[light,primary-200,1]%}.primary-100-color{color:%[light,primary-100,1]%}.primary-50-color{color:%[light,primary-50,1]%}.primary-0-color{color:%[light,primary-0,1]%}.accent-700-color{color:%[light,accent-700,1]%}.accent-600-color{color:%[light,accent-600,1]%}.accent-500-color{color:%[light,accent-500,1]%}.accent-400-color{color:%[light,accent-400,1]%}.accent-300-color{color:%[light,accent-300,1]%}.accent-200-color{color:%[light,accent-200,1]%}.accent-100-color{color:%[light,accent-100,1]%}.primary-700-stroke{stroke:%[light,primary-700,1]%}.primary-600-stroke{stroke:%[light,primary-600,1]%}.primary-500-stroke{stroke:%[light,primary-500,1]%}.primary-400-stroke{stroke:%[light,primary-400,1]%}.primary-300-stroke{stroke:%[light,primary-300,1]%}.primary-200-stroke{stroke:%[light,primary-200,1]%}.primary-100-stroke{stroke:%[light,primary-100,1]%}.primary-50-stroke{stroke:%[light,primary-50,1]%}.primary-0-stroke{stroke:%[light,primary-0,1]%}.accent-700-stroke{stroke:%[light,accent-700,1]%}.accent-600-stroke{stroke:%[light,accent-600,1]%}.accent-500-stroke{stroke:%[light,accent-500,1]%}.accent-400-stroke{stroke:%[light,accent-400,1]%}.accent-300-stroke{stroke:%[light,accent-300,1]%}.accent-200-stroke{stroke:%[light,accent-200,1]%}.accent-100-stroke{stroke:%[light,accent-100,1]%}.primary-700-stroke{stroke:%[light,primary-700,1]%}.primary-600-stroke{stroke:%[light,primary-600,1]%}.primary-500-stroke{stroke:%[light,primary-500,1]%}.primary-400-stroke{stroke:%[light,primary-400,1]%}.primary-300-stroke{stroke:%[light,primary-300,1]%}.primary-200-stroke{stroke:%[light,primary-200,1]%}.primary-100-stroke{stroke:%[light,primary-100,1]%}.primary-50-stroke{stroke:%[light,primary-50,1]%}.primary-0-stroke{stroke:%[light,primary-0,1]%}.accent-700-stroke{stroke:%[light,accent-700,1]%}.accent-600-stroke{stroke:%[light,accent-600,1]%}.accent-500-stroke{stroke:%[light,accent-500,1]%}.accent-400-stroke{stroke:%[light,accent-400,1]%}.accent-300-stroke{stroke:%[light,accent-300,1]%}.accent-200-stroke{stroke:%[light,accent-200,1]%}.accent-100-stroke{stroke:%[light,accent-100,1]%}input[type=text]{color:%[light,accent-400,1]%;background-color:%[light,primary-500,.5]%;border:1px solid %[light,primary-300,.5]%}h1{color:%[light,primary-600,1]%;background:linear-gradient(to right,%[light,primary-100,1]%,%[light,accent-100,1]%)}"} -------------------------------------------------------------------------------- /playground/whitelabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "dark": { 3 | "primary-100": "#ebf6f4" 4 | }, 5 | "light": { 6 | "primary-100": "#f3f3f3" 7 | } 8 | } -------------------------------------------------------------------------------- /src/helpers/css.util.ts: -------------------------------------------------------------------------------- 1 | const _cleanCSS = require('clean-css'); 2 | const cleanCSS = new _cleanCSS({}); 3 | 4 | export function minifyCSS(css): string { 5 | return cleanCSS.minify(css).styles; 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/js-sass.ts: -------------------------------------------------------------------------------- 1 | const { isArray } = Array; 2 | 3 | /** 4 | * Convert a JS object to SASS 5 | * Credits to https://github.com/acdlite/json-sass 6 | * @example {color: 'red'} -> (color: red) 7 | * @param jsValue 8 | */ 9 | function JSToSASS(jsValue) { 10 | function _JSToSASS(value, initialIndentLevel = 0) { 11 | let indentLevel = initialIndentLevel; 12 | 13 | switch (typeof value) { 14 | case 'boolean': 15 | case 'number': 16 | return value.toString(); 17 | case 'string': 18 | return value; 19 | case 'object': 20 | if (isObject(value)) { 21 | indentLevel += 1; 22 | const indent = indentsToSpaces(indentLevel); 23 | 24 | const jsObj = value; 25 | let sassKeyValPairs: string[] = []; 26 | 27 | sassKeyValPairs = Object.keys(jsObj).reduce( 28 | (result, key: string) => { 29 | const jsVal = jsObj[key]; 30 | const sassVal: string = _JSToSASS(jsVal, indentLevel); 31 | 32 | if (isNotUndefined(sassVal)) { 33 | result.push(`${key}: ${sassVal}`); 34 | } 35 | 36 | return result; 37 | }, 38 | [] as string[] 39 | ); 40 | 41 | const result = `(\n${indent + sassKeyValPairs.join(',\n' + indent)}\n${indentsToSpaces(indentLevel - 1)})`; 42 | indentLevel -= 1; 43 | return result; 44 | } else if (isArray(value)) { 45 | const sassVals: string[] = []; 46 | 47 | for (let i = 0; i < value.length; i++) { 48 | const v = value[i]; 49 | if (isNotUndefined(v)) { 50 | sassVals.push(_JSToSASS(v, indentLevel)); 51 | } 52 | } 53 | 54 | return '(' + sassVals.join(', ') + ')'; 55 | } else if (isNull(value)) { 56 | return 'null'; 57 | } else { 58 | return value.toString(); 59 | } 60 | default: 61 | return; 62 | } 63 | } 64 | 65 | return _JSToSASS(jsValue); 66 | } 67 | 68 | function indentsToSpaces(indentCount: number) { 69 | return Array(indentCount + 1).join(' '); 70 | } 71 | 72 | function isObject(value) { 73 | return Object.prototype.toString.call(value) === '[object Object]'; 74 | } 75 | 76 | function isNull(value) { 77 | return value === null; 78 | } 79 | 80 | function isNotUndefined(value) { 81 | return typeof value !== 'undefined'; 82 | } 83 | 84 | module.exports = JSToSASS; 85 | -------------------------------------------------------------------------------- /src/helpers/json.util.ts: -------------------------------------------------------------------------------- 1 | const removeNewLineRegex = /(\r\n|\n|\r)/gm; 2 | 3 | /** 4 | * Minify the JSON 5 | * @param jsonStr 6 | * @return {any | string | void} 7 | */ 8 | export function minifyJSON(jsonStr) { 9 | return jsonStr && jsonStr.trim().replace(removeNewLineRegex, ''); 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { minifyJSON } from './helpers/json.util'; 2 | import { minifyCSS } from './helpers/css.util'; 3 | 4 | const postcss = require('postcss'); 5 | const fs = require('fs-extra'); 6 | const THEMIFY = 'themify'; 7 | const JSToSass = require('./helpers/js-sass'); 8 | 9 | export interface ThemifyOptions { 10 | /** 11 | * Whether we would like to generate the CSS variables. 12 | * This should be true, unless you want to inject them yourself. 13 | */ 14 | createVars: boolean; 15 | 16 | /** 17 | * Palette configuration 18 | */ 19 | palette: any; 20 | 21 | /** 22 | * A class prefix to append to the generated themes classes 23 | */ 24 | classPrefix: string; 25 | 26 | /** 27 | * Whether to generate a fallback for legacy browsers (ahm..ahm..) that do not supports CSS Variables 28 | */ 29 | screwIE11: boolean; 30 | 31 | /** 32 | * Legacy browser fallback 33 | */ 34 | fallback: { 35 | /** 36 | * An absolute path to the fallback CSS. 37 | */ 38 | cssPath: string | null; 39 | 40 | /** 41 | * An absolute path to the fallback JSON. 42 | * This file contains variable that will be replace in runtime, for legacy browsers 43 | */ 44 | dynamicPath: string | null; 45 | }; 46 | } 47 | 48 | const defaultOptions: ThemifyOptions = { 49 | createVars: true, 50 | palette: {}, 51 | classPrefix: '', 52 | screwIE11: true, 53 | fallback: { 54 | cssPath: null, 55 | dynamicPath: null 56 | } 57 | }; 58 | 59 | /** supported color variations */ 60 | const ColorVariation = { 61 | DARK: 'dark', 62 | LIGHT: 'light' 63 | }; 64 | 65 | function buildOptions(options: ThemifyOptions) { 66 | if (!options) { 67 | throw new Error(`options is required.`); 68 | } 69 | 70 | // make sure we have a palette 71 | if (!options.palette) { 72 | throw new Error(`The 'palette' option is required.`); 73 | } 74 | 75 | return { ...defaultOptions, ...options }; 76 | } 77 | 78 | /** 79 | * 80 | * @param {string} filePath 81 | * @param {string} output 82 | * @returns {Promise} 83 | */ 84 | function writeToFile(filePath: string, output: string) { 85 | return fs.outputFile(filePath, output); 86 | } 87 | 88 | /** 89 | * Get the rgba as 88, 88, 33 instead rgba(88, 88, 33, 1) 90 | * @param value 91 | */ 92 | function getRgbaNumbers(value: string) { 93 | return hexToRgba(value) 94 | .replace('rgba(', '') 95 | .replace(', 1)', ''); 96 | } 97 | 98 | /** Define the default variation */ 99 | const defaultVariation = ColorVariation.LIGHT; 100 | /** An array of variation values */ 101 | const variationValues: string[] = (Object as any).values(ColorVariation); 102 | /** An array of all non-default variations */ 103 | const nonDefaultVariations: string[] = variationValues.filter(v => v !== defaultVariation); 104 | 105 | function themify(options: ThemifyOptions) { 106 | /** Regex to get the value inside the themify parenthesis */ 107 | const themifyRegExp = /themify\(([^)]+)\)/gi; 108 | 109 | /** 110 | * Define the method of color execution 111 | */ 112 | const enum ExecutionMode { 113 | CSS_VAR = 'CSS_VAR', 114 | CSS_COLOR = 'CSS_COLOR', 115 | DYNAMIC_EXPRESSION = 'DYNAMIC_EXPRESSION' 116 | } 117 | 118 | options = buildOptions(options); 119 | 120 | return root => { 121 | // process fallback CSS, without mutating the rules 122 | if (options.screwIE11 === false) { 123 | processFallbackRules(root); 124 | } 125 | 126 | // mutate the existing rules 127 | processRules(root); 128 | }; 129 | 130 | /** 131 | * @example themify({"light": ["primary-0", 0.5], "dark": "primary-700"}) 132 | * @example themify({"light": "primary-0", "dark": "primary-700"}) 133 | * @example linear-gradient(themify({"color": "primary-200", "opacity": "1"}), themify({"color": "primary-300", "opacity": "1"})) 134 | * @example themify({"light": ["primary-100", "1"], "dark": ["primary-100", "1"]}) 135 | * @example 1px solid themify({"light": ["primary-200", "1"], "dark": ["primary-200", "1"]}) 136 | */ 137 | function getThemifyValue(propertyValue: string, execMode: ExecutionMode): { [variation: string]: string } { 138 | /** Remove the start and end ticks **/ 139 | propertyValue = propertyValue.replace(/'/g, ''); 140 | const colorVariations = {}; 141 | 142 | function normalize(value, variationName) { 143 | let parsedValue; 144 | try { 145 | parsedValue = JSON.parse(value); 146 | } catch (ex) { 147 | throw new Error(`fail to parse the following expression: ${value}.`); 148 | } 149 | 150 | const currentValue = parsedValue[variationName]; 151 | 152 | /** For example: background-color: themify((light: primary-100)); */ 153 | if (!currentValue) { 154 | throw new Error(`${value} has one variation.`); 155 | } 156 | 157 | // convert to array 158 | if (!Array.isArray(currentValue)) { 159 | // color, alpha 160 | parsedValue[variationName] = [currentValue, 1]; 161 | } else if (!currentValue.length || !currentValue[0]) { 162 | throw new Error('Oops. Received an empty color!'); 163 | } 164 | 165 | if (options.palette) return parsedValue[variationName]; 166 | } 167 | 168 | // iterate through all variations 169 | variationValues.forEach(variationName => { 170 | // replace all 'themify' tokens with the right string 171 | colorVariations[variationName] = propertyValue.replace(themifyRegExp, (occurrence, value) => { 172 | // parse and normalize the color 173 | const parsedColor = normalize(value, variationName); 174 | // convert it to the right format 175 | return translateColor(parsedColor, variationName, execMode); 176 | }); 177 | }); 178 | 179 | return colorVariations; 180 | } 181 | 182 | /** 183 | * Get the underline color, according to the execution mode 184 | * @param colorArr two sized array with the color and the alpha 185 | * @param variationName the name of the variation. e.g. light / dark 186 | * @param execMode 187 | */ 188 | function translateColor(colorArr: [string, string], variationName: string, execMode: ExecutionMode) { 189 | const [colorVar, alpha] = colorArr; 190 | // returns the real color representation 191 | const underlineColor = options.palette[variationName][colorVar]; 192 | 193 | if (!underlineColor) { 194 | // variable is not mandatory in non-default variations 195 | if (variationName !== defaultVariation) { 196 | return null; 197 | } 198 | throw new Error(`The variable name '${colorVar}' doesn't exists in your palette.`); 199 | } 200 | 201 | switch (execMode) { 202 | case ExecutionMode.CSS_COLOR: 203 | // with default alpha - just returns the color 204 | if (alpha === '1') { 205 | return underlineColor; 206 | } 207 | // with custom alpha, convert it to rgba 208 | const rgbaColorArr = getRgbaNumbers(underlineColor); 209 | return `rgba(${rgbaColorArr}, ${alpha})`; 210 | case ExecutionMode.DYNAMIC_EXPRESSION: 211 | // returns it in a unique pattern, so it will be easy to replace it in runtime 212 | return `%[${variationName}, ${colorVar}, ${alpha}]%`; 213 | default: 214 | // return an rgba with the CSS variable name 215 | return `rgba(var(--${colorVar}), ${alpha})`; 216 | } 217 | } 218 | 219 | /** 220 | * Walk through all rules, and replace each themify occurrence with the corresponding CSS variable. 221 | * @example background-color: themify(primary-300, 0.5) => background-color: rgba(var(--primary-300),0.6) 222 | * @param root 223 | */ 224 | function processRules(root) { 225 | root.walkRules(rule => { 226 | if (!hasThemify(rule.toString())) { 227 | return; 228 | } 229 | 230 | let aggragatedSelectorsMap = {}; 231 | let aggragatedSelectors: string[] = []; 232 | 233 | let createdRules: any[] = []; 234 | const variationRules = { 235 | [defaultVariation]: rule 236 | }; 237 | 238 | rule.walkDecls(decl => { 239 | const propertyValue = decl.value; 240 | if (!hasThemify(propertyValue)) return; 241 | 242 | const property = decl.prop; 243 | 244 | const variationValueMap = getThemifyValue(propertyValue, ExecutionMode.CSS_VAR); 245 | const defaultVariationValue = variationValueMap[defaultVariation]; 246 | decl.value = defaultVariationValue; 247 | 248 | // indicate if we have a global rule, that cannot be nested 249 | const createNonDefaultVariationRules = isAtRule(rule); 250 | // don't create extra CSS for global rules 251 | if (createNonDefaultVariationRules) { 252 | return; 253 | } 254 | 255 | // create a new declaration and append it to each rule 256 | nonDefaultVariations.forEach(variationName => { 257 | const currentValue = variationValueMap[variationName]; 258 | 259 | // variable for non-default variation is optional 260 | if (!currentValue || currentValue === 'null') { 261 | return; 262 | } 263 | 264 | // when the declaration is the same as the default variation, 265 | // we just need to concatenate our selector to the default rule 266 | if (currentValue === defaultVariationValue) { 267 | const selector = getSelectorName(rule, variationName); 268 | // append the selector once 269 | if (!aggragatedSelectorsMap[variationName]) { 270 | aggragatedSelectorsMap[variationName] = true; 271 | aggragatedSelectors.push(selector); 272 | } 273 | } else { 274 | // creating the rule for the first time 275 | if (!variationRules[variationName]) { 276 | const clonedRule = createRuleWithVariation(rule, variationName); 277 | variationRules[variationName] = clonedRule; 278 | // append the new rule to the array, so we can append it later 279 | createdRules.push(clonedRule); 280 | } 281 | 282 | const variationDecl = createDecl(property, variationValueMap[variationName]); 283 | variationRules[variationName].append(variationDecl); 284 | } 285 | }); 286 | }); 287 | 288 | if (aggragatedSelectors.length) { 289 | rule.selectors = [...rule.selectors, ...aggragatedSelectors]; 290 | } 291 | 292 | // append each created rule 293 | if (createdRules.length) { 294 | createdRules.forEach(r => root.append(r)); 295 | } 296 | }); 297 | } 298 | 299 | /** 300 | * indicate if we have a global rule, that cannot be nested 301 | * @param rule 302 | * @return {boolean} 303 | */ 304 | function isAtRule(rule) { 305 | return rule.parent && rule.parent.type === 'atrule'; 306 | } 307 | 308 | /** 309 | * Walk through all rules, and generate a CSS fallback for legacy browsers. 310 | * Two files shall be created for full compatibility: 311 | * 1. A CSS file, contains all the rules with the original color representation. 312 | * 2. A JSON with the themify rules, in the following form: 313 | * themify(primary-100, 0.5) => %[light,primary-100,0.5)% 314 | * @param root 315 | */ 316 | function processFallbackRules(root) { 317 | // an output for each execution mode 318 | const output = { 319 | [ExecutionMode.CSS_COLOR]: [], 320 | [ExecutionMode.DYNAMIC_EXPRESSION]: {} 321 | }; 322 | // initialize DYNAMIC_EXPRESSION with all existing variations 323 | variationValues.forEach(variation => (output[ExecutionMode.DYNAMIC_EXPRESSION][variation] = [])); 324 | 325 | // define which modes need to be processed 326 | const execModes = [ExecutionMode.CSS_COLOR, ExecutionMode.DYNAMIC_EXPRESSION]; 327 | 328 | walkFallbackAtRules(root, execModes, output); 329 | walkFallbackRules(root, execModes, output); 330 | 331 | writeFallbackCSS(output); 332 | } 333 | 334 | function writeFallbackCSS(output) { 335 | // write the CSS & JSON to external files 336 | if (output[ExecutionMode.CSS_COLOR].length) { 337 | // write CSS fallback; 338 | const fallbackCss = output[ExecutionMode.CSS_COLOR].join(''); 339 | 340 | writeToFile(options.fallback.cssPath as string, minifyCSS(fallbackCss)); 341 | 342 | // creating a JSON for the dynamic expressions 343 | const jsonOutput = {}; 344 | variationValues.forEach(variationName => { 345 | jsonOutput[variationName] = output[ExecutionMode.DYNAMIC_EXPRESSION][variationName] || []; 346 | jsonOutput[variationName] = minifyJSON(jsonOutput[variationName].join('')); 347 | // minify the CSS output 348 | jsonOutput[variationName] = minifyCSS(jsonOutput[variationName]); 349 | }); 350 | 351 | // stringify and save 352 | const dynamicCss = JSON.stringify(jsonOutput); 353 | 354 | writeToFile(options.fallback.dynamicPath as string, dynamicCss); 355 | } 356 | } 357 | 358 | function walkFallbackAtRules(root, execModes, output) { 359 | root.walkAtRules(atRule => { 360 | if (atRule.nodes && hasThemify(atRule.toString())) { 361 | execModes.forEach(mode => { 362 | const clonedAtRule = atRule.clone(); 363 | 364 | clonedAtRule.nodes.forEach(rule => { 365 | rule.walkDecls(decl => { 366 | const propertyValue = decl.value; 367 | 368 | // replace the themify token, if exists 369 | if (hasThemify(propertyValue)) { 370 | const colorMap = getThemifyValue(propertyValue, mode); 371 | decl.value = colorMap[defaultVariation]; 372 | } 373 | }); 374 | }); 375 | 376 | let rulesOutput = mode === ExecutionMode.DYNAMIC_EXPRESSION ? output[mode][defaultVariation] : output[mode]; 377 | rulesOutput.push(clonedAtRule); 378 | }); 379 | } 380 | }); 381 | } 382 | 383 | function walkFallbackRules(root, execModes, output) { 384 | root.walkRules(rule => { 385 | if (isAtRule(rule) || !hasThemify(rule.toString())) { 386 | return; 387 | } 388 | 389 | const ruleModeMap = {}; 390 | 391 | rule.walkDecls(decl => { 392 | const propertyValue = decl.value; 393 | 394 | if (!hasThemify(propertyValue)) return; 395 | 396 | const property = decl.prop; 397 | 398 | execModes.forEach(mode => { 399 | const colorMap = getThemifyValue(propertyValue, mode); 400 | 401 | // lazily creating a new rule for each variation, for the specific mode 402 | if (!ruleModeMap.hasOwnProperty(mode)) { 403 | ruleModeMap[mode] = {}; 404 | 405 | variationValues.forEach(variationName => { 406 | let newRule; 407 | if (variationName === defaultVariation) { 408 | newRule = cloneEmptyRule(rule); 409 | } else { 410 | newRule = createRuleWithVariation(rule, variationName); 411 | } 412 | 413 | // push the new rule into the right place, 414 | // so we can write them later to external file 415 | let rulesOutput = mode === ExecutionMode.DYNAMIC_EXPRESSION ? output[mode][variationName] : output[mode]; 416 | rulesOutput.push(newRule); 417 | 418 | ruleModeMap[mode][variationName] = newRule; 419 | }); 420 | } 421 | 422 | // create and append a new declaration 423 | variationValues.forEach(variationName => { 424 | const underlineColor = colorMap[variationName]; 425 | if (underlineColor && underlineColor !== 'null') { 426 | const newDecl = createDecl(property, colorMap[variationName]); 427 | ruleModeMap[mode][variationName].append(newDecl); 428 | } 429 | }); 430 | }); 431 | }); 432 | }); 433 | } 434 | 435 | function createDecl(prop, value) { 436 | return postcss.decl({ prop, value }); 437 | } 438 | 439 | /** 440 | * check if there's a themify keyword in this declaration 441 | * @param propertyValue 442 | */ 443 | function hasThemify(propertyValue) { 444 | return propertyValue.indexOf(THEMIFY) > -1; 445 | } 446 | 447 | /** 448 | * Create a new rule for the given variation, out of the original rule 449 | * @param rule 450 | * @param variationName 451 | */ 452 | function createRuleWithVariation(rule, variationName) { 453 | const selector = getSelectorName(rule, variationName); 454 | return postcss.rule({ selector }); 455 | } 456 | 457 | /** 458 | * Get a selector name for the given rule and variation 459 | * @param rule 460 | * @param variationName 461 | */ 462 | function getSelectorName(rule, variationName) { 463 | const selectorPrefix = `.${options.classPrefix || ''}${variationName}`; 464 | 465 | return rule.selectors 466 | .map(selector => { 467 | return `${selectorPrefix} ${selector}`; 468 | }) 469 | .join(','); 470 | } 471 | 472 | function cloneEmptyRule(rule, overrideConfig?) { 473 | const clonedRule = rule.clone(overrideConfig); 474 | // remove all the declaration from this rule 475 | clonedRule.removeAll(); 476 | return clonedRule; 477 | } 478 | } 479 | 480 | /** 481 | * Generating a SASS definition file with the palette map and the CSS variables. 482 | * This file should be injected into your bundle. 483 | */ 484 | function init(options) { 485 | options = buildOptions(options); 486 | 487 | return root => { 488 | const palette = options.palette; 489 | const css = generateVars(palette, options.classPrefix); 490 | 491 | const parsedCss = postcss.parse(css); 492 | root.prepend(parsedCss); 493 | }; 494 | 495 | /** 496 | * This function responsible for creating the CSS variable. 497 | * 498 | * The output should look like the following: 499 | * 500 | * .light { 501 | --primary-700: 255, 255, 255; 502 | --primary-600: 248, 248, 249; 503 | --primary-500: 242, 242, 244; 504 | * } 505 | * 506 | * .dark { 507 | --primary-700: 255, 255, 255; 508 | --primary-600: 248, 248, 249; 509 | --primary-500: 242, 242, 244; 510 | * } 511 | * 512 | */ 513 | function generateVars(palette, prefix) { 514 | let cssOutput = ''; 515 | prefix = prefix || ''; 516 | 517 | // iterate through the different variations 518 | Object.keys(palette).forEach(variationName => { 519 | const selector = variationName === ColorVariation.LIGHT ? ':root' : `.${prefix}${variationName}`; 520 | const variationColors = palette[variationName]; 521 | 522 | // make sure we got colors for this variation 523 | if (!variationColors) { 524 | throw new Error(`Expected map of colors for the variation name ${variationName}`); 525 | } 526 | 527 | const variationKeys = Object.keys(variationColors); 528 | 529 | // generate CSS variables 530 | const vars = variationKeys 531 | .map(varName => { 532 | return `--${varName}: ${getRgbaNumbers(variationColors[varName])};`; 533 | }) 534 | .join(' '); 535 | 536 | // concatenate the variables to the output 537 | const output = `${selector} {${vars}}`; 538 | cssOutput = `${cssOutput} ${output}`; 539 | }); 540 | 541 | // generate the $palette variable 542 | cssOutput += `$palette: ${JSToSass(palette)};`; 543 | 544 | return cssOutput; 545 | } 546 | } 547 | 548 | function hexToRgba(hex, alpha = 1): string { 549 | hex = hex.replace('#', ''); 550 | const r = parseInt(hex.length == 3 ? hex.slice(0, 1).repeat(2) : hex.slice(0, 2), 16); 551 | const g = parseInt(hex.length == 3 ? hex.slice(1, 2).repeat(2) : hex.slice(2, 4), 16); 552 | const b = parseInt(hex.length == 3 ? hex.slice(2, 3).repeat(2) : hex.slice(4, 6), 16); 553 | 554 | return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'; 555 | } 556 | 557 | module.exports = { 558 | initThemify: postcss.plugin('datoThemes', init), 559 | themify: postcss.plugin('datoThemes', themify) 560 | }; 561 | -------------------------------------------------------------------------------- /src/rule-processor.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/themify/a22e41b784addc70eea9d39d7df3846deaed7aa8/src/rule-processor.ts -------------------------------------------------------------------------------- /src/sass/_internal.scss: -------------------------------------------------------------------------------- 1 | $PRESERVE_THEMIFY: false !default; 2 | 3 | /** 4 | * Credits to https://github.com/acss-io/atomizer/blob/master/src/lib/grammar.js 5 | */ 6 | $PSEUDO_CLASSES: ( 7 | ':a': ':active', 8 | ':c': ':checked', 9 | ':d': ':default', 10 | ':di': ':disabled', 11 | ':e': ':empty', 12 | ':en': ':enabled', 13 | ':fi': ':first', 14 | ':fc': ':first-child', 15 | ':fot': ':first-of-type', 16 | ':fs': ':fullscreen', 17 | ':f': ':focus', 18 | ':h': ':hover', 19 | ':ind': ':indeterminate', 20 | ':ir': ':in-range', 21 | ':inv': ':invalid', 22 | ':lc': ':last-child', 23 | ':lot': ':last-of-type', 24 | ':l': ':left', 25 | ':li': ':link', 26 | ':oc': ':only-child', 27 | ':oot': ':only-of-type', 28 | ':o': ':optional', 29 | ':oor': ':out-of-range', 30 | ':ro': ':read-only', 31 | ':rw' : ':read-write', 32 | ':req': ':required', 33 | ':r': ':right', 34 | ':rt' : ':root', 35 | ':s': ':scope', 36 | ':t' : ':target', 37 | ':va': ':valid', 38 | ':vi': ':visited' 39 | ); 40 | 41 | @function str-split($string, $separator) { 42 | // empty array/list 43 | $split-arr: (); 44 | // first index of separator in string 45 | $index: str-index($string, $separator); 46 | // loop through string 47 | @while $index != null { 48 | // get the substring from the first character to the separator 49 | $item: str-slice($string, 1, $index - 1); 50 | // push item to array 51 | $split-arr: append($split-arr, $item); 52 | // remove item and separator from string 53 | $string: str-slice($string, $index + 1); 54 | // find new index of separator 55 | $index: str-index($string, $separator); 56 | } 57 | // add the remaining string to list (the last item) 58 | $split-arr: append($split-arr, $string); 59 | 60 | @return $split-arr; 61 | } 62 | 63 | @function is-map($var: null, $getridoferror: none) { 64 | @return type-of($var) == 'map'; 65 | } 66 | 67 | @function themify($props...) { 68 | @if ($PRESERVE_THEMIFY == true) { 69 | @return unquote("themify(#{$props...})"); 70 | } 71 | 72 | @if (is-map($props...)) { 73 | $value: json-encode($props...); 74 | @return "themify(#{$value})"; 75 | } @else { 76 | $color: null; 77 | $opacity: 1; 78 | $theme: null; 79 | 80 | @if (length($props) >= 1) { 81 | $color: nth($props, 1); 82 | } 83 | 84 | @if (length($props) >= 2) { 85 | $opacity: nth($props, 2); 86 | } 87 | 88 | $value: json-encode((light: (#{$color}, #{$opacity}), dark: (#{$color}, #{$opacity}))); 89 | @return "themify(#{$value})"; 90 | } 91 | } 92 | 93 | @mixin generateThemeHelpers($propsArr) { 94 | 95 | @if (not $propsArr) { 96 | $propsArr: (); 97 | } 98 | 99 | $pseudoClassSep: ':'; 100 | @each $prop in $propsArr { 101 | @each $palette, $value in $palette { 102 | @each $var in map-keys($value) { 103 | 104 | // split the string, to retrieve all pseudo classes 105 | $pseudoList: str-split($prop, $pseudoClassSep); 106 | $firstPseudoClass: nth($pseudoList, 1); 107 | 108 | @each $pseudoClass in $pseudoList { 109 | 110 | $realPseudoClass: null; 111 | // if the pseudo class exists, get the real pseudo representation 112 | @if (str_length($pseudoClass) > 0 113 | and $pseudoClass != $firstPseudoClass 114 | and map_has_key($PSEUDO_CLASSES, $pseudoClassSep + $pseudoClass)) { 115 | $realPseudoClass: map_get($PSEUDO_CLASSES, $pseudoClassSep + $pseudoClass); 116 | } 117 | 118 | @include setHelperRule($firstPseudoClass, $var, $pseudoClass, $realPseudoClass); 119 | 120 | } 121 | 122 | } 123 | } 124 | } 125 | } 126 | 127 | @mixin setHelperRule($prop, $var, $pseudoClass, $realPseudoClass) { 128 | 129 | $selector: $prop; 130 | @if ($realPseudoClass) { 131 | $selector: $prop + '\\:' + $pseudoClass + $realPseudoClass; 132 | } 133 | 134 | $unquoteVar: unquote($var); 135 | .#{$unquoteVar}-#{$selector} { 136 | #{$prop}: themify($unquoteVar); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/sass/encode/encode/api/_json.scss: -------------------------------------------------------------------------------- 1 | /// Delay the encoding of ta literal to JSON to a type-specific method 2 | /// @access public 3 | /// @param {*} $value - value to be stringified 4 | /// @throw Unknown type for #{$value} (#{$type}). 5 | /// @return {String} - JSON encoded string 6 | /// @require {function} _json-encode--string 7 | /// @require {function} _json-encode--number 8 | /// @require {function} _json-encode--list 9 | /// @require {function} _json-encode--map 10 | /// @require {function} _json-encode--null 11 | /// @require {function} _json-encode--color 12 | /// @require {function} _json-encode--bool 13 | @function json-encode($value) { 14 | $type: type-of($value); 15 | 16 | @if function-exists('_json-encode--#{$type}') { 17 | @return call(get-function('_json-encode--#{$type}'), $value); 18 | } 19 | 20 | @error 'Unknown type for #{$value} (#{$type}).'; 21 | } 22 | -------------------------------------------------------------------------------- /src/sass/encode/encode/encode.scss: -------------------------------------------------------------------------------- 1 | // Helpers 2 | @import 'helpers/quote'; 3 | 4 | // Type specific encoding functions 5 | @import 'types/bool'; 6 | @import 'types/color'; 7 | @import 'types/list'; 8 | @import 'types/map'; 9 | @import 'types/number'; 10 | @import 'types/string'; 11 | @import 'types/null'; 12 | 13 | // Public API 14 | @import 'api/json'; 15 | 16 | // Mixin to pass the string to the DOM 17 | @import 'mixins/json'; 18 | -------------------------------------------------------------------------------- /src/sass/encode/encode/helpers/_quote.scss: -------------------------------------------------------------------------------- 1 | /// Proof quote a value 2 | /// @access private 3 | /// @param {*} $value - value to be quoted 4 | /// @return {String} - quoted value 5 | 6 | @function _proof-quote($value) { 7 | // $value: to-string($value); 8 | @return '"#{$value}"'; 9 | } 10 | -------------------------------------------------------------------------------- /src/sass/encode/encode/mixins/_json.scss: -------------------------------------------------------------------------------- 1 | /// JSON.stringify a value and pass it as a font-family of head element 2 | /// @access public 3 | /// @param {*} $value - value to be stringified 4 | /// @param {String} $flag (all) - output driver 5 | /// @require {function} json-encode 6 | @mixin json-encode($value, $flag: 'all') { 7 | $flag: if(index('all' 'regular' 'media' 'comment', $flag), $flag, 'all'); 8 | $json: json-encode($value); 9 | 10 | // Persistent comment 11 | @if $flag == 'comment' or $flag == 'all' { 12 | /*! json-encode: #{$json} */ 13 | } 14 | // Regular property value pair 15 | @if $flag == 'regular' or $flag == 'all' { 16 | // All browsers except IE8- 17 | body { 18 | &::before { 19 | // This element must be in the render tree to get it via getComputedStyle(document.body, ':before'); 20 | content: json-encode($value); 21 | display: block; 22 | height: 0; 23 | overflow: hidden; 24 | width: 0; 25 | } 26 | } 27 | 28 | // All browsers except Opera (Presto based) 29 | head { 30 | font-family: json-encode($value); 31 | } 32 | } 33 | 34 | // Falsy media query 35 | @if $flag == 'media' or $flag == 'all' { 36 | @media -json-encode { 37 | json { 38 | json: $json; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_bool.scss: -------------------------------------------------------------------------------- 1 | /// Encode a bool to JSON 2 | /// @access private 3 | /// @param {Bool} $bool - bool to be encoded 4 | /// @return {Bool} - encoded bool 5 | @function _json-encode--bool($boolean) { 6 | @return $boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_color.scss: -------------------------------------------------------------------------------- 1 | /// Encode a color to JSON 2 | /// @access private 3 | /// @param {Color} $color - color to be encoded 4 | /// @return {String} - encoded color 5 | /// @require {function} _proof-quote 6 | @function _json-encode--color($color) { 7 | @return _proof-quote($color); 8 | } 9 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_list.scss: -------------------------------------------------------------------------------- 1 | /// Encode a list to JSON 2 | /// @access private 3 | /// @param {List} $list - list to be encoded 4 | /// @return {String} - encoded list 5 | /// @require {function} json-encore 6 | @function _json-encode--list($list) { 7 | $str: ''; 8 | 9 | @each $item in $list { 10 | $str: $str + ', ' + json-encode($item); 11 | } 12 | 13 | @return '[' + str-slice($str, 3) + ']'; 14 | } 15 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_map.scss: -------------------------------------------------------------------------------- 1 | /// Encode a map to JSON 2 | /// @access private 3 | /// @param {Map} $map - map to be encoded 4 | /// @return {String} - encoded map 5 | /// @require {function} _proof-quote 6 | /// @require {function} json-encode 7 | @function _json-encode--map($map) { 8 | $str: ''; 9 | 10 | @each $key, $value in $map { 11 | $str: $str + ', ' + _proof-quote($key) + ': ' + json-encode($value); 12 | } 13 | 14 | @return '{' + str-slice($str, 3) + '}'; 15 | } 16 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_null.scss: -------------------------------------------------------------------------------- 1 | /// Encode `null` to JSON 2 | /// @access private 3 | /// @param {Null} $null - `null` 4 | /// @return {String} 5 | @function _json-encode--null($null) { 6 | @return 'null'; 7 | } 8 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_number.scss: -------------------------------------------------------------------------------- 1 | /// Encode a number to JSON 2 | /// @access private 3 | /// @param {Number} $number - number to be encoded 4 | /// @return {String} - encoded number 5 | /// @require {function} _proof-quote 6 | @function _json-encode--number($number) { 7 | @return if(unitless($number), $number, _proof-quote($number)); 8 | } 9 | -------------------------------------------------------------------------------- /src/sass/encode/encode/types/_string.scss: -------------------------------------------------------------------------------- 1 | /// Encode a string to JSON 2 | /// @access private 3 | /// @param {String} $string - string to be encoded 4 | /// @return {String} - encoded string 5 | /// @require {function} _proof-quote 6 | @function _json-encode--string($string) { 7 | @return _proof-quote($string); 8 | } 9 | -------------------------------------------------------------------------------- /src/sass/encode/sass-json-export.scss: -------------------------------------------------------------------------------- 1 | // Encoder 2 | @import 'encode/encode'; 3 | -------------------------------------------------------------------------------- /src/sass/themify.scss: -------------------------------------------------------------------------------- 1 | @import 'encode/sass-json-export'; 2 | @import "internal"; 3 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | let JSONFallbackCache; 2 | let _hasNativeSupport; 3 | 4 | export type Theme = { 5 | [name: string]: { 6 | [variable: string]: string; 7 | }; 8 | }; 9 | 10 | /** 11 | * 12 | * @param {string} path 13 | */ 14 | export function loadCSS(path: string, callback) { 15 | const head = document.getElementsByTagName('head')[0]; 16 | const style = document.createElement('link'); 17 | style.href = path; 18 | style.rel = 'stylesheet'; 19 | style.onload = callback; 20 | head.appendChild(style); 21 | } 22 | 23 | /** 24 | * 25 | * @param {string} style 26 | */ 27 | export function injectStyle(style: string) { 28 | /** Don't replace the style tag, otherwise you will remove the old changes */ 29 | if (hasNativeCSSProperties()) { 30 | inject(); 31 | } else { 32 | /** Use the same style tag as we replace all either way */ 33 | const styleTag = document.getElementById('themify') as HTMLLinkElement; 34 | if (!styleTag) { 35 | inject(); 36 | } else { 37 | styleTag.innerHTML = style; 38 | } 39 | } 40 | 41 | function inject() { 42 | let node = document.createElement('style'); 43 | node.id = 'themify'; 44 | node.innerHTML = style; 45 | document.head.appendChild(node); 46 | } 47 | } 48 | 49 | /** 50 | * 51 | * .dark { 52 | * --primary-100: 30, 24, 33; 53 | * } 54 | * 55 | * :root { 56 | * --primary-100: 22, 21, 22; 57 | * } 58 | * 59 | * @param customTheme 60 | * @returns {string} 61 | */ 62 | export function _generateNewVariables(customTheme: Theme) { 63 | // First, we need the variations [dark, light] 64 | const variations = Object.keys(customTheme); 65 | return variations.reduce((finalOutput, variation) => { 66 | // Next, we need the variation keys [primary-100, accent-100] 67 | const variationKeys = Object.keys(customTheme[variation]); 68 | 69 | const variationOutput = variationKeys.reduce((acc, variable) => { 70 | const value = normalizeColor(customTheme[variation][variable]); 71 | return (acc += `--${variable}: ${value};`); 72 | }, ''); 73 | 74 | return (finalOutput += `${variation === 'light' ? ':root' : '.' + variation}{${variationOutput}}`); 75 | }, ''); 76 | } 77 | 78 | /** 79 | * 80 | * @returns {boolean} 81 | */ 82 | export function hasNativeCSSProperties() { 83 | if (_hasNativeSupport != null) { 84 | return _hasNativeSupport; 85 | } 86 | 87 | _hasNativeSupport = (window as any).CSS && (window as any).CSS.supports && (window as any).CSS.supports('--fake-var', 0); 88 | 89 | return _hasNativeSupport; 90 | } 91 | 92 | /** 93 | * Load the CSS fallback file on load 94 | */ 95 | export function loadCSSVariablesFallback(path: string, cb) { 96 | if (!hasNativeCSSProperties()) { 97 | loadCSS(path, cb); 98 | } 99 | } 100 | 101 | function loadJSON(url, cb) { 102 | const req = new XMLHttpRequest(); 103 | req.overrideMimeType('application/json'); 104 | req.open('GET', url, true); 105 | req.onload = function() { 106 | cb(JSON.parse(req.responseText)); 107 | }; 108 | req.send(null); 109 | } 110 | 111 | /** 112 | * 113 | * @param customTheme 114 | */ 115 | export function replaceColors(fallbackJSONPath, customTheme, palette) { 116 | if (customTheme) { 117 | if (hasNativeCSSProperties()) { 118 | const newColors = _generateNewVariables(customTheme); 119 | injectStyle(newColors); 120 | } else { 121 | const replace = JSONFallback => { 122 | JSONFallbackCache = JSONFallback; 123 | _handleUnSupportedBrowsers(customTheme, palette, JSONFallbackCache); 124 | }; 125 | if (JSONFallbackCache) { 126 | replace(JSONFallbackCache); 127 | } else { 128 | loadJSON(fallbackJSONPath, replace); 129 | } 130 | } 131 | } 132 | } 133 | /** 134 | * 135 | * @param customTheme 136 | */ 137 | export function _handleUnSupportedBrowsers(customTheme, palette, JSONFallback) { 138 | const themifyRegExp = /%\[(.*?)\]%/gi; 139 | const merged = mergeDeep(palette, customTheme); 140 | 141 | let finalOutput = Object.keys(customTheme).reduce((acc, variation) => { 142 | let value = JSONFallback[variation].replace(themifyRegExp, (occurrence, value) => { 143 | const [variation, variable, opacity] = value.replace(/\s/g, '').split(','); 144 | const color = merged[variation][variable]; 145 | const normalized = hexToRGB(color, opacity); 146 | return normalized; 147 | }); 148 | 149 | return (acc += value); 150 | }, ''); 151 | 152 | injectStyle(finalOutput); 153 | 154 | return finalOutput; 155 | } 156 | 157 | /** 158 | * Omit the rgb and braces from rgb 159 | * rgb(235, 246, 244) => 235, 246, 244 160 | * @param rgb 161 | * @returns {string} 162 | */ 163 | function normalizeRgb(rgb: string) { 164 | return rgb.replace('rgb(', '').replace(')', ''); 165 | } 166 | 167 | /** 168 | * 169 | * @param color 170 | * @returns {*} 171 | */ 172 | function normalizeColor(color: string) { 173 | if (isHex(color)) { 174 | return normalizeRgb(hexToRGB(color)); 175 | } 176 | 177 | if (isRgb(color)) { 178 | return normalizeRgb(color); 179 | } 180 | 181 | return color; 182 | } 183 | 184 | /** 185 | * 186 | * @param color 187 | * @returns {boolean} 188 | */ 189 | function isHex(color: string) { 190 | return color.indexOf('#') > -1; 191 | } 192 | 193 | /** 194 | * 195 | * @param color 196 | * @returns {boolean} 197 | */ 198 | function isRgb(color: string) { 199 | return color.indexOf('rgb') > -1; 200 | } 201 | 202 | /** 203 | * 204 | * @param hex 205 | * @param alpha 206 | * @returns {string} 207 | */ 208 | function hexToRGB(hex: string, alpha = false) { 209 | let c; 210 | if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) { 211 | c = hex.substring(1).split(''); 212 | if (c.length == 3) { 213 | c = [c[0], c[0], c[1], c[1], c[2], c[2]]; 214 | } 215 | c = '0x' + c.join(''); 216 | if (alpha) { 217 | return `rgba(${[(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',')}, ${alpha})`; 218 | } 219 | return `rgb(${[(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',')})`; 220 | } 221 | 222 | throw new Error('Bad Hex'); 223 | } 224 | 225 | /** 226 | * 227 | * @param target 228 | * @param sources 229 | * @returns {*} 230 | */ 231 | function mergeDeep(target, ...sources) { 232 | if (!sources.length) return target; 233 | const source = sources.shift(); 234 | 235 | if (isObject(target) && isObject(source)) { 236 | for (const key in source) { 237 | if (isObject(source[key])) { 238 | if (!target[key]) 239 | Object.assign(target, { 240 | [key]: {} 241 | }); 242 | mergeDeep(target[key], source[key]); 243 | } else { 244 | Object.assign(target, { 245 | [key]: source[key] 246 | }); 247 | } 248 | } 249 | } 250 | 251 | return mergeDeep(target, ...sources); 252 | } 253 | 254 | /** 255 | * 256 | * @param item 257 | * @returns {*|boolean} 258 | */ 259 | function isObject(value: any) { 260 | return Object.prototype.toString.call(value) === '[object Object]'; 261 | } 262 | 263 | module.exports = { 264 | replaceColors, 265 | loadCSSVariablesFallback, 266 | _handleUnSupportedBrowsers, 267 | _generateNewVariables 268 | }; 269 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["dom", "es2016"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "noImplicitAny": false, 9 | "experimentalDecorators": true, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noEmitOnError": true, 13 | "noUnusedParameters": false, 14 | "typeRoots": ["./node_modules/@types"] 15 | }, 16 | "exclude": [ 17 | "__tests__", 18 | "dist", 19 | "node_modules" 20 | ] 21 | } --------------------------------------------------------------------------------