├── CNAME ├── .eslintignore ├── src ├── babel-plugin │ └── index.ts ├── stylis │ ├── built │ │ ├── umd │ │ │ ├── package.json │ │ │ ├── stylis.js │ │ │ └── stylis.js.map │ │ ├── stylis.mjs │ │ └── stylis.mjs.map │ ├── package.json │ └── index.d.ts ├── elements.ts └── index.ts ├── .commitlintrc.json ├── .huskyrc ├── tests └── react │ ├── .babelrc │ └── render.test.tsx ├── .lintstagedrc ├── .prettierrc ├── example ├── index.html └── index.tsx ├── tsconfig.build.json ├── foliage.ts ├── .editorconfig ├── .github ├── workflows │ ├── release-drafter.yml │ ├── test.yml │ └── publish.yml ├── FUNDING.yml └── release-drafter.yml ├── tsconfig.json ├── .eslintrc.json ├── rollup.config.js ├── babel.config.js ├── .gitignore ├── package.json ├── react.tsx ├── README.md ├── jest.config.ts ├── babel-plugin.test.ts ├── babel-plugin.js └── __snapshots__ └── babel-plugin.test.ts.snap /CNAME: -------------------------------------------------------------------------------- 1 | foliage.sova.dev 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | types 3 | *.snap 4 | -------------------------------------------------------------------------------- /src/babel-plugin/index.ts: -------------------------------------------------------------------------------- 1 | export const a = 1; 2 | -------------------------------------------------------------------------------- /src/stylis/built/umd/package.json: -------------------------------------------------------------------------------- 1 | { "type": "commonjs" } 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["../../babel-plugin", { 4 | "allowedModules": ["../../react"], 5 | "debug": true 6 | }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx,js,jsx,mjs}": ["eslint --fix", "prettier --write"], 3 | "{*.json,.huskyrc,.prettierrc,.lintstagedrc,.eslintrc}": [ 4 | "prettier --write --parser json" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 80, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Foliage example 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react", 6 | "declaration": true, 7 | "emitDeclarationOnly": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /foliage.ts: -------------------------------------------------------------------------------- 1 | export const css: any = () => null; 2 | export const keyframes: any = () => null; 3 | export const createGlobalStyle: any = () => null; 4 | 5 | export function assertSelector(a: any) { 6 | return `.${a.css}`; 7 | } 8 | -------------------------------------------------------------------------------- /src/stylis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "built/umd/stylis.js", 4 | "exports": { 5 | ".": { 6 | "require": "./built/umd/stylis.js", 7 | "import": "./built/stylis.mjs" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | [*] 3 | indent_style = space 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | max_line_length = 80 9 | indent_size = 2 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /src/stylis/index.d.ts: -------------------------------------------------------------------------------- 1 | interface CompilationResult { 2 | __NONE: number; 3 | } 4 | export function serialize( 5 | compilationResult: CompilationResult, 6 | // eslint-disable-next-line @typescript-eslint/ban-types 7 | stringifier: Function, 8 | ): string; 9 | export function compile(styles: string): CompilationResult; 10 | export function stringify(): void; 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test It 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-package: 7 | runs-on: ubuntu-18.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Use Node.js 16.x 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 16.x 16 | 17 | - name: Install dependencies 18 | run: yarn install --frozen-lockfile 19 | 20 | - name: Build 21 | run: yarn build 22 | 23 | - name: Run tests 24 | run: yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: sergeysova 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/sergeysova', 'https://vk.com/sovadev'] 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '⚠️ Breaking changes' 3 | label: 'BREAKING CHANGES' 4 | 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'fix' 13 | - 'bugfix' 14 | - 'bug' 15 | 16 | - title: '🧰 Maintenance' 17 | labels: 18 | - 'chore' 19 | - 'dependencies' 20 | 21 | - title: '📚 Documentation' 22 | label: 'documentation' 23 | 24 | - title: '🧪 Tests' 25 | label: 'tests' 26 | 27 | - title: '🏎 Optimizations' 28 | label: 'optimizations' 29 | 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'BREAKING CHANGES' 34 | minor: 35 | labels: 36 | - 'feature' 37 | - 'enhancement' 38 | patch: 39 | labels: 40 | - 'fix' 41 | default: patch 42 | 43 | name-template: 'v$RESOLVED_VERSION' 44 | tag-template: 'v$RESOLVED_VERSION' 45 | 46 | change-template: '- $TITLE #$NUMBER (@$AUTHOR)' 47 | template: | 48 | $CHANGES 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "allowUnreachableCode": false, 6 | "baseUrl": "src", 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es6", "es2015", "es2017", "dom"], 11 | "module": "esnext", 12 | "moduleResolution": "Node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": false, 18 | "pretty": true, 19 | "sourceMap": true, 20 | "strict": true, 21 | "jsx": "react", 22 | "strictNullChecks": true, 23 | "suppressImplicitAnyIndexErrors": true, 24 | "resolveJsonModule": true, 25 | "target": "es2016", 26 | "types": ["jest", "node"], 27 | "plugins": [ 28 | { 29 | "name": "typescript-styled-plugin", 30 | "tags": ["styled", "css", "createGlobalStyle"] 31 | } 32 | ] 33 | }, 34 | "include": ["./src/", "./types"] 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-to-npm: 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Use Node.js 16.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 16.x 18 | 19 | - name: Install dependencies 20 | run: yarn install --frozen-lockfile 21 | 22 | - name: Build 23 | run: yarn build 24 | 25 | - name: Run tests 26 | run: yarn test 27 | env: 28 | CI: true 29 | 30 | - name: Extract version 31 | id: version 32 | uses: olegtarasov/get-tag@v2.1 33 | with: 34 | tagRegex: 'v(.*)' 35 | 36 | - name: Set version from release 37 | uses: reedyuk/npm-version@1.0.1 38 | with: 39 | version: ${{ steps.version.outputs.tag }} 40 | git-tag-version: false 41 | 42 | - name: Create NPM config 43 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 44 | env: 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | 47 | - name: Publish package 48 | run: npm publish 49 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": ["**/*.test.ts", "**/*.test.tsx"], 5 | "env": { 6 | "jest": true 7 | }, 8 | "rules": { 9 | "import/no-extraneous-dependencies": "off" 10 | } 11 | }, 12 | { 13 | "files": ["**/*.ts", "**/*.tsx"], 14 | "extends": [ 15 | "plugin:@typescript-eslint/eslint-plugin/recommended", 16 | "plugin:@typescript-eslint/eslint-plugin/eslint-recommended" 17 | ], 18 | "rules": { 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | "@typescript-eslint/no-use-before-define": "off" 22 | } 23 | } 24 | ], 25 | "rules": { 26 | "import/extensions": ["off", "ignorePackages", "error"], 27 | "@typescript-eslint/no-use-before-define": "off", 28 | "@typescript-eslint/explicit-function-return-type": "off", 29 | "no-use-before-define": "off", 30 | "unicorn/no-null": "off", 31 | "no-magic-numbers": "off", 32 | "unicorn/number-literal-case": "off" 33 | }, 34 | "env": { 35 | "browser": true 36 | }, 37 | "extends": [ 38 | "@eslint-kit/base", 39 | "@eslint-kit/typescript", 40 | "@eslint-kit/node", 41 | "@eslint-kit/prettier" 42 | ], 43 | "parser": "@typescript-eslint/parser" 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import pluginResolve from '@rollup/plugin-node-resolve'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import { babel } from '@rollup/plugin-babel'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | import typescript from '@rollup/plugin-typescript'; 7 | import Package from './package.json'; 8 | 9 | const extensions = ['.mjs', '.tsx', '.ts', '.js', '.json']; 10 | 11 | function createBuild(input, format) { 12 | return { 13 | input: resolve(__dirname, `src/${input}.ts`), 14 | output: { 15 | file: `${input}.${format === 'esm' ? 'mjs' : 'js'}`, 16 | format, 17 | plugins: [terser()], 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | pluginResolve({ extensions }), 22 | commonjs({ extensions: ['.js'] }), 23 | typescript({ 24 | tsconfig: './tsconfig.build.json', 25 | }), 26 | babel({ 27 | babelHelpers: 'bundled', 28 | extensions, 29 | skipPreflightCheck: true, 30 | babelrc: false, 31 | ...require('./babel.config').generateConfig({ 32 | isEsm: format === 'esm', 33 | }), 34 | }), 35 | ].filter(Boolean), 36 | external: ['forest/forest.mjs', 'effector/effector.mjs'].concat( 37 | Object.keys(Package.peerDependencies), 38 | Object.keys(Package.dependencies), 39 | ), 40 | }; 41 | } 42 | 43 | const inputs = ['index']; 44 | const formats = ['cjs', 'esm']; 45 | 46 | const config = inputs.flatMap((i) => formats.map((f) => createBuild(i, f))); 47 | 48 | export default config; 49 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { css } from '../foliage'; 4 | import { component } from '../react'; 5 | 6 | const button = css` 7 | --main: black; 8 | --text: white; 9 | padding: 1rem 2rem; 10 | border: 2px solid gray; 11 | border-radius: 1rem; 12 | color: var(--text); 13 | background-color: var(--main); 14 | appearance: none; 15 | 16 | &:hover { 17 | box-shadow: 0 0 15px -3px black; 18 | } 19 | `; 20 | 21 | const Button = component('button', button, { 22 | defaults: { color: 'default' }, 23 | variants: { 24 | color: { 25 | primary: css` 26 | --main: blue; 27 | --text: white; 28 | `, 29 | default: css` 30 | --main: gray; 31 | --text: black; 32 | `, 33 | }, 34 | }, 35 | }); 36 | 37 | const primary = css` 38 | background-color: black; 39 | color: white; 40 | padding: 1rem 2rem; 41 | 42 | ${button} { 43 | --main: red; 44 | --text: white; 45 | } 46 | 47 | ${button}:hover { 48 | --main: pink; 49 | --text: black; 50 | } 51 | 52 | ${button} + ${button} { 53 | margin-left: 15px; 54 | } 55 | `; 56 | 57 | const Wrapper = component('div', [primary]); 58 | 59 | const vertical = css` 60 | display: flex; 61 | flex-flow: column; 62 | max-width: 20rem; 63 | 64 | & > * + * { 65 | margin-top: 1rem; 66 | } 67 | `; 68 | 69 | const Vertical = component('div', [vertical]); 70 | 71 | const root = document.querySelector('#root'); 72 | const head = document.querySelector('head'); 73 | 74 | const App = () => ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | 84 | ReactDOM.render(, root); 85 | -------------------------------------------------------------------------------- /src/elements.ts: -------------------------------------------------------------------------------- 1 | export const domElements = [ 2 | 'a', 3 | 'abbr', 4 | 'address', 5 | 'area', 6 | 'article', 7 | 'aside', 8 | 'audio', 9 | 'b', 10 | 'base', 11 | 'bdi', 12 | 'bdo', 13 | // 'big', 14 | 'blockquote', 15 | 'body', 16 | 'br', 17 | 'button', 18 | 'canvas', 19 | 'caption', 20 | 'cite', 21 | 'code', 22 | 'col', 23 | 'colgroup', 24 | 'data', 25 | 'datalist', 26 | 'dd', 27 | 'del', 28 | 'details', 29 | 'dfn', 30 | 'dialog', 31 | 'div', 32 | 'dl', 33 | 'dt', 34 | 'em', 35 | 'embed', 36 | 'fieldset', 37 | 'figcaption', 38 | 'figure', 39 | 'footer', 40 | 'form', 41 | 'h1', 42 | 'h2', 43 | 'h3', 44 | 'h4', 45 | 'h5', 46 | 'h6', 47 | 'head', 48 | 'header', 49 | 'hgroup', 50 | 'hr', 51 | 'html', 52 | 'i', 53 | 'iframe', 54 | 'img', 55 | 'input', 56 | 'ins', 57 | 'kbd', 58 | // 'keygen', 59 | 'label', 60 | 'legend', 61 | 'li', 62 | 'link', 63 | 'main', 64 | 'map', 65 | 'mark', 66 | 'marquee', 67 | 'menu', 68 | // 'menuitem', 69 | 'meta', 70 | 'meter', 71 | 'nav', 72 | 'noscript', 73 | 'object', 74 | 'ol', 75 | 'optgroup', 76 | 'option', 77 | 'output', 78 | 'p', 79 | 'param', 80 | 'picture', 81 | 'pre', 82 | 'progress', 83 | 'q', 84 | 'rp', 85 | 'rt', 86 | 'ruby', 87 | 's', 88 | 'samp', 89 | 'script', 90 | 'section', 91 | 'select', 92 | 'small', 93 | 'source', 94 | 'span', 95 | 'strong', 96 | 'style', 97 | 'sub', 98 | 'summary', 99 | 'sup', 100 | 'table', 101 | 'tbody', 102 | 'td', 103 | 'textarea', 104 | 'tfoot', 105 | 'th', 106 | 'thead', 107 | 'time', 108 | 'title', 109 | 'tr', 110 | 'track', 111 | 'u', 112 | 'ul', 113 | 'var', 114 | 'video', 115 | 'wbr', 116 | 117 | // SVG 118 | 'circle', 119 | 'clipPath', 120 | 'defs', 121 | 'ellipse', 122 | 'foreignObject', 123 | 'g', 124 | 'image', 125 | 'line', 126 | 'linearGradient', 127 | 'marker', 128 | 'mask', 129 | 'path', 130 | 'pattern', 131 | 'polygon', 132 | 'polyline', 133 | 'radialGradient', 134 | 'rect', 135 | 'stop', 136 | 'svg', 137 | 'text', 138 | 'tspan', 139 | ] as const; 140 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { resolve: resolvePath } = require('path'); 2 | 3 | module.exports = (api) => { 4 | api && api.cache && api.cache.never && api.cache.never(); 5 | // const env = api.cache(() => process.env.NODE_ENV) 6 | return generateConfig(meta, babelConfig); 7 | }; 8 | 9 | const meta = { 10 | isEsm: true, 11 | }; 12 | 13 | function generateConfig(meta, config = babelConfig) { 14 | const result = {}; 15 | for (const key in config) { 16 | const value = config[key]; 17 | result[key] = typeof value === 'function' ? value(meta) : value; 18 | } 19 | return result; 20 | } 21 | 22 | module.exports.generateConfig = generateConfig; 23 | 24 | const aliases = { 25 | effector: { 26 | esm: 'effector/effector.mjs', 27 | }, 28 | forest: { 29 | esm: 'forest/forest.mjs', 30 | }, 31 | stylis: { 32 | esm: 'stylis/dist/stylis.mjs', 33 | }, 34 | }; 35 | 36 | const babelConfig = { 37 | presets: [ 38 | '@babel/preset-env', 39 | '@babel/preset-typescript', 40 | '@babel/preset-react', 41 | ], 42 | plugins: [ 43 | // [ 44 | // './babel-plugin.js', 45 | // { 46 | // allowedModules: ['foliage', '../../react'], 47 | // }, 48 | // ], 49 | ], 50 | // plugins(meta) { 51 | // const alias = parseAliases(meta, aliases); 52 | // return [ 53 | // // [ 54 | // // require.resolve('./babel-plugin.js'), 55 | // // { 56 | // // allowedModules: ['../foliage', 'foliage', '../../react'], 57 | // // debug: false, 58 | // // }, 59 | // // ], 60 | // // ['effector/babel-plugin', { addLoc: true }], 61 | // // [ 62 | // // 'babel-plugin-module-resolver', 63 | // // { 64 | // // alias: { 65 | // // ...alias, 66 | // // foliage: './react', 67 | // // }, 68 | // // loglevel: 'silent', 69 | // // }, 70 | // // ], 71 | // ]; 72 | // }, 73 | }; 74 | 75 | // eslint-disable-next-line sonarjs/cognitive-complexity 76 | function parseAliases(meta, object) { 77 | const result = {}; 78 | for (const key in object) { 79 | const value = object[key]; 80 | if (typeof value === 'function') { 81 | const name = value(meta); 82 | if (name === undefined || name === null) continue; 83 | result[key] = name; 84 | } else if (typeof value === 'object' && value !== null) { 85 | const name = applyPaths(value); 86 | if (name === undefined || name === null) continue; 87 | result[key] = name; 88 | } else { 89 | const name = value; 90 | if (name === undefined || name === null) continue; 91 | result[key] = name; 92 | } 93 | } 94 | return result; 95 | 96 | function applyPaths(paths) { 97 | if (meta.isEsm) return paths.esm; 98 | return paths.default; 99 | } 100 | } 101 | 102 | module.exports.getAliases = (metadata = meta) => 103 | parseAliases(metadata, aliases); 104 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector'; 2 | import { DOMTag, h, node, spec } from 'forest'; 3 | import * as stylis from './stylis'; 4 | 5 | import { domElements } from './elements'; 6 | 7 | const addStyle = createEvent<{ id: string; styles: string }>(); 8 | const $styles = createStore<{ map: Map }>({ map: new Map() }); 9 | 10 | export function StyledRoot(): void { 11 | const text = $styles.map(({ map }) => [...map.values()].join(' ')); 12 | h('style', { text }); 13 | } 14 | 15 | $styles.on(addStyle, (state, { id, styles }) => { 16 | if (state.map.has(id)) return state; 17 | state.map.set(id, make(id, styles)); 18 | return { map: state.map }; 19 | }); 20 | 21 | function make(id: string, styles: string) { 22 | return stylis.serialize( 23 | stylis.compile(`.es-${id} { ${styles} }`), 24 | stylis.stringify, 25 | ); 26 | } 27 | 28 | const idCount = () => { 29 | let id = 9005000; 30 | return () => (++id).toString(36); 31 | }; 32 | 33 | const styledId = idCount(); 34 | 35 | type Callback = () => void; 36 | 37 | export type FunctionComponent = (config: Spec | Callback) => void; 38 | 39 | export type Component = FunctionComponent & { 40 | STYLED_ID: string; 41 | }; 42 | 43 | // eslint-disable-next-line @typescript-eslint/ban-types 44 | function hasStyledId(fn: object | Function): fn is { STYLED_ID: string } { 45 | // eslint-disable-next-line dot-notation 46 | return typeof fn['STYLED_ID'] === 'string'; 47 | } 48 | 49 | function join( 50 | strings: TemplateStringsArray, 51 | interps: (string | FunctionComponent | Component | number)[], 52 | ) { 53 | const result = [strings[0]]; 54 | interps.forEach((part, index) => { 55 | if (typeof part === 'function') { 56 | if (hasStyledId(part)) { 57 | result.push(`.es-${part.STYLED_ID}`); 58 | } else { 59 | throw new TypeError('Passed not an effector styled component'); 60 | } 61 | } else { 62 | result.push(String(part)); 63 | } 64 | result.push(strings[index + 1]); 65 | }); 66 | 67 | return result.join(''); 68 | } 69 | 70 | export type Spec = Parameters[0] & { fn?: Callback }; 71 | 72 | type Creator = ( 73 | content: TemplateStringsArray, 74 | ...interpolations: (string | Component | number)[] 75 | ) => Component; 76 | 77 | type TagFabric = (tag: DOMTag) => Creator; 78 | 79 | type TagMap = { 80 | [P in DOMTag]: Creator; 81 | }; 82 | 83 | const fabric: TagFabric & Partial = 84 | (tag: DOMTag) => 85 | (content, ...interpolations) => { 86 | const id = styledId(); 87 | 88 | const styles = join(content, interpolations); 89 | 90 | const Component = (config: Spec | Callback) => { 91 | addStyle({ id, styles }); 92 | 93 | h(tag, () => { 94 | node((reference) => { 95 | reference.classList.add(`es-${id}`); 96 | }); 97 | if (config) { 98 | if (typeof config === 'function') { 99 | config(); 100 | } else { 101 | spec(config); 102 | if (typeof config.fn === 'function') { 103 | config.fn(); 104 | } 105 | } 106 | } 107 | }); 108 | }; 109 | 110 | Component.STYLED_ID = id; 111 | 112 | return Component; 113 | }; 114 | 115 | domElements.forEach((element) => { 116 | fabric[element] = fabric(element); 117 | }); 118 | 119 | export const styled = fabric as TagFabric & TagMap; 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,windows,linux,macos 3 | # Edit at https://www.gitignore.io/?templates=node,windows,linux,macos 4 | ### Linux ### 5 | *~ 6 | # temporary files which can be created if a process still has a handle open of a deleted file 7 | .fuse_hidden* 8 | # KDE directory preferences 9 | .directory 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | # .nfs files are created when an open file is removed but is still being accessed 13 | .nfs* 14 | ### macOS ### 15 | # General 16 | .DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | # Icon must end with two \r 20 | Icon 21 | # Thumbnails 22 | ._* 23 | # Files that might appear in the root of a volume 24 | .DocumentRevisions-V100 25 | .fseventsd 26 | .Spotlight-V100 27 | .TemporaryItems 28 | .Trashes 29 | .VolumeIcon.icns 30 | .com.apple.timemachine.donotpresent 31 | # Directories potentially created on remote AFP share 32 | .AppleDB 33 | .AppleDesktop 34 | Network Trash Folder 35 | Temporary Items 36 | .apdisk 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | # Diagnostic reports (https://nodejs.org/api/report.html) 46 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | # Directory for instrumented libs generated by jscoverage/JSCover 53 | lib-cov 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | # nyc test coverage 58 | .nyc_output 59 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 60 | .grunt 61 | # Bower dependency directory (https://bower.io/) 62 | bower_components 63 | # node-waf configuration 64 | .lock-wscript 65 | # Compiled binary addons (https://nodejs.org/api/addons.html) 66 | build/Release 67 | # Dependency directories 68 | node_modules/ 69 | jspm_packages/ 70 | # TypeScript v1 declaration files 71 | typings/ 72 | # TypeScript cache 73 | *.tsbuildinfo 74 | # Optional npm cache directory 75 | .npm 76 | # Optional eslint cache 77 | .eslintcache 78 | # Optional REPL history 79 | .node_repl_history 80 | # Output of 'npm pack' 81 | *.tgz 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | # next.js build output 90 | .next 91 | # nuxt.js build output 92 | .nuxt 93 | # rollup.js default build output 94 | dist/ 95 | # Uncomment the public line if your project uses Gatsby 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 98 | # public 99 | # Storybook build outputs 100 | .out 101 | .storybook-out 102 | # vuepress build output 103 | .vuepress/dist 104 | # Serverless directories 105 | .serverless/ 106 | # FuseBox cache 107 | .fusebox/ 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | # Temporary folders 111 | tmp/ 112 | temp/ 113 | ### Windows ### 114 | # Windows thumbnail cache files 115 | Thumbs.db 116 | Thumbs.db:encryptable 117 | ehthumbs.db 118 | ehthumbs_vista.db 119 | # Dump file 120 | *.stackdump 121 | # Folder config file 122 | [Dd]esktop.ini 123 | # Recycle Bin used on file shares 124 | $RECYCLE.BIN/ 125 | # Windows Installer files 126 | *.cab 127 | *.msi 128 | *.msix 129 | *.msm 130 | *.msp 131 | # Windows shortcuts 132 | *.lnk 133 | # End of https://www.gitignore.io/api/node,windows,linux,macos 134 | Icon 135 | /build 136 | !.vscode/extensions.json 137 | !.vscode/launch.json 138 | !.vscode/settings.json 139 | !.vscode/tasks.json 140 | .history 141 | .idea/ 142 | .vscode/* 143 | 144 | # Built package artifacts 145 | index.js 146 | index.js.map 147 | index.mjs 148 | index.mjs.map 149 | .jest-cache 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foliage", 3 | "version": "0.0.0-real-version-will-be-set-on-ci", 4 | "description": "Styled Components for forest", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "exports": { 8 | ".": { 9 | "import": "./index.mjs", 10 | "require": "./index.js", 11 | "default": "./index.mjs" 12 | }, 13 | "./index.mjs": "./index.mjs" 14 | }, 15 | "types": "dist/index.d.ts", 16 | "homepage": "https://foliage.dev", 17 | "sideEffects": false, 18 | "scripts": { 19 | "test": "jest", 20 | "commit": "git-cz", 21 | "lint": "eslint ./", 22 | "build": "tsc --build ./tsconfig.build.json && rollup --config rollup.config.js", 23 | "format": "prettier --write \"./src/**/**.{ts,tsx,js,jsx,json}\"", 24 | "start": "parcel example/index.html --no-cache", 25 | "prepublishOnly": "rm -rf dist && yarn build" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "repository": { 31 | "url": "https://github.com/foliage-ui/foliage" 32 | }, 33 | "files": ["index.js", "index.js.map", "index.mjs", "index.mjs.map", "dist"], 34 | "keywords": [ 35 | "components", 36 | "dom", 37 | "effector", 38 | "foliage", 39 | "forest", 40 | "style", 41 | "styled" 42 | ], 43 | "author": "Sergey Sova (https://sergeysova.com/)", 44 | "license": "MIT", 45 | "config": { 46 | "commitizen": { 47 | "path": "cz-conventional-changelog" 48 | } 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.16.0", 52 | "@babel/core": "^7.16.0", 53 | "@babel/preset-env": "^7.16.4", 54 | "@babel/preset-react": "^7.16.0", 55 | "@babel/preset-typescript": "^7.16.0", 56 | "@babel/types": "^7.16.0", 57 | "@commitlint/cli": "^8.3.5", 58 | "@commitlint/config-conventional": "^8.3.4", 59 | "@eslint-kit/eslint-config-base": "^3.0.0", 60 | "@eslint-kit/eslint-config-node": "^2.0.0", 61 | "@eslint-kit/eslint-config-prettier": "^2.0.0", 62 | "@eslint-kit/eslint-config-typescript": "^3.2.0", 63 | "@rollup/plugin-babel": "^5.3.0", 64 | "@rollup/plugin-commonjs": "^20.0.0", 65 | "@rollup/plugin-node-resolve": "^13.0.4", 66 | "@rollup/plugin-typescript": "^8.2.5", 67 | "@testing-library/jest-dom": "^5.15.0", 68 | "@testing-library/react": "^12.1.2", 69 | "@types/jest": "^25.2.1", 70 | "@types/js-beautify": "^1.13.3", 71 | "@types/node": "^13.13.5", 72 | "@types/react": "^17.0.20", 73 | "@types/react-dom": "^17.0.9", 74 | "@typescript-eslint/parser": "^4.4.1", 75 | "babel-jest": "^26.6.3", 76 | "babel-plugin-module-resolver": "^4.1.0", 77 | "babel-plugin-tester": "^10.0.0", 78 | "change-case": "^4.1.2", 79 | "commitizen": "^4.1.2", 80 | "cz-conventional-changelog": "^3.2.0", 81 | "effector": "^22.1.0", 82 | "eslint": "7.10.0", 83 | "forest": "^0.20.2", 84 | "husky": "^4.2.5", 85 | "jest": "^26.0.1", 86 | "js-beautify": "^1.14.0", 87 | "lint-staged": "^10.2.2", 88 | "parcel-bundler": "^1.12.4", 89 | "prettier": "^2.0.5", 90 | "react": "^17.0.2", 91 | "react-dom": "^17.0.2", 92 | "rollup": "^2.58.0", 93 | "rollup-plugin-terser": "^7.0.2", 94 | "sharec-sova-config": "^0.1.0", 95 | "terser-webpack-plugin": "^3.0.2", 96 | "ts-node": "^9.1.1", 97 | "typescript": "^4.5.2", 98 | "typescript-styled-plugin": "^0.15.0" 99 | }, 100 | "sharec": { 101 | "config": "sharec-sova-config", 102 | "version": "0.1.1" 103 | }, 104 | "dependencies": { 105 | "autoprefixer": "^10.2.4", 106 | "csso": "^4.2.0", 107 | "postcss": "^8.2.6", 108 | "postcss-nested": "^5.0.3" 109 | }, 110 | "peerDependencies": { 111 | "effector": "^22.1.0", 112 | "forest": "^0.20.2" 113 | }, 114 | "browserslist": { 115 | "development": ["last 1 ie version", "last 5 chrome version"] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /react.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const css: any = () => { 4 | throw new Error(`Looks like you didn't setup foliage/babel-plugin`); 5 | }; 6 | export const keyframes: any = () => { 7 | throw new Error(`Looks like you didn't setup foliage/babel-plugin`); 8 | }; 9 | export const createGlobalStyle: any = () => { 10 | throw new Error(`Looks like you didn't setup foliage/babel-plugin`); 11 | }; 12 | 13 | export const assertKeyframe = (input: any) => { 14 | if (input.keyframes) { 15 | return input; 16 | } 17 | throw new Error( 18 | 'Looks like you passed wrong element to an "animation" property', 19 | ); 20 | }; 21 | 22 | export const assertVariable = (input: any) => { 23 | return input; 24 | // throw new Error('Looks like you passed wrong element as css-custom-property'); 25 | }; 26 | 27 | interface BlockCSS { 28 | content: string; 29 | css: string; 30 | } 31 | 32 | interface ComponentConfig { 33 | component?: React.FC; 34 | children?: React.FC; 35 | mapVariants?: Record any>; 36 | variants?: Record>; 37 | compound?: Array<{ css: BlockCSS } & Record>; 38 | defaults?: Record; 39 | } 40 | 41 | interface ComponentProps { 42 | className?: string; 43 | } 44 | 45 | export const Global: React.FC<{ styles: any[] | any }> = ({ styles }) => { 46 | React.useEffect(() => { 47 | if (Array.isArray(styles)) { 48 | styles.forEach((style) => add(style)); 49 | } else { 50 | add(styles); 51 | } 52 | }, [styles]); 53 | return null; 54 | }; 55 | 56 | export function component( 57 | tag: string, // eslint-disable-line @typescript-eslint/naming-convention 58 | styles: BlockCSS | BlockCSS[], 59 | { 60 | component: _c = () => null, 61 | children: _h = () => null, 62 | mapVariants = {}, 63 | variants = {}, 64 | compound = [], 65 | defaults = {}, 66 | }: ComponentConfig = {}, 67 | ): React.FC> { 68 | const styleClasses = toArray(styles).map((style) => style.css); 69 | 70 | return ({ className, children, ...props }) => { 71 | const mainRef = React.useRef(null); 72 | 73 | React.useEffect(() => { 74 | toArray(styles).forEach((block) => add(block)); 75 | }, [...styleClasses]); 76 | 77 | React.useEffect(() => { 78 | if (isString(className)) mainRef.current?.classList.add(className); 79 | styleClasses.forEach((css) => mainRef.current?.classList.add(css)); 80 | }, [className]); 81 | 82 | React.useEffect(() => { 83 | Object.keys(variants).forEach((variantName) => { 84 | const propValue = props[variantName] ?? defaults[variantName]; 85 | const mapper = mapVariants[variantName] ?? id; 86 | const variantCase = mapper(propValue); 87 | const cssBlock = variants[variantName][variantCase]; 88 | if (cssBlock) { 89 | mainRef.current?.classList.add(cssBlock.css); 90 | add(cssBlock); 91 | } 92 | }); 93 | }, [variants, props, defaults, mapVariants]); 94 | 95 | React.useEffect(() => { 96 | compound.forEach(({ css: cssBlock, ...matchers }) => { 97 | const isMatched = Object.keys(matchers).every((variantName) => { 98 | const expectedVariantCase = matchers[variantName]; 99 | const propValue = props[variantName] ?? defaults[variantName]; 100 | const mapper = mapVariants[variantName] ?? id; 101 | const variantCase = mapper(propValue); 102 | return expectedVariantCase === variantCase; 103 | }); 104 | if (isMatched) { 105 | mainRef.current?.classList.add(cssBlock.css); 106 | add(cssBlock); 107 | } 108 | }); 109 | }, [compound, props, defaults, mapVariants]); 110 | 111 | const Tag = tag as unknown as React.FC<{ className?: string; ref?: any }>; 112 | return {children}; 113 | }; 114 | } 115 | 116 | function toArray(maybe: T | T[]): T[] { 117 | return Array.isArray(maybe) ? maybe : [maybe]; 118 | } 119 | 120 | function id(value: T): T { 121 | return value; 122 | } 123 | 124 | function isString(value: unknown): value is string { 125 | return typeof value === 'string'; 126 | } 127 | 128 | function add(cssBlock: BlockCSS) { 129 | if (!document.querySelector(`[data-foliage="${cssBlock.css}"]`)) { 130 | const style = document.createElement('style'); 131 | style.dataset.foliage = cssBlock.css; 132 | style.textContent = cssBlock.content; 133 | document.head.append(style); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # foliage 🍃 2 | 3 | 4 | 5 | [GitHub](https://github.com/effector/foliage) [dev.to](https://dev.to/foliage) 6 | 7 | ## Usage with React 8 | 9 | > Work in progress. Most of this examples are just concept 10 | 11 | 12 | 13 | ```ts 14 | import { css, component } from 'foliage-react'; 15 | 16 | const button = css` 17 | display: inline-block; 18 | border-radius: 3px; 19 | padding: 0.5rem 0; 20 | margin: 0.5rem 1rem; 21 | width: 11rem; 22 | border: 2px solid white; 23 | `; 24 | 25 | export const Button = component('a', button, { 26 | defaults: { color: 'default' }, 27 | variants: { 28 | color: { 29 | primary: css` 30 | background: white; 31 | color: black; 32 | `, 33 | default: css` 34 | background: transparent; 35 | color: white; 36 | `, 37 | }, 38 | }, 39 | }); 40 | ``` 41 | 42 | ### Extending styles 43 | 44 | ```ts 45 | import { css, component } from 'foliage-react'; 46 | 47 | const button = css` 48 | color: palevioletred; 49 | font-size: 1em; 50 | margin: 1em; 51 | padding: 0.25em 1em; 52 | border: 2px solid palevioletred; 53 | border-radius: 3px; 54 | `; 55 | 56 | export const Button = component('button', button); 57 | 58 | const tomato = css` 59 | color: tomato; 60 | border-color: tomato; 61 | `; 62 | 63 | export const TomatoButton = component('button', [button, tomato]); 64 | ``` 65 | 66 | ### Pseudoelement, pseudoselectors, and nesting 67 | 68 | ```tsx 69 | import { css, component } from 'foliage-react'; 70 | 71 | const thing = css` 72 | color: blue; 73 | 74 | &:hover { 75 | color: red; // when hovered 76 | } 77 | 78 | & ~ & { 79 | background: tomato; // as a sibling of , but maybe not directly next to it 80 | } 81 | 82 | & + & { 83 | background: lime; // next to 84 | } 85 | 86 | &.something { 87 | background: orange; // tagged with an additional CSS class ".something" 88 | } 89 | 90 | .something-else & { 91 | border: 1px solid; // inside another element labeled ".something-else" 92 | } 93 | `; 94 | 95 | export const Thing = component('div', thing, { attrs: { tabIndex: 0 } }); 96 | 97 | const Example = () => ( 98 | <> 99 | Hello world! 100 | How ya doing? 101 | The sun is shining... 102 |
Pretty nice day today.
103 | Don't you think? 104 |
105 | Splendid. 106 |
107 | 108 | ); 109 | ``` 110 | 111 | ### Animations 112 | 113 | ```ts 114 | import { css, keyframes, component } from 'foliage-react'; 115 | 116 | const rotate = keyframes` 117 | from { transform: rotate(0deg) } 118 | to { transform: rotate(360deg) } 119 | `; 120 | 121 | const block = css` 122 | display: inline-block; 123 | animation: ${rotate} 2s linear infinite; 124 | padding: 2rem 1rem; 125 | font-size: 1.2rem; 126 | `; 127 | 128 | export const Block = component('div', block); 129 | ``` 130 | 131 | ### Theming 132 | 133 | ```tsx 134 | import { css, keyframes, createGlobalStyle, Global } from 'foliage-react'; 135 | const theme = { 136 | main: '--theme-main', 137 | }; 138 | 139 | const button = css` 140 | font-size: 1em; 141 | margin: 1em; 142 | padding: 0.25em 1em; 143 | border-radius: 3px; 144 | 145 | /* Color the border and text with theme.main */ 146 | color: var(${theme.main}); 147 | border: 2px solid var(${theme.main}); 148 | `; 149 | 150 | const Button = component('button', button); 151 | 152 | const primaryTheme = createGlobalStyle` 153 | :root { 154 | ${theme.main}: palevioletred; 155 | } 156 | `; 157 | 158 | const secondaryTheme = createGlobalStyle` 159 | :root { 160 | ${theme.main}: mediumseagreen; 161 | } 162 | `; 163 | 164 | const Example = () => ( 165 | <> 166 | 167 | 74 | 75 | " 76 | `); 77 | }); 78 | 79 | test('variants from readme', () => { 80 | const button = css` 81 | display: inline-block; 82 | border-radius: 3px; 83 | padding: 0.5rem 0; 84 | margin: 0.5rem 1rem; 85 | width: 11rem; 86 | border: 2px solid white; 87 | `; 88 | 89 | const Button = component('a', button, { 90 | defaults: { color: 'default' }, 91 | variants: { 92 | color: { 93 | primary: css` 94 | background: white; 95 | color: black; 96 | `, 97 | default: css` 98 | background: transparent; 99 | color: white; 100 | `, 101 | }, 102 | }, 103 | }); 104 | 105 | const defaultOption = render(); 106 | expect(getCSS(document)).toMatchInlineSnapshot(` 107 | ".f89xayi-button { 108 | display: inline-block; 109 | border-radius: 3px; 110 | padding: .5rem 0; 111 | margin: .5rem 1rem; 112 | width: 11rem; 113 | border: 2px solid #fff 114 | } 115 | 116 | .f-tt0vsb-Button-variants-color-default { 117 | background: 0 0; 118 | color: #fff 119 | }" 120 | `); 121 | expect(printDOM(defaultOption.baseElement)).toMatchInlineSnapshot(` 122 | " 123 | 130 | " 131 | `); 132 | defaultOption.unmount(); 133 | 134 | const changedOption = render(); 135 | expect(getCSS(document)).toMatchInlineSnapshot(` 136 | ".f89xayi-button { 137 | display: inline-block; 138 | border-radius: 3px; 139 | padding: .5rem 0; 140 | margin: .5rem 1rem; 141 | width: 11rem; 142 | border: 2px solid #fff 143 | } 144 | 145 | .f-tt0vsb-Button-variants-color-default { 146 | background: 0 0; 147 | color: #fff 148 | } 149 | 150 | .f-k92sfa-Button-variants-color-primary { 151 | background: #fff; 152 | color: #000 153 | }" 154 | `); 155 | expect(printDOM(changedOption.baseElement)).toMatchInlineSnapshot(` 156 | " 157 |
158 | 165 | " 166 | `); 167 | }); 168 | 169 | test('extending variants', () => { 170 | const button = css` 171 | color: palevioletred; 172 | font-size: 1em; 173 | margin: 1em; 174 | padding: 0.25em 1em; 175 | border: 2px solid palevioletred; 176 | border-radius: 3px; 177 | `; 178 | 179 | const Button = component('button', button); 180 | 181 | const tomato = css` 182 | color: tomato; 183 | border-color: tomato; 184 | `; 185 | 186 | const TomatoButton = component('button', [button, tomato]); 187 | 188 | const result = render( 189 | <> 190 | 191 | Extended 192 | , 193 | ); 194 | 195 | expect(getCSS(document)).toMatchInlineSnapshot(` 196 | ".fhjc0iy-button { 197 | color: #db7093; 198 | font-size: 1em; 199 | margin: 1em; 200 | padding: .25em 1em; 201 | border: 2px solid #db7093; 202 | border-radius: 3px 203 | } 204 | 205 | .f2d2unm-tomato { 206 | color: tomato; 207 | border-color: tomato 208 | }" 209 | `); 210 | expect(printDOM(result.baseElement)).toMatchInlineSnapshot(` 211 | " 212 |
213 | 218 | 223 |
224 | " 225 | `); 226 | }); 227 | 228 | test('preudoelement, pseudoselectors, nesting', () => { 229 | const thing = css` 230 | color: blue; 231 | 232 | &:hover { 233 | color: red; 234 | } 235 | 236 | & ~ & { 237 | background: tomato; 238 | } 239 | 240 | & + & { 241 | background: lime; 242 | } 243 | 244 | &.something { 245 | background: orange; 246 | } 247 | 248 | .something-else & { 249 | border: 1px solid; 250 | } 251 | `; 252 | 253 | const Thing = component('div', thing); 254 | 255 | const Example = () => ( 256 | <> 257 | Hello world! 258 | How ya doing? 259 | The sun is shining... 260 |
Pretty nice day today.
261 | Don't you think? 262 |
263 | Splendid. 264 |
265 | 266 | ); 267 | 268 | const result = render(); 269 | expect(getCSS(document)).toMatchInlineSnapshot(` 270 | ".f-j90c5c-thing { 271 | color: #00f 272 | } 273 | 274 | .f-j90c5c-thing:hover { 275 | color: red 276 | } 277 | 278 | .f-j90c5c-thing~.f-j90c5c-thing { 279 | background: tomato 280 | } 281 | 282 | .f-j90c5c-thing+.f-j90c5c-thing { 283 | background: #0f0 284 | } 285 | 286 | .f-j90c5c-thing.something { 287 | background: orange 288 | } 289 | 290 | .something-else .f-j90c5c-thing { 291 | border: 1px solid 292 | }" 293 | `); 294 | expect(printDOM(result.baseElement)).toMatchInlineSnapshot(` 295 | " 296 |
297 |
300 | Hello world! 301 |
302 |
305 | How ya doing? 306 |
307 |
310 | The sun is shining... 311 |
312 |
313 | Pretty nice day today. 314 |
315 |
318 | Don't you think? 319 |
320 |
323 |
326 | Splendid. 327 |
328 |
329 |
330 | " 331 | `); 332 | }); 333 | 334 | test('animations', () => { 335 | const rotate = keyframes` 336 | from { transform: rotate(0deg) } 337 | to { transform: rotate(360deg) } 338 | `; 339 | 340 | const block = css` 341 | display: inline-block; 342 | animation: ${rotate} 2s linear infinite; 343 | padding: 2rem 1rem; 344 | font-size: 1.2rem; 345 | `; 346 | 347 | const Block = component('div', block); 348 | 349 | const result = render(); 350 | 351 | // TODO: keyframes should be appended to a styles 352 | expect(getCSS(document)).toMatchInlineSnapshot(` 353 | ".f-a6lm45-block { 354 | display: inline-block; 355 | -webkit-animation: [object Object] 2s linear infinite; 356 | animation: [object Object] 2s linear infinite; 357 | padding: 2rem 1rem; 358 | font-size: 1.2rem 359 | }" 360 | `); 361 | expect(printDOM(result.baseElement)).toMatchInlineSnapshot(` 362 | " 363 |
364 |
367 |
368 | " 369 | `); 370 | }); 371 | 372 | test('theming and globalStyle', () => { 373 | const theme = { 374 | main: '--theme-main', 375 | }; 376 | 377 | const button = css` 378 | font-size: 1em; 379 | margin: 1em; 380 | padding: 0.25em 1em; 381 | border-radius: 3px; 382 | 383 | /* Color the border and text with theme.main */ 384 | color: var(${theme.main}); 385 | border: 2px solid var(${theme.main}); 386 | `; 387 | 388 | const Button = component('button', button); 389 | 390 | const primaryTheme = createGlobalStyle` 391 | :root { 392 | ${theme.main}: palevioletred; 393 | } 394 | `; 395 | 396 | const Example = () => ( 397 | <> 398 | 399 |
426 | " 427 | `); 428 | }); 429 | 430 | test('composable components', () => { 431 | const baseStyles = css` 432 | color: white; 433 | background-color: mediumseagreen; 434 | border-radius: 4px; 435 | `; 436 | 437 | const Button = component('button', baseStyles, { 438 | variants: { 439 | color: { 440 | gray: css` 441 | background-color: gainsboro; 442 | `, 443 | blue: css` 444 | background-color: dodgerblue; 445 | `, 446 | }, 447 | size: { 448 | md: css` 449 | height: 26px; 450 | `, 451 | lg: css` 452 | height: 36px; 453 | `, 454 | }, 455 | }, 456 | compound: [ 457 | { 458 | color: 'blue', 459 | size: 'lg', 460 | css: css` 461 | background-color: purple; 462 | `, 463 | }, 464 | ], 465 | defaults: { 466 | color: 'gray', 467 | size: 'md', 468 | }, 469 | }); 470 | 471 | const defaultOptions = render(
494 | " 495 | `); 496 | defaultOptions.unmount(); 497 | 498 | const compoundOptions = render(