├── .buckconfig ├── .env ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── android ├── app │ ├── _BUCK │ ├── build.gradle │ ├── build_defs.bzl │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── reactnativestyledcomponents │ │ │ └── ReactNativeFlipper.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── reactnativestyledcomponents │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.json ├── babel.config.js ├── index.tsx ├── ios ├── Podfile ├── ReactNativeStyledComponents-tvOS │ └── Info.plist ├── ReactNativeStyledComponents-tvOSTests │ └── Info.plist ├── ReactNativeStyledComponents.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── ReactNativeStyledComponents-tvOS.xcscheme │ │ └── ReactNativeStyledComponents.xcscheme ├── ReactNativeStyledComponents │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── ReactNativeStyledComponentsTests │ ├── Info.plist │ └── ReactNativeStyledComponentsTests.m ├── metro.config.js ├── package-lock.json ├── package.json ├── src ├── convertStyle.tsx ├── convertUnits.ts ├── cssToRN │ ├── convert.ts │ ├── index.ts │ ├── maths.ts │ └── mediaQueries.ts ├── features.tsx ├── generateHash.ts ├── index.tsx ├── polyfill.ts ├── react-native │ ├── index.ts │ └── index.web.ts ├── rnToCss.ts ├── styleComponent.tsx ├── types.ts └── useTheme.tsx ├── tests ├── calc.test.js ├── convert.test.js ├── generateHash.test.js └── index.test.js ├── tsconfig.json └── webpack.config.js /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | ignorePatterns: ['dist', 'demo'], 7 | extends: [ 8 | 'plugin:react/recommended', 9 | 'standard', 10 | 'plugin:jest/recommended', 11 | 'plugin:react-hooks/recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended' 14 | ], 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly' 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true 23 | }, 24 | ecmaVersion: 2018, 25 | sourceType: 'module' 26 | }, 27 | plugins: [ 28 | 'react', 29 | 'react-hooks', 30 | '@typescript-eslint', 31 | 'jest' 32 | ], 33 | rules: { 34 | '@typescript-eslint/no-use-before-define': 'off', 35 | '@typescript-eslint/explicit-function-return-type': 'off', 36 | '@typescript-eslint/no-non-null-assertion': 'warn', 37 | '@typescript-eslint/explicit-module-boundary-types': 'off', 38 | 'no-use-before-define': 'off', 39 | 'multiline-ternary': 'off', 40 | 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }], 41 | 'linebreak-style': ['error', 'unix'] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | dist/ 61 | demo/ 62 | 63 | #VSCode 64 | .vscode/ -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.11.8 4 | 5 | * Improve background support for web 6 | * Fix support for outlines 7 | * Add support for dvh, dvw, lvw, lvh, svw, svh units 8 | 9 | ## Version 1.10 10 | 11 | * Add support for percentage units on border in web and native (useful for making responsive triangles) 12 | 13 | ## Version 1.9 14 | 15 | * Add support for `&:active` pseudo selector 16 | * Add support for `&:focus` pseudo selector 17 | 18 | ## Version 1.8 19 | 20 | * Accept returning an RN Style object in the tagged template string 21 | * Fix a type issue in the style prop of the components 22 | 23 | ## Version 1.7 24 | 25 | * Improve type support for the Theming system 26 | 27 | ## Version 1.6 28 | 29 | * Creation of RemContext to control rem units value 30 | * Important performance fix (500% faster!) 31 | 32 | ## Version 1.5 33 | 34 | * Add Theming features with the same [API](https://styled-components.com/docs/advanced) as `styled-components` lib. 35 | * Remove support for deprecated components: *ListView*, *SwipeableListView*, *TabBarIOS*, *ToolbarAndroid* and *ViewPagerAndroid* 36 | * Fix font-weight to accept numeric values 37 | 38 | ## Version 1.4 39 | 40 | * Change the type of `rnCSS` from `string` to `${string};` to ensure that it will end with a semicolon 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 François BILLIOUD 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RN-CSS 2 | 3 | This is basically [styled-components](https://github.com/styled-components/styled-components) with a much better support for React-Native, and some awesome additional features. You can check the docs of [styled-components](https://github.com/styled-components/styled-components) for more details about the basic API. I'll focus here on the differences. 4 | 5 | **Current version: 1.10** [See the Changelog](./CHANGELOG.md) 6 | 7 | --- 8 | 9 | ## Purpose 10 | 11 | The goal here is to be able to write a common code for React-Native-Web and React-Native without having to worry about the limitations of React-Native. React-Native supports only a subset of CSS but with `rn-css` you will be able to style your components as if you were using React. See: 12 | 13 | ```javascript 14 | const MyComponent = styled.View` 15 | width: 2em; 16 | height: 2em; 17 | border-radius: 50%; 18 | &:hover { 19 | background: red; 20 | } 21 | &:active { 22 | background: blue; 23 | } 24 | @media (min-width: 600px) { 25 | border: 1px solid rgb(128, 128, 128); 26 | } 27 | ` 28 | ``` 29 | 30 | --- 31 | 32 | ## Supported units: 33 | 34 | Here is a list of the units supported by `rn-css`. You can use them as you wish within your components: 35 | 36 | * px 37 | * pc 38 | * em 39 | * rem 40 | * cm 41 | * mm 42 | * vh 43 | * vw 44 | * vmin 45 | * vmax 46 | 47 | ### percentage support: 48 | 49 | There is only partial support for `%` units as I didn't find a way to retrieve the parent's size without a reference to the parent... I'll detail here what you can do and what you probably can't. 50 | 51 | **`%` without `calc`, `min`, `max`**: You should be able to use `%` for the following CSS properties, as long as you don't use `calc`, `min` or `max`: 52 | 53 | * width 54 | * height 55 | * min-width 56 | * min-height 57 | * max-width 58 | * max-height 59 | * top 60 | * bottom 61 | * left 62 | * right 63 | * flex-basis 64 | * border 65 | 66 | **`%` with `calc`, `min`, `max`**: You can try using `%` inside `calc`, `min` or `max` with the following CSS props, it should work as expected: 67 | 68 | * font-size 69 | * margin-left 70 | * margin-right 71 | * margin-top 72 | * margin-bottom 73 | 74 | --- 75 | 76 | ## Supported features: 77 | 78 | Here is a bunch of cool feature that you can use with this lib! 79 | 80 | ### inner functions: 81 | 82 | Just like `styled-components`, you can access your props with a function: 83 | 84 | ```javascript 85 | const View = styled.View` 86 | color: ${props => props.color || 'black'} 87 | ` 88 | ``` 89 | 90 | Here is for usage with **typescript**: 91 | 92 | ```typescript 93 | const View = styled.View<{ color: string }>` 94 | color: ${props => props.color || 'black'} 95 | ` 96 | ``` 97 | 98 | Here is an example returning directly a Style object: 99 | 100 | ```javascript 101 | const View = styled.View` 102 | color: black; 103 | ${props => ({ justifyContent: props.center })} 104 | ` 105 | ``` 106 | 107 | 108 | 109 | --- 110 | 111 | ### attrs: 112 | 113 | You can inject props with attrs: `styled(MyComp).attrs({ ...props })` or `styled(MyComp).attrs(props => ({ ...newProps }))` 114 | 115 | ```javascript 116 | const View = styled.View.attrs({ color: 'blue' })` 117 | width: calc(200px - 10em); 118 | background: ${props => props.color}; 119 | ` 120 | ``` 121 | 122 | Here is the **typescript** version: 123 | 124 | ```typescript 125 | const View = styled.View.attrs<{ color: string }>({ color: 'blue' })` 126 | width: calc(200px - 10em); 127 | background: ${props => props.color}; 128 | ` 129 | ``` 130 | 131 | Here is the version with a function: 132 | 133 | ```javascript 134 | const View = styled.View.attrs(props => ({ fontSize: props.fontSize * 2 }))` 135 | fontSize: ${props => props.fontSize}; 136 | background: ${props => props.color}; 137 | ` 138 | ``` 139 | 140 | --- 141 | 142 | ### inline css with rnCSS: 143 | 144 | This is very handy! You can inject any css string with the rnCSS props: 145 | 146 | ```javascript 147 | const View = styled.View`` 148 | return 149 | ``` 150 | 151 | **Do not forget to close the string with a semicolon** 152 | 153 | --- 154 | 155 | ### Extends components: 156 | 157 | You can extend components this way: 158 | 159 | ```javascript 160 | const MyComponent = styled.View` 161 | background-color: red; 162 | ` 163 | const Extended = styled(MyComponent)` 164 | background-color: blue; 165 | ` 166 | ``` 167 | 168 | ** IMPORTANT:** To extend custom components, you need to propagate the style prop: 169 | 170 | ```javascript 171 | const MyComponent = ({ style, ...props }) => { 172 | return 173 | ... 174 | 175 | } 176 | const Extended = styled(MyComponent)` 177 | background-color: blue; 178 | ` 179 | ``` 180 | 181 | --- 182 | 183 | ## Access current font size value 184 | 185 | If, somewhere within your app, you need to access the current font size value in px to be able to manually convert em into px, you can use the `FontSizeContext`. This can be helpful if you want to change some behaviour within your app depending on the font size. 186 | 187 | 188 | ```javascript 189 | import { FontSizeContext } from 'rn-css' 190 | 191 | ... 192 | const [width, setWidth] = React.useState(0) 193 | const em = React.useContext(FontSizeContext) 194 | if(width < 70 * em) { /* Do something when width is lesser than 70em */ } 195 | return setWidth(event.nativeEvent.layout.width)}>... 196 | ``` 197 | 198 | Default value for `rem` units is 16. If you want to declare another value, you can use `RemContext`: 199 | 200 | ```javascript 201 | import { RemContext } from 'rn-css' 202 | ... 203 | return {children} 204 | ``` 205 | 206 | --- 207 | 208 | ## Convert a CSS string to React-Native Style 209 | 210 | If, for some reason, you just want to convert a css string to a ReactNative Style object, you can use this feature: 211 | 212 | ```javascript 213 | import { cssToRNStyle } from 'rn-css' 214 | 215 | ... 216 | const style = cssToRNStyle('width: 2em; border-width: 12px; background: blue;') 217 | const { width = 32, borderLeftWidth = 12, backgroundColor = 'blue' } = style 218 | ``` 219 | 220 | The second parameter lets you provide: 221 | * **em** : *(Default: 16)* the current value of em unit for font size. You can retrieve the current context value with the `FontSizeContext`. 222 | * **width** *(Default: 100)* the reference width that will be used to calculate percentages for the following properties: `marginLeft`, `marginRight`, `translateX` and `borderRadius` 223 | * **height** *(Default: 100)* the reference width that will be used to calculate percentages for the following properties: `marginTop`, `marginBottom`, `translateY` and `borderRadius` 224 | 225 | ```javascript 226 | import { cssToRNStyle, FontSizeContext } from 'rn-css' 227 | 228 | ... 229 | const style = cssToRNStyle('width: 2em; margin: 10%;', { em: React.useContext(FontSizeContext), width: 100, height: 100 }) 230 | const { width: 32, marginTop: 10, marginLeft: 10, marginRight: 10, marginBottom: 10 } = style 231 | ``` 232 | 233 | If you want to use this feature in a non react project, you can use `import { cssToRNStyle } from 'rn-css/cssToRN'`. 234 | 235 | --- 236 | 237 | ## Extended CSS support 238 | 239 | We support some cool CSS feature that React-Native doesn't normally handle 240 | 241 | ### hover: 242 | 243 | You can add hover with `&:hover { }` 244 | 245 | ```javascript 246 | const Hoverable = styled.View` 247 | background: red; 248 | &:hover { 249 | background: blue; 250 | } 251 | ` 252 | ``` 253 | 254 | ### active: 255 | 256 | You can add styles on the element the user is interacting with by using `&:active { }`. A button is considered `active` from the moment the touch start until the touch ends 257 | 258 | ```javascript 259 | const Activable = styled.View` 260 | background: red; 261 | &:active { 262 | background: blue; 263 | } 264 | ` 265 | ``` 266 | 267 | ### focus: 268 | 269 | You can add styles on the element if it owns the focus with `&:focus { }`. 270 | 271 | ```javascript 272 | const Activable = styled.View` 273 | background: red; 274 | &:focus { 275 | background: blue; 276 | } 277 | ` 278 | ``` 279 | 280 | ### media queries: 281 | 282 | You can add media queries with `@media { }` 283 | 284 | ```javascript 285 | const ResponsiveView = styled.View` 286 | background: red; 287 | @media (min-width: 600px) { 288 | background: blue; 289 | } 290 | ` 291 | ``` 292 | 293 | You can use all supported units in the media query. 294 | 295 | 296 | ### text-overflow: 297 | 298 | If **rn-css** encounters `text-overflow: ellipsis;`, it will be transform into `numberOfLines: 1`, which should give the desired effect. 299 | 300 | ```javascript 301 | const Text = styled.Text` 302 | text-overflow: ellipsis; 303 | ` 304 | ``` 305 | 306 | ### z-index: 307 | 308 | **rn-css** will try to handle z-index as best as possible so that components from different trees can be correctly compared and positioned. In **iOS**, when a z-index is set, each parent will automatically receive this z-index, unless another value is set. This generally ensure that the behaviour matches the one from web. If you encounter an issue, please report. We might probably fix this. 309 | 310 | ```javascript 311 | const View = styled.View` 312 | z-index: 10; 313 | ` 314 | ``` 315 | 316 | ### calc: 317 | 318 | You can write things like `calc(2em - 1px)`. Keep in mind that the support for % is limited right now. 319 | 320 | ```javascript 321 | const View = styled.View` 322 | width: calc(200px - 10em); 323 | ` 324 | ``` 325 | 326 | ### min: 327 | 328 | You can write things like `min(2em, 10px)`. Keep in mind that the support for % is limited right now. 329 | 330 | ```javascript 331 | const View = styled.View` 332 | width: min(2em, 10px); 333 | ` 334 | ``` 335 | 336 | ### max: 337 | 338 | You can write things like `max(2em, 10px)`. Keep in mind that the support for % is limited right now. 339 | 340 | ```javascript 341 | const View = styled.View` 342 | width: max(2em, 10px); 343 | ` 344 | ``` 345 | 346 | --- 347 | 348 | ## Shared value: 349 | 350 | If you want to share some data with all of your components, like a theme, you can use the `SharedValues` context. Use it this way: 351 | 352 | 353 | ### Set the value: 354 | 355 | ```javascript 356 | return {children} 357 | ``` 358 | 359 | ### Use the value: 360 | 361 | ```javascript 362 | const View = styled.View` 363 | border-color: ${props => props.shared.green}; 364 | ` 365 | ``` 366 | 367 | ### Typescript: 368 | 369 | For Typescript, `shared` will always be typed with `unknown`. You need to manually declare the type of your shared object. Read the Theming section to learn more about workarounds. 370 | 371 | ```typescript 372 | // Create your theme 373 | const theme = { green: '#00FF00', red: '#FF0000' } as const 374 | type Theme = typeof theme 375 | 376 | // Somewhere in your React tree: 377 | // {children} 378 | 379 | // Use your shared theme 380 | const View = styled.View` 381 | border-color: ${props => (props.shared as Theme).green; 382 | ` 383 | ``` 384 | 385 | --- 386 | 387 | ## Theming: 388 | 389 | To match the API of styled-components, we offer the same abilities for theming [See the documentation](https://styled-components.com/docs/advanced). 390 | 391 | This relies on the [SharedValue](#shared-value) context. This means that you cannot use the Shared Value system **and** this theming système. Pick the one that best suits your needs. 392 | 393 | ### Custom theme type for typescript 394 | 395 | When you use the theme prop in your components, it is initially typed as `unknown`. But you can make it follow your custom type by extending the type declaration of **rn-css**. You will need to create a file `rncss.d.ts` in the `src` of your project root and add the following lines: 396 | 397 | ```ts 398 | import 'rn-css'; 399 | 400 | declare module 'rn-css' { 401 | type MyTheme = { 402 | // Describe your theme here. 403 | // Alternatively, you can use: `type MyTheme = typeof theme` if you have a fixed `theme` object. 404 | } 405 | export interface DefaultTheme extends MyTheme {} 406 | } 407 | ``` 408 | 409 | --- 410 | 411 | ## Coming later: 412 | 413 | linear-gradient, background-repeat, transitions, animations 414 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.reactnativestyledcomponents", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.reactnativestyledcomponents", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation. If none specified and 19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is 20 | * // default. Can be overridden with ENTRY_FILE environment variable. 21 | * entryFile: "index.android.js", 22 | * 23 | * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format 24 | * bundleCommand: "ram-bundle", 25 | * 26 | * // whether to bundle JS and assets in debug mode 27 | * bundleInDebug: false, 28 | * 29 | * // whether to bundle JS and assets in release mode 30 | * bundleInRelease: true, 31 | * 32 | * // whether to bundle JS and assets in another build variant (if configured). 33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 34 | * // The configuration property can be in the following formats 35 | * // 'bundleIn${productFlavor}${buildType}' 36 | * // 'bundleIn${buildType}' 37 | * // bundleInFreeDebug: true, 38 | * // bundleInPaidRelease: true, 39 | * // bundleInBeta: true, 40 | * 41 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 42 | * // for example: to disable dev mode in the staging build type (if configured) 43 | * devDisabledInStaging: true, 44 | * // The configuration property can be in the following formats 45 | * // 'devDisabledIn${productFlavor}${buildType}' 46 | * // 'devDisabledIn${buildType}' 47 | * 48 | * // the root of your project, i.e. where "package.json" lives 49 | * root: "../../", 50 | * 51 | * // where to put the JS bundle asset in debug mode 52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 53 | * 54 | * // where to put the JS bundle asset in release mode 55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 56 | * 57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 58 | * // require('./image.png')), in debug mode 59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 60 | * 61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 62 | * // require('./image.png')), in release mode 63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 64 | * 65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 69 | * // for example, you might want to remove it from here. 70 | * inputExcludes: ["android/**", "ios/**"], 71 | * 72 | * // override which node gets called and with what additional arguments 73 | * nodeExecutableAndArgs: ["node"], 74 | * 75 | * // supply additional arguments to the packager 76 | * extraPackagerArgs: [] 77 | * ] 78 | */ 79 | 80 | project.ext.react = [ 81 | enableHermes: false, // clean and rebuild if changing 82 | ] 83 | 84 | apply from: "../../node_modules/react-native/react.gradle" 85 | 86 | /** 87 | * Set this to true to create two separate APKs instead of one: 88 | * - An APK that only works on ARM devices 89 | * - An APK that only works on x86 devices 90 | * The advantage is the size of the APK is reduced by about 4MB. 91 | * Upload all the APKs to the Play Store and people will download 92 | * the correct one based on the CPU architecture of their device. 93 | */ 94 | def enableSeparateBuildPerCPUArchitecture = false 95 | 96 | /** 97 | * Run Proguard to shrink the Java bytecode in release builds. 98 | */ 99 | def enableProguardInReleaseBuilds = false 100 | 101 | /** 102 | * The preferred build flavor of JavaScriptCore. 103 | * 104 | * For example, to use the international variant, you can use: 105 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 106 | * 107 | * The international variant includes ICU i18n library and necessary data 108 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 109 | * give correct results when using with locales other than en-US. Note that 110 | * this variant is about 6MiB larger per architecture than default. 111 | */ 112 | def jscFlavor = 'org.webkit:android-jsc:+' 113 | 114 | /** 115 | * Whether to enable the Hermes VM. 116 | * 117 | * This should be set on project.ext.react and mirrored here. If it is not set 118 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode 119 | * and the benefits of using Hermes will therefore be sharply reduced. 120 | */ 121 | def enableHermes = project.ext.react.get("enableHermes", false); 122 | 123 | android { 124 | compileSdkVersion rootProject.ext.compileSdkVersion 125 | 126 | compileOptions { 127 | sourceCompatibility JavaVersion.VERSION_1_8 128 | targetCompatibility JavaVersion.VERSION_1_8 129 | } 130 | 131 | defaultConfig { 132 | applicationId "com.reactnativestyledcomponents" 133 | minSdkVersion rootProject.ext.minSdkVersion 134 | targetSdkVersion rootProject.ext.targetSdkVersion 135 | versionCode 1 136 | versionName "1.0" 137 | } 138 | splits { 139 | abi { 140 | reset() 141 | enable enableSeparateBuildPerCPUArchitecture 142 | universalApk false // If true, also generate a universal APK 143 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 144 | } 145 | } 146 | signingConfigs { 147 | debug { 148 | storeFile file('debug.keystore') 149 | storePassword 'android' 150 | keyAlias 'androiddebugkey' 151 | keyPassword 'android' 152 | } 153 | } 154 | buildTypes { 155 | debug { 156 | signingConfig signingConfigs.debug 157 | } 158 | release { 159 | // Caution! In production, you need to generate your own keystore file. 160 | // see https://facebook.github.io/react-native/docs/signed-apk-android. 161 | signingConfig signingConfigs.debug 162 | minifyEnabled enableProguardInReleaseBuilds 163 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 164 | } 165 | } 166 | 167 | packagingOptions { 168 | pickFirst "lib/armeabi-v7a/libc++_shared.so" 169 | pickFirst "lib/arm64-v8a/libc++_shared.so" 170 | pickFirst "lib/x86/libc++_shared.so" 171 | pickFirst "lib/x86_64/libc++_shared.so" 172 | } 173 | 174 | // applicationVariants are e.g. debug, release 175 | applicationVariants.all { variant -> 176 | variant.outputs.each { output -> 177 | // For each separate APK per architecture, set a unique version code as described here: 178 | // https://developer.android.com/studio/build/configure-apk-splits.html 179 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 180 | def abi = output.getFilter(OutputFile.ABI) 181 | if (abi != null) { // null for the universal-debug, universal-release variants 182 | output.versionCodeOverride = 183 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 184 | } 185 | 186 | } 187 | } 188 | } 189 | 190 | dependencies { 191 | implementation fileTree(dir: "libs", include: ["*.jar"]) 192 | //noinspection GradleDynamicVersion 193 | implementation "com.facebook.react:react-native:+" // From node_modules 194 | 195 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" 196 | 197 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { 198 | exclude group:'com.facebook.fbjni' 199 | } 200 | 201 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 202 | exclude group:'com.facebook.flipper' 203 | } 204 | 205 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { 206 | exclude group:'com.facebook.flipper' 207 | } 208 | 209 | if (enableHermes) { 210 | def hermesPath = "../../node_modules/hermes-engine/android/"; 211 | debugImplementation files(hermesPath + "hermes-debug.aar") 212 | releaseImplementation files(hermesPath + "hermes-release.aar") 213 | } else { 214 | implementation jscFlavor 215 | } 216 | } 217 | 218 | // Run this once to be able to run the application with BUCK 219 | // puts all compile dependencies into folder libs for BUCK to use 220 | task copyDownloadableDepsToLibs(type: Copy) { 221 | from configurations.compile 222 | into 'libs' 223 | } 224 | 225 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 226 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/reactnativestyledcomponents/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.reactnativestyledcomponents; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativestyledcomponents/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.reactnativestyledcomponents; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "ReactNativeStyledComponents"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativestyledcomponents/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.reactnativestyledcomponents; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 53 | * 54 | * @param context 55 | * @param reactInstanceManager 56 | */ 57 | private static void initializeFlipper( 58 | Context context, ReactInstanceManager reactInstanceManager) { 59 | if (BuildConfig.DEBUG) { 60 | try { 61 | /* 62 | We use reflection here to pick up the class that initializes Flipper, 63 | since Flipper library is not available in release mode 64 | */ 65 | Class aClass = Class.forName("com.reactnativestyledcomponents.ReactNativeFlipper"); 66 | aClass 67 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 68 | .invoke(null, context, reactInstanceManager); 69 | } catch (ClassNotFoundException e) { 70 | e.printStackTrace(); 71 | } catch (NoSuchMethodException e) { 72 | e.printStackTrace(); 73 | } catch (IllegalAccessException e) { 74 | e.printStackTrace(); 75 | } catch (InvocationTargetException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeStyledComponents 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.33.1 29 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sharcoux/rn-css/d8fa214e8a08dd35c8b1b7fafe14f2d59a319b73/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ReactNativeStyledComponents' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeStyledComponents", 3 | "displayName": "ReactNativeStyledComponents" 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'] 3 | } 4 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import React from 'react' 4 | import { Animated, AppRegistry, Platform, StyleProp, Text, TextStyle, TouchableOpacity, TouchableOpacityProps, ViewStyle } from 'react-native' 5 | import { name as appName } from './app.json' 6 | import styled from './src' 7 | 8 | const value = 100 9 | 10 | const View = styled.View` 11 | background: green; 12 | border-radius: 50%; 13 | width: 200px; 14 | height: 200px; 15 | ` 16 | 17 | const Dot = styled(Animated.View)` 18 | width: 5em; 19 | height: 5em; 20 | margin: 2em; 21 | border-radius: 50%; 22 | z-index: 10; 23 | ` 24 | 25 | const StyledText = styled.Text<{col: string}>` 26 | color: ${props => props.col || 'black'} 27 | font-size: 1em; 28 | @media (max-width: 40em) { 29 | color: blue; 30 | font-size: 2em; 31 | } 32 | @media (max-width: 20em) { 33 | color: red; 34 | font-size: 3em; 35 | } 36 | ` 37 | 38 | const Box = styled.View` 39 | width: ${value}em; 40 | max-width: 50vw; 41 | ` 42 | const Box2 = styled.View` 43 | width: 100vw; 44 | height: 2em; 45 | background: blue; 46 | ` 47 | const Popup = styled.View` 48 | z-index: 20; 49 | position: absolute; 50 | top: calc(100% + 2px); 51 | background-color: black; 52 | box-shadow: 2px 2px 2px red; 53 | ` 54 | 55 | const Hoverable = styled.View` 56 | width: 100px; 57 | height: 100px; 58 | background: red; 59 | &:hover { 60 | background: blue; 61 | } 62 | ` 63 | 64 | const HoverableText = styled.Text` 65 | &:hover { 66 | fontSize: 2em 67 | } 68 | ` 69 | const Options = styled.FlatList.attrs<{selected: boolean; pressed: boolean}>(props => ({ pressed: props.selected || props.pressed }))` 70 | position: absolute; 71 | top: 100%; 72 | z-index: 1; 73 | ` 74 | 75 | const ColorCircle = styled.TouchableOpacity<{color: string; size?: number}>` 76 | background-color: ${props => props.color}; 77 | width: ${props => props.size || 2}em; 78 | height: ${props => props.size || 2}em; 79 | opacity: 1; 80 | border-radius: 50%; 81 | &:hover { 82 | background-color: red; 83 | opacity: 0.5; 84 | } 85 | ` 86 | 87 | const FlatList = () => { 88 | return ({item})}/> 89 | } 90 | 91 | const Comp = ({ style, text }: { style?: ViewStyle; text: string }) => { 92 | return 93 | {text} 94 | 95 | } 96 | const ExtendedComp = styled(Comp).attrs({ text: 'test' })`` 97 | // const ExtendedComp2 = styled(Comp)<{ small: boolean }>` 98 | // ${props => props.small ? 'font-size: 0.8em' : ''} 99 | // ` 100 | 101 | const CustomTouchable = styled.TouchableOpacity.attrs<{ extra: string }>({ activeOpacity: 1 })`` 102 | 103 | const Touchable = styled.TouchableOpacity<{pressed: boolean}>` 104 | background-color: ${props => props.pressed ? 'blue' : 'red'}; 105 | &:active { 106 | background-color: purple; 107 | } 108 | &:focus { 109 | background-color: pink; 110 | } 111 | &:hover { 112 | background-color: yellow; 113 | } 114 | ` 115 | 116 | const Triangle = styled.View` 117 | width: 30em; 118 | height: 30em; 119 | border-top: 50% solid blue; 120 | border-left: 50% solid blue; 121 | border-right: 50% solid transparent; 122 | border-bottom: 50% solid transparent; 123 | ` 124 | 125 | // const CustomSelectContainer = styled.TouchableOpacity.attrs({ activeOpacity: 1 })` 126 | // padding: 2px; 127 | // margin: 0.2em; 128 | // border-radius: 0.6em; 129 | // width: 8em; 130 | // height: 3.6em; 131 | // flex-direction: row; 132 | // background-color: white; 133 | // border-width: 1px; 134 | // border-style: solid; 135 | // ` 136 | 137 | const Forward = React.forwardRef((props: TouchableOpacityProps, ref) => { 138 | return 139 | }) 140 | Forward.displayName = 'Forward' 141 | 142 | const Button = ({ color, style }: { color: string; style?: StyleProp }) => { 143 | const [pressed, setPressed] = React.useState(false) 144 | return setPressed(!pressed)}>Press Me! 145 | } 146 | 147 | const StyledButton = styled(Button).attrs<{ fallbackColor: string }>(({ color: 'black' }))`` 148 | const StyledButton2 = styled(Button).attrs(({ color: 'black' }))`` 149 | 150 | const App = () => { 151 | const ref = React.useRef(null) 152 | const ref2 = React.useRef(null) 153 | React.useLayoutEffect(() => console.log(ref), []) 154 | const dotLeft = React.useRef(new Animated.Value(0)) 155 | const dotStyle: Animated.WithAnimatedValue = React.useMemo( 156 | () => ({ 157 | left: dotLeft.current.interpolate({ 158 | inputRange: [0, 50], 159 | outputRange: ['0%', '50%'] 160 | }) 161 | }), 162 | [] 163 | ) 164 | 165 | // const LangDropdownItem = styled.View.attrs<{ label: string; value: number }>( 166 | // ({ label }) => ({ 167 | // label: label + 2 168 | // }) 169 | // )` 170 | // z-index: ${props => props.value + ''}; 171 | // ` 172 | 173 | const touchableProps = { activeOpacity: 0 } as TouchableOpacityProps 174 | 175 | return ( 176 | 177 | 178 | 179 | Welcome to ReactNativeStyledComponents 180 | 181 | 182 | 183 | 184 | Placeholder 185 | 186 | Should be over 187 | 188 | 189 | 190 | 191 | Placeholder 192 | 193 | 194 | 195 | Hover me ! 196 | 197 | Hover me ! 198 | 199 | 200 | 201 | 202 | 203 | { console.log(e.nativeEvent.layout) }}/> 204 | 205 | 206 | 207 | 208 | 209 | ) 210 | } 211 | 212 | AppRegistry.registerComponent(appName, () => App) 213 | 214 | if (Platform.OS === 'web') { 215 | AppRegistry.runApplication(appName, { 216 | rootTag: document.getElementsByTagName('body')[0] 217 | }) 218 | } 219 | 220 | export default App 221 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | def add_flipper_pods!(versions = {}) 5 | versions['Flipper'] ||= '~> 0.33.1' 6 | versions['DoubleConversion'] ||= '1.1.7' 7 | versions['Flipper-Folly'] ||= '~> 2.1' 8 | versions['Flipper-Glog'] ||= '0.3.6' 9 | versions['Flipper-PeerTalk'] ||= '~> 0.0.4' 10 | versions['Flipper-RSocket'] ||= '~> 1.0' 11 | 12 | pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug' 13 | pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug' 14 | pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 15 | pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug' 16 | pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug' 17 | 18 | # List all transitive dependencies for FlipperKit pods 19 | # to avoid them being linked in Release builds 20 | pod 'Flipper', versions['Flipper'], :configuration => 'Debug' 21 | pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug' 22 | pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug' 23 | pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug' 24 | pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug' 25 | pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug' 26 | pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug' 27 | pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug' 28 | pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug' 29 | pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug' 30 | pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug' 31 | pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug' 32 | pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug' 33 | pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 34 | end 35 | 36 | # Post Install processing for Flipper 37 | def flipper_post_install(installer) 38 | installer.pods_project.targets.each do |target| 39 | if target.name == 'YogaKit' 40 | target.build_configurations.each do |config| 41 | config.build_settings['SWIFT_VERSION'] = '4.1' 42 | end 43 | end 44 | end 45 | end 46 | 47 | target 'ReactNativeStyledComponents' do 48 | # Pods for ReactNativeStyledComponents 49 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 50 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 51 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 52 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 53 | pod 'React', :path => '../node_modules/react-native/' 54 | pod 'React-Core', :path => '../node_modules/react-native/' 55 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 56 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 57 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 58 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 59 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 60 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 61 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 62 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 63 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 64 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 65 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 66 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 67 | 68 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 69 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 70 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 71 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 72 | pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon" 73 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 74 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true 75 | 76 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 77 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 78 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 79 | 80 | target 'ReactNativeStyledComponentsTests' do 81 | inherit! :complete 82 | # Pods for testing 83 | end 84 | 85 | use_native_modules! 86 | 87 | # Enables Flipper. 88 | # 89 | # Note that if you have use_frameworks! enabled, Flipper will not work and 90 | # you should disable these next few lines. 91 | add_flipper_pods! 92 | post_install do |installer| 93 | flipper_post_install(installer) 94 | end 95 | end 96 | 97 | target 'ReactNativeStyledComponents-tvOS' do 98 | # Pods for ReactNativeStyledComponents-tvOS 99 | 100 | target 'ReactNativeStyledComponents-tvOSTests' do 101 | inherit! :search_paths 102 | # Pods for testing 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents.xcodeproj/xcshareddata/xcschemes/ReactNativeStyledComponents-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents.xcodeproj/xcshareddata/xcschemes/ReactNativeStyledComponents.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #if DEBUG 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #if DEBUG 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 36 | moduleName:@"ReactNativeStyledComponents" 37 | initialProperties:nil]; 38 | 39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 40 | 41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 42 | UIViewController *rootViewController = [UIViewController new]; 43 | rootViewController.view = rootView; 44 | self.window.rootViewController = rootViewController; 45 | [self.window makeKeyAndVisible]; 46 | return YES; 47 | } 48 | 49 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 50 | { 51 | #if DEBUG 52 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 53 | #else 54 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 55 | #endif 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ReactNativeStyledComponents 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponents/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponentsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/ReactNativeStyledComponentsTests/ReactNativeStyledComponentsTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface ReactNativeStyledComponentsTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation ReactNativeStyledComponentsTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false 14 | } 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-css", 3 | "version": "1.11.10", 4 | "exports": { 5 | ".": { 6 | "import": "./dist/index.js", 7 | "require": "./dist/index.js" 8 | }, 9 | "./*": { 10 | "import": "./dist/*/index.js", 11 | "require": "./dist/*/index.js" 12 | } 13 | }, 14 | "typesVersions": { 15 | "*": { 16 | ".": [ 17 | "dist/index.d.ts" 18 | ], 19 | "./*": [ 20 | "dist/*/index.d.ts" 21 | ] 22 | } 23 | }, 24 | "scripts": { 25 | "test": "jest", 26 | "prepare": "rm -rf dist && tsc && webpack", 27 | "android": "react-native run-android", 28 | "ios": "react-native run-ios", 29 | "start": "react-native start", 30 | "lint": "eslint --fix .", 31 | "build": "webpack", 32 | "release": "npm run prepare && release-it", 33 | "web": "webpack serve --open --mode development" 34 | }, 35 | "peerDependencies": { 36 | "react": ">=16.13.1", 37 | "react-native": ">=0.62.2" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.16.5", 41 | "@babel/runtime": "^7.16.5", 42 | "@react-native-community/eslint-config": "^3.0.1", 43 | "@types/jest": "^27.0.3", 44 | "@types/react": "^17.0.37", 45 | "@types/react-native": "^0.66.9", 46 | "@typescript-eslint/eslint-plugin": "^5.7.0", 47 | "@typescript-eslint/parser": "^5.7.0", 48 | "babel-jest": "^27.4.5", 49 | "babel-loader": "^8.2.3", 50 | "dotenv": "^10.0.0", 51 | "eslint": "^7.32.0", 52 | "eslint-config-standard": "^16.0.3", 53 | "eslint-plugin-import": "^2.25.3", 54 | "eslint-plugin-jest": "^25.3.0", 55 | "eslint-plugin-node": "^11.1.0", 56 | "eslint-plugin-promise": "^5.2.0", 57 | "eslint-plugin-react": "^7.27.1", 58 | "eslint-plugin-react-hooks": "^4.4.0", 59 | "eslint-plugin-standard": "^5.0.0", 60 | "html-loader": "^3.0.1", 61 | "html-webpack-plugin": "^5.5.0", 62 | "husky": "^7.0.4", 63 | "jest": "^27.5.1", 64 | "lint-staged": "^12.1.2", 65 | "metro-react-native-babel-preset": "^0.70.0", 66 | "react": "^17.0.2", 67 | "react-dom": "^17.0.2", 68 | "react-native": "^0.68.0", 69 | "react-native-typescript-transformer": "^1.2.13", 70 | "react-native-web": "^0.17.7", 71 | "react-test-renderer": "17.0.2", 72 | "release-it": "^15.6.0", 73 | "ts-jest": "^27.1.1", 74 | "ts-loader": "^9.2.6", 75 | "typescript": "^4.5.4", 76 | "webpack": "^5.72.0", 77 | "webpack-cli": "^4.9.2", 78 | "webpack-dev-server": "^4.8.1" 79 | }, 80 | "jest": { 81 | "preset": "react-native" 82 | }, 83 | "files": [ 84 | "dist", 85 | "src" 86 | ], 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "tsc --noEmit && lint-staged" 90 | } 91 | }, 92 | "lint-staged": { 93 | "*.[tj]s?(x)": [ 94 | "eslint . --fix", 95 | "git add" 96 | ] 97 | }, 98 | "main": "dist/index.js", 99 | "types": "dist/index.d.ts", 100 | "homepage": "https://github.com/Sharcoux/rn-css", 101 | "author": { 102 | "name": "François Billioud", 103 | "email": "f.billioud@gmail.com" 104 | }, 105 | "bugs": { 106 | "url": "https://github.com/Sharcoux/rn-css/issues", 107 | "email": "f.billioud@gmail.com" 108 | }, 109 | "repository": { 110 | "type": "git", 111 | "url": "git@github.com:Sharcoux/rn-css.git" 112 | }, 113 | "sideEffects": false, 114 | "license": "ISC" 115 | } 116 | -------------------------------------------------------------------------------- /src/convertStyle.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import type { /* StyleSheet, */ Animated, TextStyle } from 'react-native' 3 | import { convertValue } from './convertUnits' 4 | import type { AnyStyle, CompleteStyle, PartialStyle, Units } from './types' 5 | 6 | /** Mix the calculated RN style within the object style */ 7 | const convertStyle = = AnyStyle>(rnStyle: PartialStyle, units: Units) => { 8 | /** This is the result of the convertions from css style into RN style */ 9 | const convertedStyle: CompleteStyle = {}; 10 | // If width and height are specified, we can use those values for the first render 11 | (['width', 'height'] as const).forEach(key => { 12 | if (!units[key] && rnStyle[key]) { 13 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 14 | const converted = convertValue(key, rnStyle[key]!, units) 15 | if (!Number.isNaN(converted)) units[key] = converted as number 16 | } 17 | }) 18 | ;(Object.keys(rnStyle) as (keyof PartialStyle)[]).forEach(key => { 19 | const value = rnStyle[key] || '0' 20 | // Handle object values 21 | if (key === 'transform' && rnStyle.transform) { 22 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 23 | convertedStyle.transform = rnStyle.transform!.map(transformation => { 24 | const result = {} as { [trans: string]: string | number } 25 | (Object.keys(transformation) as Array).forEach(k => (result[k] = convertValue(k, transformation[k]!, units))) 26 | return result 27 | }) as unknown as TextStyle['transform'] 28 | } 29 | else if (key === 'shadowOffset' && rnStyle.shadowOffset) { 30 | convertedStyle.shadowOffset = { 31 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 32 | width: convertValue(key, rnStyle.shadowOffset!.width || '0', units) as number, 33 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 34 | height: convertValue(key, rnStyle.shadowOffset!.height || '0', units) as number 35 | } 36 | } 37 | else if (key === 'textShadowOffset' && rnStyle.textShadowOffset) { 38 | convertedStyle.textShadowOffset = { 39 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 40 | width: convertValue(key, rnStyle.textShadowOffset!.width || '0', units) as number, 41 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 42 | height: convertValue(key, rnStyle.textShadowOffset!.height || '0', units) as number 43 | } 44 | } 45 | // Font family should not be transformed (same as cursor for web in case of base64 value, and boxShadow/textShadow for web) 46 | else if (['cursor', 'fontFamily', 'boxShadow', 'textShadow'].includes(key)) { 47 | convertedStyle[key as 'fontFamily'] = value 48 | } 49 | else { 50 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 51 | // @ts-ignore 52 | convertedStyle[key] = convertValue(key, value, units) 53 | } 54 | }) 55 | return convertedStyle as T 56 | } 57 | 58 | export default convertStyle 59 | -------------------------------------------------------------------------------- /src/convertUnits.ts: -------------------------------------------------------------------------------- 1 | import type { PartialStyle, Transform, Units } from './types' 2 | import { calculate, min, max } from './cssToRN/maths' 3 | import { Platform } from './react-native' 4 | 5 | /** Take a css value like 12em and return [12, 'em'] */ 6 | export function parseValue (value: string): [number, string | undefined] { 7 | // Match a single unit 8 | const unit = value.match(/([+-]?\b\d+(\.\d+)?)([a-z]+\b|%)?/i) 9 | return unit ? [parseFloat(unit[1]), unit[3] as (string | undefined)] : [0, undefined] 10 | } 11 | 12 | /** Convert a value using the provided unit transform table */ 13 | export function convertValue (key: keyof PartialStyle | keyof Transform, value: string, units: Units): string | number { 14 | if (typeof value !== 'string') { 15 | console.error(`Failed to parse CSS instruction: ${key}=${value}. We expect a string, but ${value} was of type ${typeof value}.`) 16 | return 0 17 | } 18 | // colors should be left untouched 19 | if (value.startsWith('#')) return value 20 | 21 | // Percentage values need to rely on an other unit as reference 22 | const finalUnits = { ...units } 23 | if (value.includes('%')) { 24 | // Percentage is not supported on borders in web 25 | if (Platform.OS === 'web' && (!key.toLowerCase().includes('border') || key.toLowerCase().includes('radius'))) return value 26 | if (['marginTop', 'marginBottom', 'translateY'].includes(key) || key.startsWith('borderTop') || key.startsWith('borderBottom')) finalUnits['%'] = (units.height || 0) / 100 27 | else if (['marginLeft', 'marginRight', 'translateX'].includes(key) || key.startsWith('borderLeft') || key.startsWith('borderRight')) finalUnits['%'] = (units.width || 0) / 100 28 | else if (key.startsWith('border') && key.endsWith('Radius')) finalUnits['%'] = ((units.width || 0) + (units.height || 0)) / 200 29 | else if (['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight', 'top', 'left', 'bottom', 'right', 'flexBasis', 'rotate3d'].includes(key)) { 30 | if (value.startsWith('calc') || value.startsWith('max') || value.startsWith('min')) { 31 | if (['height', 'minHeight', 'maxHeight', 'top', 'bottom'].includes(key)) finalUnits['%'] = (units.height || 0) / 100 32 | else finalUnits['%'] = (units.width || 0) / 100 33 | } 34 | // width: 100%, height: 100% are supported 35 | else return value 36 | } 37 | else if (['lineHeight'].includes(key)) finalUnits['%'] = units.em / 100 38 | else finalUnits['%'] = 0.01 39 | } 40 | 41 | // We replace all units within the value 42 | const convertedValue = value.replace(/(\b\d+(\.\d+)?)([a-z]+\b|%)/ig, occ => { 43 | const [val, unit] = parseValue(occ) 44 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 45 | if (['deg', 'rad', 'turn', 's'].includes(unit!)) return occ // We don't want to convert deg, rad, turn, second units 46 | return val * (finalUnits[unit as keyof Units || 'px'] ?? 1) + '' 47 | }) 48 | 49 | // We handle extra calculations (calc, min, max, parsing...) 50 | if (convertedValue.startsWith('calc(')) return calculate(convertedValue.substring(4))// remove calc. We can keep the parenthesis 51 | else if (convertedValue.startsWith('max(')) return max(convertedValue.substring(4, convertedValue.length - 1))// Remove max() 52 | else if (convertedValue.startsWith('min(')) return min(convertedValue.substring(4, convertedValue.length - 1))// remove min() 53 | else if (key === 'fontWeight') return convertedValue // fontWeight must be a string even when it is an integer value. 54 | else if (parseFloat(convertedValue) + '' === convertedValue) return parseFloat(convertedValue) 55 | else return convertedValue 56 | } 57 | -------------------------------------------------------------------------------- /src/cssToRN/convert.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from '../react-native' 2 | import type { Style, Transform } from '../types' 3 | 4 | /** 5 | * Check if the value is a number. Numbers start with a digit, a decimal point or calc(, max( ou min(. 6 | * Optionally accept "auto" value (for margins) 7 | * @param value The value to check 8 | * @param acceptAuto true if auto is an accepted value 9 | * @returns true if the value is a number 10 | */ 11 | function isNumber (value: string, acceptAuto?: boolean) { 12 | if (acceptAuto && value === 'auto') return true 13 | return value.match(/^[+-]?(\.\d|\d|calc\(|max\(|min\()/mg) 14 | } 15 | 16 | /** 17 | * Check if the value is a number. Numbers start with a digit, a decimal point or calc(, max( ou min(. 18 | * Optionally accept "auto" value (for margins) 19 | * @param value The value to check 20 | * @param acceptAuto true if auto is an accepted value 21 | * @returns true if the value is a number 22 | */ 23 | /** 24 | * Split the value into numbers values and non numbers values 25 | * @param value The value to check 26 | * @param acceptAuto true if auto is an accepted value 27 | * @returns An object containing the number and non number values as arrays. 28 | */ 29 | function findNumbers (value: string, acceptAuto?: boolean) { 30 | const result = { 31 | nonNumbers: [] as string[], 32 | numbers: [] as string[] 33 | } 34 | let group = '' 35 | value.split(/\s+/mg).forEach(val => { 36 | // HACK: we prevent some parts of font-family names like "Rounded Mplus 1c" to be interpreted as numbers 37 | if (val.startsWith('"') || val.startsWith("'")) group = val.charAt(0) 38 | if (group && val.endsWith(group)) group = '' 39 | if (group) result.nonNumbers.push(val) 40 | else result[isNumber(val, acceptAuto) ? 'numbers' : 'nonNumbers'].push(val) 41 | }) 42 | return result 43 | } 44 | 45 | /** Parse a css value for border */ 46 | export function border (value: string): { [x:string]: string } { 47 | const values = value.split(/\s+/mg) 48 | const result = { 49 | borderWidth: '0', 50 | borderColor: 'black', 51 | borderStyle: 'solid' 52 | } 53 | values.forEach((value: string) => { 54 | if (['solid', 'dotted', 'dashed'].includes(value)) result.borderStyle = value 55 | else if (isNumber(value)) result.borderWidth = value 56 | // eslint-disable-next-line no-useless-return 57 | else if (value === 'none') return 58 | else result.borderColor = value 59 | }) 60 | return { 61 | ...sideValue('border', result.borderWidth, 'Width'), 62 | ...sideValue('border', result.borderColor, 'Color'), 63 | ...sideValue('border', result.borderStyle, 'Style') 64 | } 65 | } 66 | 67 | /** Parse a css value for border-like elements */ 68 | export function borderLike (prefixKey: 'outline' | 'borderLeft' | 'borderRight' | 'borderTop' | 'borderBottom', value: string): { [x:string]: string } { 69 | const values = value.split(/\s+/mg) 70 | const result = { 71 | [prefixKey + 'Width']: '0', 72 | [prefixKey + 'Color']: 'black', 73 | [prefixKey + 'Style']: 'solid' 74 | } 75 | if (value === 'none') return result 76 | values.forEach((value: string) => { 77 | if (['solid', 'dotted', 'dashed'].includes(value)) result[prefixKey + 'Style'] = value 78 | else if (isNumber(value)) result[prefixKey + 'Width'] = value 79 | else result[prefixKey + 'Color'] = value 80 | }) 81 | return result 82 | } 83 | 84 | export function shadow (prefix: 'textShadow' | 'shadow', value: string): { [x:string]: string | { width: string, height: string } } { 85 | if (value === 'none') return shadow(prefix, '0 0 0 black') 86 | const { nonNumbers, numbers } = findNumbers(value) 87 | return { 88 | [prefix + 'Offset']: { width: numbers[0] || '0', height: numbers[1] || '0' }, 89 | [prefix + 'Radius']: numbers[2] || '0', 90 | [prefix + 'Color']: nonNumbers[0] || 'black' 91 | } 92 | } 93 | 94 | export function flex (value: string) { 95 | const [flexGrow, flexShrink = '0', flexBasis = '0'] = value.split(/\s/g) 96 | // If the only property is a not a number, its value is flexBasis. See https://developer.mozilla.org/en-US/docs/Web/CSS/flex 97 | if ((parseFloat(flexGrow) + '') !== flexGrow) return { flexBasis: flexGrow } 98 | // If the second property is not a number, its value is flexBasis. 99 | if (((parseFloat(flexShrink) + '') !== flexShrink)) return { flexGrow, flexBasis: flexShrink } 100 | return { 101 | flexGrow, flexShrink, flexBasis 102 | } 103 | } 104 | 105 | export function flexFlow (value: string) { 106 | const values = value.split(/\s/g) 107 | const result = {} as {[prop: string]: string} 108 | values.forEach(val => { 109 | if (['wrap', 'nowrap', 'wrap-reverse'].includes(val)) result.flexWrap = val 110 | else if (['row', 'column', 'row-reverse', 'column-reverse'].includes(val)) result.flexDirection = val 111 | }) 112 | return result 113 | } 114 | 115 | export function placeContent (value: string) { 116 | const [alignContent, justifyContent = alignContent] = value.split(/\s/g) 117 | return { alignContent, justifyContent } 118 | } 119 | 120 | export function background (value: string) { 121 | const values = value.match(/(linear-gradient\(|url\().*?\)|[^\s]+/mg) || [] 122 | const backgroundColor = values.reverse().find(isColor) || 'transparent' 123 | // We support everything on web 124 | return Platform.OS === 'web' ? { backgroundColor, background: value } : { backgroundColor } 125 | } 126 | 127 | export function textDecoration (value: string) { 128 | const values = value.split(/\s+/mg) 129 | const result = { 130 | textDecorationLine: 'none', 131 | textDecorationStyle: 'solid', 132 | textDecorationColor: 'black' 133 | } 134 | values.forEach(value => { 135 | if (['none', 'solid', 'double', 'dotted', 'dashed'].includes(value)) result.textDecorationStyle = value 136 | else if (['none', 'underline', 'line-through'].includes(value)) { 137 | // To accept 'underline line-throught' as a value, we need to concatenate 138 | if (result.textDecorationLine !== 'none') result.textDecorationLine += ' ' + value 139 | else result.textDecorationLine = value 140 | } 141 | else result.textDecorationColor = value 142 | }) 143 | return result 144 | } 145 | 146 | function read2D (prefix: 'translate' | 'scale' | 'skew', value: string) { 147 | const [x, y = x] = value.split(',').map(val => val.trim()) as string[] 148 | return [ 149 | { [prefix + 'X']: x }, 150 | { [prefix + 'Y']: y } 151 | ] 152 | } 153 | 154 | function read3D (prefix: 'rotate', value: string): Transform[] { 155 | const [x, y, z] = value.split(',').map(val => val.trim()) 156 | const transform = [] 157 | if (x) transform.push({ [prefix + 'X']: x }) 158 | if (y) transform.push({ [prefix + 'Y']: y }) 159 | if (z) transform.push({ [prefix + 'Z']: z }) 160 | return transform 161 | } 162 | 163 | export function transform (value: string) { 164 | // Parse transform operations 165 | const transform = [...value.matchAll(/(\w+)\((.*?)\)/gm)].reduce((acc, val) => { 166 | const operation = val[1] 167 | const values = val[2].trim() 168 | if (['translate', 'scale', 'skew'].includes(operation)) return acc.concat(read2D(operation as 'translate' | 'scale' | 'skew', values)) 169 | else if (operation === 'rotate3d') return acc.concat(read3D('rotate', values)) 170 | else return acc.concat({ [operation]: values }) 171 | }, [] as Transform[]) 172 | return { transform } 173 | } 174 | 175 | export function font (value: string) { 176 | const { nonNumbers, numbers } = findNumbers(value) 177 | const result: Style = { 178 | fontStyle: 'normal', 179 | fontWeight: 'normal' as string 180 | } 181 | for (let i = 0; i < nonNumbers.length; i++) { 182 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 183 | const val = nonNumbers.shift()! 184 | if (val === 'italic') result.fontStyle = val 185 | else if (val === 'bold') result.fontWeight = val 186 | else if (val === 'normal') continue// can be both fontStyle or fontWeight, but as it is the default we can just ignore. 187 | else if (['small-caps', 'oldstyle-nums', 'lining-nums', 'tabular-nums', 'proportional-nums'].includes(val)) result.fontVariant = val 188 | else { 189 | nonNumbers.unshift(val) 190 | break 191 | } 192 | } 193 | // The font family is the last property and can contain spaces 194 | if (nonNumbers.length > 0) result.fontFamily = nonNumbers.join(' ') 195 | 196 | // The font size is always defined and is the last number 197 | const size = numbers.pop() 198 | if (!size) return result 199 | const [fontSize, lineHeight] = size.split('/') // We can define the line height like this : fontSize/lineHeight 200 | result.fontSize = fontSize 201 | if (lineHeight) result.lineHeight = lineHeight 202 | // The font size is always after the font weight 203 | if (numbers.length) result.fontWeight = numbers[0] 204 | 205 | return result 206 | } 207 | 208 | /** Parses a css value for the side of an element (border-width, margin, padding) */ 209 | export function sideValue (prefixKey: T, value: string, postFix: T extends 'border' | 'outline' ? 'Width' | 'Style' | 'Color' | '' : '' = ''): { [x: string]: string} { 210 | if (value === 'none') return sideValue(prefixKey, '0', postFix) 211 | const [top = value, right = top, bottom = top, left = right] = findNumbers(value, prefixKey === 'margin').numbers 212 | return { 213 | [prefixKey + 'Top' + postFix]: top, 214 | [prefixKey + 'Left' + postFix]: left, 215 | [prefixKey + 'Right' + postFix]: right, 216 | [prefixKey + 'Bottom' + postFix]: bottom 217 | } 218 | } 219 | 220 | /** Parses a css value for the corner of an element (border-radius) */ 221 | export function cornerValue (prefixKey: 'border', value: string, postFix: 'Radius') { 222 | const [topLeft, topRight = topLeft, bottomRight = topLeft, bottomLeft = topRight] = findNumbers(value).numbers 223 | return { 224 | [prefixKey + 'TopLeft' + postFix]: topLeft, 225 | [prefixKey + 'TopRight' + postFix]: topRight, 226 | [prefixKey + 'BottomLeft' + postFix]: bottomLeft, 227 | [prefixKey + 'BottomRight' + postFix]: bottomRight 228 | } 229 | } 230 | 231 | function isColor (value?: string) { 232 | if (!value) return false 233 | if (value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl')) return true 234 | if (Platform.OS === 'web' && value.match(/^\s*linear-gradient\(.*\)/s)) return true 235 | const CSS_COLOR_NAMES = [ 236 | 'aliceblue', 237 | 'antiquewhite', 238 | 'aqua', 239 | 'aquamarine', 240 | 'azure', 241 | 'beige', 242 | 'bisque', 243 | 'black', 244 | 'blanchedalmond', 245 | 'blue', 246 | 'blueviolet', 247 | 'brown', 248 | 'burlywood', 249 | 'cadetblue', 250 | 'chartreuse', 251 | 'chocolate', 252 | 'coral', 253 | 'cornflowerblue', 254 | 'cornsilk', 255 | 'crimson', 256 | 'cyan', 257 | 'darkblue', 258 | 'darkcyan', 259 | 'darkgoldenrod', 260 | 'darkgray', 261 | 'darkgrey', 262 | 'darkgreen', 263 | 'darkkhaki', 264 | 'darkmagenta', 265 | 'darkolivegreen', 266 | 'darkorange', 267 | 'darkorchid', 268 | 'darkred', 269 | 'darksalmon', 270 | 'darkseagreen', 271 | 'darkslateblue', 272 | 'darkslategray', 273 | 'darkslategrey', 274 | 'darkturquoise', 275 | 'darkviolet', 276 | 'deeppink', 277 | 'deepskyblue', 278 | 'dimgray', 279 | 'dimgrey', 280 | 'dodgerblue', 281 | 'firebrick', 282 | 'floralwhite', 283 | 'forestgreen', 284 | 'fuchsia', 285 | 'gainsboro', 286 | 'ghostwhite', 287 | 'gold', 288 | 'goldenrod', 289 | 'gray', 290 | 'grey', 291 | 'green', 292 | 'greenyellow', 293 | 'honeydew', 294 | 'hotpink', 295 | 'indianred', 296 | 'indigo', 297 | 'ivory', 298 | 'khaki', 299 | 'lavender', 300 | 'lavenderblush', 301 | 'lawngreen', 302 | 'lemonchiffon', 303 | 'lightblue', 304 | 'lightcoral', 305 | 'lightcyan', 306 | 'lightgoldenrodyellow', 307 | 'lightgray', 308 | 'lightgrey', 309 | 'lightgreen', 310 | 'lightpink', 311 | 'lightsalmon', 312 | 'lightseagreen', 313 | 'lightskyblue', 314 | 'lightslategray', 315 | 'lightslategrey', 316 | 'lightsteelblue', 317 | 'lightyellow', 318 | 'lime', 319 | 'limegreen', 320 | 'linen', 321 | 'magenta', 322 | 'maroon', 323 | 'mediumaquamarine', 324 | 'mediumblue', 325 | 'mediumorchid', 326 | 'mediumpurple', 327 | 'mediumseagreen', 328 | 'mediumslateblue', 329 | 'mediumspringgreen', 330 | 'mediumturquoise', 331 | 'mediumvioletred', 332 | 'midnightblue', 333 | 'mintcream', 334 | 'mistyrose', 335 | 'moccasin', 336 | 'navajowhite', 337 | 'navy', 338 | 'oldlace', 339 | 'olive', 340 | 'olivedrab', 341 | 'orange', 342 | 'orangered', 343 | 'orchid', 344 | 'palegoldenrod', 345 | 'palegreen', 346 | 'paleturquoise', 347 | 'palevioletred', 348 | 'papayawhip', 349 | 'peachpuff', 350 | 'peru', 351 | 'pink', 352 | 'plum', 353 | 'powderblue', 354 | 'purple', 355 | 'rebeccapurple', 356 | 'red', 357 | 'rosybrown', 358 | 'royalblue', 359 | 'saddlebrown', 360 | 'salmon', 361 | 'sandybrown', 362 | 'seagreen', 363 | 'seashell', 364 | 'sienna', 365 | 'silver', 366 | 'skyblue', 367 | 'slateblue', 368 | 'slategray', 369 | 'slategrey', 370 | 'snow', 371 | 'springgreen', 372 | 'steelblue', 373 | 'tan', 374 | 'teal', 375 | 'thistle', 376 | 'tomato', 377 | 'turquoise', 378 | 'violet', 379 | 'wheat', 380 | 'white', 381 | 'whitesmoke', 382 | 'yellow', 383 | 'yellowgreen' 384 | ] 385 | return CSS_COLOR_NAMES.includes(value) 386 | } 387 | -------------------------------------------------------------------------------- /src/cssToRN/index.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, Platform } from '../react-native' 2 | import convertStyle from '../convertStyle' 3 | import { CompleteStyle, Context, PartialStyle, Style, Units } from '../types' 4 | import { sideValue, border, borderLike, cornerValue, font, textDecoration, shadow, placeContent, flex, flexFlow, transform, background } from './convert' 5 | import { createMedia } from './mediaQueries' 6 | 7 | function kebab2camel (string: string) { 8 | return string.replace(/-./g, x => x.toUpperCase()[1]) 9 | } 10 | 11 | function stripSpaces (string: string) { 12 | return string.replace(/(calc|max|min|rgb|rgba)\(.*?\)/mg, res => res.replace(/\s/g, '')) 13 | } 14 | 15 | function cssToStyle (css: string) { 16 | const result: Style = {} 17 | // Find media queries (We use [\s\S] instead of . because dotall flag (s) is not supported by react-native-windows) 18 | const cssWithoutMediaQueries = css.replace(/@media([\s\S]*?){[^{}]*}/gmi, res => { 19 | const { css, isValid } = createMedia(res) 20 | const style = cssChunkToStyle(css) 21 | const mediaQuery = (context: Context) => isValid(context) && style 22 | if (!result.media) result.media = [] 23 | result.media.push(mediaQuery) 24 | return '' 25 | }) 26 | // Find hover (we don't support hover within media queries) (We use [\s\S] instead of . because dotall flag (s) is not supported by react-native-windows) 27 | const cssWithoutHover = cssWithoutMediaQueries.replace(/&:hover\s*{([\s\S]*?)}/gmi, res => { 28 | const hoverInstructions = res.substring(0, res.length - 1).replace(/&:hover\s*{/mi, '')// We remove the `&:hover {` and `}` 29 | result.hover = cssChunkToStyle(hoverInstructions) 30 | return '' 31 | }) 32 | // Find active (we don't support active within media queries) (We use [\s\S] instead of . because dotall flag (s) is not supported by react-native-windows) 33 | const cssWithoutActive = cssWithoutHover.replace(/&:active\s*{([\s\S]*?)}/gmi, res => { 34 | const activeInstructions = res.substring(0, res.length - 1).replace(/&:active\s*{/mi, '')// We remove the `&:active {` and `}` 35 | result.active = cssChunkToStyle(activeInstructions) 36 | return '' 37 | }) 38 | // Find focus (we don't support focus within media queries) (We use [\s\S] instead of . because dotall flag (s) is not supported by react-native-windows) 39 | const cssWithoutFocus = cssWithoutActive.replace(/&:focus\s*{([\s\S]*?)}/gmi, res => { 40 | const activeInstructions = res.substring(0, res.length - 1).replace(/&:focus\s*{/mi, '')// We remove the `&:focus {` and `}` 41 | result.focus = cssChunkToStyle(activeInstructions) 42 | return '' 43 | }) 44 | Object.assign(result, cssChunkToStyle(cssWithoutFocus)) 45 | return result 46 | } 47 | 48 | export function cssToRNStyle (css: string, units: { em?: number, width?: number, height?: number } = {}) { 49 | const { width, height } = Dimensions.get('window') 50 | const finalUnits: Units = { 51 | em: 16, 52 | '%': 0.01, 53 | vw: width / 100, 54 | vh: height / 100, 55 | vmin: Math.min(width, height) / 100, 56 | vmax: Math.max(width, height) / 100, 57 | lvw: width / 100, 58 | lvh: height / 100, 59 | svw: width / 100, 60 | svh: height / 100, 61 | dvw: width / 100, 62 | dvh: height / 100, 63 | width: 100, 64 | height: 100, 65 | rem: 16, 66 | px: 1, 67 | pt: 96 / 72, 68 | in: 96, 69 | pc: 16, 70 | cm: 96 / 2.54, 71 | mm: 96 / 25.4, 72 | ...units 73 | } 74 | const rnStyle = cssChunkToStyle(css) 75 | return convertStyle(rnStyle, finalUnits) 76 | } 77 | 78 | function cssChunkToStyle (css: string) { 79 | const result: PartialStyle = {} 80 | css.split(/\s*;\s*(?!base64)/mg).forEach((entry: string) => { 81 | const [rawKey, ...rest] = entry.split(':') 82 | const rawValue = rest.join(':') 83 | if (!rawValue) return 84 | const key = kebab2camel(rawKey.trim()) 85 | const value = stripSpaces(rawValue.trim())// We need this to correctly read calc() values 86 | switch (key) { 87 | case 'border': 88 | Object.assign(result, border(value)) 89 | break 90 | case 'borderTop': 91 | case 'borderLeft': 92 | case 'borderRight': 93 | case 'borderBottom': 94 | case 'outline': 95 | Object.assign(result, borderLike(key, value)) 96 | break 97 | case 'borderStyle': 98 | case 'borderColor': 99 | case 'borderWidth': 100 | Object.assign(result, sideValue('border', value, key.split('border').pop() as '' | 'Width' | 'Style' | 'Color')) 101 | break 102 | case 'outlineStyle': 103 | case 'outlineColor': 104 | case 'outlineWidth': 105 | Object.assign(result, sideValue('outline', value, key.split('outline').pop() as '' | 'Width' | 'Style' | 'Color')) 106 | break 107 | case 'background': 108 | Object.assign(result, background(value)) 109 | break 110 | case 'padding': 111 | case 'margin': 112 | Object.assign(result, sideValue(key, value)) 113 | break 114 | case 'borderRadius': 115 | Object.assign(result, cornerValue('border', value, 'Radius')) 116 | break 117 | case 'font': 118 | Object.assign(result, font(value)) 119 | break 120 | case 'textDecoration': 121 | Object.assign(result, textDecoration(value)) 122 | break 123 | case 'placeContent': 124 | Object.assign(result, placeContent(value)) 125 | break 126 | case 'flex': 127 | Object.assign(result, flex(value)) 128 | break 129 | case 'flexFlow': 130 | Object.assign(result, flexFlow(value)) 131 | break 132 | case 'transform': 133 | Object.assign(result, transform(value)) 134 | break 135 | case 'boxShadow': 136 | case 'textShadow': 137 | // To provide support for the 4th element (spread-radius) at least for web 138 | if (Platform.OS === 'web') Object.assign(result, { [key]: value }) 139 | // We need to replace boxShadow by shadow 140 | else Object.assign(result, shadow(key === 'boxShadow' ? 'shadow' : key, value)) 141 | break 142 | case 'userSelect': 143 | Object.assign(result, { userSelect: value, WebkitUserSelect: value }) 144 | break 145 | // Other keys don't require any special treatment 146 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 147 | // @ts-ignore 148 | default: result[key] = value 149 | } 150 | }) 151 | return result 152 | } 153 | 154 | export default cssToStyle 155 | -------------------------------------------------------------------------------- /src/cssToRN/maths.ts: -------------------------------------------------------------------------------- 1 | type Group = { 2 | type: 'group'; 3 | right?: Element; 4 | parent?: Node; 5 | } 6 | 7 | type Operator = { 8 | type: 'additive' | 'multiplicative'; 9 | parent: Node; 10 | priority: 1 | 2; 11 | operation: '*' | '/' | '+' | '-'; 12 | left: Element; 13 | right?: Element; 14 | } 15 | 16 | type Value = { 17 | type: 'number'; 18 | value: string; 19 | } 20 | 21 | type Node = Group | Operator; 22 | type Element = Node | Value; 23 | 24 | /** Evaluate the string operation without relying on eval */ 25 | export function calculate (string: string) { 26 | function applyOperator (left: number, op: Operator['operation'], right: number): number { 27 | if (op === '+') return left + right 28 | else if (op === '-') return left - right 29 | else if (op === '*') return left * right 30 | else if (op === '/') return left / right 31 | else return right || left 32 | } 33 | function evaluate (root: Element): number { 34 | switch (root.type) { 35 | case 'group': return evaluate(root.right!) 36 | case 'additive': 37 | case 'multiplicative': 38 | return applyOperator(evaluate(root.left), root.operation, evaluate(root.right!)) 39 | case 'number': return parseFloat(root.value) 40 | } 41 | } 42 | const rootNode: Group = { type: 'group' } 43 | let currentNode: Node = rootNode 44 | function openGroup () { 45 | const newGroup: Group = { type: 'group', parent: currentNode } 46 | currentNode.right = newGroup 47 | currentNode = newGroup 48 | } 49 | function closeGroup () { 50 | while (currentNode.type !== 'group') currentNode = currentNode.parent! 51 | currentNode = currentNode.parent! 52 | } 53 | function addNumber (char: string) { 54 | const currentNumber = currentNode.right as (Value | undefined) 55 | if (currentNumber === undefined) currentNode.right = { type: 'number', value: char } 56 | else currentNumber.value += char 57 | } 58 | function addOperator (char: Operator['operation']) { 59 | const additive = '+-'.includes(char) 60 | const priority = additive ? 1 : 2 61 | // If it is a sign and not an operation, we add it to the comming number 62 | if (additive && !currentNode.right) return addNumber(char) 63 | while ((currentNode as Operator).priority && ((currentNode as Operator).priority >= priority)) currentNode = currentNode.parent! 64 | 65 | const operator: Operator = { 66 | type: additive ? 'additive' : 'multiplicative', 67 | priority, 68 | parent: currentNode, 69 | operation: char, 70 | left: currentNode.right! 71 | } 72 | 73 | currentNode.right = operator 74 | currentNode = operator 75 | } 76 | string.split('').forEach(char => { 77 | if (char === '(') openGroup() 78 | else if (char === ')') closeGroup() 79 | else if ('0123456789.'.includes(char)) addNumber(char) 80 | else if ('+*-/'.includes(char)) addOperator(char as Operator['operation']) 81 | }) 82 | return evaluate(rootNode) 83 | } 84 | 85 | export function min (string: string) { 86 | const values = string.split(',').map(val => parseFloat(val.trim())) 87 | return Math.min(...values) 88 | } 89 | export function max (string: string) { 90 | const values = string.split(',').map(val => parseFloat(val.trim())) 91 | return Math.max(...values) 92 | } 93 | -------------------------------------------------------------------------------- /src/cssToRN/mediaQueries.ts: -------------------------------------------------------------------------------- 1 | import type { Units, Context, PartialStyle, Transform } from '../types' 2 | import { convertValue, parseValue } from '../convertUnits' 3 | import { PixelRatio, Platform } from '../react-native' 4 | 5 | type Constraint = { 6 | all?: undefined, 7 | sprint?: undefined, 8 | speech?: undefined, 9 | screen?: undefined, 10 | width?: string 11 | widthMin?: string 12 | widthMax?: string 13 | height?: string 14 | heightMin?: string 15 | heightMax?: string 16 | aspectRatio?: string 17 | aspectRatioMin?: string 18 | aspectRatioMax?: string 19 | orientation?: 'portrait' | 'landscape' 20 | resolution?: string 21 | resolutionMin?: string 22 | resolutionMax?: string 23 | scan?: 'interlace' | 'progressive' 24 | grid?: 0 | 1 25 | update?: 'none' | 'slow' | 'fast' 26 | overflowBlock?: 'none' | 'scroll' | 'paged' 27 | overflowInline?: 'none' | 'scroll' 28 | environmentBlending?: 'opaque' | 'additive' | 'subtractive' 29 | color?: string 30 | colorMin?: string 31 | colorMax?: string 32 | colorGamut?: 'srgb' | 'p3' | 'rec2020' 33 | colorIndex?: string 34 | colorIndexMin?: string 35 | colorIndexMax?: string 36 | dynamicRange?: 'standard' | 'high' 37 | monochrome?: string 38 | monochromeMin?: string 39 | monochromeMax?: string 40 | invertedColors?: 'none' | 'inverted' 41 | pointer?: 'none' | 'coarse' | 'fine' 42 | hover?: 'none' | 'hover' 43 | anyPointer?: 'none' | 'coarse' | 'fine' 44 | anyHover?: 'none' | 'hover' 45 | prefersReducedMotion?: 'no-preference' | 'reduce' 46 | prefersReducedTransparency?: 'no-preference' | 'reduce' 47 | prefersReducedData?: 'no-preference' | 'reduce' 48 | prefersContrast?: 'no-preference' | 'high' | 'low' | 'forced' 49 | prefersColorScheme?: 'light' | 'dark' 50 | forcedColor?: 'none' | 'active' 51 | scripting?: 'none' | 'initial-only' | 'enabled' 52 | deviceWidth?: string 53 | deviceWidthMin?: string 54 | deviceWidthMax?: string 55 | deviceHeight?: string 56 | deviceHeightMin?: string 57 | deviceHeightMax?: string 58 | deviceAspectRatio?: string 59 | deviceAspectRatioMin?: string 60 | deviceAspectRatioMax?: string 61 | } 62 | 63 | export function createContext (units: Units): Context { 64 | const vw = (units.vw || 1) * 100 65 | const vh = (units.vh || 1) * 100 66 | return { 67 | anyHover: 'hover', 68 | anyPointer: Platform.OS === 'web' ? 'fine' : 'coarse', 69 | aspectRatio: vw / vh, 70 | color: 16, 71 | colorGamut: 'srgb', 72 | colorIndex: 0, 73 | deviceAspectRatio: vw / vh, 74 | deviceHeight: vh, 75 | deviceWidth: vw, 76 | dynamicRange: 'standard', 77 | environmentBlending: 'opaque', 78 | forcedColor: 'none', 79 | grid: 0, 80 | height: vh, 81 | hover: 'hover', 82 | invertedColors: 'none', 83 | monochrome: 0, 84 | orientation: vw > vh ? 'landscape' : 'portrait', 85 | overflowBlock: 'scroll', 86 | overflowInline: 'scroll', 87 | pointer: 'coarse', 88 | prefersColorScheme: 'dark', 89 | prefersContrast: 'no-preference', 90 | prefersReducedData: 'no-preference', 91 | prefersReducedMotion: 'no-preference', 92 | prefersReducedTransparency: 'no-preference', 93 | resolution: PixelRatio.getPixelSizeForLayoutSize(vw), 94 | scan: 'progressive', 95 | scripting: 'enabled', 96 | type: 'screen', 97 | units, 98 | update: 'fast', 99 | width: vw 100 | } 101 | } 102 | 103 | function convertAnyValue (key: keyof Context | keyof PartialStyle | keyof Transform, value: string, units: Units) { 104 | if (key === 'resolution') { 105 | // Convert density 106 | if (value === 'infinite') return Infinity 107 | const densityUnitsEquivalence = { 108 | dpi: 'in', 109 | dpcm: 'cm', 110 | dppx: 'px', 111 | x: 'px' 112 | } 113 | const [num, unit] = parseValue(value) 114 | return num + densityUnitsEquivalence[unit as keyof typeof densityUnitsEquivalence] 115 | } 116 | else if (key === 'deviceAspectRatio' || key === 'aspectRatio') { 117 | // Convert ratio 118 | const [w, h] = value.split('/').map(v => parseInt(v, 10)) 119 | return w / h 120 | } 121 | 122 | return convertValue(key as keyof PartialStyle, value, units) 123 | } 124 | 125 | /** Check if a constraint is respected by the provided context */ 126 | function evaluateConstraint (constraint: Constraint, context: Context): boolean { 127 | return (Object.keys(constraint) as (keyof Constraint)[]).every(key => { 128 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 129 | const [, baseKey, minMax] = key.match(/(.*?)(Min|Max|$)/)! as [string, keyof Context, 'Min' | 'Max' | ''] 130 | const value = convertAnyValue(baseKey, constraint[key] + '', context.units) 131 | if (minMax === 'Min') { 132 | return context[baseKey] >= value 133 | } 134 | else if (key.endsWith('Max')) { 135 | return context[baseKey] <= value 136 | } 137 | else if (['all', 'sprint', 'speech', 'screen'].includes(key)) { 138 | return context.type === key || key === 'all' 139 | } 140 | else { 141 | // Boolean check: we want the value to be defined and not equal to 'none' 142 | if (value === undefined) return !!context[baseKey] && context[baseKey] !== 'none' 143 | // float comparison 144 | if (baseKey.endsWith('aspectRatio')) return Math.abs((context[baseKey] as number) - (value as number)) < ((value as number) + (context[baseKey] as number)) / 100 145 | return context[baseKey] === value 146 | } 147 | }) 148 | } 149 | 150 | /** Parse media query constraint such as min-width: 600px, or screen */ 151 | function parseConstraintValue (constraintString: string): Evaluation { 152 | let [key, value] = constraintString.split(':').map(v => v.trim()) 153 | if (key.startsWith('min-')) key = key.substring(4) + 'Min' 154 | else if (key.startsWith('max-')) key = key.substring(4) + 'Max' 155 | const constraint: Constraint = { [key]: value } 156 | return (context: Context) => evaluateConstraint(constraint, context) 157 | } 158 | 159 | export type Evaluation = (context: Context) => boolean 160 | 161 | function parse (constraint: string, previous?: Evaluation): Evaluation { 162 | const result = constraint.match(/\sand\s|,|\sonly\s|\(|\snot\s/im) 163 | 164 | if (!result) { 165 | // If we reached the end of the string, we just return the last constraint 166 | if (constraint.match(/\w/)) return parseConstraintValue(constraint) 167 | // If there is just an empty string, we just ignore it by returning a truthy evaluation 168 | else return previous || (() => true) 169 | } 170 | 171 | const token = result[0] // The next command we found 172 | const tail = constraint.substring(result.index! + token.length) // The rest of the constraint 173 | const current = constraint.substring(0, result.index!) // The current constraint 174 | if (token === '(') { 175 | try { 176 | const { index } = tail.match(/\)/)! 177 | const parenthesis = tail.substring(0, index!) 178 | const postParenthesis = tail.substring(index! + 1) 179 | return parse(postParenthesis, parse(parenthesis, previous)) 180 | } 181 | catch (err) { 182 | console.error('No matching parenthesis in the media query', constraint) 183 | throw err 184 | } 185 | } 186 | else if (token.includes('and')) { 187 | const left = previous || parseConstraintValue(current) 188 | const right = parse(tail) 189 | return (context: Context) => left(context) && right(context) 190 | } 191 | else if (token.includes('not')) { 192 | const evaluate = parse(tail) 193 | return (context: Context) => !evaluate(context) 194 | } 195 | else if (token.includes('only')) { 196 | return parse(tail, previous || parseConstraintValue(current)) 197 | } 198 | else if (token === ',') { 199 | const left = previous || parseConstraintValue(current) 200 | const right = parse(tail) 201 | return (context: Context) => left(context) || right(context) 202 | } 203 | else { 204 | throw new Error(`Error while parsing media query '${constraint}'. No token found`) 205 | } 206 | } 207 | 208 | export const createMedia = (query: string) => { 209 | // We use [\s\S] instead of dotall flag (s) because it is not supported by react-native-windows 210 | const parsed = query.match(/@media([\s\S]*?){([^{}]*)}/mi) 211 | if (!parsed) throw new Error(`Parsing error: check the syntax of media query ${query}.`) 212 | const [, constraints, css] = parsed 213 | const isValid = parse(constraints) 214 | return { 215 | css, 216 | isValid 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/features.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React, { MouseEvent } from 'react' 3 | import type { Style, Units, MediaQuery, PartialStyle } from './types' 4 | import { useWindowDimensions, LayoutChangeEvent, GestureResponderEvent, TouchableHighlightProps, TextInputProps } from 'react-native' 5 | import { parseValue } from './convertUnits' 6 | import { createContext } from './cssToRN/mediaQueries' 7 | 8 | /** Hook that will apply the screen size to the styles defined with vmin, vmax, vw, vh units, and handle media queries constraints */ 9 | export const useScreenSize = () => { 10 | const { width, height } = useWindowDimensions() 11 | return React.useMemo(() => ({ 12 | vw: width / 100, 13 | vh: height / 100, 14 | vmin: Math.min(width, height) / 100, 15 | vmax: Math.max(width, height) / 100, 16 | dvw: width / 100, 17 | dvh: height / 100, 18 | lvw: width / 100, 19 | lvh: height / 100, 20 | svw: width / 100, 21 | svh: height / 100 22 | }), [height, width]) 23 | } 24 | 25 | /** Hook that will apply the style reserved for hover state if needed */ 26 | export const useHover = (onMouseEnter: undefined | ((event: MouseEvent) => void), onMouseLeave: undefined | ((event: MouseEvent) => void | undefined), needsHover: boolean) => { 27 | const [hover, setHover] = React.useState(false) 28 | const hoverStart = React.useMemo(() => needsHover ? (event: MouseEvent) => { 29 | if (onMouseEnter) onMouseEnter(event) 30 | setHover(true) 31 | } : undefined, [needsHover, onMouseEnter]) 32 | const hoverStop = React.useMemo(() => needsHover ? (event: MouseEvent) => { 33 | if (onMouseLeave) onMouseLeave(event) 34 | setHover(false) 35 | } : undefined, [needsHover, onMouseLeave]) 36 | return { hover, onMouseEnter: hoverStart || onMouseEnter, onMouseLeave: hoverStop || onMouseLeave } 37 | } 38 | 39 | /** Hook that will apply the style reserved for active state if needed */ 40 | export const useActive = ( 41 | onPressIn: undefined | ((event: GestureResponderEvent) => void), 42 | onPressOut: undefined | ((event: GestureResponderEvent) => void), 43 | onResponderStart: undefined | ((event: GestureResponderEvent) => void), 44 | onResponderRelease: undefined | ((event: GestureResponderEvent) => void), 45 | onStartShouldSetResponder: undefined | ((event: GestureResponderEvent) => void), 46 | needsTouch: boolean 47 | ) => { 48 | const [active, setActive] = React.useState(false) 49 | const touchStart = React.useMemo(() => needsTouch ? (event: GestureResponderEvent) => { 50 | if (onPressIn) onPressIn(event) 51 | else if (onResponderStart) onResponderStart(event) 52 | setActive(true) 53 | } : undefined, [needsTouch, onResponderStart, onPressIn]) 54 | const touchEnd = React.useMemo(() => needsTouch ? (event: GestureResponderEvent) => { 55 | if (onPressOut) onPressOut(event) 56 | else if (onResponderRelease) onResponderRelease(event) 57 | setActive(false) 58 | } : undefined, [needsTouch, onResponderRelease, onPressOut]) 59 | const grantTouch = React.useMemo(() => 60 | needsTouch ? onStartShouldSetResponder || (() => true) : undefined 61 | , [needsTouch, onStartShouldSetResponder] 62 | ) 63 | return { 64 | active, 65 | onPressIn: touchStart || onPressIn, 66 | onPressOut: touchEnd || onPressOut, 67 | onResponderStart: touchStart || onResponderStart, 68 | onResponderRelease: touchEnd || onResponderRelease, 69 | onStartShouldSetResponder: grantTouch || onStartShouldSetResponder 70 | } 71 | } 72 | 73 | export type FocusEventListener = TouchableHighlightProps['onFocus'] | TextInputProps['onFocus'] 74 | export type BlurEventListener = TouchableHighlightProps['onBlur'] | TextInputProps['onBlur'] 75 | /** Hook that will apply the style reserved for active state if needed */ 76 | export const useFocus = (onFocus: undefined | FocusEventListener, onBlur: undefined | BlurEventListener, needsFocus: boolean) => { 77 | const [focused, setFocused] = React.useState(false) 78 | const focusStart = React.useMemo(() => needsFocus ? (event: any) => { 79 | if (onFocus) onFocus(event) 80 | setFocused(true) 81 | } : undefined, [needsFocus, onFocus]) 82 | const focusStop = React.useMemo(() => needsFocus ? (event: any) => { 83 | if (onBlur) onBlur(event) 84 | setFocused(false) 85 | } : undefined, [needsFocus, onBlur]) 86 | return { focused, onFocus: focusStart || onFocus, onBlur: focusStop || onBlur } 87 | } 88 | 89 | /** Hook that will apply the style provided in the media queries */ 90 | export const useMediaQuery = (media: undefined | MediaQuery[], units: Units): Style | undefined => { 91 | const mediaStyle = React.useMemo(() => { 92 | if (media) { 93 | const context = createContext(units) 94 | const mediaStyles = media.map(m => m(context)).filter(m => !!m) as PartialStyle[] 95 | if (!mediaStyles.length) return 96 | const mq = {} as Style 97 | Object.assign(mq, ...mediaStyles) 98 | return mq 99 | } 100 | }, [media, units]) 101 | return mediaStyle 102 | } 103 | 104 | /** Hook that will measure the layout to handle styles that use % units */ 105 | export const useLayout = (onLayout: undefined | ((event: LayoutChangeEvent) => void), needsLayout: boolean) => { 106 | const [layout, setLayout] = React.useState({ width: 0, height: 0 }) 107 | // Prevent calling setState if the component is unmounted 108 | const unmounted = React.useRef(false) 109 | React.useEffect(() => () => { unmounted.current = true }, []) 110 | const updateLayout = React.useMemo(() => needsLayout ? (event: LayoutChangeEvent) => { 111 | if (unmounted.current) return 112 | if (onLayout) onLayout(event) 113 | const { width, height } = event.nativeEvent.layout 114 | setLayout(layout => layout.width === width && layout.height === height ? layout : { width, height }) 115 | } : undefined, [needsLayout, onLayout]) 116 | return { onLayout: updateLayout || onLayout, ...layout } 117 | } 118 | 119 | /** Apply the new fontSize to the component before we can calculate em units */ 120 | export const useFontSize = (fontSizeTarget: string | undefined, rem: number, em: number): { em: number } => { 121 | const [fontSize, fontUnit] = React.useMemo(() => fontSizeTarget === undefined ? [] : parseValue(fontSizeTarget), [fontSizeTarget]) 122 | const isRelative = fontUnit && ['rem', 'em', '%'].includes(fontUnit) 123 | const newSize = React.useMemo(() => { 124 | if (fontSize && isRelative) { 125 | const newSize = fontUnit === 'em' ? em * fontSize 126 | : fontUnit === 'rem' ? fontSize * rem 127 | : fontUnit === '%' ? em * (1 + fontSize / 100) 128 | : fontSize 129 | return newSize 130 | } 131 | else return fontSize || em 132 | }, [em, fontSize, fontUnit, isRelative, rem]) 133 | return { em: newSize } 134 | } 135 | -------------------------------------------------------------------------------- /src/generateHash.ts: -------------------------------------------------------------------------------- 1 | export default (value: string) => { 2 | let h = 0 3 | for (let i = 0; i < value.length; i++) h = Math.imul(31, h) + value.charCodeAt(i) | 0 4 | return h.toString(36) 5 | } 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as RN from 'react-native' 3 | import './polyfill' 4 | import styledComponent, { styledFlatList, styledSectionList, styledVirtualizedList } from './styleComponent' 5 | export { cssToRNStyle } from './cssToRN' 6 | export { SharedValue, FontSizeContext, RemContext, DefaultTheme } from './styleComponent' 7 | export * from './useTheme' 8 | 9 | const styled = }>(Component: React.ComponentType) => styledComponent(Component) 10 | 11 | styled.ActivityIndicator = styled(RN.ActivityIndicator) 12 | styled.DrawerLayoutAndroid = styled(RN.DrawerLayoutAndroid) 13 | styled.Image = styled(RN.Image) 14 | styled.ImageBackground = styled(RN.ImageBackground) 15 | styled.KeyboardAvoidingView = styled(RN.KeyboardAvoidingView) 16 | styled.Modal = styled(RN.Modal) 17 | styled.NavigatorIOS = styled(RN.NavigatorIOS) 18 | styled.ScrollView = styled(RN.ScrollView) 19 | styled.SnapshotViewIOS = styled(RN.SnapshotViewIOS) 20 | styled.Switch = styled(RN.Switch) 21 | styled.RecyclerViewBackedScrollView = styled(RN.RecyclerViewBackedScrollView) 22 | styled.RefreshControl = styled(RN.RefreshControl) 23 | styled.SafeAreaView = styled(RN.SafeAreaView) 24 | styled.Text = styled(RN.Text) 25 | styled.TextInput = styled(RN.TextInput) 26 | styled.TouchableHighlight = styled(RN.TouchableHighlight) 27 | styled.TouchableNativeFeedback = styled(RN.TouchableNativeFeedback) 28 | styled.TouchableOpacity = styled(RN.TouchableOpacity) 29 | styled.TouchableWithoutFeedback = styled(RN.TouchableWithoutFeedback) 30 | styled.View = styled(RN.View) 31 | styled.FlatList = styledFlatList 32 | styled.SectionList = styledSectionList 33 | styled.VirtualizedList = styledVirtualizedList 34 | 35 | export default styled 36 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | /** polyfill for Node < 12 */ 2 | // eslint-disable-next-line no-extend-native 3 | if (!Array.prototype.flat) Array.prototype.flat = function (depth?: number) { return flat(this as unknown as A, depth) } 4 | // eslint-disable-next-line no-extend-native 5 | if (!String.prototype.matchAll) String.prototype.matchAll = function (regex: RegExp) { return matchAll(this as unknown as string, regex) } 6 | 7 | /** polyfill for Node < 12 */ 8 | function flat (array: A, depth = 1): FlatArray[] { 9 | if (!depth || depth < 1 || !Array.isArray(array)) return array as unknown as FlatArray[] 10 | return array.reduce((result, current) => result.concat(flat(current as unknown as unknown[], depth - 1)), 11 | [] 12 | ) as FlatArray[] 13 | } 14 | 15 | function matchAll (str: string, regex: RegExp): IterableIterator { 16 | const matches = [] 17 | let groups 18 | // eslint-disable-next-line no-cond-assign 19 | while (groups = regex.exec(str)) { 20 | matches.push(groups) 21 | } 22 | return matches[Symbol.iterator]() 23 | } 24 | -------------------------------------------------------------------------------- /src/react-native/index.ts: -------------------------------------------------------------------------------- 1 | export { Dimensions, Platform, PixelRatio } from 'react-native' 2 | -------------------------------------------------------------------------------- /src/react-native/index.web.ts: -------------------------------------------------------------------------------- 1 | // We use those fallbacks to make cssToRN not depend on react-native 2 | 3 | export const Platform = { 4 | OS: 'web' 5 | } 6 | 7 | export const Dimensions = { 8 | get: (dimension: 'width' | 'height') => dimension === 'width' ? window.innerWidth : window.innerHeight 9 | } 10 | 11 | export const PixelRatio = { 12 | getPixelSizeForLayoutSize: (dp: number) => dp * window.devicePixelRatio 13 | } 14 | -------------------------------------------------------------------------------- /src/rnToCss.ts: -------------------------------------------------------------------------------- 1 | import { CompleteStyle } from './types' 2 | 3 | const rnToCSS = (rn: Partial) => 4 | Object.entries(rn) 5 | .map(([key, value]) => `${camelToKebab(key)}: ${convertValue(value)};`) 6 | .join('\n') 7 | 8 | const camelToKebab = (str: string) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() 9 | const convertValue = (value: unknown) => isNaN(value as number) ? value : (value + 'px') 10 | 11 | export default rnToCSS 12 | -------------------------------------------------------------------------------- /src/styleComponent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable react/display-name */ 4 | import React, { MouseEvent } from 'react' 5 | import { FlatList, FlatListProps, Platform, SectionList, SectionListProps, StyleProp, StyleSheet, TouchableHighlightProps, ViewProps, ViewStyle, VirtualizedList, VirtualizedListProps } from 'react-native' 6 | import convertStyle from './convertStyle' 7 | import cssToStyle from './cssToRN' 8 | import { useFontSize, useHover, useLayout, useScreenSize, useMediaQuery, useActive, useFocus, FocusEventListener, BlurEventListener } from './features' 9 | import type { AnyStyle, CompleteStyle, Style, Units } from './types' 10 | import generateHash from './generateHash' 11 | import rnToCSS from './rnToCss' 12 | 13 | export const defaultUnits: Units = { em: 16, vw: 1, vh: 1, vmin: 1, vmax: 1, rem: 16, px: 1, pt: 72 / 96, in: 96, pc: 9, cm: 96 / 2.54, mm: 96 / 25.4 } 14 | export const RemContext = React.createContext(defaultUnits.rem) 15 | export const FontSizeContext = React.createContext(defaultUnits.em) 16 | 17 | // We use this to share value within the component (Theme, Translation, whatever) 18 | export const SharedValue = React.createContext(undefined) 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 21 | export interface DefaultTheme {} 22 | 23 | type Primitive = number | string | null | undefined | boolean | CompleteStyle 24 | type Functs = (arg: T & { rnCSS?: string; shared: unknown, theme: DefaultTheme }) => Primitive 25 | type OptionalProps = { 26 | rnCSS?: `${string};`; 27 | onFocus?: FocusEventListener; 28 | onBlur?: BlurEventListener; 29 | onPressIn?: TouchableHighlightProps['onPressIn']; 30 | onPressOut?: TouchableHighlightProps['onPressOut']; 31 | onResponderStart?: ViewProps['onResponderStart']; 32 | onResponderRelease?: ViewProps['onResponderRelease']; 33 | onStartShouldSetResponder?: ViewProps['onStartShouldSetResponder']; 34 | onMouseEnter?: (event: MouseEvent) => void; 35 | onMouseLeave?: (event: MouseEvent) => void; 36 | onLayout?: ViewProps['onLayout']; 37 | children?: React.ReactNode; 38 | } 39 | 40 | /** Converts the tagged template string into a css string */ 41 | function buildCSSString (chunks: TemplateStringsArray, functs: (Primitive | Functs)[], props: T, shared: unknown) { 42 | let computedString = chunks 43 | // Evaluate the chunks from the tagged template 44 | .map((chunk, i) => ([chunk, (functs[i] instanceof Function) ? (functs[i] as Functs)({ shared, theme: (shared as DefaultTheme), ...props }) : functs[i]])) 45 | .flat() 46 | // Convert the objects to string if the result is not a primitive 47 | .map(chunk => typeof chunk === 'object' ? rnToCSS(chunk as Partial) : chunk) 48 | .join('') 49 | if (props.rnCSS) computedString += props.rnCSS.replace(/=/gm, ':') + ';' 50 | return computedString 51 | } 52 | 53 | const styleMap: Record = {} 54 | function getStyle (hash: string, style: T) { 55 | const styleInfo = styleMap[hash] 56 | if (styleInfo) { 57 | styleInfo.usage++ 58 | return styleInfo.style as T 59 | } 60 | else { 61 | const sheet = StyleSheet.create({ [hash]: style }) 62 | return (styleMap[hash] = { style: sheet[hash], usage: 1 }).style as T 63 | } 64 | } 65 | function removeStyle (hash: string) { 66 | styleMap[hash].usage-- 67 | if (styleMap[hash].usage <= 0) delete styleMap[hash] 68 | } 69 | 70 | const styled = }, Props extends InitialProps & OptionalProps = InitialProps & OptionalProps>(Component: React.ComponentType) => { 71 | const styledComponent = (chunks: TemplateStringsArray, ...functs: (Primitive | Functs)[]) => { 72 | const ForwardRefComponent = React.forwardRef((props: S & Props, ref) => { 73 | const rem = React.useContext(RemContext) 74 | const shared = React.useContext(SharedValue) 75 | // Build the css string with the context 76 | const css = React.useMemo(() => buildCSSString(chunks, functs, props, shared), [props, shared]) 77 | // Store the style in RN format 78 | const rnStyle = React.useMemo(() => cssToStyle(css), [css]) 79 | 80 | const { needsLayout, needsHover, needsFocus, needsTouch } = React.useMemo(() => ({ 81 | // needsFontSize: !!css.match(/\b(\d+)(\.\d+)?em\b/) 82 | // needsScreenSize: !!css.match(/\b(\d+)(\.\d*)?v([hw]|min|max)\b/) || !!rnStyle.media, 83 | needsLayout: !!css.match(/\d%/), 84 | needsHover: !!rnStyle.hover, 85 | needsTouch: !!rnStyle.active, 86 | needsFocus: !!rnStyle.focus 87 | }), [css, rnStyle.active, rnStyle.focus, rnStyle.hover]) 88 | 89 | // Handle hover 90 | const { onMouseEnter, onMouseLeave, hover } = useHover(props.onMouseEnter, props.onMouseLeave, needsHover) 91 | // Handle active 92 | const { onPressIn, onPressOut, onStartShouldSetResponder, onResponderRelease, onResponderStart, active } = useActive( 93 | props.onPressIn, props.onPressOut, 94 | props.onResponderStart, props.onResponderRelease, props.onStartShouldSetResponder, 95 | needsTouch 96 | ) 97 | // Handle focus 98 | const { onFocus, onBlur, focused } = useFocus(props.onFocus, props.onBlur, needsFocus) 99 | const tempStyle = React.useMemo