├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── index.js ├── package.json └── src ├── components ├── BottomNavigation.js ├── BottomNavigationItem.js ├── Button.js ├── Card.js ├── Divider.js ├── Icon.js ├── Image.js ├── NativeModules.js ├── PropTypes.js ├── PureComponent.js ├── Ripple.js ├── StatusBar.js ├── StyleSheet.js ├── TabItem.js ├── Tabs.js ├── Text.js ├── ViewPager.js ├── index.js └── internal │ ├── Button.js │ └── ViewPager.js ├── index.js ├── libs ├── immutable.js ├── resolver.js └── utils.js ├── providers ├── loader.js └── theme.js └── theme ├── default.js └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "__DEV__": true 10 | }, 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "experimentalObjectRestSpread": true, 15 | "jsx": true 16 | }, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react", 21 | "react-native" 22 | ], 23 | "rules": { 24 | "indent": [ 25 | "error", 26 | 2, 27 | { 28 | "SwitchCase": 1 29 | } 30 | ], 31 | "linebreak-style": [ 32 | "error", 33 | "unix" 34 | ], 35 | "no-empty": [ 36 | "error", 37 | { 38 | "allowEmptyCatch": true 39 | } 40 | ], 41 | "no-unused-vars": [ 42 | "error", 43 | { 44 | "args": "after-used", 45 | "vars": "all", 46 | "ignoreRestSiblings": true, 47 | "argsIgnorePattern": "^_" 48 | } 49 | ], 50 | "quotes": [ 51 | "error", 52 | "single" 53 | ], 54 | "react-native/no-unused-styles": 2, 55 | "react-native/split-platform-components": 2, 56 | "react-native/no-inline-styles": 0, 57 | "react-native/no-color-literals": 2, 58 | "react/jsx-uses-react": "error", 59 | "react/jsx-uses-vars": "error", 60 | "semi": [ 61 | "error", 62 | "never" 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | package-lock.json 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-mdcore 2 | 3 | Material Design for both Android and iOS. `ThemeProvider` has to be set at root of component tree. All children components can access theme properties via `context`. Features: 4 | 5 | - Provide [default theme properties](src/libs/theme/default.js) for all material components. 6 | - Easy to customize theme. 7 | - Provide most of material components to build cross platform apps with consistent APIs. 8 | 9 | 10 | ## Installation 11 | 12 | ```node 13 | npm install react-native-mdcore --save 14 | ``` 15 | 16 | 17 | ## Sample app 18 | 19 | Checkout [https://github.com/henrytao-me/react-native-workshop](https://github.com/henrytao-me/react-native-workshop) 20 | 21 | 22 | ## Usages 23 | 24 | ### Setup ThemeProvider 25 | 26 | Use default theme: 27 | 28 | ```js 29 | import React from 'react' 30 | import { 31 | PureComponent, 32 | ThemeProvider 33 | } from 'react-native-mdcore' 34 | 35 | export default class Main extends PureComponent { 36 | 37 | render() { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | } 45 | ``` 46 | 47 | Use custom theme: 48 | 49 | ```js 50 | import React from 'react' 51 | import { 52 | PureComponent, 53 | Theme, 54 | ThemeProvider 55 | } from 'react-native-mdcore' 56 | 57 | cons CUSTOM_THEME = Theme.extend({ 58 | palette: { 59 | primary: '#006f7b', 60 | primaryDark: '#005a64', 61 | primaryLight: '#7fb7bd', 62 | } 63 | }) 64 | 65 | export default class Main extends PureComponent { 66 | 67 | render() { 68 | return ( 69 | 70 | 71 | 72 | ) 73 | } 74 | } 75 | ``` 76 | 77 | Access theme: 78 | 79 | ```js 80 | import React from 'react' 81 | import { 82 | PropTypes, 83 | PureComponent, 84 | Text, 85 | View 86 | } from 'react-native-mdcore' 87 | 88 | export default class HomeComponent extends PureComponent { 89 | 90 | static contextTypes = { 91 | theme: PropTypes.any 92 | } 93 | 94 | render() { 95 | const { theme } = this.context 96 | return ( 97 | 98 | Home 99 | 100 | ) 101 | } 102 | } 103 | ``` 104 | 105 | ### All-in-one place 106 | 107 | All components are now in `react-native-mdcore` that makes it easiest to remember and use. 108 | 109 | ```js 110 | import React from 'react' 111 | import { 112 | AppRegistry, AppState, Animated, 113 | Easing, 114 | FlatList, 115 | InteractionManager, 116 | ListView, 117 | PixelRatio, 118 | Platform, 119 | ScrollView, SectionList, StatusBar, 120 | TouchableHighlight, TouchableOpacity, TouchableNativeFeedback, TouchableWithoutFeedback, 121 | View, VirtualizedList, 122 | WebView, 123 | 124 | BottomNavigation, 125 | BottomNavigationItem, 126 | Button, 127 | Card, 128 | Divider, 129 | Icon, 130 | Image, 131 | NativeModules, 132 | PropTypes, 133 | PureComponent, 134 | Ripple, 135 | StyleSheet, 136 | TabItem, 137 | Tabs, 138 | Text, 139 | ViewPager, 140 | } from 'react-native-mdcore' 141 | 142 | export default class CustomComponent extends PureComponent { 143 | 144 | render() { 145 | return null 146 | } 147 | } 148 | ``` 149 | 150 | 151 | ## License 152 | 153 | Copyright 2017 "Henry Tao " 154 | 155 | Licensed under the Apache License, Version 2.0 (the "License"); 156 | you may not use this file except in compliance with the License. 157 | You may obtain a copy of the License at 158 | 159 | http://www.apache.org/licenses/LICENSE-2.0 160 | 161 | Unless required by applicable law or agreed to in writing, software 162 | distributed under the License is distributed on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 164 | See the License for the specific language governing permissions and 165 | limitations under the License. 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * from './src' 2 | import * as mdcore from './src' 3 | export default mdcore 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-mdcore", 3 | "version": "0.1.9", 4 | "description": "Material Design for both Android and iOS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/henrytao-me/react-native-mdcore.git" 12 | }, 13 | "keywords": [ 14 | "android", 15 | "appcompat", 16 | "design", 17 | "ios", 18 | "material", 19 | "material-design", 20 | "mdcore", 21 | "palette", 22 | "react", 23 | "support", 24 | "theme" 25 | ], 26 | "author": "Henry Tao ", 27 | "license": "Apache-2.0", 28 | "bugs": { 29 | "url": "https://github.com/henrytao-me/react-native-mdcore/issues" 30 | }, 31 | "homepage": "https://github.com/henrytao-me/react-native-mdcore#readme", 32 | "devDependencies": { 33 | "babel-eslint": "7.2.3", 34 | "babel-jest": "20.0.3", 35 | "babel-preset-react-native": "1.9.2", 36 | "eslint": "4.0.0", 37 | "eslint-plugin-react": "7.0.1", 38 | "eslint-plugin-react-native": "2.3.2", 39 | "react": "16.0.0-alpha.12", 40 | "react-native": "0.45.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/BottomNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TouchableWithoutFeedback, View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | export default class BottomNavigation extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | static propTypes = { 15 | backgroundColor: PropTypes.color, 16 | initialItem: PropTypes.number, 17 | onItemSelected: PropTypes.func 18 | } 19 | 20 | static defaultProps = { 21 | initialItem: 0, 22 | onItemSelected: (_options = { index: 0 }) => { } 23 | } 24 | 25 | state = { 26 | index: undefined 27 | } 28 | 29 | render() { 30 | const { theme } = this.context 31 | const styles = Styles.get(theme, this.props) 32 | return ( 33 | 34 | {this.props.children && this.props.children.map(this._renderItem)} 35 | 36 | ) 37 | } 38 | 39 | setItem = (index) => { 40 | if (index === this.state.index) { 41 | return 42 | } 43 | this.setState({ index }) 44 | } 45 | 46 | _getIndex = () => { 47 | return this.state.index !== undefined ? this.state.index : this.props.initialItem 48 | } 49 | 50 | _onItemPress = (index) => { 51 | this.setState({ index }) 52 | this.props.onItemSelected({ index }) 53 | } 54 | 55 | _renderItem = (item, index) => { 56 | const { theme } = this.context 57 | const styles = Styles.get(theme, this.props) 58 | const active = index === this._getIndex() 59 | item = item.props.active === active ? item : React.cloneElement(item, { active }) 60 | return ( 61 | this._onItemPress(index)}> 62 | 63 | {item} 64 | 65 | 66 | ) 67 | } 68 | } 69 | 70 | const Styles = StyleSheet.create((theme, { backgroundColor, style }) => { 71 | const container = { 72 | backgroundColor, 73 | flexDirection: 'row', 74 | ...style 75 | } 76 | const item = { 77 | flex: 1, 78 | flexDirection: 'row', 79 | alignItems: 'center', 80 | justifyContent: 'center' 81 | } 82 | return { container, item } 83 | }) 84 | -------------------------------------------------------------------------------- /src/components/BottomNavigationItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from 'react-native' 3 | 4 | import Icon from './Icon' 5 | import PropTypes from './PropTypes' 6 | import PureComponent from './PureComponent' 7 | import StyleSheet from './StyleSheet' 8 | import Text from './Text' 9 | 10 | import * as Utils from '../libs/utils' 11 | 12 | export default class BottomNavigationItem extends PureComponent { 13 | 14 | static contextTypes = { 15 | theme: PropTypes.any 16 | } 17 | 18 | static propTypes = { 19 | active: PropTypes.bool, 20 | color: PropTypes.color, 21 | iconName: PropTypes.string, 22 | iconSet: PropTypes.string, 23 | palette: PropTypes.palette, 24 | title: PropTypes.text, 25 | titleStyle: PropTypes.style 26 | } 27 | 28 | static defaultProps = { 29 | active: false, 30 | palette: 'background' 31 | } 32 | 33 | render() { 34 | const { theme } = this.context 35 | const color = this._getColor() 36 | const styles = Styles.get(theme) 37 | return ( 38 | 39 | {this.props.iconName && } 46 | {this.props.title && 52 | {this._getTitle()} 53 | } 54 | 55 | ) 56 | } 57 | 58 | _getColor = () => { 59 | if (Utils.isFunction(this.props.color)) { 60 | return this.props.color(this.props) 61 | } 62 | return this.props.color 63 | } 64 | 65 | _getTitle = () => { 66 | return Utils.isString(this.props.title) ? this.props.title.toUpperCase() : this.props.title 67 | } 68 | } 69 | 70 | const Styles = StyleSheet.create((theme) => { 71 | const container = { 72 | alignItems: 'center', 73 | justifyContent: 'center', 74 | flex: 1, 75 | height: theme.bottomNavigation.height 76 | } 77 | return { container } 78 | }) 79 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import RNButton from './internal/Button' 7 | 8 | export default class Button extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | static propTypes = { 15 | palette: PropTypes.palette, 16 | title: PropTypes.string.isRequired, 17 | type: PropTypes.oneOf(['default', 'borderless']), 18 | onPress: PropTypes.func.isRequired 19 | } 20 | 21 | static defaultProps = { 22 | palette: 'background', 23 | type: 'default' 24 | } 25 | 26 | render() { 27 | const { theme } = this.context 28 | const fontFamily = theme.fontFamily.medium 29 | let { palette, type } = this.props 30 | let backgroundColor, textColor, textColorBorderless 31 | switch (palette) { 32 | case 'primary': 33 | backgroundColor = theme.palette.primary 34 | textColor = theme.textColor.primary.primary 35 | textColorBorderless = theme.palette.primary 36 | break 37 | case 'accent': 38 | backgroundColor = theme.palette.accent 39 | textColor = theme.textColor.primary.accent 40 | textColorBorderless = theme.palette.accent 41 | break 42 | case 'warn': 43 | backgroundColor = theme.palette.warn 44 | textColor = theme.textColor.primary.warn 45 | textColorBorderless = theme.palette.warn 46 | break 47 | default: 48 | backgroundColor = theme.palette.background 49 | textColor = theme.textColor.primary.background 50 | textColorBorderless = theme.textColor.primary.background 51 | break 52 | } 53 | return ( 54 | 55 | 67 | 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | export default class Card extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | render() { 15 | const { theme } = this.context 16 | const styles = Styles.get(theme) 17 | return ( 18 | 19 | {this.props.children} 20 | 21 | ) 22 | } 23 | } 24 | 25 | const Styles = StyleSheet.create((theme) => { 26 | const container = { 27 | borderRadius: 2, 28 | shadowColor: '#000000', 29 | shadowOpacity: 1, 30 | shadowRadius: 10, 31 | shadowOffset: { 32 | height: 1, 33 | width: 0.3, 34 | } 35 | } 36 | return { container } 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/Divider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | export default class Divider extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | static propTypes = { 15 | largePadding: PropTypes.bool 16 | } 17 | 18 | render() { 19 | const { theme } = this.context 20 | const styles = Styles.get(theme, this.props) 21 | return ( 22 | 23 | ) 24 | } 25 | } 26 | 27 | const Styles = StyleSheet.create((theme, opts = {}) => { 28 | let { largePadding } = opts 29 | const container = { 30 | backgroundColor: theme.divider.color, 31 | height: theme.divider.size, 32 | marginLeft: largePadding ? (2 * theme.list.paddingLeft + theme.list.avatarSize) : 0 33 | } 34 | return { container } 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { createIconSet } from 'react-native-vector-icons' 4 | 5 | import Entypo from 'react-native-vector-icons/Entypo' 6 | import EvilIcons from 'react-native-vector-icons/EvilIcons' 7 | import FontAwesome from 'react-native-vector-icons/FontAwesome' 8 | import Foundation from 'react-native-vector-icons/Foundation' 9 | import Ionicons from 'react-native-vector-icons/Ionicons' 10 | import MaterialIcons from 'react-native-vector-icons/MaterialIcons' 11 | import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' 12 | import Octicons from 'react-native-vector-icons/Octicons' 13 | import Zocial from 'react-native-vector-icons/Zocial' 14 | import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons' 15 | 16 | import PropTypes from './PropTypes' 17 | import PureComponent from './PureComponent' 18 | 19 | import * as Utils from '../libs/utils' 20 | 21 | export default class Icon extends PureComponent { 22 | 23 | static DEFAULT_ICON_SET = 'MaterialIcons' 24 | 25 | static ICON_SETS = { Entypo, EvilIcons, FontAwesome, Foundation, Ionicons, MaterialIcons, MaterialCommunityIcons, Octicons, Zocial, SimpleLineIcons } 26 | 27 | static contextTypes = { 28 | theme: PropTypes.any 29 | } 30 | 31 | static propTypes = { 32 | active: PropTypes.bool, 33 | color: PropTypes.color, 34 | focus: PropTypes.bool, 35 | name: PropTypes.string.isRequired, 36 | palette: PropTypes.palette, 37 | set: PropTypes.string, 38 | size: PropTypes.number 39 | } 40 | 41 | static defaultProps = { 42 | active: true, 43 | focus: false, 44 | palette: 'background' 45 | } 46 | 47 | static addIconSet = (iconSet, glyphMap, fontFamily, fontFile) => { 48 | Icon.ICON_SETS[iconSet] = createIconSet(glyphMap, fontFamily, fontFile) 49 | } 50 | 51 | static setDefaultIconSet = (iconSet) => { 52 | Icon.DEFAULT_ICON_SET = iconSet 53 | } 54 | 55 | render() { 56 | const { theme } = this.context 57 | const color = this._getColor() 58 | const size = this.props.size || theme.icon.size 59 | const IconView = Icon.ICON_SETS[this.props.set || Icon.DEFAULT_ICON_SET] 60 | return ( 61 | 65 | ) 66 | } 67 | 68 | _getColor = () => { 69 | if (Utils.isFunction(this.props.color)) { 70 | return this.props.color(this.props) 71 | } 72 | const { theme } = this.context 73 | const state = this.props.focus ? 'focused' : (this.props.active ? 'active' : 'inactive') 74 | return this.props.color || theme.iconColor[state][this.props.palette] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/Image.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Image as RNImage, View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | 7 | import * as Utils from '../libs/utils' 8 | 9 | export default class Image extends PureComponent { 10 | 11 | static propTypes = { 12 | height: PropTypes.number, 13 | placeholder: PropTypes.imageSource, 14 | radius: PropTypes.imageRadius, 15 | resizeMode: PropTypes.imageResizeMode, 16 | scaleType: PropTypes.imageScaleType, 17 | source: PropTypes.imageSource, 18 | width: PropTypes.number 19 | } 20 | 21 | static defaultProps = { 22 | radius: 'none', 23 | resizeMode: 'contain', 24 | scaleType: 'width' 25 | } 26 | 27 | state = { 28 | width: 0, 29 | height: 0 30 | } 31 | 32 | _mounted = false 33 | 34 | componentDidMount() { 35 | this._mounted = true 36 | } 37 | 38 | componentWillUnmount() { 39 | this._mounted = false 40 | } 41 | 42 | render() { 43 | const image = this._getImage() 44 | const radius = this._getRadius() 45 | return ( 46 | 47 | 50 | 51 | ) 52 | } 53 | 54 | _getHeight = () => { 55 | return this.props.height || this.state.height 56 | } 57 | 58 | _getImage = () => { 59 | const source = this._getSource() 60 | const placeholder = this._getPlaceholder() 61 | return (!!source && !!source.uri ? source : null) || (!!placeholder && !!placeholder.uri ? placeholder : null) 62 | } 63 | 64 | _getPlaceholder = () => { 65 | const placeholder = Utils.isString(this.props.placeholder) ? { uri: this.props.placeholder } : this.props.placeholder 66 | return RNImage.resolveAssetSource(placeholder) || { uri: undefined, width: undefined, height: undefined } 67 | } 68 | 69 | _getRadius = () => { 70 | const { width, height } = this.state 71 | const { radius } = this.props 72 | switch (radius) { 73 | case 'none': 74 | return undefined 75 | case 'auto': 76 | return Math.min(width, height) / 2 77 | default: 78 | return radius 79 | } 80 | } 81 | 82 | _getSource = () => { 83 | const source = Utils.isString(this.props.source) ? { uri: this.props.source } : this.props.source 84 | return RNImage.resolveAssetSource(source) || { uri: undefined, width: undefined, height: undefined } 85 | } 86 | 87 | _getWidth = () => { 88 | return this.props.width || this.state.width 89 | } 90 | 91 | _onLayout = (e) => { 92 | const { scaleType } = this.props 93 | const { layout } = e.nativeEvent 94 | const image = this._getImage() 95 | if (!!image && !!image.uri) { 96 | if (!!image.width && !!image.height) { 97 | this._computeImageSize(scaleType, layout, image.width, image.height) 98 | } else { 99 | RNImage.getSize(image.uri, (width, height) => { 100 | this._computeImageSize(scaleType, layout, width, height) 101 | }) 102 | } 103 | } 104 | } 105 | 106 | _computeImageSize = (scaleType, layout, width, height) => { 107 | if (!this._mounted) { 108 | return 109 | } 110 | this.setState({ 111 | width: scaleType === 'width' || scaleType === 'none' ? layout.width : layout.height * width / height, 112 | height: scaleType === 'height' || scaleType === 'none' ? layout.height : layout.width * height / width 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/NativeModules.js: -------------------------------------------------------------------------------- 1 | import { NativeModules, Platform } from 'react-native' 2 | const { StatusBarManager } = NativeModules 3 | 4 | StatusBarManager._getHeight = StatusBarManager.getHeight 5 | StatusBarManager.getHeight = (callback = (_data) => { }) => { 6 | switch (Platform.OS) { 7 | case 'ios': 8 | StatusBarManager._getHeight(callback) 9 | break 10 | case 'android': 11 | callback({ height: StatusBarManager.HEIGHT }) 12 | break 13 | } 14 | } 15 | 16 | export default NativeModules 17 | -------------------------------------------------------------------------------- /src/components/PropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | export default Object.assign({}, PropTypes, { 4 | color: PropTypes.oneOfType([ 5 | PropTypes.string, 6 | PropTypes.func 7 | ]), 8 | ellipsizeMode: PropTypes.oneOf(['head', 'middle', 'tail', 'clip']), 9 | imageRadius: PropTypes.oneOfType([ 10 | PropTypes.number, 11 | PropTypes.oneOf(['auto', 'none']) 12 | ]), 13 | imageResizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'center']), 14 | imageScaleType: PropTypes.oneOf(['width', 'height', 'none']), 15 | imageSource: PropTypes.oneOfType([ 16 | PropTypes.number, 17 | PropTypes.shape({ 18 | uri: PropTypes.string 19 | }), 20 | PropTypes.string, 21 | ]), 22 | opacity: PropTypes.number, 23 | palette: PropTypes.oneOf(['primary', 'primaryDark', 'primaryLight', 'accent', 'accentDark', 'accentLight', 'warn', 'warnDark', 'warnLight', 'background', 'backgroundDark', 'backgroundLight']), 24 | style: PropTypes.oneOfType([ 25 | PropTypes.object, 26 | PropTypes.arrayOf(PropTypes.object) 27 | ]), 28 | text: PropTypes.oneOfType([ 29 | PropTypes.element, 30 | PropTypes.string 31 | ]), 32 | textSubType: PropTypes.oneOf(['primary', 'secondary', 'hint']), 33 | textType: PropTypes.oneOf(['button', 'caption', 'body1', 'body2', 'subhead1', 'subhead2', 'title', 'headline', 'display1', 'display2', 'display3', 'display4']), 34 | view: PropTypes.oneOfType([ 35 | PropTypes.element, 36 | PropTypes.func 37 | ]) 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/PureComponent.js: -------------------------------------------------------------------------------- 1 | import { PureComponent as RNPureComponent } from 'react' 2 | 3 | export default class PureComponent extends RNPureComponent { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Ripple.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TouchableWithoutFeedback, View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | export default class Ripple extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | static propTypes = { 15 | color: PropTypes.color, 16 | duration: PropTypes.number, 17 | opacity: PropTypes.opacity, 18 | size: PropTypes.number, 19 | onPress: PropTypes.func 20 | } 21 | 22 | static defaultProps = { 23 | opPress: () => { } 24 | } 25 | 26 | state = { 27 | ripples: [] 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 | {this.props.children} 35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | const Styles = StyleSheet.create((theme) => { 42 | return {} 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/StatusBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Platform, 4 | StatusBar as RNStatusBar, 5 | View 6 | } from 'react-native' 7 | 8 | import NativeModules from './NativeModules' 9 | import PropTypes from './PropTypes' 10 | import PureComponent from './PureComponent' 11 | 12 | export default class StatusBar extends PureComponent { 13 | 14 | static contextTypes = { 15 | theme: PropTypes.any 16 | } 17 | 18 | static propTypes = { 19 | backgroundColor: PropTypes.color, 20 | barStyle: PropTypes.oneOf(['light-content', 'dark-content', 'default']) 21 | } 22 | 23 | state = { 24 | statusBarHeight: 0 25 | } 26 | 27 | componentDidMount() { 28 | NativeModules.StatusBarManager.getHeight(({ height }) => this.setState({ statusBarHeight: height })) 29 | } 30 | 31 | render() { 32 | const backgroundColor = this._getBackgroundColor() 33 | const barStyle = this._getBarStyle() 34 | const height = this.state.statusBarHeight 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | _getBackgroundColor = () => { 43 | const { theme } = this.context 44 | const backgroundColor = this.props.backgroundColor || (Platform.OS === 'ios' ? 'transparent' : theme.palette.backgroundDark) 45 | return theme.palette[backgroundColor] || backgroundColor 46 | } 47 | 48 | _getBarStyle = () => { 49 | return this.props.barStyle || (Platform.OS === 'ios' ? 'dark-content' : 'light-content') 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/StyleSheet.js: -------------------------------------------------------------------------------- 1 | import * as Utils from '../libs/utils' 2 | 3 | export default class StyleSheet { 4 | 5 | static create(creator = (_theme, ..._args) => { }) { 6 | return new StyleSheet(Utils.isFunction(creator) ? creator : () => creator) 7 | } 8 | 9 | static _getThemeId(theme) { 10 | return theme ? theme.__id : null 11 | } 12 | 13 | constructor(creator) { 14 | this._creator = creator 15 | this._theme = undefined 16 | this._themeId = undefined 17 | this._args = undefined 18 | this._style = undefined 19 | } 20 | 21 | get(theme = null, ...args) { 22 | if (this._shouldRenewCache(theme, args)) { 23 | this._theme = theme 24 | this._themeId = this.constructor._getThemeId(theme) 25 | this._args = args 26 | this._style = this._creator(this._theme, ...this._args) 27 | } 28 | return this._style 29 | } 30 | 31 | _shouldRenewCache(theme = null, args) { 32 | return theme !== this._theme || this.constructor._getThemeId(theme) !== this._themeId || !Utils.deepEqual(args, this._args) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/TabItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from 'react-native' 3 | 4 | import Icon from './Icon' 5 | import PropTypes from './PropTypes' 6 | import PureComponent from './PureComponent' 7 | import StyleSheet from './StyleSheet' 8 | import Text from './Text' 9 | 10 | import * as Utils from '../libs/utils' 11 | 12 | export default class TabItem extends PureComponent { 13 | 14 | static contextTypes = { 15 | theme: PropTypes.any 16 | } 17 | 18 | static propTypes = { 19 | active: PropTypes.bool, 20 | color: PropTypes.color, 21 | iconName: PropTypes.string, 22 | iconSet: PropTypes.string, 23 | palette: PropTypes.palette, 24 | title: PropTypes.text, 25 | titleStyle: PropTypes.style 26 | } 27 | 28 | static defaultProps = { 29 | active: false, 30 | palette: 'primary' 31 | } 32 | 33 | render() { 34 | const { theme } = this.context 35 | const color = this._getColor() 36 | const styles = Styles.get(theme, this.props) 37 | return ( 38 | 39 | {this.props.iconName && } 46 | {this.props.title && 52 | {this._getTitle()} 53 | } 54 | 55 | ) 56 | } 57 | 58 | _getColor = () => { 59 | if (Utils.isFunction(this.props.color)) { 60 | return this.props.color(this.props) 61 | } 62 | return this.props.color 63 | } 64 | 65 | _getTitle = () => { 66 | return Utils.isString(this.props.title) ? this.props.title.toUpperCase() : this.props.title 67 | } 68 | } 69 | 70 | const Styles = StyleSheet.create((theme, { iconName, title }) => { 71 | const container = { 72 | alignItems: 'center', 73 | justifyContent: 'center', 74 | flex: 1, 75 | height: iconName && title ? theme.tab.iconWithTextHeight : (iconName ? theme.tab.iconOnlyHeight : theme.tab.textOnlyHeight) 76 | } 77 | const tabTitle = { 78 | marginTop: iconName && title ? theme.tab.spacing : 0 79 | } 80 | return { container, title: tabTitle } 81 | }) 82 | -------------------------------------------------------------------------------- /src/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TouchableWithoutFeedback, View } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | export default class Tabs extends PureComponent { 9 | 10 | static contextTypes = { 11 | theme: PropTypes.any 12 | } 13 | 14 | static propTypes = { 15 | backgroundColor: PropTypes.color, 16 | indicatorEnabled: PropTypes.bool, 17 | indicatorStyle: PropTypes.style, 18 | initialItem: PropTypes.number, 19 | onItemSelected: PropTypes.func 20 | } 21 | 22 | static defaultProps = { 23 | indicatorEnabled: true, 24 | initialItem: 0, 25 | onItemSelected: (_options = { index: 0 }) => { } 26 | } 27 | 28 | state = { 29 | index: undefined 30 | } 31 | 32 | render() { 33 | const { theme } = this.context 34 | const styles = Styles.get(theme, this.props) 35 | return ( 36 | 37 | {this.props.children && this.props.children.map(this._renderItem)} 38 | 39 | ) 40 | } 41 | 42 | setItem = (index) => { 43 | this.setState({ index }) 44 | } 45 | 46 | _getIndex = () => { 47 | return this.state.index !== undefined ? this.state.index : this.props.initialItem 48 | } 49 | 50 | _onItemPress = (index) => { 51 | this.setState({ index }) 52 | this.props.onItemSelected({ index }) 53 | } 54 | 55 | _renderItem = (item, index) => { 56 | const { theme } = this.context 57 | const styles = Styles.get(theme, this.props) 58 | const active = index === this._getIndex() 59 | item = item.props.active === active ? item : React.cloneElement(item, { active }) 60 | return ( 61 | this._onItemPress(index)}> 62 | 63 | {this.props.indicatorEnabled && active && } 64 | {item} 65 | 66 | 67 | ) 68 | } 69 | } 70 | 71 | const Styles = StyleSheet.create((theme, { backgroundColor }) => { 72 | const container = { 73 | backgroundColor: backgroundColor || theme.palette.primary, 74 | flexDirection: 'row' 75 | } 76 | const indicator = { 77 | position: 'absolute', 78 | left: 0, 79 | right: 0, 80 | bottom: 0, 81 | backgroundColor: theme.palette.accent, 82 | height: theme.tab.indicatorHeight 83 | } 84 | const item = { 85 | flex: 1, 86 | flexDirection: 'row', 87 | alignItems: 'center', 88 | justifyContent: 'center' 89 | } 90 | return { container, indicator, item } 91 | }) 92 | -------------------------------------------------------------------------------- /src/components/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text as RNText } from 'react-native' 3 | 4 | import PropTypes from './PropTypes' 5 | import PureComponent from './PureComponent' 6 | import StyleSheet from './StyleSheet' 7 | 8 | import * as Utils from '../libs/utils' 9 | 10 | export default class Text extends PureComponent { 11 | 12 | static contextTypes = { 13 | theme: PropTypes.any 14 | } 15 | 16 | static propTypes = { 17 | color: PropTypes.color, 18 | ellipsizeMode: PropTypes.ellipsizeMode, 19 | enable: PropTypes.bool, 20 | numberOfLines: PropTypes.number, 21 | palette: PropTypes.palette, 22 | subType: PropTypes.textSubType, 23 | type: PropTypes.textType, 24 | value: PropTypes.text 25 | } 26 | 27 | static defaultProps = { 28 | ellipsizeMode: 'tail', 29 | enable: true, 30 | palette: 'background', 31 | subType: 'primary', 32 | type: 'body1' 33 | } 34 | 35 | render() { 36 | const color = this._getColor() 37 | const defaultStyle = this._getDefaultStyle() 38 | const ellipsizeMode = this.props.ellipsizeMode 39 | const numberOfLines = this._getNumberOfLines() 40 | return ( 41 | 44 | {this.props.children || this.props.value} 45 | 46 | ) 47 | } 48 | 49 | _getColor = () => { 50 | if (Utils.isFunction(this.props.color)) { 51 | return this.props.color(this.props) 52 | } 53 | const { theme } = this.context 54 | const subType = this.props.enable ? this.props.subType : 'hint' 55 | const color = this.props.color || theme.textColor[subType][this.props.palette] 56 | return color 57 | } 58 | 59 | _getDefaultStyle = () => { 60 | const { theme } = this.context 61 | const styles = Styles.get(theme) 62 | return styles[this.props.type] 63 | } 64 | 65 | _getNumberOfLines = () => { 66 | let numberOfLines = undefined 67 | switch (this.props.type) { 68 | case 'button': 69 | case 'caption': 70 | case 'title': 71 | case 'display3': 72 | case 'display4': 73 | numberOfLines = 1 74 | break 75 | default: 76 | numberOfLines = undefined 77 | break 78 | } 79 | numberOfLines = this.props.numberOfLines || numberOfLines 80 | numberOfLines = numberOfLines === 0 ? undefined : numberOfLines 81 | return numberOfLines 82 | } 83 | } 84 | 85 | const Styles = StyleSheet.create((theme) => { 86 | return { 87 | button: { 88 | fontFamily: theme.fontFamily.medium, 89 | fontSize: theme.fontSize.button, 90 | lineHeight: theme.lineHeight.button 91 | }, 92 | caption: { 93 | fontFamily: theme.fontFamily.regular, 94 | fontSize: theme.fontSize.caption, 95 | lineHeight: theme.lineHeight.caption 96 | }, 97 | body1: { 98 | fontFamily: theme.fontFamily.regular, 99 | fontSize: theme.fontSize.body1, 100 | lineHeight: theme.lineHeight.body1 101 | }, 102 | body2: { 103 | fontFamily: theme.fontFamily.medium, 104 | fontSize: theme.fontSize.body2, 105 | lineHeight: theme.lineHeight.body2 106 | }, 107 | subhead1: { 108 | fontFamily: theme.fontFamily.regular, 109 | fontSize: theme.fontSize.subhead1, 110 | lineHeight: theme.lineHeight.subhead1 111 | }, 112 | subhead2: { 113 | fontFamily: theme.fontFamily.regular, 114 | fontSize: theme.fontSize.subhead2, 115 | lineHeight: theme.lineHeight.subhead2 116 | }, 117 | title: { 118 | fontFamily: theme.fontFamily.medium, 119 | fontSize: theme.fontSize.title, 120 | lineHeight: theme.lineHeight.title 121 | }, 122 | headline: { 123 | fontFamily: theme.fontFamily.regular, 124 | fontSize: theme.fontSize.headline, 125 | lineHeight: theme.lineHeight.headline 126 | }, 127 | display1: { 128 | fontFamily: theme.fontFamily.regular, 129 | fontSize: theme.fontSize.display1, 130 | lineHeight: theme.lineHeight.display1 131 | }, 132 | display2: { 133 | fontFamily: theme.fontFamily.regular, 134 | fontSize: theme.fontSize.display2, 135 | lineHeight: theme.lineHeight.display2 136 | }, 137 | display3: { 138 | fontFamily: theme.fontFamily.regular, 139 | fontSize: theme.fontSize.display3, 140 | lineHeight: theme.lineHeight.display3 141 | }, 142 | display4: { 143 | fontFamily: theme.fontFamily.light, 144 | fontSize: theme.fontSize.display4, 145 | lineHeight: theme.lineHeight.display4 146 | } 147 | } 148 | }) 149 | -------------------------------------------------------------------------------- /src/components/ViewPager.js: -------------------------------------------------------------------------------- 1 | import RNViewPager from './internal/ViewPager' 2 | export default RNViewPager 3 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | export { Component } 3 | 4 | import { 5 | AppRegistry, AppState, Animated, 6 | Easing, 7 | FlatList, 8 | InteractionManager, 9 | ListView, 10 | PixelRatio, 11 | Platform, 12 | ScrollView, SectionList, 13 | TouchableHighlight, TouchableOpacity, TouchableNativeFeedback, TouchableWithoutFeedback, 14 | View, VirtualizedList, 15 | WebView 16 | } from 'react-native' 17 | export { 18 | AppRegistry, AppState, Animated, 19 | Easing, 20 | FlatList, 21 | InteractionManager, 22 | ListView, 23 | PixelRatio, 24 | Platform, 25 | ScrollView, SectionList, 26 | TouchableHighlight, TouchableOpacity, TouchableNativeFeedback, TouchableWithoutFeedback, 27 | View, VirtualizedList, 28 | WebView 29 | } 30 | 31 | export { default as BottomNavigation } from './BottomNavigation' 32 | export { default as BottomNavigationItem } from './BottomNavigationItem' 33 | export { default as Button } from './Button' 34 | export { default as Card } from './Card' 35 | export { default as Divider } from './Divider' 36 | export { default as Icon } from './Icon' 37 | export { default as Image } from './Image' 38 | export { default as NativeModules } from './NativeModules' 39 | export { default as PropTypes } from './PropTypes' 40 | export { default as PureComponent } from './PureComponent' 41 | export { default as Ripple } from './Ripple' 42 | export { default as StatusBar } from './StatusBar' 43 | export { default as StyleSheet } from './StyleSheet' 44 | export { default as TabItem } from './TabItem' 45 | export { default as Tabs } from './Tabs' 46 | export { default as Text } from './Text' 47 | export { default as ViewPager } from './ViewPager' 48 | -------------------------------------------------------------------------------- /src/components/internal/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Platform, 4 | StyleSheet, 5 | Text, 6 | TouchableNativeFeedback, 7 | TouchableOpacity, 8 | View 9 | } from 'react-native' 10 | import PropTypes from 'prop-types' 11 | 12 | export default class Button extends Component { 13 | 14 | static propTypes = { 15 | /** 16 | * Text to display inside the button 17 | */ 18 | title: PropTypes.string.isRequired, 19 | /** 20 | * Text to display for blindness accessibility features 21 | */ 22 | accessibilityLabel: PropTypes.string, 23 | /** 24 | * Color of the text (iOS), or background color of the button (Android) 25 | */ 26 | color: PropTypes.any, 27 | /** 28 | * If true, disable all interactions for this component. 29 | */ 30 | disabled: PropTypes.bool, 31 | /** 32 | * Handler to be called when the user taps the button 33 | */ 34 | onPress: PropTypes.func.isRequired, 35 | /** 36 | * Used to locate this view in end-to-end tests. 37 | */ 38 | testID: PropTypes.string, 39 | 40 | buttonStyle: React.PropTypes.object, 41 | textStyle: React.PropTypes.object 42 | } 43 | 44 | render() { 45 | const { 46 | accessibilityLabel, 47 | color, 48 | onPress, 49 | disabled, 50 | testID, 51 | } = this.props 52 | const title = `${this.props.title}` 53 | const buttonStyles = [styles.button] 54 | const textStyles = [styles.text] 55 | const Touchable = Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity 56 | if (color && Platform.OS === 'ios') { 57 | textStyles.push({ color: color }) 58 | } else if (color) { 59 | buttonStyles.push({ backgroundColor: color }) 60 | } 61 | if (disabled) { 62 | buttonStyles.push(styles.buttonDisabled) 63 | textStyles.push(styles.textDisabled) 64 | } 65 | const formattedTitle = Platform.OS === 'android' ? title.toUpperCase() : title 66 | const accessibilityTraits = ['button'] 67 | if (disabled) { 68 | accessibilityTraits.push('disabled') 69 | } 70 | buttonStyles.push(this.props.buttonStyle) 71 | textStyles.push(this.props.textStyle) 72 | return ( 73 | 80 | 81 | {formattedTitle} 82 | 83 | 84 | ) 85 | } 86 | } 87 | 88 | // Material design blue from https://material.google.com/style/color.html#color-color-palette 89 | let defaultBlue = '#2196F3' 90 | if (Platform.OS === 'ios') { 91 | // Measured default tintColor from iOS 10 92 | defaultBlue = '#007AFF' 93 | } 94 | 95 | const styles = StyleSheet.create({ 96 | button: Platform.select({ 97 | ios: { 98 | elevation: 4, 99 | backgroundColor: defaultBlue, 100 | borderRadius: 2, 101 | }, 102 | android: { 103 | elevation: 4, 104 | backgroundColor: defaultBlue, 105 | borderRadius: 2, 106 | }, 107 | }), 108 | text: Platform.select({ 109 | ios: { 110 | color: defaultBlue, 111 | textAlign: 'center', 112 | padding: 8, 113 | fontSize: 18, 114 | }, 115 | android: { 116 | textAlign: 'center', 117 | color: 'white', 118 | padding: 8, 119 | fontWeight: '500', 120 | }, 121 | }), 122 | buttonDisabled: Platform.select({ 123 | ios: {}, 124 | android: { 125 | elevation: 0, 126 | backgroundColor: '#dfdfdf', 127 | } 128 | }), 129 | textDisabled: Platform.select({ 130 | ios: { 131 | color: '#cdcdcd', 132 | }, 133 | android: { 134 | color: '#a1a1a1', 135 | } 136 | }), 137 | }) 138 | -------------------------------------------------------------------------------- /src/components/internal/ViewPager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Platform, 4 | ScrollView, 5 | StyleSheet, 6 | View, 7 | ViewPagerAndroid 8 | } from 'react-native' 9 | import PropTypes from 'prop-types' 10 | 11 | export default class ViewPager extends Component { 12 | 13 | static propTypes = { 14 | bounces: PropTypes.any, 15 | count: PropTypes.number, 16 | onSelectedIndexChange: PropTypes.func, 17 | selectedIndex: PropTypes.number 18 | } 19 | 20 | static defaultProps = { 21 | onSelectedIndexChange: (_index) => { } 22 | } 23 | 24 | constructor(props) { 25 | super(props) 26 | this.state = { 27 | width: 0, 28 | height: 0, 29 | selectedIndex: this.props.selectedIndex, 30 | initialSelectedIndex: this.props.selectedIndex || 0, 31 | scrollingTo: null 32 | } 33 | this.handleHorizontalScroll = this.handleHorizontalScroll.bind(this) 34 | this.adjustCardSize = this.adjustCardSize.bind(this) 35 | } 36 | 37 | render() { 38 | if (Platform.OS === 'ios') { 39 | return this.renderIOS() 40 | } else { 41 | return this.renderAndroid() 42 | } 43 | } 44 | 45 | renderIOS() { 46 | return ( 47 | 67 | {this.renderContent()} 68 | 69 | ) 70 | } 71 | 72 | renderAndroid() { 73 | return ( 74 | 80 | {this.renderContent()} 81 | 82 | ) 83 | } 84 | 85 | adjustCardSize(e) { 86 | this.setState({ 87 | width: e.nativeEvent.layout.width, 88 | height: e.nativeEvent.layout.height, 89 | }) 90 | } 91 | 92 | componentWillReceiveProps(nextProps) { 93 | if (nextProps.selectedIndex !== this.state.selectedIndex) { 94 | if (Platform.OS === 'ios') { 95 | this.refs.scrollview.scrollTo({ 96 | x: nextProps.selectedIndex * this.state.width, 97 | animated: true, 98 | }) 99 | this.setState({ scrollingTo: nextProps.selectedIndex }) 100 | } else { 101 | this.refs.scrollview.setPage(nextProps.selectedIndex) 102 | this.setState({ selectedIndex: nextProps.selectedIndex }) 103 | } 104 | } 105 | } 106 | 107 | renderContent() { 108 | const { width, height } = this.state 109 | const style = Platform.OS === 'ios' && styles.card 110 | if (!this.props.children) { 111 | return null 112 | } 113 | return this.props.children.map((child, i) => ( 114 | 115 | {child} 116 | 117 | )) 118 | } 119 | 120 | handleHorizontalScroll(e) { 121 | let selectedIndex = e.nativeEvent.position 122 | if (selectedIndex === undefined) { 123 | selectedIndex = Math.round( 124 | e.nativeEvent.contentOffset.x / this.state.width, 125 | ) 126 | } 127 | if (selectedIndex < 0 || selectedIndex >= this.props.count) { 128 | return 129 | } 130 | if (this.state.scrollingTo !== null && this.state.scrollingTo !== selectedIndex) { 131 | return 132 | } 133 | if (this.props.selectedIndex !== selectedIndex || this.state.scrollingTo !== null) { 134 | this.setState({ selectedIndex, scrollingTo: null }) 135 | const { onSelectedIndexChange } = this.props 136 | onSelectedIndexChange && onSelectedIndexChange(selectedIndex) 137 | } 138 | } 139 | 140 | setPage = (index) => { 141 | if (Platform.OS === 'ios') { 142 | this.refs.scrollview.scrollTo({ 143 | x: index * this.state.width, 144 | animated: true, 145 | }) 146 | this.setState({ scrollingTo: index }) 147 | } else { 148 | this.refs.scrollview.setPage(index) 149 | } 150 | } 151 | } 152 | 153 | const styles = StyleSheet.create({ 154 | container: { 155 | flex: 1, 156 | }, 157 | scrollview: { 158 | flex: 1, 159 | backgroundColor: 'transparent', 160 | }, 161 | card: { 162 | backgroundColor: 'transparent', 163 | } 164 | }) 165 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | 3 | import Theme from './theme' 4 | import ThemeProvider from './providers/theme' 5 | import * as ThemeUtils from './libs/utils' 6 | 7 | export { 8 | Theme, 9 | ThemeProvider, 10 | ThemeUtils 11 | } 12 | -------------------------------------------------------------------------------- /src/libs/immutable.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Copyright (c) 2014-2015, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | export function isImmutable(maybeImmutable) { 12 | return (isCollection(maybeImmutable) || isRecord(maybeImmutable)) && 13 | !maybeImmutable.__ownerID; 14 | } 15 | 16 | export function isCollection(maybeCollection) { 17 | return !!(maybeCollection && maybeCollection[IS_ITERABLE_SENTINEL]); 18 | } 19 | 20 | export function isKeyed(maybeKeyed) { 21 | return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL]); 22 | } 23 | 24 | export function isIndexed(maybeIndexed) { 25 | return !!(maybeIndexed && maybeIndexed[IS_INDEXED_SENTINEL]); 26 | } 27 | 28 | export function isAssociative(maybeAssociative) { 29 | return isKeyed(maybeAssociative) || isIndexed(maybeAssociative); 30 | } 31 | 32 | export function isOrdered(maybeOrdered) { 33 | return !!(maybeOrdered && maybeOrdered[IS_ORDERED_SENTINEL]); 34 | } 35 | 36 | export function isRecord(maybeRecord) { 37 | return !!(maybeRecord && maybeRecord[IS_RECORD_SENTINEL]); 38 | } 39 | 40 | export function isValueObject(maybeValue) { 41 | return !!(maybeValue && 42 | typeof maybeValue.equals === 'function' && 43 | typeof maybeValue.hashCode === 'function'); 44 | } 45 | 46 | export const IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@'; 47 | export const IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@'; 48 | export const IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@'; 49 | export const IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@'; 50 | export const IS_RECORD_SENTINEL = '@@__IMMUTABLE_RECORD__@@'; -------------------------------------------------------------------------------- /src/libs/resolver.js: -------------------------------------------------------------------------------- 1 | import * as Utils from './utils' 2 | 3 | export default class Resolver { 4 | 5 | static create(defaultValue, values) { 6 | return new Resolver(Object.assign({}, defaultValue), Object.assign({}, values)) 7 | } 8 | 9 | constructor(defaultValue = {}, values = {}) { 10 | this._defaultValue = defaultValue 11 | this._values = values 12 | // Ex: [{ group: 1, key: 'land', regex: ... }] 13 | this._regexMap = Object 14 | .keys(values) 15 | .map(key => { 16 | const keys = key.split('-').sort() 17 | const regex = new RegExp(`.*-${keys.join('-.*-')}-.*`) 18 | return { group: keys.length, key, regex } 19 | }) 20 | .sort((a, b) => a.group - b.group) 21 | } 22 | 23 | clone() { 24 | return Resolver.create(this._defaultValue, this._valuesMap) 25 | } 26 | 27 | extend(defaultValue, values) { 28 | return Resolver.create(Utils.merge({}, this._defaultValue, defaultValue), Utils.merge({}, this._values, values)) 29 | } 30 | 31 | getKeys() { 32 | return Object.keys(this._values) 33 | } 34 | 35 | getOrderedKeys() { 36 | return this.getKeys().sort() 37 | } 38 | 39 | resolve(keys = []) { 40 | keys = Utils.isArray(keys) ? keys : keys.split('-') 41 | const requestKey = `-${keys.filter(key => !!key).sort().join('-')}-` 42 | return this._regexMap.reduce((acc, { key, regex }) => { 43 | if (requestKey && regex.test(requestKey)) { 44 | Utils.merge(acc, this._values[key]) 45 | } 46 | return acc 47 | }, Utils.merge({}, this._defaultValue)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/libs/utils.js: -------------------------------------------------------------------------------- 1 | import { isImmutable } from './immutable' 2 | 3 | export const deepEqual = (a, b, comparator = _comparator) => { 4 | if (isImmutable(a) && isImmutable(b)) { 5 | return a === b 6 | } 7 | if (a instanceof Date && b instanceof Date) { 8 | return _comparator(a, b) 9 | } 10 | if (isArray(a) && isArray(b)) { 11 | if (a.length !== b.length) { 12 | return false 13 | } 14 | for (let i = 0; i < a.length; i++) { 15 | if (!deepEqual(a[i], b[i], comparator)) { 16 | return false 17 | } 18 | } 19 | return true 20 | } 21 | if (isObject(a) && isObject(b)) { 22 | const aKeys = Object.keys(a) 23 | const bKeys = Object.keys(b) 24 | if (aKeys.length !== bKeys.length || aKeys.sort().join(',') !== bKeys.sort().join(',')) { 25 | return false 26 | } 27 | const keys = aKeys 28 | for (let i = 0; i < keys.length; i++) { 29 | if (!deepEqual(a[keys[i]], b[keys[i]], comparator)) { 30 | return false 31 | } 32 | } 33 | return true 34 | } 35 | return comparator(a, b) 36 | } 37 | 38 | export const flattenObject = (obj, prefix) => { 39 | if (!isObject(obj)) { 40 | return {} 41 | } 42 | prefix = prefix ? `${prefix}.` : '' 43 | return Object.keys(obj).reduce((acc, key) => { 44 | let value = obj[key] 45 | if (!isObject(value)) { 46 | acc[prefix + key] = isArray(value) ? JSON.stringify(value) : value 47 | } else { 48 | let children = flattenObject(value, key) 49 | Object.keys(children).forEach(k => { 50 | acc[prefix + k] = children[k] 51 | }) 52 | } 53 | return acc 54 | }, {}) 55 | } 56 | 57 | export const hook = (target, action, onError = (_error, _index) => { }, callback = (_next, _resolve, _reject) => _next()) => { 58 | callback = isObject(callback) ? callback : { retry: callback } 59 | action = action.bind(target) 60 | onError = onError.bind(target) 61 | const callbacks = Object.keys(callback).reduce((acc, key) => { 62 | acc[key] = callback[key].bind(target) 63 | return acc 64 | }, {}) 65 | const options = { 66 | args: [], 67 | finished: false, 68 | index: 0, 69 | reject: null, 70 | resolve: null 71 | } 72 | const fn = (...args) => { 73 | options.args = args 74 | options.finished = false 75 | options.index = 0 76 | return new Promise((resolve, reject) => { 77 | options.reject = reject 78 | options.resolve = resolve 79 | next() 80 | }) 81 | } 82 | const next = (() => { 83 | try { 84 | const res = action(...options.args) 85 | if (res instanceof Promise) { 86 | return res 87 | .then(data => resolve(data)) 88 | .catch(error => onError(error, options.index++)) 89 | } else { 90 | resolve(res) 91 | } 92 | } catch (error) { 93 | onError(error, options.index++) 94 | } 95 | }).bind(target) 96 | const reject = ((error) => { 97 | const { finished, reject } = options 98 | if (!finished && !!reject) { 99 | options.finished = true 100 | reject(error) 101 | } 102 | }).bind(target) 103 | const resolve = ((data) => { 104 | const { finished, resolve } = options 105 | if (!finished && !!resolve) { 106 | options.finished = true 107 | resolve(data) 108 | } 109 | }).bind(target) 110 | Object.keys(callbacks).forEach(key => fn[key] = () => { 111 | callbacks[key](next, resolve, reject) 112 | }) 113 | return fn 114 | } 115 | 116 | export const idx = (obj, callback = (_obj) => { }) => { 117 | try { 118 | return callback(obj) 119 | } catch (ignore) { 120 | } 121 | return undefined 122 | } 123 | 124 | export const isArray = (items) => { 125 | return Array.isArray(items) 126 | } 127 | 128 | export const isFunction = (obj = null) => { 129 | return obj !== null && obj instanceof Function 130 | } 131 | 132 | export const isObject = (item = null) => { 133 | return item !== null && typeof item === 'object' && !isArray(item) 134 | } 135 | 136 | export const isString = (item = null) => { 137 | return item !== null && typeof item === 'string' 138 | } 139 | 140 | export const merge = (...args) => { 141 | const target = args[0] 142 | args.filter((value, key) => key > 0).forEach(value => _mergeAPair(target, value)) 143 | return target 144 | } 145 | 146 | export const shallowEqual = (a, b, comparator = _comparator, excludes = []) => { 147 | if (isImmutable(a) && isImmutable(b)) { 148 | return a === b 149 | } 150 | if (a instanceof Date && b instanceof Date) { 151 | return comparator(a, b) 152 | } 153 | if (isArray(a) && isArray(b)) { 154 | if (a.length !== b.length) { 155 | return false 156 | } 157 | for (let i = 0; i < a.length; i++) { 158 | if (!comparator(a[i], b[i])) { 159 | return false 160 | } 161 | } 162 | return true 163 | } 164 | if (isObject(a) && isObject(b)) { 165 | const aKeys = Object.keys(a) 166 | const bKeys = Object.keys(b) 167 | if (aKeys.length !== bKeys.length || aKeys.sort().join(',') !== bKeys.sort().join(',')) { 168 | return false 169 | } 170 | const keys = aKeys.filter(key => excludes.indexOf(key) < 0) 171 | for (let i = 0; i < keys.length; i++) { 172 | if (!comparator(a[keys[i]], b[keys[i]])) { 173 | return false 174 | } 175 | } 176 | return true 177 | } 178 | return comparator(a, b) 179 | } 180 | 181 | const _comparator = (a, b) => { 182 | if (a instanceof Date && b instanceof Date) { 183 | return a.getTime() === b.getTime() 184 | } 185 | if (a instanceof Function && b instanceof Function) { 186 | return true 187 | } 188 | return a === b 189 | } 190 | 191 | const _mergeAPair = (target, source) => { 192 | if (isObject(target) && isObject(source)) { 193 | Object.keys(source).forEach(key => { 194 | if (isObject(source[key])) { 195 | if (!target[key]) { 196 | Object.assign(target, { [key]: {} }) 197 | } 198 | _mergeAPair(target[key], source[key]) 199 | } else { 200 | Object.assign(target, { [key]: source[key] }) 201 | } 202 | }) 203 | } 204 | return target 205 | } 206 | -------------------------------------------------------------------------------- /src/providers/loader.js: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react' 2 | 3 | let READY = true 4 | 5 | const LISTENERS = [] 6 | const addListener = (listener) => { 7 | if (READY) { 8 | listener() 9 | } else { 10 | LISTENERS.push(listener) 11 | } 12 | } 13 | const removeListener = (listener) => { 14 | const index = LISTENERS.indexOf(listener) 15 | if (index >= 0) { 16 | LISTENERS.slice(index, 1) 17 | } 18 | } 19 | 20 | export default class Loader extends PureComponent { 21 | 22 | static defer = () => { 23 | READY = false 24 | LISTENERS.splice(0, LISTENERS.length) 25 | } 26 | 27 | static isReady = () => { 28 | return READY 29 | } 30 | 31 | static ready = () => { 32 | READY = true 33 | LISTENERS.forEach(listener => listener()) 34 | LISTENERS.splice(0, LISTENERS.length) 35 | } 36 | 37 | state = { 38 | ready: READY 39 | } 40 | 41 | componentDidMount() { 42 | addListener(this._onReady) 43 | } 44 | 45 | componentWillUnmount() { 46 | removeListener(this._onReady) 47 | } 48 | 49 | render() { 50 | const { ready } = this.state 51 | if (!ready) { 52 | return null 53 | } else { 54 | return this.props.children 55 | } 56 | } 57 | 58 | _onReady = () => { 59 | this.setState({ 60 | ready: true 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/providers/theme.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { AppState, I18nManager, View } from 'react-native' 3 | 4 | import { 5 | PropTypes, 6 | StyleSheet 7 | } from '../components' 8 | import Theme from '../theme' 9 | import * as Utils from '../libs/utils' 10 | 11 | import Loader from './loader' 12 | 13 | const LAND = 'land' 14 | const LDLTR = 'ldltr' 15 | const LDRTL = 'ldrtl' 16 | const PORT = 'port' 17 | 18 | export default class ThemeProvider extends PureComponent { 19 | 20 | static childContextTypes = { 21 | theme: PropTypes.any 22 | } 23 | 24 | static propTypes = { 25 | layoutDirection: PropTypes.string, 26 | orientation: PropTypes.string, 27 | theme: PropTypes.instanceOf(Theme.constructor), 28 | smallestWidth: PropTypes.string, 29 | onConfigChange: PropTypes.func 30 | } 31 | 32 | static defaultProps = { 33 | layoutDirection: '', 34 | orientation: '', 35 | theme: Theme, 36 | smallestWidth: '', 37 | onConfigChange: (_config = {}) => { } 38 | } 39 | 40 | static defer = () => { 41 | Loader.defer() 42 | } 43 | 44 | static ready = () => { 45 | Loader.ready() 46 | } 47 | 48 | state = { 49 | layoutDirection: undefined, 50 | orientation: undefined, 51 | smallestWidth: undefined 52 | } 53 | 54 | _theme = {} 55 | 56 | componentDidMount() { 57 | this._mounted = true 58 | AppState.addEventListener('change', this._onAppStateChange) 59 | } 60 | 61 | componentDidUpdate() { 62 | if (Loader.isReady()) { 63 | this.props.onConfigChange(this._getConfigProps()) 64 | } 65 | } 66 | 67 | componentWillUnmount() { 68 | this._mounted = false 69 | AppState.removeEventListener('change', this._onAppStateChange) 70 | } 71 | 72 | getChildContext() { 73 | return { theme: this._theme } 74 | } 75 | 76 | render() { 77 | const theme = this._theme 78 | theme.__id = (theme.__id || new Date().getTime()) + 1 79 | Object.assign(theme, this.props.theme.resolve(Object.values(this._getConfigProps()))) 80 | const styles = Styles.get(theme, this.props) 81 | return ( 82 | 83 | {this.state._ready && {this.props.children}} 84 | 85 | ) 86 | } 87 | 88 | _getConfigProps = () => { 89 | return Object.keys(this.props) 90 | .filter(prop => Utils.isString(this.props[prop])) 91 | .reduce((acc, prop) => { 92 | acc[prop] = this.props[prop] || this.state[prop] 93 | return acc 94 | }, {}) 95 | } 96 | 97 | _onAppStateChange = () => { 98 | if (I18nManager.isRTL && this.state.layoutDirection !== LDRTL) { 99 | this.setState({ layoutDirection: LDRTL }) 100 | } else if (!I18nManager.isRTL && this.state.layoutDirection !== LDLTR) { 101 | this.setState({ layoutDirection: LDLTR }) 102 | } 103 | } 104 | 105 | _onLayout = ({ nativeEvent: { layout: { width, height } } }) => { 106 | const keys = this.props.theme.getOrderedKeys() 107 | const newState = Object.assign({ _ready: true }, this.state, { 108 | smallestWidth: undefined 109 | }) 110 | keys.forEach(key => { 111 | const smallestWidth = Utils.idx(key, key => parseInt(/^sw([0-9]+)$/.exec(key)[1])) 112 | if (smallestWidth && width >= smallestWidth) { 113 | newState.smallestWidth = key 114 | } 115 | }) 116 | if (width > height && this.state.orientation !== LAND) { 117 | newState.orientation = LAND 118 | } else if (width <= height && this.state.orientation !== PORT) { 119 | newState.orientation = PORT 120 | } 121 | this.setState(newState) 122 | } 123 | } 124 | 125 | const Styles = StyleSheet.create((theme, { style }) => { 126 | const container = { 127 | backgroundColor: theme.palette.background, 128 | flex: 1, 129 | ...style 130 | } 131 | return { container } 132 | }) 133 | -------------------------------------------------------------------------------- /src/theme/default.js: -------------------------------------------------------------------------------- 1 | // Dark text on light background: 87%:DE 54%:8A 38%:61 12%:1E 2 | // White text on dark background: 100%:FF 70%:B3 50%:80 12%:1E 3 | // Icon on light background: 54%:8A 38%:61 4 | // Icon on dark background: 100%:FF 50%:80 5 | 6 | export default { 7 | bottomNavigation: { 8 | height: 56 9 | }, 10 | button: { 11 | minWidth: 88 12 | }, 13 | card: { 14 | borderRadius: 2, 15 | shadowColor: '#000000', 16 | shadowOpacity: 0.8, 17 | shadowRadius: 10, 18 | shadowOffset: { 19 | height: 1, 20 | width: 0.3, 21 | } 22 | }, 23 | dialog: { 24 | background: '#303030B3', 25 | spacing: 24 26 | }, 27 | divider: { 28 | color: '#0000001E', 29 | size: 0.87 30 | }, 31 | fontFamily: { 32 | bold: undefined, 33 | light: undefined, 34 | medium: undefined, 35 | regular: undefined 36 | }, 37 | fontSize: { 38 | button: 14, 39 | caption: 12, 40 | body1: 14, 41 | body2: 14, 42 | subhead1: 16, 43 | subhead2: 16, 44 | title: 20, 45 | headline: 24, 46 | display1: 34, 47 | display2: 45, 48 | display3: 56, 49 | display4: 112 50 | }, 51 | fontWeight: { 52 | button: undefined, 53 | caption: undefined, 54 | body1: undefined, 55 | body2: undefined, 56 | subhead1: undefined, 57 | subhead2: undefined, 58 | title: undefined, 59 | headline: undefined, 60 | display1: undefined, 61 | display2: undefined, 62 | display3: undefined, 63 | display4: undefined 64 | }, 65 | icon: { 66 | size: 24, 67 | sizeSm: 16, 68 | activeColor: '#0000008A', 69 | inactiveColor: '#00000061' 70 | }, 71 | iconColor: { 72 | active: { 73 | primary: '#FFFFFFB3', 74 | primaryDark: '#FFFFFFB3', 75 | primaryLight: '#0000008A', 76 | accent: '#0000008A', 77 | accentDark: '#0000008A', 78 | accentLight: '#0000008A', 79 | warn: '#FFFFFFB3', 80 | warnDark: '#FFFFFFB3', 81 | warnLight: '#0000008A', 82 | background: '#0000008A', 83 | backgroundDark: '#0000008A', 84 | backgroundLight: '#0000008A' 85 | }, 86 | focused: { 87 | primary: '#FFFFFFFF', 88 | primaryDark: '#FFFFFFFF', 89 | primaryLight: '#000000DE', 90 | accent: '#000000DE', 91 | accentDark: '#000000DE', 92 | accentLight: '#000000DE', 93 | warn: '#FFFFFFFF', 94 | warnDark: '#FFFFFFFF', 95 | warnLight: '#000000DE', 96 | background: '#000000DE', 97 | backgroundDark: '#000000DE', 98 | backgroundLight: '#000000DE' 99 | }, 100 | inactive: { 101 | primary: '#FFFFFF80', 102 | primaryDark: '#FFFFFF80', 103 | primaryLight: '#00000061', 104 | accent: '#00000061', 105 | accentDark: '#00000061', 106 | accentLight: '#00000061', 107 | warn: '#FFFFFF80', 108 | warnDark: '#FFFFFF80', 109 | warnLight: '#00000061', 110 | background: '#00000061', 111 | backgroundDark: '#00000061', 112 | backgroundLight: '#00000061' 113 | } 114 | }, 115 | iconToggle: { 116 | size: 48 117 | }, 118 | layout: { 119 | spacing: 16, 120 | spacingLg: 32, 121 | spacingSm: 8, 122 | spacingXs: 4 123 | }, 124 | lineHeight: { 125 | button: 20, 126 | caption: 18, 127 | body1: 20, 128 | body2: 24, 129 | subhead1: 24, 130 | subhead2: 28, 131 | title: 30, 132 | headline: 32, 133 | display1: 40, 134 | display2: 48, 135 | display3: 60, 136 | display4: 120 137 | }, 138 | list: { 139 | avatarSize: 40, 140 | paddingTop: 8, 141 | paddingBottom: 8, 142 | paddingLeft: 16, 143 | paddingRight: 16, 144 | singleLineTextOnlyHeight: 48, 145 | singleLineIconWithTextHeight: 48, 146 | singleLineAvatarWithTextHeight: 56, 147 | singleLineAvatarWithTextAndIconHeight: 56, 148 | twoLineTextOnlyHeight: 72, 149 | twoLineIconWithTextHeight: 72, 150 | twoLineAvatarWithTextHeight: 72, 151 | twoLineAvatarWithTextAndIconHeight: 72, 152 | threeLineTextOnlyHeight: 88, 153 | threeLineIconWithTextHeight: 88, 154 | threeLineAvatarWithTextHeight: 88, 155 | threeLineAvatarWithTextAndIconHeight: 88 156 | }, 157 | palette: { 158 | transparent: 'transparent', 159 | primary: '#03A9F4', 160 | primaryDark: '#0288D1', 161 | primaryLight: '#B3E5FC', 162 | accent: '#FF4081', 163 | accentDark: '#F50057', 164 | accentLight: '#FF80AB', 165 | warn: '#EC4058', 166 | warnDark: '#D91631', 167 | warnLight: '#F27889', 168 | background: '#FAFAFA', 169 | backgroundDark: '#EEEEEE', 170 | backgroundLight: '#FFFFFF' 171 | }, 172 | tab: { 173 | iconOnlyHeight: 48, 174 | iconWithTextHeight: 72, 175 | indicatorHeight: 2, 176 | spacing: 8, 177 | textOnlyHeight: 48 178 | }, 179 | textColor: { 180 | primary: { 181 | primary: '#FFFFFFFF', 182 | primaryDark: '#FFFFFFFF', 183 | primaryLight: '#000000DE', 184 | accent: '#000000DE', 185 | accentDark: '#000000DE', 186 | accentLight: '#000000DE', 187 | warn: '#FFFFFFFF', 188 | warnDark: '#FFFFFFFF', 189 | warnLight: '#000000DE', 190 | background: '#000000DE', 191 | backgroundDark: '#000000DE', 192 | backgroundLight: '#000000DE' 193 | }, 194 | secondary: { 195 | primary: '#FFFFFFB3', 196 | primaryDark: '#FFFFFFB3', 197 | primaryLight: '#0000008A', 198 | accent: '#0000008A', 199 | accentDark: '#0000008A', 200 | accentLight: '#0000008A', 201 | warn: '#FFFFFFB3', 202 | warnDark: '#FFFFFFB3', 203 | warnLight: '#0000008A', 204 | background: '#0000008A', 205 | backgroundDark: '#0000008A', 206 | backgroundLight: '#0000008A' 207 | }, 208 | hint: { 209 | primary: '#FFFFFF80', 210 | primaryDark: '#FFFFFF80', 211 | primaryLight: '#00000061', 212 | accent: '#00000061', 213 | accentDark: '#00000061', 214 | accentLight: '#00000061', 215 | warn: '#FFFFFF80', 216 | warnDark: '#FFFFFF80', 217 | warnLight: '#00000061', 218 | background: '#00000061', 219 | backgroundDark: '#00000061', 220 | backgroundLight: '#00000061' 221 | } 222 | }, 223 | toolbar: { 224 | minHeight: 56 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../libs/resolver' 2 | 3 | import theme from './default' 4 | 5 | export default Resolver.create(theme) 6 | --------------------------------------------------------------------------------