├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── integration.js └── integration.ts ├── babel-plugin.js ├── benchmark ├── .babelrc ├── .gitignore ├── index.js ├── mocks │ └── react-native.js ├── package.json ├── tests.js ├── webpack.config.js └── yarn.lock ├── compiler ├── babel │ ├── __testUtil__ │ │ └── index.ts │ ├── __tests__ │ │ ├── animations.ts │ │ ├── basic.ts │ │ ├── buildCondition.ts │ │ ├── interpolation.ts │ │ ├── mediaQueries.ts │ │ ├── mixed.ts │ │ ├── mixin-include.ts │ │ ├── mixin.ts │ │ ├── transform.ts │ │ ├── transitions.ts │ │ ├── variables.ts │ │ └── viewport.ts │ ├── buildCondition.ts │ ├── buildElement.ts │ ├── buildMixin.ts │ ├── environment │ │ ├── index.ts │ │ ├── useColorScheme.ts │ │ ├── useMediaQuery.ts │ │ └── usePlatform.ts │ ├── extractSubstitutionMap.ts │ ├── forwardRefComponent.ts │ ├── returnElement.ts │ ├── style.ts │ ├── styleSheet │ │ ├── __tests__ │ │ │ └── styleBody.ts │ │ ├── index.ts │ │ ├── simpleUnitTypes.ts │ │ ├── styleBody.ts │ │ ├── styleTuples.ts │ │ ├── substituteSimpleUnit.ts │ │ ├── substitutionUtil.ts │ │ ├── util.ts │ │ └── viewport.ts │ ├── useAnimation.ts │ ├── useCustomProperties.ts │ ├── useKeyframes.ts │ ├── useTransition.ts │ └── util.ts ├── css │ ├── __tests__ │ │ ├── applyGlobals.ts │ │ ├── extractRules.ts │ │ └── getRoot.ts │ ├── applyGlobals.ts │ ├── extractRules.ts │ ├── getRoot.ts │ ├── types.ts │ └── util.ts └── options.ts ├── docs ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _includes │ └── sidebar-link.html ├── _layouts │ └── page.html ├── assets │ └── style.css ├── configuration.md ├── css-support.md ├── custom-properties.md ├── editor-integration.md ├── index.md ├── interpolation.md ├── media-queries.md ├── mixins.md ├── run-local.sh ├── technical-details.md ├── theming.md └── transitions-animations.md ├── native.d.ts ├── native.macro.js ├── package.json ├── runtime ├── VariablesContext.ts ├── __mocks__ │ └── react-native.ts ├── __tests__ │ ├── animationUtil.ts │ ├── resolveVariableDependencies.ts │ ├── useAnimation.ts │ ├── useCustomPropertyStyle.ts │ └── useTransition.ts ├── animationShorthandUtil.ts ├── animationTypes.ts ├── animationUtil.ts ├── bloomFilter.ts ├── css-transforms │ ├── __tests__ │ │ ├── colors.ts │ │ ├── variables.ts │ │ └── viewport.ts │ ├── calc.ts │ ├── colors.ts │ ├── variables.ts │ └── viewport.ts ├── cssRegExp.ts ├── cssUtil.ts ├── flattenAnimation.ts ├── flattenTransition.ts ├── resolveVariableDependencies.ts ├── useAnimation.ts ├── useCustomProperties.ts ├── useCustomPropertyShorthandParts.ts ├── useCustomPropertyStyle.ts ├── useTransition.ts ├── useViewportStyle.ts ├── useViewportStyleTuples.ts └── useWindowDimensions.ts ├── tsconfig.json ├── types └── css-color-function.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "current" } }], 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | parser: "@typescript-eslint/parser", 13 | plugins: ["@typescript-eslint"], 14 | globals: { 15 | Atomics: "readonly", 16 | SharedArrayBuffer: "readonly" 17 | }, 18 | parserOptions: { 19 | ecmaVersion: 2018, 20 | sourceType: "module" 21 | }, 22 | rules: { 23 | "@typescript-eslint/explicit-function-return-type": [0], 24 | "@typescript-eslint/no-explicit-any": [0] 25 | }, 26 | overrides: [ 27 | { 28 | files: ["**/__tests__/*.js"], 29 | env: { 30 | jest: true 31 | } 32 | } 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ########## 2 | # GITHUB # 3 | ########## 4 | 5 | # See http://help.github.com/ignore-files/ for more about ignoring files. 6 | 7 | # dependencies 8 | node_modules 9 | 10 | # testing 11 | coverage 12 | 13 | # production 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | npm-debug.log 19 | 20 | ################ 21 | # REACT NATIVE # 22 | ################ 23 | 24 | # OSX 25 | # 26 | .DS_Store 27 | 28 | # Xcode 29 | # 30 | build/ 31 | *.pbxuser 32 | !default.pbxuser 33 | *.mode1v3 34 | !default.mode1v3 35 | *.mode2v3 36 | !default.mode2v3 37 | *.perspectivev3 38 | !default.perspectivev3 39 | xcuserdata 40 | *.xccheckout 41 | *.moved-aside 42 | DerivedData 43 | *.hmap 44 | *.ipa 45 | *.xcuserstate 46 | project.xcworkspace 47 | 48 | # Android/IJ 49 | # 50 | *.iml 51 | .idea 52 | .gradle 53 | local.properties 54 | 55 | # node.js 56 | # 57 | node_modules/ 58 | npm-debug.log 59 | 60 | # BUCK 61 | buck-out/ 62 | \.buckd/ 63 | android/app/libs 64 | *.keystore 65 | 66 | ########## 67 | # CUSTOM # 68 | ########## 69 | 70 | compiler/**/*.js 71 | compiler/**/*.d.ts 72 | runtime/**/*.js 73 | runtime/**/*.d.ts 74 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "compiler/**/*.js": true 4 | } 5 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We used to support web and generating CSS files. Some of the code is written in a generic way to support both. I may re-add this later if there's interest 2 | 3 | The benchmark likely doesn't work 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jacob Parker 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. 22 | -------------------------------------------------------------------------------- /__tests__/integration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importStar = (this && this.__importStar) || function (mod) { 3 | if (mod && mod.__esModule) return mod; 4 | var result = {}; 5 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 6 | result["default"] = mod; 7 | return result; 8 | }; 9 | exports.__esModule = true; 10 | var babel = __importStar(require("@babel/core")); 11 | it("Works with babel-plugin-cssta", function () { 12 | var code = babel.transform("import styled from 'cssta/native';" + 13 | "const Test1 = styled(Button)`" + 14 | " color: red;" + 15 | "`;" + 16 | "const Test2 = styled(Button)`" + 17 | " color: red;" + 18 | "`;" + 19 | "const testMixin = styled.mixin`" + 20 | " color: red;" + 21 | "`", { 22 | filename: __filename, 23 | plugins: [require.resolve("../babel-plugin")], 24 | babelrc: false 25 | }).code; 26 | expect(code).toMatchInlineSnapshot("\n \"import React from \\\"react\\\";\n const styles0 = {\n color: \\\"red\\\"\n };\n const Test1 = React.forwardRef((props, ref) => {\n const style = props.style != null ? [styles0, props.style] : styles0;\n return React.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\n const styles1 = {\n color: \\\"red\\\"\n };\n const Test2 = React.forwardRef((props, ref) => {\n const style = props.style != null ? [styles1, props.style] : styles1;\n return React.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\n const styles2 = {\n color: \\\"red\\\"\n };\n\n const testMixin = () => {\n const style = styles2;\n return style;\n };\"\n "); 27 | }); 28 | it("Works with babel-plugin-macros", function () { 29 | var code = babel.transform("import styled from '../native.macro';" + 30 | "const Test1 = styled(Button)`" + 31 | " color: red;" + 32 | "`;" + 33 | "const Test2 = styled(Button)`" + 34 | " color: red;" + 35 | "`;" + 36 | "const testMixin = styled.mixin`" + 37 | " color: red;" + 38 | "`", { 39 | filename: __filename, 40 | plugins: ["babel-plugin-macros"], 41 | babelrc: false 42 | }).code; 43 | expect(code).toMatchInlineSnapshot("\n \"import React from \\\"react\\\";\n const styles0 = {\n color: \\\"red\\\"\n };\n const Test1 = React.forwardRef((props, ref) => {\n const style = props.style != null ? [styles0, props.style] : styles0;\n return React.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\n const styles1 = {\n color: \\\"red\\\"\n };\n const Test2 = React.forwardRef((props, ref) => {\n const style = props.style != null ? [styles1, props.style] : styles1;\n return React.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\n const styles2 = {\n color: \\\"red\\\"\n };\n\n const testMixin = () => {\n const style = styles2;\n return style;\n };\"\n "); 44 | }); 45 | it("Works with plugin-transform-modules-commonjs", function () { 46 | var code = babel.transform("import styled from 'cssta/native';" + 47 | "const Test1 = styled(Button)`" + 48 | " color: var(--color);" + 49 | " --color: red;" + 50 | "`;" + 51 | "const Test2 = styled(Button)`" + 52 | " color: red;" + 53 | "`;", { 54 | filename: __filename, 55 | plugins: [ 56 | require.resolve("../babel-plugin"), 57 | "@babel/plugin-transform-modules-commonjs", 58 | ], 59 | babelrc: false 60 | }).code; 61 | code = babel.transform(code).code; // Reformat 62 | expect(code).toMatchInlineSnapshot("\n \"\\\"use strict\\\";\n\n var _react = _interopRequireDefault(require(\\\"react\\\"));\n\n var _useCustomProperties = _interopRequireDefault(require(\\\"cssta/runtime/useCustomProperties\\\"));\n\n var _useCustomPropertyStyle = _interopRequireDefault(require(\\\"cssta/runtime/useCustomPropertyStyle\\\"));\n\n var _VariablesContext = _interopRequireDefault(require(\\\"cssta/runtime/VariablesContext\\\"));\n\n function _interopRequireDefault(obj) {\n return obj && obj.__esModule ? obj : {\n default: obj\n };\n }\n\n const exportedCustomProperties = {\n \\\"color\\\": \\\"red\\\"\n };\n const unresolvedStyleTuples0 = [[\\\"color\\\", \\\"var(--color)\\\"]];\n\n const Test1 = _react.default.forwardRef((props, ref) => {\n const customProperties = (0, _useCustomProperties.default)(exportedCustomProperties);\n const styles = (0, _useCustomPropertyStyle.default)(unresolvedStyleTuples0, customProperties);\n const style = props.style != null ? [styles, props.style] : styles;\n return _react.default.createElement(_VariablesContext.default.Provider, {\n value: customProperties\n }, _react.default.createElement(Button, { ...props,\n ref: ref,\n style: style\n }));\n });\n\n const styles0 = {\n color: \\\"red\\\"\n };\n\n const Test2 = _react.default.forwardRef((props, ref) => {\n const style = props.style != null ? [styles0, props.style] : styles0;\n return _react.default.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\"\n "); 63 | }); 64 | it("Works with options", function () { 65 | var code = babel.transform("import styled from 'cssta/native';" + 66 | "const Test1 = styled(Button)`" + 67 | " color: var(--color);" + 68 | "`;", { 69 | filename: __filename, 70 | plugins: [ 71 | [require.resolve("../babel-plugin"), { globals: { color: "red" } }], 72 | ], 73 | babelrc: false 74 | }).code; 75 | expect(code).toMatchInlineSnapshot("\n \"import React from \\\"react\\\";\n const styles0 = {\n color: \\\"red\\\"\n };\n const Test1 = React.forwardRef((props, ref) => {\n const style = props.style != null ? [styles0, props.style] : styles0;\n return React.createElement(Button, { ...props,\n ref: ref,\n style: style\n });\n });\"\n "); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/integration.ts: -------------------------------------------------------------------------------- 1 | import * as babel from "@babel/core"; 2 | 3 | it("Works with babel-plugin-cssta", () => { 4 | const { code } = babel.transform( 5 | "import styled from 'cssta/native';" + 6 | "const Test1 = styled(Button)`" + 7 | " color: red;" + 8 | "`;" + 9 | "const Test2 = styled(Button)`" + 10 | " color: red;" + 11 | "`;" + 12 | "const testMixin = styled.mixin`" + 13 | " color: red;" + 14 | "`", 15 | { 16 | filename: __filename, 17 | plugins: [require.resolve("../babel-plugin")], 18 | babelrc: false, 19 | } 20 | ); 21 | expect(code).toMatchInlineSnapshot(` 22 | "import React from \\"react\\"; 23 | const styles0 = { 24 | color: \\"red\\" 25 | }; 26 | const Test1 = React.forwardRef((props, ref) => { 27 | const style = props.style != null ? [styles0, props.style] : styles0; 28 | return React.createElement(Button, { ...props, 29 | ref: ref, 30 | style: style 31 | }); 32 | }); 33 | const styles1 = { 34 | color: \\"red\\" 35 | }; 36 | const Test2 = React.forwardRef((props, ref) => { 37 | const style = props.style != null ? [styles1, props.style] : styles1; 38 | return React.createElement(Button, { ...props, 39 | ref: ref, 40 | style: style 41 | }); 42 | }); 43 | const styles2 = { 44 | color: \\"red\\" 45 | }; 46 | 47 | const testMixin = () => { 48 | const style = styles2; 49 | return style; 50 | };" 51 | `); 52 | }); 53 | 54 | it("Works with babel-plugin-macros", () => { 55 | const { code } = babel.transform( 56 | "import styled from '../native.macro';" + 57 | "const Test1 = styled(Button)`" + 58 | " color: red;" + 59 | "`;" + 60 | "const Test2 = styled(Button)`" + 61 | " color: red;" + 62 | "`;" + 63 | "const testMixin = styled.mixin`" + 64 | " color: red;" + 65 | "`", 66 | { 67 | filename: __filename, 68 | plugins: ["babel-plugin-macros"], 69 | babelrc: false, 70 | } 71 | ); 72 | expect(code).toMatchInlineSnapshot(` 73 | "import React from \\"react\\"; 74 | const styles0 = { 75 | color: \\"red\\" 76 | }; 77 | const Test1 = React.forwardRef((props, ref) => { 78 | const style = props.style != null ? [styles0, props.style] : styles0; 79 | return React.createElement(Button, { ...props, 80 | ref: ref, 81 | style: style 82 | }); 83 | }); 84 | const styles1 = { 85 | color: \\"red\\" 86 | }; 87 | const Test2 = React.forwardRef((props, ref) => { 88 | const style = props.style != null ? [styles1, props.style] : styles1; 89 | return React.createElement(Button, { ...props, 90 | ref: ref, 91 | style: style 92 | }); 93 | }); 94 | const styles2 = { 95 | color: \\"red\\" 96 | }; 97 | 98 | const testMixin = () => { 99 | const style = styles2; 100 | return style; 101 | };" 102 | `); 103 | }); 104 | 105 | it("Works with plugin-transform-modules-commonjs", () => { 106 | let { code } = babel.transform( 107 | "import styled from 'cssta/native';" + 108 | "const Test1 = styled(Button)`" + 109 | " color: var(--color);" + 110 | " --color: red;" + 111 | "`;" + 112 | "const Test2 = styled(Button)`" + 113 | " color: red;" + 114 | "`;", 115 | { 116 | filename: __filename, 117 | plugins: [ 118 | require.resolve("../babel-plugin"), 119 | "@babel/plugin-transform-modules-commonjs", 120 | ], 121 | babelrc: false, 122 | } 123 | ); 124 | code = babel.transform(code).code; // Reformat 125 | expect(code).toMatchInlineSnapshot(` 126 | "\\"use strict\\"; 127 | 128 | var _react = _interopRequireDefault(require(\\"react\\")); 129 | 130 | var _useCustomProperties = _interopRequireDefault(require(\\"cssta/runtime/useCustomProperties\\")); 131 | 132 | var _useCustomPropertyStyle = _interopRequireDefault(require(\\"cssta/runtime/useCustomPropertyStyle\\")); 133 | 134 | var _VariablesContext = _interopRequireDefault(require(\\"cssta/runtime/VariablesContext\\")); 135 | 136 | function _interopRequireDefault(obj) { 137 | return obj && obj.__esModule ? obj : { 138 | default: obj 139 | }; 140 | } 141 | 142 | const exportedCustomProperties = { 143 | \\"color\\": \\"red\\" 144 | }; 145 | const unresolvedStyleTuples0 = [[\\"color\\", \\"var(--color)\\"]]; 146 | 147 | const Test1 = _react.default.forwardRef((props, ref) => { 148 | const customProperties = (0, _useCustomProperties.default)(exportedCustomProperties); 149 | const styles = (0, _useCustomPropertyStyle.default)(unresolvedStyleTuples0, customProperties); 150 | const style = props.style != null ? [styles, props.style] : styles; 151 | return _react.default.createElement(_VariablesContext.default.Provider, { 152 | value: customProperties 153 | }, _react.default.createElement(Button, { ...props, 154 | ref: ref, 155 | style: style 156 | })); 157 | }); 158 | 159 | const styles0 = { 160 | color: \\"red\\" 161 | }; 162 | 163 | const Test2 = _react.default.forwardRef((props, ref) => { 164 | const style = props.style != null ? [styles0, props.style] : styles0; 165 | return _react.default.createElement(Button, { ...props, 166 | ref: ref, 167 | style: style 168 | }); 169 | });" 170 | `); 171 | }); 172 | 173 | it("Works with options", () => { 174 | const { code } = babel.transform( 175 | "import styled from 'cssta/native';" + 176 | "const Test1 = styled(Button)`" + 177 | " color: var(--color);" + 178 | "`;", 179 | { 180 | filename: __filename, 181 | plugins: [ 182 | [require.resolve("../babel-plugin"), { globals: { color: "red" } }], 183 | ], 184 | babelrc: false, 185 | } 186 | ); 187 | expect(code).toMatchInlineSnapshot(` 188 | "import React from \\"react\\"; 189 | const styles0 = { 190 | color: \\"red\\" 191 | }; 192 | const Test1 = React.forwardRef((props, ref) => { 193 | const style = props.style != null ? [styles0, props.style] : styles0; 194 | return React.createElement(Button, { ...props, 195 | ref: ref, 196 | style: style 197 | }); 198 | });" 199 | `); 200 | }); 201 | -------------------------------------------------------------------------------- /babel-plugin.js: -------------------------------------------------------------------------------- 1 | const { default: buildElement } = require("./compiler/babel/buildElement"); 2 | const { default: buildMixin } = require("./compiler/babel/buildMixin"); 3 | 4 | const csstaModules = { 5 | "cssta/native": "native" 6 | }; 7 | 8 | const getCsstaTypeForCallee = ({ types: t }, path) => { 9 | if (path == null) return null; 10 | 11 | const { node } = path; 12 | const csstaIdentifier = t.isCallExpression(node) 13 | ? node.callee 14 | : t.isMemberExpression(node) 15 | ? node.object 16 | : null; 17 | 18 | if (!t.isIdentifier(csstaIdentifier)) return null; 19 | 20 | const importSpecifierHub = path.scope.getBinding(csstaIdentifier.name); 21 | const importSpecifier = 22 | importSpecifierHub != null ? importSpecifierHub.path : null; 23 | if (importSpecifier == null || !t.isImportDefaultSpecifier(importSpecifier)) { 24 | return null; 25 | } 26 | 27 | const importDeclaration = importSpecifier.findParent(t.isImportDeclaration); 28 | if (importDeclaration == null) return null; 29 | 30 | const source = importDeclaration.node.source.value; 31 | const csstaType = csstaModules[source]; 32 | if (csstaType == null) return null; 33 | 34 | return csstaType; 35 | }; 36 | 37 | const removeCsstaImports = { 38 | ImportDeclaration(importPath) { 39 | if (csstaModules[importPath.node.source.value] != null) { 40 | importPath.remove(); 41 | } 42 | } 43 | }; 44 | 45 | module.exports = babel => ({ 46 | name: "cssta", 47 | visitor: { 48 | Program: { 49 | exit(programPath) { 50 | programPath.traverse(removeCsstaImports); 51 | } 52 | }, 53 | TaggedTemplateExpression(path, state) { 54 | switch (getCsstaTypeForCallee(babel, path.get("tag"))) { 55 | case "native": { 56 | const { types: t } = babel; 57 | const tag = path.node.tag; 58 | const css = path.get("quasi").node; 59 | 60 | if ( 61 | t.isMemberExpression(tag) && 62 | t.isIdentifier(tag.property, { name: "mixin" }) 63 | ) { 64 | buildMixin(babel, path, css, state.opts); 65 | } else if (t.isCallExpression(tag)) { 66 | const element = path.get("tag.arguments.0").node; 67 | buildElement(babel, path, element, css, state.opts); 68 | } 69 | break; 70 | } 71 | default: 72 | break; 73 | } 74 | } 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /benchmark/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["latest", { "modules": false }], "react"], 3 | "plugins": ["babel-plugin-cssta"] 4 | } 5 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | tests-compiled.js 2 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | require('console.table'); // eslint-disable-line 3 | const testFunctions = require('./tests-compiled').default; 4 | 5 | // Benchmark.options.onError = e => console.error(e); 6 | 7 | const suite = new Benchmark.Suite(); 8 | 9 | const tests = Object.keys(testFunctions); 10 | const contestantsSet = new Set(); 11 | 12 | const getRunName = (testName, contestant) => `${testName}: ${contestant}`; 13 | 14 | tests.forEach((testName) => { 15 | Object.keys(testFunctions[testName]).forEach((contestant) => { 16 | contestantsSet.add(contestant); 17 | suite.add(getRunName(testName, contestant), testFunctions[testName][contestant]); 18 | }); 19 | }); 20 | 21 | const contestants = Array.from(contestantsSet); 22 | 23 | suite.on('complete', () => { 24 | const suiteResults = Array.from(suite); 25 | 26 | const header = [''].concat(contestants); 27 | const rows = tests.map(testName => ( 28 | [testName].concat(contestants.map((contestant) => { 29 | const runName = getRunName(testName, contestant); 30 | const run = suiteResults.find(run => run.name === runName); // eslint-disable-line 31 | return run 32 | ? `${(run.stats.mean * 1000).toFixed(3)}±${(run.stats.deviation * 1000).toFixed(3)}ms` 33 | : '-'; 34 | })) 35 | )); 36 | 37 | console.table(header, rows); 38 | }); 39 | 40 | suite.run(); 41 | -------------------------------------------------------------------------------- /benchmark/mocks/react-native.js: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | 3 | export const StyleSheet = { 4 | create: () => ({}) 5 | }; 6 | 7 | export const View = props => createElement("view", props); 8 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssta-benchmark", 3 | "version": "0.8.0", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "dependencies": { 8 | "babel-core": "^6.22.1", 9 | "babel-loader": "^8.0.6", 10 | "babel-preset-latest": "^6.22.0", 11 | "babel-preset-react": "^6.22.0", 12 | "benchmark": "^2.1.3", 13 | "console.table": "^0.10.0", 14 | "cssta": "^0.9.0", 15 | "eslint": "^6.3.0", 16 | "react": "^16.9.0", 17 | "react-test-renderer": "^16.9.0", 18 | "styled-components": "^4.3.2", 19 | "webpack": "^4.39.3" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.22.1", 23 | "babel-loader": "^6.2.10", 24 | "babel-plugin-cssta": "^0.9.0", 25 | "babel-preset-latest": "^6.22.0", 26 | "eslint": "^3.15.0", 27 | "eslint-config-airbnb": "^18.0.1", 28 | "eslint-plugin-import": "^2.2.0", 29 | "eslint-plugin-jsx-a11y": "^6.2.3", 30 | "eslint-plugin-react": "^7.14.3", 31 | "webpack": "^2.2.1" 32 | }, 33 | "scripts": { 34 | "test": "echo \"Error: no test specified\" && exit 1" 35 | }, 36 | "keywords": [], 37 | "author": "", 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /benchmark/tests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; // eslint-disable-line 3 | import { create as render } from 'react-test-renderer'; 4 | import styled, { resetStyleCache } from 'styled-components/native'; 5 | import cssta from 'cssta/native'; 6 | 7 | 8 | export default { 9 | 'simple component': { 10 | styled: () => { 11 | resetStyleCache(); 12 | 13 | const Component = styled(View)` 14 | color: red; 15 | `; 16 | 17 | render(); 18 | }, 19 | cssta: () => { 20 | const Component = cssta(View)` 21 | color: red; 22 | `; 23 | 24 | render(); 25 | }, 26 | }, 27 | 28 | 'prop changes': { 29 | styled: () => { 30 | resetStyleCache(); 31 | 32 | const Component = styled(View)` 33 | color: ${props => (props.danger ? 'red' : 'black')}; 34 | `; 35 | 36 | const instance = render(); 37 | instance.update(); 38 | instance.update(); 39 | instance.update(); 40 | instance.update(); 41 | instance.update(); 42 | instance.update(); 43 | instance.update(); 44 | }, 45 | cssta: () => { 46 | const Component = cssta(View)` 47 | color: black; 48 | 49 | [danger] { 50 | color: red; 51 | } 52 | `; 53 | 54 | const instance = render(); 55 | instance.update(); 56 | instance.update(); 57 | instance.update(); 58 | instance.update(); 59 | instance.update(); 60 | instance.update(); 61 | instance.update(); 62 | }, 63 | }, 64 | 65 | 'prop shorthands': { 66 | styled: () => { 67 | resetStyleCache(); 68 | 69 | const Component = styled(View)` 70 | color: red; 71 | flex: 1; 72 | font: bold 12/14 "Helvetica"; 73 | margin: 1 2; 74 | padding: 3 4; 75 | border: 1 solid black; 76 | `; 77 | 78 | render(); 79 | }, 80 | cssta: () => { 81 | const Component = cssta(View)` 82 | color: red; 83 | flex: 1; 84 | font: bold 12px/14px "Helvetica"; 85 | margin: 1px 2px; 86 | padding: 3px 4px; 87 | border: 1px solid black; 88 | `; 89 | 90 | render(); 91 | }, 92 | }, 93 | 94 | 'prop shorthands with prop changes': { 95 | styled: () => { 96 | resetStyleCache(); 97 | 98 | const Component = styled(View)` 99 | color: ${props => (props.danger ? 'red' : 'black')}; 100 | flex: 1; 101 | font: bold 12/14 "Helvetica"; 102 | margin: 1 2; 103 | padding: 3 4; 104 | border: 1 solid black; 105 | `; 106 | 107 | const instance = render(); 108 | instance.update(); 109 | instance.update(); 110 | instance.update(); 111 | instance.update(); 112 | instance.update(); 113 | instance.update(); 114 | instance.update(); 115 | }, 116 | cssta: () => { 117 | const Component = cssta(View)` 118 | color: black; 119 | flex: 1; 120 | font: bold 12px/14px "Helvetica"; 121 | margin: 1px 2px; 122 | padding: 3px 4px; 123 | border: 1px solid black; 124 | 125 | [danger] { 126 | color: red; 127 | } 128 | `; 129 | 130 | const instance = render(); 131 | instance.update(); 132 | instance.update(); 133 | instance.update(); 134 | instance.update(); 135 | instance.update(); 136 | instance.update(); 137 | instance.update(); 138 | }, 139 | }, 140 | }; 141 | -------------------------------------------------------------------------------- /benchmark/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './tests', 6 | output: { 7 | filename: 'tests-compiled.js', 8 | libraryTarget: 'commonjs', 9 | }, 10 | resolve: { 11 | alias: { 12 | 'react-native': join(__dirname, '/mocks/react-native'), 13 | }, 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] }, 18 | ], 19 | }, 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | NODE_ENV: JSON.stringify('production'), 24 | }, 25 | }), 26 | new webpack.optimize.UglifyJsPlugin(), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /compiler/babel/__testUtil__/index.ts: -------------------------------------------------------------------------------- 1 | import * as babel from "@babel/core"; 2 | import generate from "@babel/generator"; 3 | import buildElement from "../buildElement"; 4 | import { Options } from "../../options"; 5 | 6 | const { types: t } = babel; 7 | 8 | export const styled: { test: (x: TemplateStringsArray) => string } = { 9 | test: String.raw, 10 | } as any; 11 | 12 | export const build = (css: string, options: Options = {}) => { 13 | const ast = babel.parse("const Example = 'replaceMe'"); 14 | babel.traverse(ast, { 15 | StringLiteral(path: any) { 16 | if (path.node.value === "replaceMe") { 17 | buildElement( 18 | babel, 19 | path, 20 | t.identifier("Element"), 21 | t.templateLiteral([t.templateElement({ raw: css, cooked: css })], []), 22 | { jsx: true, ...options } 23 | ); 24 | } 25 | }, 26 | }); 27 | const { code } = generate(ast); 28 | return code.replace(/"/g, "'"); 29 | }; 30 | -------------------------------------------------------------------------------- /compiler/babel/__tests__/animations.ts: -------------------------------------------------------------------------------- 1 | import { styled, build } from "../__testUtil__"; 2 | 3 | it("Supports keyframes", () => { 4 | const css = styled.test` 5 | @keyframes fade-in { 6 | 0% { 7 | opacity: 0; 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | `; 14 | 15 | const code = build(css); 16 | expect(code).toMatchInlineSnapshot(` 17 | "import React from 'react'; 18 | const keyframes = { 19 | 'fade-in': [{ 20 | 'time': 0, 21 | 'style': { 22 | opacity: 0 23 | } 24 | }, { 25 | 'time': 1, 26 | 'style': { 27 | opacity: 1 28 | } 29 | }] 30 | }; 31 | const Example = React.forwardRef((props, ref) => { 32 | return ; 33 | });" 34 | `); 35 | }); 36 | 37 | it("Supports animations", () => { 38 | const css = styled.test` 39 | animation: fade-in 1s; 40 | 41 | @keyframes fade-in { 42 | 0% { 43 | opacity: 0; 44 | } 45 | 100% { 46 | opacity: 1; 47 | } 48 | } 49 | `; 50 | 51 | const code = build(css); 52 | expect(code).toMatchInlineSnapshot(` 53 | "import React from 'react'; 54 | import useAnimation from 'cssta/runtime/useAnimation'; 55 | const keyframes = { 56 | 'fade-in': [{ 57 | 'time': 0, 58 | 'style': { 59 | opacity: 0 60 | } 61 | }, { 62 | 'time': 1, 63 | 'style': { 64 | opacity: 1 65 | } 66 | }] 67 | }; 68 | const animation = { 69 | 'delay': 0, 70 | 'duration': 1000, 71 | 'iterations': 1, 72 | 'name': 'fade-in', 73 | 'timingFunction': 'ease' 74 | }; 75 | const Example = React.forwardRef((props, ref) => { 76 | let style = props.style; 77 | style = useAnimation(keyframes, animation, style); 78 | return ; 79 | });" 80 | `); 81 | }); 82 | 83 | it("Supports conditional animations", () => { 84 | const css = styled.test` 85 | animation: fade-in 1s; 86 | 87 | &[@slow] { 88 | animation: fade-in 5s; 89 | } 90 | 91 | @keyframes fade-in { 92 | 0% { 93 | opacity: 0; 94 | } 95 | 100% { 96 | opacity: 1; 97 | } 98 | } 99 | `; 100 | 101 | const code = build(css); 102 | expect(code).toMatchInlineSnapshot(` 103 | "import React from 'react'; 104 | import flattenAnimation from 'cssta/runtime/flattenAnimation'; 105 | import useAnimation from 'cssta/runtime/useAnimation'; 106 | const keyframes = { 107 | 'fade-in': [{ 108 | 'time': 0, 109 | 'style': { 110 | opacity: 0 111 | } 112 | }, { 113 | 'time': 1, 114 | 'style': { 115 | opacity: 1 116 | } 117 | }] 118 | }; 119 | const Example = React.forwardRef(({ 120 | slow, 121 | ...props 122 | }, ref) => { 123 | let style = props.style; 124 | const animation = flattenAnimation([{ 125 | '_': 'fade-in 1s' 126 | }, slow === true ? { 127 | '_': 'fade-in 5s' 128 | } : null]); 129 | style = useAnimation(keyframes, animation, style); 130 | return ; 131 | });" 132 | `); 133 | }); 134 | -------------------------------------------------------------------------------- /compiler/babel/__tests__/basic.ts: -------------------------------------------------------------------------------- 1 | import { styled, build } from "../__testUtil__"; 2 | 3 | it("Supports basic styles", () => { 4 | const css = styled.test` 5 | color: green; 6 | `; 7 | 8 | const code = build(css); 9 | expect(code).toMatchInlineSnapshot(` 10 | "import React from 'react'; 11 | const styles0 = { 12 | color: 'green' 13 | }; 14 | const Example = React.forwardRef((props, ref) => { 15 | const style = props.style != null ? [styles0, props.style] : styles0; 16 | return ; 17 | });" 18 | `); 19 | }); 20 | 21 | it("Supports basic multiple implicitly-scoped declarations", () => { 22 | const css = styled.test` 23 | color: green; 24 | width: 100px; 25 | `; 26 | 27 | const code = build(css); 28 | expect(code).toMatchInlineSnapshot(` 29 | "import React from 'react'; 30 | const styles0 = { 31 | color: 'green', 32 | width: 100 33 | }; 34 | const Example = React.forwardRef((props, ref) => { 35 | const style = props.style != null ? [styles0, props.style] : styles0; 36 | return ; 37 | });" 38 | `); 39 | }); 40 | 41 | it("Supports boolean conditional styles", () => { 42 | const css = styled.test` 43 | color: green; 44 | 45 | &[@test] { 46 | color: blue; 47 | } 48 | `; 49 | 50 | const code = build(css); 51 | expect(code).toMatchInlineSnapshot(` 52 | "import React from 'react'; 53 | const styles0 = { 54 | color: 'green' 55 | }; 56 | const styles1 = { 57 | color: 'blue' 58 | }; 59 | const Example = React.forwardRef(({ 60 | test, 61 | ...props 62 | }, ref) => { 63 | const baseStyle = test === true ? styles1 : styles0; 64 | const style = props.style != null ? [baseStyle, props.style] : baseStyle; 65 | return ; 66 | });" 67 | `); 68 | }); 69 | 70 | it("Supports string conditional styles", () => { 71 | const css = styled.test` 72 | width: 100px; 73 | 74 | &[@size="small"] { 75 | width: 80px; 76 | } 77 | 78 | &[@size="large"] { 79 | width: 120px; 80 | } 81 | `; 82 | 83 | const code = build(css); 84 | expect(code).toMatchInlineSnapshot(` 85 | "import React from 'react'; 86 | const styles0 = { 87 | width: 100 88 | }; 89 | const styles1 = { 90 | width: 80 91 | }; 92 | const styles2 = { 93 | width: 120 94 | }; 95 | const Example = React.forwardRef(({ 96 | size, 97 | ...props 98 | }, ref) => { 99 | const baseStyle = size === 'large' ? styles2 : size === 'small' ? styles1 : styles0; 100 | const style = props.style != null ? [baseStyle, props.style] : baseStyle; 101 | return ; 102 | });" 103 | `); 104 | }); 105 | 106 | it("Optimizes a single conditional style", () => { 107 | const css = styled.test` 108 | color: green; 109 | 110 | &[@test] { 111 | margin: 10px; 112 | } 113 | `; 114 | 115 | const code = build(css); 116 | expect(code).toMatchInlineSnapshot(` 117 | "import React from 'react'; 118 | const styles0 = { 119 | color: 'green' 120 | }; 121 | const styles1 = { 122 | color: 'green', 123 | marginTop: 10, 124 | marginRight: 10, 125 | marginBottom: 10, 126 | marginLeft: 10 127 | }; 128 | const Example = React.forwardRef(({ 129 | test, 130 | ...props 131 | }, ref) => { 132 | const baseStyle = test === true ? styles1 : styles0; 133 | const style = props.style != null ? [baseStyle, props.style] : baseStyle; 134 | return ; 135 | });" 136 | `); 137 | }); 138 | -------------------------------------------------------------------------------- /compiler/babel/__tests__/interpolation.ts: -------------------------------------------------------------------------------- 1 | import * as babel from "@babel/core"; 2 | import generate from "@babel/generator"; 3 | import buildElement from "../buildElement"; 4 | 5 | it("Works with substititions", () => { 6 | const ast = babel.parse(` 7 | const Test = styled(Button)\` 8 | color: \${red}; 9 | margin: \${small}; 10 | top: \${small}; 11 | opacity: \${half}; 12 | \`; 13 | `); 14 | babel.traverse(ast, { 15 | TaggedTemplateExpression(path: any) { 16 | const { tag, quasi: body } = path.node; 17 | const element = tag.arguments[0]; 18 | buildElement(babel, path, element, body, { jsx: true }); 19 | }, 20 | }); 21 | const { code } = generate(ast); 22 | expect(code).toMatchInlineSnapshot(` 23 | "import React from \\"react\\"; 24 | import { transformStyleTuples } from \\"cssta/runtime/cssUtil\\"; 25 | import { transformRawValue } from \\"cssta/runtime/cssUtil\\"; 26 | const styles0 = Object.assign({ 27 | color: String(red).trim() 28 | }, transformStyleTuples([[\\"margin\\", \`\${small}\`]]), { 29 | top: transformRawValue(small), 30 | opacity: Number(half) 31 | }); 32 | const Test = React.forwardRef((props, ref) => { 33 | const style = props.style != null ? [styles0, props.style] : styles0; 34 | return 36 | ; 37 | ``` 38 | 39 | You can also dynamically change the values of the variables through prop selectors. 40 | 41 | ```jsx 42 | const LightBox = cssta(View)` 43 | background-color: black; 44 | --primary: white; 45 | 46 | &[@inverted] { 47 | background-color: white; 48 | --primary: black; 49 | } 50 | `; 51 | 52 | 53 | 54 | ; 55 | ``` 56 | 57 | ## 💉 Using JavaScript Variables 58 | 59 | If you need more control over variables, there’s `VariablesProvider` component. Just pass in an object of your variables omitting the double dash. You can see more information over in [custom properties]({{ site.baseurl }}/custom-properties). 60 | 61 | ```jsx 62 | import VariablesContext from "cssta/runtime/VariablesContext"; 63 | 64 | 65 | 66 | ; 67 | ``` 68 | 69 | ## 🌍 Global Variables 70 | 71 | If all your custom properties are global, you can configure them in the Cssta build configuration. There’s more information on this over in the [configuration]({{ site.baseurl }}/configuration) section. 72 | -------------------------------------------------------------------------------- /docs/transitions-animations.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Transitions and Animations 4 | permalink: /transitions-animations/ 5 | --- 6 | 7 | 44 | 45 | # 🍿 Transitions and Animations 46 | 47 | Cssta provides convenience features around React’s Animated API. The syntax is identical to CSS (where supported). 48 | 49 | Because of the declarative nature of CSS, only components that use these features will run code for the features. What’s more, if _no_ components use these features, the code isn’t even shipped in your production bundle! 50 | 51 | Because we use the CSS spec, you can find a tonne of stuff on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/animation). Check our tables below to see what we support. 52 | 53 |
54 |
55 | 56 | ## Transitions 57 | 58 | {:.split-view\_\_title} 59 | 60 | | ✅ | transition _ | 61 | | ✅ | transition-delay | 62 | | ✅ | transition-duration | 63 | | ✅ | transition-property _ | 64 | | ✅ | transition-timing-function | 65 | {:.feature-table} 66 | 67 |
68 |
69 | 70 | ```jsx 71 | const ButtonWithTransition = cssta(Animated.View)` 72 | background-color: blue; 73 | color: white; 74 | 75 | transition: 76 | background-color 0.5s ease-in, 77 | color 0.7s ease-in 78 | 79 | &[@disabled] { 80 | background-color: gray; 81 | color: light-gray; 82 | } 83 | `; 84 | ``` 85 | 86 | {:.split-view\_\_code} 87 | 88 |
89 |
90 | 91 |
92 |
93 | 94 | ## Animations 95 | 96 | {:.split-view\_\_title} 97 | 98 | | ✅ | animation | 99 | | ✅ | animation-delay | 100 | | ❌ | animation-direction | 101 | | ✅ | animation-duration | 102 | | ❌ | animation-fill-mode ** | 103 | | ✅ | animation-iteration-count \*** | 104 | | ❌ | animation-play-state | 105 | | ✅ | animation-name \*\*\*\* | 106 | | ✅ | animation-timing-function | 107 | | ✅ | @keyframes | 108 | {:.feature-table} 109 | 110 |
111 |
112 | 113 | ```jsx 114 | const ButtonWithKeyframes = cssta(Animated.View)` 115 | animation: fade-in 1s ease-in; 116 | 117 | @keyframes fade-in { 118 | 0% { 119 | opacity: 0; 120 | } 121 | 122 | 100% { 123 | opacity: 1; 124 | } 125 | } 126 | `; 127 | ``` 128 | 129 | {:.split-view\_\_code} 130 | 131 |
132 |
133 | 134 | ### Notes 135 | 136 | You can animate multiple transforms, but the nodes of the transform must not change. I.e, you can animate `transform: scaleX(1) rotateX(0deg)` to `transform: scaleX(5) rotateX(30deg)`, but you cannot then transform to `transform: scaleY(5) skew(30deg)`. 137 | 138 | \* Transition properties cannot currently be shorthands, including things like `border-width`, but you can write `transition-property: border-top-width, border-right-width …`. You also cannot use custom properties to define them. 139 | 140 | \*\* Currently uses value of `fill-forwards` 141 | 142 | \*\*\* Must be a whole number or `infinite` 143 | 144 | \*\*\*\* Animations currently only support up to one animation 145 | 146 | ## 🎥 Custom Animations via `Animated.Value` 147 | 148 | For more complicated animations, you’ll want to define a base component that has all your non-changing styles. You’ll then want a second component that has control over some `Animated.Value`s. 149 | 150 | In your second component’s render function, return the base component along with your animated values passed in the `style` prop. 151 | 152 | ```jsx 153 | const BaseStyles = cssta(View)` 154 | height: 100px; 155 | width: 100px; 156 | background-color: red; 157 | `; 158 | 159 | const AnimateOpacity = () => { 160 | const [opacity] = React.useState(() => new Animated.Value(0)); 161 | 162 | React.useLayoutEffect(() => { 163 | Animated.timing(opacity, { 164 | toValue: 1, 165 | duration: 1000 166 | }).start(); 167 | }, []); 168 | 169 | return ; 170 | }; 171 | ``` 172 | -------------------------------------------------------------------------------- /native.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cssta/native' { 2 | type CssConstructor = ( 3 | css: TemplateStringsArray, 4 | ...values: (string | number | (() => any))[] 5 | ) => T; 6 | 7 | type ComponentConstructor = ( 8 | component: React.ElementType, 9 | ) => CssConstructor>>; 10 | 11 | type Mixin = { 12 | mixin: CssConstructor; 13 | }; 14 | 15 | const styled: ComponentConstructor & Mixin; 16 | export default styled; 17 | } 18 | -------------------------------------------------------------------------------- /native.macro.js: -------------------------------------------------------------------------------- 1 | const { createMacro } = require("babel-plugin-macros"); 2 | const { default: buildElement } = require("./compiler/babel/buildElement"); 3 | const { default: buildMixin } = require("./compiler/babel/buildMixin"); 4 | 5 | module.exports = createMacro(({ babel, references, config }) => { 6 | const { types: t } = babel; 7 | 8 | references.default 9 | .map(path => path.findParent(t.isTaggedTemplateExpression)) 10 | .forEach(path => { 11 | const tag = path.node.tag; 12 | const css = path.get("quasi").node; 13 | 14 | if ( 15 | t.isMemberExpression(tag) && 16 | t.isIdentifier(tag.property, { name: "mixin" }) 17 | ) { 18 | buildMixin(babel, path, css, config); 19 | } else if (t.isCallExpression(tag)) { 20 | const element = path.get("tag.arguments.0").node; 21 | buildElement(babel, path, element, css, config); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssta", 3 | "version": "0.10.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsc", 9 | "prepack": "tsc", 10 | "clean": "git clean -xf compiler runtime", 11 | "readme": "cp ./docs/index.md ./README.md; sed -i -e 's%{{ site.baseurl }}%https://jacobp100.github.io/cssta%g' ./README.md; sed -i -e 's/{% raw %}//g' ./README.md; sed -i -e 's/{% endraw %}//g' ./README.md; sed -i -e '1,5d' ./README.md; rm ./README.md-e" 12 | }, 13 | "keywords": [ 14 | "postcss", 15 | "modules", 16 | "css-modules", 17 | "css", 18 | "minify", 19 | "min", 20 | "class", 21 | "className", 22 | "react", 23 | "css-in-js" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/jacobp100/cssta" 28 | }, 29 | "files": [ 30 | "compiler/**/*.js", 31 | "runtime/**/*.js", 32 | "native.macro.js", 33 | "babel-plugin.js", 34 | "native.d.ts" 35 | ], 36 | "devDependencies": { 37 | "@babel/core": "^7.7.5", 38 | "@babel/generator": "^7.7.4", 39 | "@babel/plugin-transform-modules-commonjs": "^7.7.5", 40 | "@babel/preset-env": "^7.7.6", 41 | "@babel/preset-typescript": "^7.7.4", 42 | "@types/jest": "^24.0.23", 43 | "@types/node": "^12.12.17", 44 | "@types/react-test-renderer": "^16.9.1", 45 | "@typescript-eslint/eslint-plugin": "^2.11.0", 46 | "@typescript-eslint/parser": "^2.11.0", 47 | "babel-plugin-macros": "^2.8.0", 48 | "eslint": "^6.7.2", 49 | "jest": "^24.9.0", 50 | "prettier": "^2.2.1", 51 | "react": "^16.12.0", 52 | "react-test-renderer": "^16.12.0", 53 | "typescript": "^3.7.3" 54 | }, 55 | "dependencies": { 56 | "@types/react": "16.9.5", 57 | "@types/react-native": "^0.60.25", 58 | "css-color-function": "^1.3.3", 59 | "css-to-react-native": "^3.0.0", 60 | "postcss": "^7.0.24", 61 | "postcss-selector-parser": "^6.0.2" 62 | }, 63 | "jest": { 64 | "testRegex": "/__tests__/.*\\.ts?$", 65 | "moduleFileExtensions": [ 66 | "ts", 67 | "tsx", 68 | "js", 69 | "jsx", 70 | "json", 71 | "node" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /runtime/VariablesContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export type Variables = Record; 4 | 5 | /* 6 | When react supports this, we can enable it 7 | const { keyBloom } = require("./bloomFilter"); 8 | 9 | const diff = (prev, current) => { 10 | let bloom = 0; 11 | for (const key in prev) { 12 | if (!current.hasOwnProperty(key) || prev[key] !== current[key]) { 13 | bloom |= keyBloom(key); 14 | } 15 | } 16 | for (const key in current) { 17 | if (!prev.hasOwnProperty(key)) { 18 | bloom |= keyBloom(key); 19 | } 20 | } 21 | return bloom; 22 | } 23 | */ 24 | 25 | export default createContext(/*:: */ {}); 26 | -------------------------------------------------------------------------------- /runtime/__mocks__/react-native.ts: -------------------------------------------------------------------------------- 1 | export const Easing = { 2 | linear: "linear", 3 | ease: "ease", 4 | bezier: (a: number, b: number, c: number, d: number) => 5 | `bezier(${a}, ${b}, ${c}, ${d})`, 6 | }; 7 | 8 | export class AnimatedNode { 9 | type: string; 10 | value: AnimatedNode | number; 11 | options?: any; 12 | 13 | constructor(type: string, value: AnimatedNode | number, options?: any) { 14 | this.type = type; 15 | this.value = value; 16 | 17 | if (options !== undefined) { 18 | this.options = options; 19 | } 20 | } 21 | 22 | start() { 23 | return this; 24 | } 25 | 26 | interpolate(options: any) { 27 | return new AnimatedNode("interpolate", this.value, options); 28 | } 29 | 30 | setValue() { 31 | return this; 32 | } 33 | } 34 | 35 | export const Animated = { 36 | Value: function (value: number) { 37 | return new AnimatedNode("value", value); 38 | }, 39 | timing: (value: AnimatedNode, options: any) => 40 | new AnimatedNode("timing", value, options), 41 | loop: (value: AnimatedNode, options: any) => 42 | new AnimatedNode("loop", value, options), 43 | parallel: (value: AnimatedNode, options: any) => 44 | new AnimatedNode("parallel", value, options), 45 | sequence: (value: AnimatedNode, options: any) => 46 | new AnimatedNode("sequence", value, options), 47 | }; 48 | 49 | export const StyleSheet = { 50 | flatten: (values: any) => 51 | Array.isArray(values) 52 | ? Object.assign({}, ...values.map(StyleSheet.flatten)) 53 | : values != null 54 | ? values 55 | : {}, 56 | }; 57 | -------------------------------------------------------------------------------- /runtime/__tests__/animationUtil.ts: -------------------------------------------------------------------------------- 1 | import { Animated } from "react-native"; 2 | import { interpolateValue } from "../animationUtil"; 3 | 4 | it("Inteprolates string values", () => { 5 | const animation = new Animated.Value(0); 6 | const interpolated = interpolateValue( 7 | [0, 1], 8 | ["red", "green"], 9 | animation, 10 | false 11 | ); 12 | expect(interpolated).toEqual({ 13 | type: "interpolate", 14 | value: 0, 15 | options: { inputRange: [0, 1], outputRange: ["red", "green"] }, 16 | }); 17 | }); 18 | 19 | it("Inteprolates number values with interpolate numbers = false", () => { 20 | const animation = new Animated.Value(0); 21 | const interpolated = interpolateValue([0, 1], [5, 10], animation, false); 22 | expect(interpolated).toEqual({ 23 | type: "interpolate", 24 | value: 0, 25 | options: { inputRange: [0, 1], outputRange: [5, 10] }, 26 | }); 27 | }); 28 | 29 | it("Inteprolates number values with interpolate numbers = true", () => { 30 | const animation = new Animated.Value(0); 31 | const interpolated = interpolateValue([0, 1], [5, 10], animation, true); 32 | expect(interpolated).toEqual({ type: "value", value: 0 }); 33 | }); 34 | 35 | it("Inteprolates transforms", () => { 36 | const animation = new Animated.Value(0); 37 | const interpolated = interpolateValue( 38 | [0, 1], 39 | [ 40 | [{ translateY: 5 }, { rotate: "30deg" }], 41 | [{ translateY: 10 }, { rotate: "45deg" }], 42 | ], 43 | animation, 44 | false 45 | ); 46 | expect(interpolated).toEqual([ 47 | { 48 | translateY: { 49 | type: "interpolate", 50 | value: 0, 51 | options: { inputRange: [0, 1], outputRange: [5, 10] }, 52 | }, 53 | }, 54 | { 55 | rotate: { 56 | type: "interpolate", 57 | value: 0, 58 | options: { inputRange: [0, 1], outputRange: ["30deg", "45deg"] }, 59 | }, 60 | }, 61 | ]); 62 | }); 63 | 64 | it("Logs console error when animated transform nodes don't match", () => { 65 | const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); 66 | const animation = new Animated.Value(0); 67 | interpolateValue( 68 | [0, 1], 69 | [ 70 | [{ translateY: 5 }, { rotate: "30deg" }], 71 | [{ translateY: 10 }, { rotate: "45deg" }, { scale: 3 }], 72 | ], 73 | animation, 74 | false 75 | ); 76 | expect(consoleSpy).toBeCalledWith( 77 | "Expected transforms to have same shape between transitions" 78 | ); 79 | consoleSpy.mockRestore(); 80 | }); 81 | 82 | it("Doesn't crash when transform nodes don't match", () => { 83 | const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); 84 | const animation = new Animated.Value(0); 85 | expect(() => { 86 | interpolateValue( 87 | [0, 1], 88 | [ 89 | [{ translateY: 10 }, { rotate: "45deg" }, { scale: 3 }], 90 | [{ translateY: 5 }, { rotate: "30deg" }], 91 | ], 92 | animation, 93 | false 94 | ); 95 | }).not.toThrow(); 96 | consoleSpy.mockRestore(); 97 | }); 98 | -------------------------------------------------------------------------------- /runtime/__tests__/resolveVariableDependencies.ts: -------------------------------------------------------------------------------- 1 | import resolveVariableDependencies from "../resolveVariableDependencies"; 2 | 3 | it("Resolves a single variable", () => { 4 | const actualVariables = resolveVariableDependencies({ color: "red" }, {}); 5 | 6 | expect(actualVariables).toEqual({ color: "red" }); 7 | }); 8 | 9 | it("Resolves a variable refercing a higher scope", () => { 10 | const actualVariables = resolveVariableDependencies( 11 | { primary: "red" }, 12 | { color: "var(--primary)" } 13 | ); 14 | 15 | expect(actualVariables).toEqual({ primary: "red", color: "red" }); 16 | }); 17 | 18 | it("Resolves a variable refercing the current scope", () => { 19 | const actualVariables = resolveVariableDependencies( 20 | {}, 21 | { color: "var(--primary)", primary: "red" } 22 | ); 23 | 24 | expect(actualVariables).toEqual({ color: "red", primary: "red" }); 25 | }); 26 | 27 | it("Has the current scope override higher scopes", () => { 28 | const actualVariables = resolveVariableDependencies( 29 | { primary: "blue" }, 30 | { color: "var(--primary)", primary: "red" } 31 | ); 32 | 33 | expect(actualVariables).toEqual({ color: "red", primary: "red" }); 34 | }); 35 | 36 | it("Allows defaults", () => { 37 | const actualVariables = resolveVariableDependencies( 38 | { color: "var(--primary, blue)" }, 39 | {} 40 | ); 41 | 42 | expect(actualVariables).toEqual({ color: "blue" }); 43 | }); 44 | 45 | it("Throws on allow circular dependencies", () => { 46 | expect(() => { 47 | resolveVariableDependencies( 48 | { color: "var(--primary)", primary: "var(--color)" }, 49 | {} 50 | ); 51 | }).toThrow(); 52 | }); 53 | -------------------------------------------------------------------------------- /runtime/__tests__/useCustomPropertyStyle.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { create } from "react-test-renderer"; 3 | import useCustomProperties from "../useCustomProperties"; 4 | import useCustomPropertyStyle from "../useCustomPropertyStyle"; 5 | import VariablesContext from "../VariablesContext"; 6 | 7 | it("Converts style tuples and variables into a style object", () => { 8 | const styleTuples: [string, string][] = [ 9 | ["color", "red"], 10 | ["margin", "var(--small)"], 11 | ]; 12 | 13 | let style: any; 14 | const Component = () => { 15 | const customProperties = useCustomProperties(); 16 | style = useCustomPropertyStyle(styleTuples, customProperties); 17 | return null; 18 | }; 19 | 20 | const instance = create( 21 | React.createElement( 22 | VariablesContext.Provider, 23 | { value: { small: "10px" } }, 24 | React.createElement(Component) 25 | ) 26 | ); 27 | 28 | expect(style).toEqual({ 29 | color: "red", 30 | marginTop: 10, 31 | marginRight: 10, 32 | marginBottom: 10, 33 | marginLeft: 10, 34 | }); 35 | 36 | instance.unmount(); 37 | }); 38 | -------------------------------------------------------------------------------- /runtime/__tests__/useTransition.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { create } from "react-test-renderer"; 3 | import { Transition } from "../animationTypes"; 4 | import useTransition from "../useTransition"; 5 | 6 | it("Transitions when changing styles", () => { 7 | const transition: Transition = [ 8 | { 9 | property: "color", 10 | delay: 0, 11 | duration: 1000, 12 | timingFunction: "linear", 13 | }, 14 | ]; 15 | 16 | let transitionStyle: any; 17 | const Test = ({ styles }) => { 18 | [, /* inputStyle */ transitionStyle] = useTransition(transition, styles); 19 | return null; 20 | }; 21 | 22 | const instance = create( 23 | React.createElement(Test, { styles: { color: "red" } }) 24 | ); 25 | 26 | expect(transitionStyle).toEqual({ 27 | color: { 28 | options: { inputRange: [0, 1], outputRange: ["red", "red"] }, 29 | type: "interpolate", 30 | value: 0, 31 | }, 32 | }); 33 | 34 | instance.update(React.createElement(Test, { styles: { color: "blue" } })); 35 | 36 | expect(transitionStyle).toEqual({ 37 | color: { 38 | options: { inputRange: [0, 1], outputRange: ["red", "blue"] }, 39 | type: "interpolate", 40 | value: 0, 41 | }, 42 | }); 43 | 44 | instance.unmount(); 45 | }); 46 | 47 | it("Does not transition when style has no value", () => { 48 | const transition: Transition = [ 49 | { 50 | property: "color", 51 | delay: 0, 52 | duration: 1000, 53 | timingFunction: "linear", 54 | }, 55 | ]; 56 | 57 | let transitionStyle: any; 58 | const Test = ({ styles }) => { 59 | [, /* inputStyle */ transitionStyle] = useTransition(transition, styles); 60 | return null; 61 | }; 62 | 63 | const instance = create(React.createElement(Test, { styles: {} })); 64 | 65 | expect(transitionStyle).toEqual({}); 66 | 67 | instance.unmount(); 68 | }); 69 | 70 | it("Handles going from no value to a new value", () => { 71 | const transition: Transition = [ 72 | { 73 | property: "color", 74 | delay: 0, 75 | duration: 1000, 76 | timingFunction: "linear", 77 | }, 78 | ]; 79 | 80 | let transitionStyle: any; 81 | const Test = ({ styles }) => { 82 | [, /* inputStyle */ transitionStyle] = useTransition(transition, styles); 83 | return null; 84 | }; 85 | 86 | const instance = create(React.createElement(Test, { styles: {} })); 87 | 88 | expect(transitionStyle).toEqual({}); 89 | 90 | instance.update(React.createElement(Test, { styles: { color: "red" } })); 91 | 92 | expect(transitionStyle).toEqual({}); 93 | 94 | instance.update(React.createElement(Test, { styles: { color: "blue" } })); 95 | 96 | expect(transitionStyle).toEqual({ 97 | color: { 98 | options: { inputRange: [0, 1], outputRange: ["red", "blue"] }, 99 | type: "interpolate", 100 | value: 0, 101 | }, 102 | }); 103 | 104 | instance.unmount(); 105 | }); 106 | 107 | it("Handles adding more transitioned values", () => { 108 | const transition1: Transition = [ 109 | { 110 | property: "color", 111 | delay: 0, 112 | duration: 1000, 113 | timingFunction: "linear", 114 | }, 115 | ]; 116 | const transition2: Transition = [ 117 | ...transition1, 118 | { 119 | property: "opacity", 120 | delay: 0, 121 | duration: 1000, 122 | timingFunction: "linear", 123 | }, 124 | ]; 125 | 126 | let transitionStyle: any; 127 | const Test = ({ styles, transition }) => { 128 | [, /* inputStyle */ transitionStyle] = useTransition(transition, styles); 129 | return null; 130 | }; 131 | 132 | const instance = create( 133 | React.createElement(Test, { 134 | styles: { color: "red", opacity: 0 }, 135 | transition: transition1, 136 | }) 137 | ); 138 | 139 | expect(transitionStyle).toEqual({ 140 | color: { 141 | options: { inputRange: [0, 1], outputRange: ["red", "red"] }, 142 | type: "interpolate", 143 | value: 0, 144 | }, 145 | }); 146 | 147 | instance.update( 148 | React.createElement(Test, { 149 | styles: { color: "blue", opacity: 0.5 }, 150 | transition: transition1, 151 | }) 152 | ); 153 | 154 | expect(transitionStyle).toEqual({ 155 | color: { 156 | options: { inputRange: [0, 1], outputRange: ["red", "blue"] }, 157 | type: "interpolate", 158 | value: 0, 159 | }, 160 | }); 161 | 162 | instance.update( 163 | React.createElement(Test, { 164 | styles: { color: "green", opacity: 1 }, 165 | transition: transition2, 166 | }) 167 | ); 168 | 169 | expect(transitionStyle).toEqual({ 170 | color: { 171 | options: { inputRange: [0, 1], outputRange: ["blue", "green"] }, 172 | type: "interpolate", 173 | value: 0, 174 | }, 175 | opacity: { type: "value", value: 0.5 }, 176 | }); 177 | 178 | instance.unmount(); 179 | }); 180 | 181 | it("Handles no more transitions", () => { 182 | const transition: Transition = []; 183 | 184 | let style: any; 185 | const Test = ({ styles }) => { 186 | style = useTransition(transition, styles); 187 | return null; 188 | }; 189 | 190 | const instance = create( 191 | React.createElement(Test, { styles: { color: "red" } }) 192 | ); 193 | 194 | expect(style).toEqual({ color: "red" }); 195 | 196 | instance.unmount(); 197 | }); 198 | -------------------------------------------------------------------------------- /runtime/animationShorthandUtil.ts: -------------------------------------------------------------------------------- 1 | export const getDurationInMs = (duration: string): number => { 2 | const time = parseFloat(duration); 3 | const factor = /ms$/i.test(duration) ? 1 : 1000; 4 | return time * factor; 5 | }; 6 | 7 | export const durationRegExp = /^[\d.]+m?s$/; 8 | 9 | export const easingRegExp = /^(linear|ease(?:-in)?(?:-out)+)$/i; 10 | -------------------------------------------------------------------------------- /runtime/animationTypes.ts: -------------------------------------------------------------------------------- 1 | export type TimingFunction = 2 | | "linear" 3 | | "ease" 4 | | "ease-in" 5 | | "ease-out" 6 | | "ease-in-out"; 7 | 8 | export type TransitionPart = { 9 | _?: string; 10 | delay?: string; 11 | duration?: string; 12 | property?: string; 13 | timingFunction?: string; 14 | }; 15 | 16 | export type Transition = Array<{ 17 | property: string; 18 | timingFunction: TimingFunction; 19 | delay: number; 20 | duration: number; 21 | }>; 22 | 23 | export type Keyframe = { 24 | time: number; 25 | style: Record; 26 | }; 27 | 28 | export type Keyframes = Record; 29 | 30 | export type AnimationPart = { 31 | _?: string; 32 | timingFunction?: string; 33 | delay?: string; 34 | duration?: string; 35 | iterations?: string; 36 | name?: string; 37 | }; 38 | 39 | export type Animation = { 40 | delay: number; 41 | duration: number; 42 | iterations: number; 43 | name: string | null; 44 | timingFunction: TimingFunction; 45 | }; 46 | -------------------------------------------------------------------------------- /runtime/animationUtil.ts: -------------------------------------------------------------------------------- 1 | import { Easing, EasingFunction } from "react-native"; 2 | import { TimingFunction } from "./AnimationTypes"; 3 | 4 | type TransformInterpolation = Array<{ [key: string]: string | number }>; 5 | export type Interpolation = number | string | TransformInterpolation; 6 | export type OutputRange = Interpolation[]; 7 | export type InterpolatedValue = Object | Object[]; 8 | 9 | export const interpolateValue = ( 10 | inputRange: number[], 11 | outputRange: OutputRange, 12 | animation: any, 13 | interpolateNumbers: boolean = false 14 | ): InterpolatedValue => { 15 | const firstValue = outputRange[0]; 16 | if (interpolateNumbers && typeof firstValue === "number") { 17 | return animation; 18 | } else if (!Array.isArray(firstValue)) { 19 | return animation.interpolate({ inputRange, outputRange }); 20 | } 21 | 22 | // transforms 23 | if (process.env.NODE_ENV !== "production") { 24 | const currentProperties = String(firstValue.map(Object.keys)); 25 | // Not the *best* practise here... 26 | const transformsAreConsistent = outputRange.every((range) => { 27 | const rangeProperties = String( 28 | (range as TransformInterpolation).map(Object.keys) 29 | ); 30 | return currentProperties === rangeProperties; 31 | }); 32 | 33 | if (!transformsAreConsistent) { 34 | // eslint-disable-next-line no-console 35 | console.error( 36 | "Expected transforms to have same shape between transitions" 37 | ); 38 | } 39 | } 40 | 41 | return firstValue.map((transform, index) => { 42 | const transformProperty = Object.keys(transform)[0]; 43 | const innerOutputRange = outputRange.map((range) => { 44 | const rangeValue = range[index]; 45 | return rangeValue != null ? rangeValue[transformProperty] : null; 46 | }); 47 | 48 | // We *have* to interpolate even numeric values, as we will always animate between 0--1 49 | const interpolation = animation.interpolate({ 50 | inputRange, 51 | outputRange: innerOutputRange, 52 | }); 53 | 54 | return { [transformProperty]: interpolation }; 55 | }); 56 | }; 57 | 58 | export const easingFunctions: Record = { 59 | linear: Easing.linear, 60 | ease: Easing.ease, 61 | "ease-in": Easing.bezier(0.42, 0, 1.0, 1.0), 62 | "ease-out": Easing.bezier(0, 0, 0.58, 1.0), 63 | "ease-in-out": Easing.bezier(0.42, 0, 0.58, 1.0), 64 | }; 65 | -------------------------------------------------------------------------------- /runtime/bloomFilter.ts: -------------------------------------------------------------------------------- 1 | const keyBloom = (key: string): number => 1 << key.charCodeAt(0) % 31; 2 | -------------------------------------------------------------------------------- /runtime/css-transforms/__tests__/colors.ts: -------------------------------------------------------------------------------- 1 | import colors from "../colors"; 2 | 3 | it("Converts colors", () => { 4 | const actual = colors("color(red lightness(25%))"); 5 | expect(actual).toBe("rgb(128, 0, 0)"); 6 | }); 7 | 8 | it("Converts colors that contain rgb", () => { 9 | const actual = colors("color(rgb(255, 0, 0) lightness(25%))"); 10 | expect(actual).toBe("rgb(128, 0, 0)"); 11 | }); 12 | -------------------------------------------------------------------------------- /runtime/css-transforms/__tests__/variables.ts: -------------------------------------------------------------------------------- 1 | import variables from "../variables"; 2 | 3 | it("Handles multiple variables", () => { 4 | const actual = variables("var(--a) var(--b)", { a: "1px", b: "2px" }); 5 | expect(actual).toEqual("1px 2px"); 6 | }); 7 | -------------------------------------------------------------------------------- /runtime/css-transforms/__tests__/viewport.ts: -------------------------------------------------------------------------------- 1 | import viewport from "../viewport"; 2 | 3 | it("Transforms viewport units", () => { 4 | const actual = viewport("10vw 10vh", { width: 100, height: 200 }); 5 | expect(actual).toBe("10px 20px"); 6 | }); 7 | 8 | it("Transforms viewport min/max units", () => { 9 | const actual = viewport("10vmin 10vmax", { width: 100, height: 200 }); 10 | expect(actual).toBe("10px 20px"); 11 | }); 12 | -------------------------------------------------------------------------------- /runtime/css-transforms/calc.ts: -------------------------------------------------------------------------------- 1 | // Two levels of nesting 2 | const calcFnRe = /calc\(((?:[^(]|\((?:[^(]|\([^(]+\))+\))+)\)/g; 3 | const bracketsRe = /\(([^)]+)\)/g; 4 | const addSubtractRe = /([^+-]+)([+-])(.*)/; 5 | const multiplyDivideRe = /([^*/]+)([*/])(.*)/; 6 | const unitRe = /([\d.]+)(px|)/; 7 | 8 | type Node = { value: number; unit: "" | "px" }; 9 | 10 | const resolveValue = (value: string): Node | null => { 11 | const match = value.match(unitRe); 12 | if (match === null) return null; 13 | return { value: Number(match[1]), unit: match[2] as any }; 14 | }; 15 | 16 | const resolveMultiplyDivide = (value: string): Node | null => { 17 | const match = value.match(multiplyDivideRe); 18 | if (match === null) return resolveValue(value); 19 | 20 | const lhs = resolveValue(match[1]); 21 | if (lhs === null) return null; 22 | const rhs = resolveMultiplyDivide(match[3]); 23 | if (rhs === null) return null; 24 | 25 | if (match[2] === "*") { 26 | if (lhs.unit.length === 0) { 27 | return { value: lhs.value * rhs.value, unit: rhs.unit }; 28 | } else if (rhs.unit.length === 0) { 29 | return { value: lhs.value * rhs.value, unit: lhs.unit }; 30 | } 31 | } else if (match[2] === "/") { 32 | if (rhs.unit.length === 0) { 33 | return { value: lhs.value / rhs.value, unit: lhs.unit }; 34 | } 35 | } 36 | 37 | return null; 38 | }; 39 | 40 | const resolveAddSubtract = (value: string): Node | null => { 41 | const match = value.match(addSubtractRe); 42 | if (match === null) return resolveMultiplyDivide(value); 43 | 44 | const lhs = resolveMultiplyDivide(match[1]); 45 | if (lhs === null) return null; 46 | const rhs = resolveAddSubtract(match[3]); 47 | if (rhs === null) return null; 48 | 49 | if (lhs.unit !== rhs.unit) return null; 50 | 51 | return { 52 | value: match[2] === "+" ? lhs.value + rhs.value : lhs.value - rhs.value, 53 | unit: lhs.unit, 54 | }; 55 | }; 56 | 57 | const resolveBrackets = (value: string): string => { 58 | const out = value.replace(bracketsRe, (_, inner) => resolveBrackets(inner)); 59 | const node = resolveAddSubtract(out); 60 | 61 | if (node !== null) { 62 | return `${node.value}${node.unit}`; 63 | } else { 64 | throw new Error("Failed to parse calc"); 65 | } 66 | }; 67 | 68 | export default (inputValue: string): string => { 69 | let value = inputValue; 70 | let didReplace: boolean; 71 | do { 72 | didReplace = false; 73 | value = value.replace(calcFnRe, (_, rest) => { 74 | didReplace = true; 75 | return resolveBrackets(rest); 76 | }); 77 | } while (didReplace); 78 | return value; 79 | }; 80 | -------------------------------------------------------------------------------- /runtime/css-transforms/colors.ts: -------------------------------------------------------------------------------- 1 | import { convert } from "css-color-function"; 2 | 3 | const colorFnRe = /color\((?:[^()]+|\([^)]+\))+\)/g; 4 | 5 | export default (value: string): string => value.replace(colorFnRe, convert); 6 | -------------------------------------------------------------------------------- /runtime/css-transforms/variables.ts: -------------------------------------------------------------------------------- 1 | import { varRegExp } from "../cssRegExp"; 2 | 3 | export default ( 4 | value: string, 5 | appliedVariables: { [key: string]: string } 6 | ): string => 7 | value.replace( 8 | varRegExp, 9 | (_, variableName, fallback) => appliedVariables[variableName] || fallback 10 | ); 11 | -------------------------------------------------------------------------------- /runtime/css-transforms/viewport.ts: -------------------------------------------------------------------------------- 1 | import { viewportUnitRegExp } from "../cssRegExp"; 2 | 3 | export default ( 4 | value: string, 5 | { width, height }: { width: number; height: number } 6 | ): string => 7 | value.replace(viewportUnitRegExp, (m, value, unit) => { 8 | switch (unit) { 9 | case "vw": 10 | return (Number(value) * width) / 100 + "px"; 11 | case "vh": 12 | return (Number(value) * height) / 100 + "px"; 13 | case "vmin": 14 | return (Number(value) * Math.min(width, height)) / 100 + "px"; 15 | case "vmax": 16 | return (Number(value) * Math.max(width, height)) / 100 + "px"; 17 | default: 18 | return m; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /runtime/cssRegExp.ts: -------------------------------------------------------------------------------- 1 | export const varRegExp = /var\s*\(\s*--([_a-z0-9-]+)\s*(?:,\s*([^)]+))?\)/gi; 2 | export const varRegExpNonGlobal = /var\s*\(\s*--([_a-z0-9-]+)\s*(?:,\s*([^)]+))?\)/i; 3 | export const viewportUnitRegExp = /([+-\d.Ee]+)(vw|vh|vmin|vmax)/g; 4 | export const viewportUnitRegExpNonGlobal = /([+-\d.Ee]+)(vw|vh|vmin|vmax)/; 5 | -------------------------------------------------------------------------------- /runtime/cssUtil.ts: -------------------------------------------------------------------------------- 1 | import cssToReactNative, { transformRawValue } from "css-to-react-native"; 2 | import transformVariables from "./css-transforms/variables"; 3 | import transformColors from "./css-transforms/colors"; 4 | import transformCalc from "./css-transforms/calc"; 5 | import { Variables } from "./VariablesContext"; 6 | // Viewport (hopefully) already transformed 7 | 8 | export type StyleTuple = [string, string]; 9 | export type Style = Record; 10 | 11 | export { transformRawValue }; 12 | 13 | export const transformStyleTuples = ( 14 | styleTuples: StyleTuple[], 15 | appliedVariables?: Variables 16 | ): Style => { 17 | const transformedStyleTuples = styleTuples.map(([property, value]) => { 18 | let transformedValue = value; 19 | if (appliedVariables != null) { 20 | transformedValue = transformVariables(transformedValue, appliedVariables); 21 | } 22 | transformedValue = transformColors(transformedValue); 23 | transformedValue = transformCalc(transformedValue); 24 | return [property, transformedValue]; 25 | }); 26 | return cssToReactNative(transformedStyleTuples); 27 | }; 28 | -------------------------------------------------------------------------------- /runtime/flattenAnimation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getDurationInMs, 3 | durationRegExp, 4 | easingRegExp, 5 | } from "./animationShorthandUtil"; 6 | import { Animation, AnimationPart } from "./animationTypes"; 7 | 8 | const iterationsRegExp = /^(\d+|infinite)$/i; 9 | 10 | const getIterationCount = (iteration: string): number => 11 | /infinite/i.test(iteration) ? -1 : parseInt(iteration, 10); 12 | 13 | const defaultValue = (): Animation => ({ 14 | delay: 0, 15 | duration: 0, 16 | iterations: 1, 17 | name: null, 18 | timingFunction: "ease", 19 | }); 20 | 21 | const TIMING_FUNCTION = 1 << 0; 22 | const DURATION = 1 << 1; 23 | const DELAY = 1 << 2; 24 | const ITERATION_COUNT = 1 << 3; 25 | const NAME = 1 << 4; 26 | 27 | const getAnimationShorthand = (shorthandParts: string[]) => { 28 | const accum = defaultValue(); 29 | let set = 0; 30 | 31 | shorthandParts.forEach((part) => { 32 | if (!(set & TIMING_FUNCTION) && easingRegExp.test(part)) { 33 | accum.timingFunction = part as any; 34 | set &= TIMING_FUNCTION; 35 | } else if (!(set & DURATION) && durationRegExp.test(part)) { 36 | accum.duration = getDurationInMs(part); 37 | set &= DURATION; 38 | } else if (!(set & DELAY) && durationRegExp.test(part)) { 39 | accum.delay = getDurationInMs(part); 40 | set &= DURATION; 41 | } else if (!(set & ITERATION_COUNT) && iterationsRegExp.test(part)) { 42 | accum.iterations = getIterationCount(part); 43 | set &= ITERATION_COUNT; 44 | } else if (!(set & NAME)) { 45 | accum.name = part; 46 | set &= NAME; 47 | } else { 48 | throw new Error("Failed to parse shorthand"); 49 | } 50 | }); 51 | 52 | return accum; 53 | }; 54 | 55 | export default (styles: AnimationPart[]): Animation => { 56 | let accum = defaultValue(); 57 | styles.forEach((style) => { 58 | if (style == null) return; 59 | 60 | if (style._ != null) { 61 | accum = getAnimationShorthand(style._.trim().split(/\s+/)); 62 | } 63 | if (style.timingFunction != null) { 64 | accum.timingFunction = style.timingFunction as any; 65 | } 66 | if (style.delay != null) { 67 | accum.delay = getDurationInMs(style.delay); 68 | } 69 | if (style.duration != null) { 70 | accum.duration = getDurationInMs(style.duration); 71 | } 72 | if (style.iterations != null) { 73 | accum.iterations = getIterationCount(style.iterations); 74 | } 75 | if (style.name != null) { 76 | accum.name = style.name !== "none" ? style.name : null; 77 | } 78 | }); 79 | return accum; 80 | }; 81 | -------------------------------------------------------------------------------- /runtime/flattenTransition.ts: -------------------------------------------------------------------------------- 1 | import { getPropertyName } from "css-to-react-native"; 2 | import { 3 | getDurationInMs, 4 | durationRegExp, 5 | easingRegExp, 6 | } from "./animationShorthandUtil"; 7 | import { Transition, TimingFunction, TransitionPart } from "./animationTypes"; 8 | 9 | type IntermediateTransition = { 10 | property: string | null; 11 | timingFunction: TimingFunction; 12 | delay: number; 13 | duration: number; 14 | }; 15 | 16 | const DELAY = 1; 17 | const DURATION = 1 << 1; 18 | const TIMING_FUNCTION = 1 << 2; 19 | const PROPERTY = 1 << 3; 20 | 21 | const getTransitionShorthand = ( 22 | shorthandParts: string[] 23 | ): IntermediateTransition => { 24 | const accum: IntermediateTransition = { 25 | delay: 0, 26 | duration: 0, 27 | timingFunction: "ease", 28 | property: null, 29 | }; 30 | let set = 0; 31 | 32 | shorthandParts.forEach((part) => { 33 | if (!(set & TIMING_FUNCTION) && easingRegExp.test(part)) { 34 | accum.timingFunction = part as any; 35 | set &= TIMING_FUNCTION; 36 | } else if (!(set & DURATION) && durationRegExp.test(part)) { 37 | accum.duration = getDurationInMs(part); 38 | set &= DURATION; 39 | } else if (!(set & DELAY) && durationRegExp.test(part)) { 40 | accum.delay = getDurationInMs(part); 41 | set &= DELAY; 42 | } else if (!(set & PROPERTY)) { 43 | accum.property = getPropertyName(part); 44 | set &= PROPERTY; 45 | } else { 46 | throw new Error("Failed to parse shorthand"); 47 | } 48 | }); 49 | 50 | return accum; 51 | }; 52 | 53 | const split = (value: string) => value.trim().split(/\s*,\s*/); 54 | 55 | export default (styles: TransitionPart[]): Transition => { 56 | let delays = []; 57 | let durations = []; 58 | let timingFunctions = []; 59 | let properties: string[] = []; 60 | 61 | styles.forEach((style) => { 62 | if (style == null) return; 63 | 64 | if (style._ != null) { 65 | delays = []; 66 | durations = []; 67 | timingFunctions = []; 68 | properties = []; 69 | 70 | split(style._).forEach((shorthand) => { 71 | const resolved = getTransitionShorthand(shorthand.trim().split(/\s+/)); 72 | const property = resolved.property; 73 | 74 | if (property != null) { 75 | timingFunctions.push(resolved.timingFunction); 76 | delays.push(resolved.delay); 77 | durations.push(resolved.duration); 78 | properties.push(property); 79 | } 80 | }); 81 | } 82 | if (style.timingFunction != null) { 83 | timingFunctions = split(style.timingFunction); 84 | } 85 | if (style.delay != null) { 86 | delays = split(style.delay).map(getDurationInMs); 87 | } 88 | if (style.duration != null) { 89 | durations = split(style.duration).map(getDurationInMs); 90 | } 91 | if (style.property != null) { 92 | properties = 93 | style.property === "none" 94 | ? [] 95 | : split(style.property).map(getPropertyName); 96 | } 97 | }); 98 | 99 | const transitions: Transition = properties.map((property: string, index) => { 100 | /* 101 | Per spec, cycle through multiple values if transition-property length 102 | exceeds the length of the other property 103 | */ 104 | const delay = delays[index % delays.length]; 105 | const duration = durations[index % durations.length]; 106 | const timingFunction: TimingFunction = 107 | timingFunctions[index % timingFunctions.length]; 108 | return { property, timingFunction, delay, duration }; 109 | }); 110 | 111 | return transitions; 112 | }; 113 | -------------------------------------------------------------------------------- /runtime/resolveVariableDependencies.ts: -------------------------------------------------------------------------------- 1 | import { varRegExp } from "./cssRegExp"; 2 | import { Variables } from "./VariablesContext"; 3 | 4 | /* eslint-disable no-prototype-builtins */ 5 | 6 | export default (scope: Variables, next: Variables): Variables => { 7 | const out = {}; 8 | 9 | /* 10 | Used to check for circular dependencies when calling resolve 11 | We can actully use one mutable value here, even though the function is recursive 12 | */ 13 | const resolveChain = []; 14 | 15 | const resolve = (key: string) => { 16 | const existing = out[key]; 17 | if (existing != null) return existing; 18 | 19 | let unresolvedValue: string | null; 20 | if (next.hasOwnProperty(key)) { 21 | const value = next[key]; 22 | if (value === "initial") { 23 | unresolvedValue = null; 24 | } else if (value === "inherit" || value === "unset") { 25 | unresolvedValue = scope[key]; 26 | } else { 27 | unresolvedValue = value; 28 | } 29 | } else if (scope.hasOwnProperty(key)) { 30 | unresolvedValue = scope[key]; 31 | } else { 32 | unresolvedValue = null; 33 | } 34 | 35 | if (unresolvedValue == null) return undefined; 36 | 37 | const chainIndex = resolveChain.indexOf(key); 38 | if (chainIndex !== -1) { 39 | const circularLoop = resolveChain 40 | .slice(chainIndex) 41 | .concat(key) 42 | .join(" -> "); 43 | throw new Error( 44 | `Circular dependency found in CSS custom properties: ${circularLoop}` 45 | ); 46 | } 47 | 48 | resolveChain.push(key); 49 | 50 | let missingValues = false; 51 | let resolvedValue = unresolvedValue.replace( 52 | varRegExp, 53 | (_, reference, fallback) => { 54 | const resolved = resolve(reference); 55 | if (resolved != null) { 56 | return resolved; 57 | } else if (fallback != null) { 58 | return fallback; 59 | } else { 60 | missingValues = true; 61 | return ""; 62 | } 63 | } 64 | ); 65 | 66 | if (!missingValues) { 67 | out[key] = resolvedValue; 68 | } 69 | 70 | resolveChain.pop(); 71 | 72 | return resolvedValue; 73 | }; 74 | 75 | Object.keys(scope).forEach(resolve); 76 | Object.keys(next).forEach(resolve); 77 | 78 | return out; 79 | }; 80 | -------------------------------------------------------------------------------- /runtime/useAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from "react"; 2 | import { StyleSheet, Animated } from "react-native"; 3 | import { Keyframes, Animation } from "./AnimationTypes"; 4 | import { 5 | interpolateValue, 6 | easingFunctions, 7 | OutputRange, 8 | InterpolatedValue, 9 | } from "./animationUtil"; 10 | import { Style } from "./cssUtil"; 11 | 12 | type AnimationState = { 13 | animations: Record | undefined; 14 | animationValues: Record | undefined; 15 | delay: number; 16 | duration: number; 17 | iterations: number; 18 | name: string | undefined; 19 | timingFunction: any; 20 | }; 21 | 22 | const noAnimations: AnimationState = { 23 | animations: null, 24 | animationValues: null, 25 | delay: 0, 26 | duration: 0, 27 | iterations: 1, 28 | name: null, 29 | timingFunction: "ease", 30 | }; 31 | 32 | const getAnimationState = ( 33 | keyframes: Keyframes, 34 | animation: Animation | null | undefined, 35 | style: Style 36 | ): AnimationState => { 37 | if (animation == null) return noAnimations; 38 | 39 | const { delay, duration, iterations, name, timingFunction } = animation; 40 | const currentStyles = StyleSheet.flatten(style); 41 | 42 | const animationSequence = name != null ? keyframes[name] : null; 43 | if (animationSequence == null) return noAnimations; 44 | 45 | const animatedProperties = Object.keys( 46 | Object.assign({}, ...animationSequence.map((frame) => frame.style)) 47 | ); 48 | 49 | const animationValues = {}; 50 | const animations = {}; 51 | animatedProperties.forEach((animationProperty) => { 52 | animationValues[animationProperty] = new Animated.Value(0); 53 | 54 | const currentValue = currentStyles[animationProperty]; 55 | 56 | let keyframes = animationSequence 57 | .filter((frame) => animationProperty in frame.style) 58 | .map(({ time, style }) => ({ time, value: style[animationProperty] })); 59 | // Fixes missing start/end values 60 | keyframes = [].concat( 61 | keyframes[0].time > 0 ? [{ time: 0, value: currentValue }] : [], 62 | keyframes, 63 | keyframes[keyframes.length - 1].time < 1 64 | ? [{ time: 1, value: currentValue }] 65 | : [] 66 | ); 67 | 68 | const inputRange = keyframes.map((frame) => frame.time); 69 | const outputRange: OutputRange = keyframes.map((frame) => frame.value); 70 | const animation = animationValues[animationProperty]; 71 | animations[animationProperty] = interpolateValue( 72 | inputRange, 73 | outputRange, 74 | animation 75 | ); 76 | }, {}); 77 | 78 | return { 79 | animations, 80 | animationValues, 81 | delay, 82 | duration, 83 | iterations, 84 | name, 85 | timingFunction, 86 | }; 87 | }; 88 | 89 | const animate = ({ 90 | delay, 91 | duration, 92 | iterations, 93 | timingFunction, 94 | animationValues: animationValuesObject, 95 | }) => { 96 | if (animationValuesObject == null) return; 97 | 98 | const animationValues: Animated.Value[] = Object.values( 99 | animationValuesObject 100 | ); 101 | 102 | animationValues.forEach((animation) => animation.setValue(0)); 103 | 104 | const timings = animationValues.map((animation) => { 105 | const config = { 106 | toValue: 1, 107 | duration, 108 | delay, 109 | easing: 110 | typeof timingFunction === "string" 111 | ? easingFunctions[timingFunction] 112 | : timingFunction, 113 | }; 114 | let res = Animated.timing(animation, config); 115 | 116 | if (iterations !== 1) { 117 | res = Animated.sequence([ 118 | res, 119 | // Reset animation 120 | Animated.timing(animation, { toValue: 0, duration: 0 }), 121 | ]); 122 | res = Animated.loop(res, { iterations }); 123 | } 124 | 125 | return res; 126 | }); 127 | 128 | Animated.parallel(timings).start(); 129 | /* 130 | ({ finished }) => { 131 | // FIXME: This doesn't seem to clear the animation 132 | if (finished) { 133 | setState({ animations: null, animationValues: null }); 134 | } 135 | } 136 | */ 137 | }; 138 | 139 | export default ( 140 | keyframes: Keyframes, 141 | animation: Animation | null | undefined, 142 | style: any 143 | ) => { 144 | const state = getAnimationState(keyframes, animation, style); 145 | 146 | const { animations, name } = state; 147 | useLayoutEffect(() => { 148 | animate(state); 149 | }, [name]); 150 | 151 | const nextStyle = animations == null ? style : [style, animations]; 152 | 153 | return nextStyle; 154 | }; 155 | -------------------------------------------------------------------------------- /runtime/useCustomProperties.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from "react"; 2 | import VariablesContext from "./VariablesContext"; 3 | import resolveVariableDependencies from "./resolveVariableDependencies"; 4 | import { Variables } from "./VariablesContext"; 5 | 6 | export default ( 7 | exportedCustomProperties?: Variables 8 | // bloom?: number 9 | ): Variables => { 10 | // let scope = React.useContext(VariablesContext, bloom); 11 | const inputScope = useContext(VariablesContext); 12 | 13 | const outputScope = useMemo(() => { 14 | return exportedCustomProperties != null 15 | ? resolveVariableDependencies(inputScope, exportedCustomProperties) 16 | : inputScope; 17 | }, [inputScope, exportedCustomProperties]); 18 | 19 | return outputScope; 20 | }; 21 | -------------------------------------------------------------------------------- /runtime/useCustomPropertyShorthandParts.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import variables from "./css-transforms/variables"; 3 | import { Variables } from "./VariablesContext"; 4 | 5 | export default ( 6 | unresolvedShorthandParts: Record[], 7 | customProperties: Variables 8 | ): Record[] => { 9 | return useMemo(() => { 10 | return unresolvedShorthandParts.map((part) => { 11 | const accum = {}; 12 | Object.keys(part).forEach((key) => { 13 | accum[key] = variables(part[key], customProperties); 14 | }); 15 | return accum; 16 | }); 17 | }, [unresolvedShorthandParts, customProperties]); 18 | }; 19 | -------------------------------------------------------------------------------- /runtime/useCustomPropertyStyle.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { transformStyleTuples } from "./cssUtil"; 3 | import { StyleTuple } from "./cssUtil"; 4 | import { Variables } from "./VariablesContext"; 5 | 6 | export default ( 7 | unresolvedStyleTuples: StyleTuple[], 8 | customProperties: Variables 9 | ): any => { 10 | return useMemo( 11 | () => transformStyleTuples(unresolvedStyleTuples, customProperties), 12 | [unresolvedStyleTuples, customProperties] 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /runtime/useTransition.ts: -------------------------------------------------------------------------------- 1 | import { useState, useLayoutEffect, useMemo } from "react"; 2 | import { StyleSheet, Animated } from "react-native"; 3 | import { Transition } from "./animationTypes"; 4 | import { interpolateValue, easingFunctions } from "./animationUtil"; 5 | import { Style } from "./cssUtil"; 6 | 7 | const getInitialValue = (targetValue: any): number => 8 | typeof targetValue === "number" ? targetValue : 0; 9 | 10 | const useStyleGroup = (transition: Style, inputStyleUnflattened: any) => { 11 | const inputStyle = StyleSheet.flatten(inputStyleUnflattened); 12 | const [{ style, previousStyle }, setStyleGroup] = useState(() => ({ 13 | style: inputStyle, 14 | previousStyle: inputStyle, 15 | })); 16 | 17 | const styleChanged = transition.some( 18 | ({ property }) => inputStyle[property] !== style[property] 19 | ); 20 | if (styleChanged) { 21 | setStyleGroup({ style: inputStyle, previousStyle: style }); 22 | } 23 | 24 | return { style, previousStyle }; 25 | }; 26 | 27 | type AnimationValues = Record; 28 | 29 | const useAnimationValues = ( 30 | transition: Transition, 31 | style: Style 32 | ): AnimationValues => { 33 | const [animationValues, setAnimationValues] = useState(() => { 34 | const values = {}; 35 | transition.forEach(({ property }) => { 36 | const initialValue = getInitialValue(style[property]); 37 | values[property] = new Animated.Value(initialValue); 38 | }); 39 | return values; 40 | }); 41 | 42 | const needsNewAnimationValues = 43 | Object.keys(animationValues).length !== transition.length || 44 | transition.some(({ property }) => animationValues[property] == null); 45 | 46 | if (needsNewAnimationValues) { 47 | const values = {}; 48 | transition.forEach(({ property }) => { 49 | const existing = animationValues[property]; 50 | if (existing != null) { 51 | values[property] = existing; 52 | } else { 53 | const initialValue = getInitialValue(style[property]); 54 | values[property] = new Animated.Value(initialValue); 55 | } 56 | }); 57 | 58 | setAnimationValues(values); 59 | } 60 | 61 | return animationValues; 62 | }; 63 | 64 | const animate = ( 65 | transition: Transition, 66 | style: Style, 67 | previousStyle: Style, 68 | animationValues: AnimationValues 69 | ) => { 70 | // Don't run on initial mount 71 | if (style === previousStyle) return; 72 | 73 | const animations = transition.map( 74 | ({ property, delay, duration, timingFunction }) => { 75 | const animation = animationValues[property]; 76 | 77 | const targetValue = style[property]; 78 | const needsInterpolation = typeof targetValue !== "number"; 79 | const toValue = !needsInterpolation ? targetValue : 1; 80 | 81 | if (needsInterpolation) animation.setValue(0); 82 | 83 | return Animated.timing(animation, { 84 | toValue, 85 | duration, 86 | delay, 87 | easing: easingFunctions[timingFunction], 88 | useNativeDriver: false, 89 | }); 90 | } 91 | ); 92 | 93 | Animated.parallel(animations).start(); 94 | }; 95 | 96 | export default (transition: Transition, inputStyleUnflattened: any) => { 97 | const { style, previousStyle } = useStyleGroup( 98 | transition, 99 | inputStyleUnflattened 100 | ); 101 | const animationValues = useAnimationValues(transition, style); 102 | 103 | useLayoutEffect(() => { 104 | animate(transition, style, previousStyle, animationValues); 105 | }, [style, previousStyle]); 106 | 107 | const nextStyle = useMemo(() => { 108 | const animationNames = Object.keys(animationValues); 109 | 110 | if (animationNames.length === 0) return inputStyleUnflattened; 111 | 112 | const transitionStyle = {}; 113 | 114 | animationNames.forEach((animationName) => { 115 | const previousValue = previousStyle[animationName]; 116 | const nextValue = style[animationName]; 117 | 118 | if (previousValue != null && nextValue != null) { 119 | transitionStyle[animationName] = interpolateValue( 120 | [0, 1], 121 | [previousValue, nextValue], 122 | animationValues[animationName], 123 | true /* interpolate numbers */ 124 | ); 125 | } 126 | }); 127 | 128 | return [style, transitionStyle]; 129 | }, [animationValues, style, previousStyle]); 130 | 131 | return nextStyle; 132 | }; 133 | -------------------------------------------------------------------------------- /runtime/useViewportStyle.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import transformViewport from "./css-transforms/viewport"; 3 | import useWindowDimensions from "./useWindowDimensions"; 4 | import { transformStyleTuples } from "./cssUtil"; 5 | import { StyleTuple, Style } from "./cssUtil"; 6 | 7 | export default (unresolvedStyleTuples: StyleTuple[]): Style => { 8 | const windowDimensions = useWindowDimensions(); 9 | 10 | return useMemo(() => { 11 | const styleTuples: StyleTuple[] = unresolvedStyleTuples.map( 12 | ([prop, value]) => [prop, transformViewport(value, windowDimensions)] 13 | ); 14 | return transformStyleTuples(styleTuples); 15 | }, [unresolvedStyleTuples, windowDimensions]); 16 | }; 17 | -------------------------------------------------------------------------------- /runtime/useViewportStyleTuples.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import useWindowDimensions from "./useWindowDimensions"; 3 | import transformViewport from "./css-transforms/viewport"; 4 | import { StyleTuple } from "./cssUtil"; 5 | 6 | export default (unresolvedStyleTuples: StyleTuple[]): StyleTuple[] => { 7 | const windowDimensions = useWindowDimensions(); 8 | 9 | return useMemo(() => { 10 | return unresolvedStyleTuples.map(([prop, value]) => [ 11 | prop, 12 | transformViewport(value, windowDimensions), 13 | ]); 14 | }, [unresolvedStyleTuples, windowDimensions]); 15 | }; 16 | -------------------------------------------------------------------------------- /runtime/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Dimensions } from "react-native"; 3 | 4 | export default (): { width: number; height: number } => { 5 | const [dimensions, setDimensions] = useState(Dimensions.get("window")); 6 | 7 | useEffect(() => { 8 | setDimensions(Dimensions.get("window")); 9 | 10 | const listener = ({ window }) => { 11 | setDimensions(window); 12 | }; 13 | Dimensions.addEventListener("change", listener); 14 | 15 | return () => Dimensions.removeEventListener("change", listener); 16 | }, []); 17 | 18 | return dimensions; 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2019"], 4 | "esModuleInterop": true, 5 | "jsx": "react-native" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types/css-color-function.d.ts: -------------------------------------------------------------------------------- 1 | declare module "css-color-function" { 2 | export const convert: (value: string) => string; 3 | } 4 | --------------------------------------------------------------------------------