Skip to content

Commit f7b0d13

Browse files
committed
v0.0.9
1 parent 26f3cdb commit f7b0d13

21 files changed

+11201
-3
lines changed

.eslintrc.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
root: true,
3+
extends: ['@spaceship/eslint-config/native'],
4+
rules: {
5+
// false positives on types
6+
'@typescript-eslint/no-unused-vars': 'off',
7+
},
8+
};

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
node_modules

.prettierrc

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"printWidth": 100,
3+
"singleQuote": true,
4+
"arrowParens": "avoid",
5+
"jsxBracketSameLine": false
6+
}

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@pulsar/core
2+
---
3+
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#)
5+
6+
> Handy style utilities for React Native and React Native Web.
7+
8+
![@pulsar/ui logo](assets/logo.svg)
9+
10+
## Install
11+
12+
```sh
13+
yarn add @pulsar/core
14+
```

assets/logo.svg

+1
Loading

babel.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: ['module:metro-react-native-babel-preset'],
3+
};

jest.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
preset: 'react-native',
3+
moduleFileExtensions: ['js', 'ts'],
4+
transformIgnorePatterns: ['node_modules/(?!react-native)/'],
5+
testRegex: '\\.test\\.ts$',
6+
};

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pulsar/core",
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"main": "dist/index.js",
55
"types": "dist/index.d.ts",
66
"license": "MIT",

src/DynamicStyleSheet.ts

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
import { Appearance, ColorSchemeName, Dimensions, ScaledSize, StyleSheet } from 'react-native';
3+
import { Theme, NamedStyles, Props } from './types';
4+
import { flattenVariants, hasVariants, compileVariants } from './variants';
5+
import {
6+
compileMediaQueries,
7+
flattenMediaQueries,
8+
getMediaQueryDetailsFromKey,
9+
hasMediaQuery,
10+
isBoundaryIntersected,
11+
isMediaQueryKey,
12+
} from './mediaQueries';
13+
14+
type ThemeFunc<T extends NamedStyles<T>> = ((theme: Theme) => T) | T;
15+
type ThemeHook<T extends NamedStyles<T>> = (props?: Props) => NamedStyles<T>;
16+
17+
/**
18+
* The sacred magic.
19+
*/
20+
export class DynamicStyleSheet {
21+
public static lightTheme: Theme = {};
22+
public static darkTheme: Theme = {};
23+
24+
public static create<T extends NamedStyles<T> | NamedStyles<any>>(
25+
themeFunc: ThemeFunc<T>
26+
): ThemeHook<T> {
27+
const cache = new Map<ColorSchemeName, T>();
28+
const colorScheme = Appearance.getColorScheme();
29+
// @todo Optimize this. No need to use dimensions if there are no media queries.
30+
const dimensions = Dimensions.get('window');
31+
32+
const cacheTheme = (scheme: ColorSchemeName) =>
33+
cache.set(
34+
scheme,
35+
compileStyles(
36+
themeFunc,
37+
scheme === 'dark' ? DynamicStyleSheet.darkTheme : DynamicStyleSheet.lightTheme
38+
)
39+
);
40+
41+
cacheTheme(colorScheme);
42+
43+
const mediaQueryKeys = Object.keys(cache.get(colorScheme) as T)
44+
.filter(isMediaQueryKey)
45+
.map(getMediaQueryDetailsFromKey);
46+
47+
/**
48+
* Custom React Hook for subscriptions and components' updating.
49+
*/
50+
return (props: Props = {}) => {
51+
const [_, forceUpdate] = useState({});
52+
const schemeRef = useRef(colorScheme);
53+
const dimensionsRef = useRef(dimensions);
54+
const stylesRef = useRef(cache.get(colorScheme) as T);
55+
56+
useEffect(() => {
57+
const appearanceListener: Appearance.AppearanceListener = ({ colorScheme: newScheme }) => {
58+
if (newScheme !== schemeRef.current) {
59+
schemeRef.current = newScheme;
60+
cacheTheme(newScheme);
61+
forceUpdate({});
62+
}
63+
};
64+
65+
const dimensionsListener = ({ window }: { window: ScaledSize }) => {
66+
mediaQueryKeys.forEach(({ type, value }) => {
67+
if (isBoundaryIntersected(type, value, dimensionsRef.current, window)) {
68+
forceUpdate({});
69+
return;
70+
}
71+
});
72+
73+
dimensionsRef.current = window;
74+
};
75+
76+
Appearance.addChangeListener(appearanceListener);
77+
78+
if (mediaQueryKeys.length > 0) {
79+
Dimensions.addEventListener('change', dimensionsListener);
80+
}
81+
82+
return () => {
83+
Appearance.removeChangeListener(appearanceListener);
84+
Dimensions.removeEventListener('change', dimensionsListener);
85+
};
86+
}, []);
87+
88+
return compileMediaQueries(compileVariants(stylesRef.current, props));
89+
};
90+
}
91+
}
92+
93+
/**
94+
* Apply theme function and flatten variants.
95+
*/
96+
const compileStyles = <T extends NamedStyles<T>>(styles: ThemeFunc<T>, theme: Theme): T => {
97+
const styleObject = typeof styles === 'function' ? styles(theme) : styles;
98+
99+
for (let key in styleObject) {
100+
if (styleObject.hasOwnProperty(key)) {
101+
if (hasVariants(styleObject[key])) {
102+
flattenVariants(styleObject, key);
103+
}
104+
if (hasMediaQuery(styleObject[key])) {
105+
flattenMediaQueries(styleObject, key);
106+
}
107+
}
108+
}
109+
110+
return StyleSheet.create(styleObject);
111+
};

0 commit comments

Comments
 (0)