├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── example
├── .npmignore
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── header.png
├── package.json
├── src
└── index.ts
├── test
└── index.test.ts
├── tsconfig.json
└── yarn.lock
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 |
7 | steps:
8 | - name: Begin CI...
9 | uses: actions/checkout@v2
10 |
11 | - name: Use Node 12
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 12.x
15 |
16 | - name: Use cached node_modules
17 | uses: actions/cache@v1
18 | with:
19 | path: node_modules
20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }}
21 | restore-keys: |
22 | nodeModules-
23 |
24 | - name: Install dependencies
25 | run: yarn install --frozen-lockfile
26 | env:
27 | CI: true
28 |
29 | - name: Test
30 | run: yarn test --ci --coverage --maxWorkers=2
31 | env:
32 | CI: true
33 |
34 | - name: Build
35 | run: yarn build
36 | env:
37 | CI: true
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Fatih Kalifa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](.)
6 |
7 | [](https://www.npmjs.com/package/theme-in-css)
8 | [](https://www.npmjs.com/package/theme-in-css)
9 |
10 |
11 |
12 | ---
13 |
14 | - [Why?](#why)
15 | - [Usage](#usage)
16 | - [CSS Integration](#css-integration)
17 | - [.css.string: `string`](#cssstring-string)
18 | - [.css.properties: `Array<[key: string, value: string]>`](#cssproperties-arraykey-string-value-string)
19 | - [License](#license)
20 |
21 | ---
22 |
23 | ## Why?
24 |
25 | - **Strictly-typed design token, no more typo**. As you might know: CSS is forgiving, but in this case we don't want that. We want it to fail at compile time and this library gives you that
26 | - **Works in CSS and JS**. Using CSS custom properties means you can reference your design token in both CSS and JS. Separation of concern!
27 | - **UI Library agnostic**. The only purpose of this library is to provide type-safe CSS custom properties in your TS modules, nothing less, nothing more.
28 |
29 | ## Usage
30 |
31 | First create your theme object, you can group theme variables by its function (color, spacing, etc). Think of it as design token
32 |
33 | ```ts
34 | import { createTheme } from 'theme-in-css';
35 |
36 | export const Theme = createTheme({
37 | color: {
38 | lightPrimary: '#fff',
39 | darkPrimary: '#000',
40 | },
41 | spacing: {
42 | xs: 2,
43 | s: 4,
44 | m: 8,
45 | l: 16,
46 | xl: 32,
47 | },
48 | typography: {
49 | family: {
50 | serif: 'Times New Roman',
51 | sans: 'Calibri',
52 | mono: 'Menlo',
53 | },
54 | }
55 | });
56 |
57 | // If you hate typing you can also use a shorter property name
58 | // const t = createTheme({ c: { l1: '#fff', d1: '#000' } });
59 | ```
60 |
61 | You can use any any UI libraries/framework that can define style in JS/TS, for example [React](https://reactjs.org) and [Lit](https://lit.dev/).
62 |
63 | ```tsx
64 | // React
65 | import React from 'react';
66 | import { Theme } from './theme';
67 |
68 | export default function Component() {
69 | // use css prop via emotion/styled-components
70 | // of course inline style works as well
71 | return (
72 |
80 |
It works
81 |
82 | );
83 | }
84 |
85 | // Lit
86 | // You need to wrap Theme inside `unsafeCSS`
87 | import { LitElement, html, css, unsafeCSS as cv } from 'lit';
88 | import { Theme } from './theme';
89 |
90 | export default class Component extends LitElement {
91 | static styles = css`
92 | div {
93 | background-color: ${cv(Theme.color.darkPrimary)};
94 | color: ${cv(Theme.color.lightPrimary)};
95 | margin: ${cv(Theme.spacing.m)};
96 | font-family: ${cv(Theme.typography.family.serif)};
97 | }
98 | `;
99 |
100 | render() {
101 | return html`
102 |
103 |
It works
104 |
105 | `;
106 | }
107 | }
108 | ```
109 |
110 | ## CSS Integration
111 |
112 | If you only create theme and use them in your app, you'll notice that your app now uses CSS variables to reference a value, but it doesn't work properly yet because you need to add the CSS into your stylesheet.
113 |
114 | ### .css.string: `string`
115 |
116 | `theme-in-css` provides `.css.string` property to dump all theme values as CSS properties. You can create 2 themes light and dark and output them in different style declaration, like this:
117 |
118 | ```ts
119 | import { Theme, DarkTheme } from './theme';
120 |
121 | const html = `
122 |
123 |
124 |
125 |
134 |
135 |
136 |
137 |
138 | `;
139 | ```
140 |
141 | You can open [example](example) to see it in action.
142 |
143 | ### .css.properties: `Array<[key: string, value: string]>`
144 |
145 | You can also use `.css.properties` if you want to update the CSS custom property manually using JS.
146 |
147 | ```js
148 | const root = document.documentElement;
149 |
150 | theme.css.properties.forEach(([key, value]) => {
151 | root.style.setProperty(key, value);
152 | });
153 | ```
154 |
155 | If you prefer `Record` instead, you can use `Object.fromEntries`
156 |
157 | ```js
158 | const obj = Object.fromEntries(theme.css.properties);
159 | ```
160 |
161 | ## License
162 |
163 | MIT
164 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import { createTheme } from '../.';
5 |
6 | const Theme = createTheme({
7 | color: {
8 | lightPrimary: '#fff',
9 | darkPrimary: '#000',
10 | }
11 | });
12 |
13 | const DarkTheme = createTheme({
14 | color: {
15 | lightPrimary: '#000',
16 | darkPrimary: '#fff',
17 | }
18 | })
19 |
20 | const style = document.createElement('style');
21 | style.innerHTML = `
22 | :root {
23 | ${Theme.css.string}
24 | }
25 |
26 | @media (prefers-color-scheme: dark) {
27 | :root {
28 | ${DarkTheme.css.string}
29 | }
30 | }
31 | `
32 |
33 | document.head.appendChild(style)
34 |
35 | const App = () => {
36 | return (
37 |
38 |
It Works
39 |
40 | );
41 | };
42 |
43 | ReactDOM.render(, document.getElementById('root'));
44 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.9.11",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^1.12.3",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pveyes/theme-in-css/46336bec12c0a826290a29db32fa2f1561c0bb60/header.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "theme-in-css",
3 | "author": "Fatih Kalifa",
4 | "version": "0.2.2",
5 | "license": "MIT",
6 | "main": "dist/index.js",
7 | "module": "dist/theme-in-css.esm.js",
8 | "typings": "dist/index.d.ts",
9 | "files": [
10 | "dist",
11 | "src"
12 | ],
13 | "engines": {
14 | "node": ">=10"
15 | },
16 | "scripts": {
17 | "start": "tsdx watch",
18 | "build": "tsdx build",
19 | "format": "prettier --write \"**/*.ts\"",
20 | "test": "tsdx test --passWithNoTests",
21 | "lint": "tsdx lint",
22 | "prepare": "tsdx build"
23 | },
24 | "devDependencies": {
25 | "@types/react": "^17.0.11",
26 | "@types/react-dom": "^17.0.8",
27 | "husky": "^4.2.5",
28 | "react": "^17.0.2",
29 | "react-dom": "^17.0.2",
30 | "tsdx": "^0.14.1",
31 | "tslib": "^2.3.0",
32 | "typescript": "^4.3.4"
33 | },
34 | "husky": {
35 | "hooks": {
36 | "pre-commit": "exit 0"
37 | }
38 | },
39 | "prettier": {
40 | "printWidth": 80,
41 | "semi": true,
42 | "singleQuote": true,
43 | "trailingComma": "es5"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | type ThemeValue = string | number;
2 |
3 | interface ThemeValueMap {
4 | [key: string]: ThemeValue;
5 | };
6 |
7 | interface ThemeConfig {
8 | [token: string]: ThemeValueMap | ThemeConfig | ThemeValue;
9 | };
10 |
11 | interface ThemeCSSObject {
12 | css: {
13 | string: string;
14 | properties: Array<[string, string]>;
15 | };
16 | };
17 |
18 | type ThemeVariableValueMap