}`
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