├── .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 | 
2 | [](https://travis-ci.org/datorama/themify)
3 | [](#contributors)
4 | []()
5 | []()
6 | [](https://github.com/prettier/prettier)
7 | [](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 | }
--------------------------------------------------------------------------------