Initial library setup

This commit is contained in:
Eric Bailey
2023-10-20 15:56:43 -04:00
parent 5fa43530f6
commit 87809b4eac
8 changed files with 1241 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import {View, Text as RNText} from 'react-native'
import {createSystem} from './lib/system'
import {light, dark} from './themes'
import {web} from './lib/utils'
export * from './lib/utils'
const {
ThemeProvider,
useTheme,
useTokens,
useBreakpoints,
useStyle,
useStyles,
styled,
} = createSystem({
light,
dark,
})
export {
ThemeProvider,
useTheme,
useTokens,
useBreakpoints,
useStyle,
useStyles,
styled,
}
export const Box = styled(View)
export const Text = styled(RNText, {
color: 'l8',
fontSize: 's',
})
/**
* @see https://necolas.github.io/react-native-web/docs/accessibility/#semantic-html
* @see https://docs.expo.dev/develop/user-interface/fonts/
*/
export const H1 = styled(RNText, {
color: 'l8',
fontSize: 'l',
gtMobile: {
fontSize: 'xl',
},
...web({
role: 'heading',
'aria-level': 1,
}),
})
export const H2 = styled(RNText, {
color: 'l8',
fontSize: 'm',
gtMobile: {
fontSize: 'l',
},
...web({
role: 'heading',
'aria-level': 2,
}),
})
export const H3 = styled(RNText, {
color: 'l8',
fontSize: 'm',
...web({
role: 'heading',
'aria-level': 3,
}),
})
export const H4 = styled(RNText, {
color: 'l8',
fontSize: 's',
...web({
role: 'heading',
'aria-level': 4,
}),
})
export const H5 = styled(RNText, {
color: 'l8',
fontSize: 'xs',
...web({
role: 'heading',
'aria-level': 5,
}),
})
export const H6 = styled(RNText, {
color: 'l8',
fontSize: 'xxs',
...web({
role: 'heading',
'aria-level': 6,
}),
})
export const P = styled(RNText, {
color: 'l8',
fontSize: 's',
...web({
role: 'paragraph',
}),
})
+147
View File
@@ -0,0 +1,147 @@
import React from 'react'
import type {ViewProps} from 'react-native'
import TestRenderer from 'react-test-renderer'
import {jest, test, expect} from '@jest/globals'
import {createTheme} from '../theme'
import {createSystem} from '../system'
jest.mock('react-native', () => ({
Dimensions: {
get: () => ({width: 0, height: 0}),
addEventListener() {},
},
StyleSheet: {
create(style: any) {
return style
},
},
}))
const theme = createTheme({
tokens: {
space: {
s: 4,
m: 8,
l: 16,
},
color: {
theme: 'tomato',
text: 'black',
},
},
properties: {
px: ['paddingLeft', 'paddingRight'],
},
macros: {
caps: (_: boolean) => ({textTransform: 'uppercase'}),
},
breakpoints: {
gtPhone: 640,
gtTablet: 1024,
},
})
const {ThemeProvider, useTheme, useStyle, useStyles, styled} = createSystem({
theme,
})
function Root(props: React.PropsWithChildren<{}>) {
return <ThemeProvider theme="theme">{props.children}</ThemeProvider>
}
test('useTheme', () => {
function Component() {
const theme = useTheme()
return <>{JSON.stringify(theme)}</>
}
const dom = TestRenderer.create(
<Root>
<Component />
</Root>,
)
const json = JSON.parse(dom.toJSON() as unknown as string)
expect(json.themeName).toEqual('theme')
expect(json.theme).toBeTruthy()
expect(Object.keys(json.themes)).toEqual(['theme'])
})
test('useStyle', () => {
function Component() {
const style = useStyle({color: 'red'})
return <>{JSON.stringify(style)}</>
}
const dom = TestRenderer.create(
<Root>
<Component />
</Root>,
)
const json = JSON.parse(dom.toJSON() as unknown as string)
expect(json.color).toEqual('red')
})
test('useStyles', () => {
function Component() {
const {text} = useStyles({
text: {color: 'red'},
})
return <>{JSON.stringify(text)}</>
}
const dom = TestRenderer.create(
<Root>
<Component />
</Root>,
)
const json = JSON.parse(dom.toJSON() as unknown as string)
expect(json.color).toEqual('red')
})
test('styled', () => {
function Component(props: ViewProps) {
return <>{JSON.stringify(props)}</>
}
const Box = styled(Component, {
color: 'theme',
})
const dom = TestRenderer.create(
<Root>
<Box color="red" id="foo" style={{padding: 10}} />
</Root>,
)
const json = JSON.parse(dom.toJSON() as unknown as string)
expect(json.style).toEqual([{color: 'red'}, {padding: 10}])
expect(json.id).toEqual('foo')
})
test('types', () => {
function Component(props: ViewProps & {foo: boolean}) {
// @ts-expect-error
const {foo} = useStyles({
text: {color: 'red'},
})
// just need a way to use `foo`
return <React.Fragment key={foo}>{JSON.stringify(props)}</React.Fragment>
}
const Box = styled(Component, {
id: true,
// @ts-expect-error
color: true,
})
TestRenderer.create(
<Root>
{/* @ts-expect-error */}
<Box color="red" id="foo" style={{padding: 10}}>
<Component foo />
</Box>
</Root>,
)
})
+278
View File
@@ -0,0 +1,278 @@
import type {ViewProps} from 'react-native'
import {describe, test, expect} from '@jest/globals'
import {createTheme} from '../theme'
const theme = createTheme({
tokens: {
space: {
s: 4,
m: 8,
l: 16,
},
color: {
theme: 'tomato',
text: 'black',
},
},
properties: {
px: ['paddingLeft', 'paddingRight'],
},
macros: {
caps: (_: boolean) => ({textTransform: 'uppercase'}),
},
breakpoints: {
gtPhone: 640,
gtTablet: 1024,
},
})
describe('non-style props', () => {
test('e.g. className', () => {
const {styles, props} = theme.style<ViewProps>(
{
id: 'foo',
},
[],
)
expect(styles).toEqual({})
expect(props).toEqual({
id: 'foo',
})
})
})
describe('properties', () => {
test('standard', () => {
const {styles} = theme.style(
{
color: 'blue',
},
[],
)
expect(styles).toEqual({
color: 'blue',
})
})
test('custom', () => {
const {styles} = theme.style(
{
px: 20,
},
[],
)
expect(styles).toEqual({
paddingLeft: 20,
paddingRight: 20,
})
})
test('undefined values', () => {
const {styles} = theme.style(
{
px: undefined,
},
[],
)
expect(styles).toEqual({})
})
})
describe('tokens', () => {
test('match', () => {
const {styles} = theme.style(
{
color: 'theme',
paddingTop: 's',
},
[],
)
expect(styles).toEqual({
color: 'tomato',
paddingTop: 4,
})
})
test('no match', () => {
const {styles} = theme.style(
{
color: 'blue',
paddingTop: 20,
},
[],
)
expect(styles).toEqual({
color: 'blue',
paddingTop: 20,
})
})
})
describe('macros', () => {
test('truthy', () => {
const {styles} = theme.style(
{
caps: true,
},
[],
)
expect(styles).toEqual({
textTransform: 'uppercase',
})
})
test('falsy', () => {
const {styles} = theme.style(
{
caps: false,
},
[],
)
expect(styles).toEqual({})
})
})
describe('breakpoints', () => {
test('applies', () => {
const {styles} = theme.style(
{
paddingTop: 's',
gtPhone: {
paddingTop: 'm',
},
},
['gtPhone'],
)
expect(styles).toEqual({
paddingTop: 8,
})
})
test('not applies', () => {
const {styles} = theme.style(
{
paddingTop: 's',
gtPhone: {
paddingTop: 'm',
},
},
[],
)
expect(styles).toEqual({
paddingTop: 4,
})
})
test('applies in order, asc', () => {
const {styles} = theme.style(
{
paddingTop: 's',
gtPhone: {
paddingTop: 'm',
},
gtTablet: {
paddingTop: 'l',
},
},
['gtTablet', 'gtPhone'],
)
expect(styles).toEqual({
paddingTop: 16,
})
})
test('applies in order even if defined in reverse', () => {
const theme = createTheme({
tokens: {
space: {
s: 4,
},
},
breakpoints: {
// reverse order
gtTablet: 1024,
gtPhone: 640,
},
})
const {styles} = theme.style(
{
paddingTop: 20,
gtTablet: {
paddingTop: 30,
},
gtPhone: {
paddingTop: 25,
},
},
['gtTablet', 'gtPhone'],
)
expect(styles).toEqual({
paddingTop: 30,
})
})
})
describe('utils', () => {
test('getActiveBreakpoints', () => {
const breakpoints = theme.getActiveBreakpoints({width: 1000})
expect(breakpoints).toEqual({
current: 'gtPhone',
active: ['gtPhone'],
})
})
})
describe('types', () => {
const theme = createTheme({
tokens: {
space: {
s: 4,
},
color: {
theme: 'tomato',
},
},
properties: {
px: ['paddingLeft', 'paddingRight'],
},
macros: {
caps: (_: boolean) => ({textTransform: 'uppercase'}),
},
breakpoints: {
gtPhone: 640,
},
})
test('style', () => {
theme.style(
{
// @ts-expect-error
color: false,
px: 20,
// @ts-expect-error
caps: 1,
},
[],
)
theme.style(
{},
// @ts-expect-error
['taco'],
)
})
})
+24
View File
@@ -0,0 +1,24 @@
import {test, expect} from '@jest/globals'
import * as utils from '../utils'
test(`web`, async () => {
const result = utils.web('foo')
expect(result).toBe(undefined)
})
test(`ios`, async () => {
const result = utils.ios('foo')
// TODO will fail in CI
expect(result).toBe('foo')
})
test(`android`, async () => {
const result = utils.android('foo')
expect(result).toBe(undefined)
})
test(`native`, async () => {
const result = utils.native('foo')
expect(result).toBe('foo')
})
+158
View File
@@ -0,0 +1,158 @@
import React from 'react'
import {Dimensions} from 'react-native'
import {
Theme,
ResponsiveStyles,
Tokens,
Properties,
Macros,
Breakpoints,
} from './theme'
type ThemeConfig<
T extends Tokens,
P extends Properties,
M extends Macros<T>,
B extends Breakpoints,
> = {
[key: string]: Theme<T, P, M, B>
}
export function createSystem<
T extends Tokens,
P extends Properties,
M extends Macros<T>,
B extends Breakpoints,
>(themes: ThemeConfig<T, P, M, B>) {
const defaultTheme = Object.values(themes)[0]
type SystemTheme = Theme<T, P, M, B>
type ResponsiveStyleProps = ResponsiveStyles<T, P, M, B>
type StylesAndProps = Parameters<typeof defaultTheme.style<any>>[0]
type DebugProps = {
/**
* Debug mode will log the styles and props to the console
*/
debug?: boolean
}
type ThemeName = keyof typeof themes
const Context = React.createContext<{
themeName: ThemeName
theme: SystemTheme
themes: {
[key in ThemeName]: SystemTheme
}
}>({
themeName: Object.keys(themes)[0],
theme: defaultTheme,
themes,
})
const ThemeProvider = ({
children,
theme,
}: React.PropsWithChildren<{theme: ThemeName}>) => (
<Context.Provider
value={{
themeName: theme,
theme: themes[theme],
themes,
}}>
{children}
</Context.Provider>
)
function useTheme() {
return React.useContext(Context)
}
function useTokens() {
return React.useContext(Context).theme.config.tokens
}
function useBreakpoints() {
const {theme} = useTheme()
const [breakpoints, setBreakpoints] = React.useState(
theme.getActiveBreakpoints({width: Dimensions.get('window').width}),
)
React.useEffect(() => {
const listener = Dimensions.addEventListener('change', ({window}) => {
const bp = theme.getActiveBreakpoints({width: window.width})
if (bp.current !== breakpoints.current) setBreakpoints(bp)
})
return () => {
listener.remove()
}
}, [breakpoints, theme])
return breakpoints
}
function useStyle(props: ResponsiveStyleProps) {
const {theme} = useTheme()
const breakpoints = useBreakpoints()
return theme.style(props as StylesAndProps, breakpoints.active).styles
}
function useStyles<O extends Record<string, ResponsiveStyleProps>>(
styles: O,
): {
[Name in keyof O]: ReturnType<SystemTheme['style']>['styles']
} {
const {theme} = useTheme()
const breakpoints = useBreakpoints()
return React.useMemo(() => {
return Object.entries(styles).reduce((acc, [key, style]) => {
acc[key as keyof O] = theme.style(
style as StylesAndProps,
breakpoints.active,
).styles
return acc
}, {} as {[Name in keyof O]: ReturnType<SystemTheme['style']>['styles']})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [styles, breakpoints.current, theme])
}
function styled<Props extends Record<string, any>>(
Component: React.ComponentType<Props>,
defaultProps: ResponsiveStyleProps = {},
) {
const comp = React.forwardRef<
Props,
Props &
ResponsiveStyleProps &
DebugProps & {children?: React.ReactNode | React.ReactNodeArray}
>((props, ref) => {
const {theme} = useTheme()
const breakpoints = useBreakpoints()
const {styles, props: rest} = theme.style<Props>(
{
...defaultProps,
...props,
},
breakpoints.active,
)
if (props.debug) console.log({styles, props: rest})
return <Component {...rest} style={[styles, rest.style]} ref={ref} />
})
comp.displayName = Component.displayName || 'NovaComponent'
return comp
}
return {
Context,
ThemeProvider,
useTheme,
useTokens,
useBreakpoints,
useStyle,
useStyles,
styled,
}
}
+394
View File
@@ -0,0 +1,394 @@
import type {
ViewStyle,
TextStyle,
ImageStyle,
DimensionValue,
ColorValue,
} from 'react-native'
import {StyleSheet} from 'react-native'
type StyleObject = ViewStyle & TextStyle & ImageStyle
type StyleObjectProperties = keyof StyleObject
type ColorProperties =
| 'color'
| 'backgroundColor'
| 'borderColor'
| 'borderTopColor'
| 'borderRightColor'
| 'borderBottomColor'
| 'borderLeftColor'
type DimensionProperties =
| 'padding'
| 'paddingTop'
| 'paddingBottom'
| 'paddingLeft'
| 'paddingRight'
| 'paddingVertical'
| 'paddingHorizontal'
| 'margin'
| 'marginTop'
| 'marginBottom'
| 'marginLeft'
| 'marginRight'
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'gap'
| 'rowGap'
| 'columnGap'
export type Tokens = {
space?: DimensionValue[] | Record<string, DimensionValue>
} & {
[Property in StyleObjectProperties]?: Record<
string,
Required<StyleObject>[Property]
>
}
export type Properties = Record<string, Readonly<StyleObjectProperties[]>>
export type Macros<T extends Tokens> = Record<
string,
(value: any, tokens: T) => StyleObject
>
export type Breakpoints = {
[Breakpoint: string]: number
}
export type Styles<
T extends Tokens,
P extends Properties,
M extends Macros<T>,
> = {
[Property in ColorProperties]?:
| keyof T['color']
| Omit<ColorValue, keyof T['color']>
} & {
[Property in DimensionProperties]?: keyof T['space'] | DimensionValue
} & {
[Property in Exclude<
StyleObjectProperties,
ColorProperties | DimensionProperties
>]?: keyof T[Property] | StyleObject[Property]
} & {
// this empty P is to avoid circular references back to Properties type
[Property in keyof P]?: Styles<T, {}, M>[P[Property][0]]
} & {
[Property in keyof M]?: M[Property] extends (
value: infer Value,
tokens: T,
) => StyleObject
? Value
: never
}
export type ResponsiveStyles<
T extends Tokens,
P extends Properties,
M extends Macros<T>,
B extends Breakpoints,
> =
| Styles<T, P, M>
| {
[Breakpoint in keyof B]?: Styles<T, P, M>
}
export type Theme<
T extends Tokens,
P extends Properties,
M extends Macros<T>,
B extends Breakpoints,
> = {
config: {
tokens: T
properties: P
macros: M
breakpoints: B
}
style: <Props>(
props: ResponsiveStyles<T, P, M, B> & Omit<Props, keyof P>,
breakpoints?: (keyof B)[],
) => {
styles: StyleObject
props: Props
}
getActiveBreakpoints: ({width}: {width: number}) => {
active: (keyof B)[]
current: keyof B
}
}
const specialTokenMapping = {
color: 'color',
backgroundColor: 'color',
borderColor: 'color',
borderTopColor: 'color',
borderRightColor: 'color',
borderBottomColor: 'color',
borderLeftColor: 'color',
padding: 'space',
paddingTop: 'space',
paddingBottom: 'space',
paddingLeft: 'space',
paddingRight: 'space',
paddingVertical: 'space',
paddingHorizontal: 'space',
margin: 'space',
marginTop: 'space',
marginBottom: 'space',
marginLeft: 'space',
marginRight: 'space',
top: 'space',
bottom: 'space',
left: 'space',
right: 'space',
gap: 'space',
rowGap: 'space',
columnGap: 'space',
}
const propertyMapping: Properties = {
// FlexStyle
alignContent: ['alignContent'],
alignItems: ['alignItems'],
alignSelf: ['alignSelf'],
aspectRatio: ['aspectRatio'],
borderBottomWidth: ['borderBottomWidth'],
borderEndWidth: ['borderEndWidth'],
borderLeftWidth: ['borderLeftWidth'],
borderRightWidth: ['borderRightWidth'],
borderStartWidth: ['borderStartWidth'],
borderTopWidth: ['borderTopWidth'],
borderWidth: ['borderWidth'],
bottom: ['bottom'],
display: ['display'],
end: ['end'],
flex: ['flex'],
flexBasis: ['flexBasis'],
flexDirection: ['flexDirection'],
rowGap: ['rowGap'],
gap: ['gap'],
columnGap: ['columnGap'],
flexGrow: ['flexGrow'],
flexShrink: ['flexShrink'],
flexWrap: ['flexWrap'],
height: ['height'],
justifyContent: ['justifyContent'],
left: ['left'],
margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
marginBottom: ['marginBottom'],
marginEnd: ['marginEnd'],
marginHorizontal: ['marginLeft', 'marginRight'],
marginLeft: ['marginLeft'],
marginRight: ['marginRight'],
marginStart: ['marginStart'],
marginTop: ['marginTop'],
marginVertical: ['marginTop', 'marginBottom'],
maxHeight: ['maxHeight'],
maxWidth: ['maxWidth'],
overflow: ['overflow'],
padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
paddingBottom: ['paddingBottom'],
paddingEnd: ['paddingEnd'],
paddingHorizontal: ['paddingLeft', 'paddingRight'],
paddingLeft: ['paddingLeft'],
paddingRight: ['paddingRight'],
paddingStart: ['paddingStart'],
paddingTop: ['paddingTop'],
paddingVertical: ['paddingTop', 'paddingBottom'],
position: ['position'],
right: ['right'],
start: ['start'],
top: ['top'],
width: ['width'],
zIndex: ['zIndex'],
direction: ['direction'],
// ShadowStyle
shadowColor: ['shadowColor'],
shadowOffset: ['shadowOffset'],
shadowOpacity: ['shadowOpacity'],
shadowRadius: ['shadowRadius'],
// TransformStyle
transform: ['transform'],
transformMatrix: ['transformMatrix'],
rotation: ['rotation'],
scaleX: ['scaleX'],
scaleY: ['scaleY'],
translateX: ['translateX'],
translateY: ['translateY'],
// ViewStyle
backfaceVisibility: ['backfaceVisibility'],
backgroundColor: ['backgroundColor'],
borderBlockColor: ['borderBlockColor'],
borderBlockEndColor: ['borderBlockEndColor'],
borderBlockStartColor: ['borderBlockStartColor'],
borerBottomColor: ['borderBottomColor'],
borderBottomEndRadius: ['borderBottomEndRadius'],
borderBottomLeftRadius: ['borderBottomLeftRadius'],
borderBottomRightRadius: ['borderBottomRightRadius'],
borderBottomStartRadius: ['borderBottomStartRadius'],
borderColor: ['borderColor'],
borderCurve: ['borderCurve'],
borderEndColor: ['borderEndColor'],
borderEndEndRadius: ['borderEndEndRadius'],
borderEndStartRadius: ['borderEndStartRadius'],
borderLeftColor: ['borderLeftColor'],
borderRadius: ['borderRadius'],
borderRightColor: ['borderRightColor'],
borderStartColor: ['borderStartColor'],
borderStartEndRadius: ['borderStartEndRadius'],
borderStartStartRadius: ['borderStartStartRadius'],
borderStyle: ['borderStyle'],
borderTopColor: ['borderTopColor'],
borderTopEndRadius: ['borderTopEndRadius'],
borderTopLeftRadius: ['borderTopLeftRadius'],
borderTopRightRadius: ['borderTopRightRadius'],
borderTopStartRadius: ['borderTopStartRadius'],
opacity: ['opacity'],
elevation: ['elevation'],
pointerEvents: ['pointerEvents'],
// TextStyleIOS
fontVariant: ['fontVariant'],
textDecorationColor: ['textDecorationColor'],
textDecorationStyle: ['textDecorationStyle'],
writingDirection: ['writingDirection'],
// TextStyleAndroid
textAlignVertical: ['textAlignVertical'],
verticalAlign: ['verticalAlign'],
includeFontPadding: ['includeFontPadding'],
// TextStyle
color: ['color'],
fontFamily: ['fontFamily'],
fontSize: ['fontSize'],
fontStyle: ['fontStyle'],
fontWeight: ['fontWeight'],
letterSpacing: ['letterSpacing'],
lineHeight: ['lineHeight'],
textAlign: ['textAlign'],
textDecorationLine: ['textDecorationLine'],
textShadowColor: ['textShadowColor'],
textShadowOffset: ['textShadowOffset'],
textTransform: ['textTransform'],
// ImageStyle
resizeMode: ['resizeMode'],
overlayColor: ['overlayColor'],
tintColor: ['tintColor'],
objectFit: ['objectFit'],
}
export const createTheme = <
T extends Tokens,
P extends Properties,
M extends Macros<T>,
B extends Breakpoints,
>({
tokens,
properties: userProperties = {},
macros: userMacros = {},
breakpoints: userBreakpoints = {},
}: {
tokens: T
properties?: Partial<P>
macros?: Partial<M>
breakpoints?: Partial<B>
}): Theme<T, P, M, B> => {
const properties = Object.assign({}, propertyMapping, userProperties) as P
const macros = userMacros as M
const breakpoints = Object.entries(userBreakpoints)
.sort(([_a, a], [_b, b]) => {
return a - b
})
.reduce((breakpoints, [key, value]) => {
breakpoints[key as keyof B] = value
return breakpoints
}, {} as B)
type InnerTheme = Theme<
typeof tokens,
typeof properties,
typeof macros,
typeof breakpoints
>
function _applyStyleProperty(styles: StyleObject, prop: string, value: any) {
for (const _prop of properties[prop]) {
// @ts-ignore no index sig, it's fine
const t = specialTokenMapping[_prop]
? // @ts-ignore no index sig, it's fine
tokens[specialTokenMapping[_prop]]
: tokens[_prop]
styles[_prop] = t?.[value] || value
}
}
const style: InnerTheme['style'] = (rawProps, activeBreakpoints = []) => {
let styles: StyleObject = {}
const props: any = {}
Object.keys(rawProps).forEach(prop => {
// @ts-ignore no index sig, it's fine
const value = rawProps[prop]
if (value === undefined) return
if (properties[prop]) {
_applyStyleProperty(styles, prop, value)
} else if (macros[prop]) {
styles = {
...styles,
// @ts-ignore no index sig, it's fine
...(rawProps[prop] === true ? macros[prop](value, tokens) : {}),
}
} else if (breakpoints[prop]) {
for (const b of Object.keys(breakpoints)) {
if (activeBreakpoints.includes(b)) {
// @ts-ignore no index sig, it's fine
const breakpointStyles = rawProps[b] || {}
const r = style(breakpointStyles, activeBreakpoints)
styles = {...styles, ...r.styles}
} else {
// @ts-ignore no index sig, it's fine
delete rawProps[b]
}
}
} else {
// @ts-ignore no index sig, it's fine
props[prop] = value
}
})
return {
styles: StyleSheet.create({sheet: styles}).sheet,
props: props,
}
}
function getActiveBreakpoints({width}: {width: number}) {
const active: (keyof typeof breakpoints)[] = Object.keys(
breakpoints,
).filter(breakpoint => width >= breakpoints[breakpoint])
return {
active,
current: active[active.length - 1],
}
}
return {
config: {
tokens,
properties,
macros,
breakpoints,
},
style,
getActiveBreakpoints,
}
}
+25
View File
@@ -0,0 +1,25 @@
import {Platform} from 'react-native'
export function web(value: any) {
return Platform.select({
web: value,
})
}
export function ios(value: any) {
return Platform.select({
ios: value,
})
}
export function android(value: any) {
return Platform.select({
android: value,
})
}
export function native(value: any) {
return Platform.select({
native: value,
})
}
+113
View File
@@ -0,0 +1,113 @@
import {createTheme} from './lib/theme'
const palette = {
white: '#FFFFFF',
black: '#0A0B0D',
gray1: '#F7F9FC',
gray2: '#E4E8EE',
gray3: '#CED5DE',
gray4: '#828A99',
gray5: '#4D5564',
gray6: '#1D2126',
blue: '#1185FE',
green: '#54D469',
red: '#FB4566',
}
export const light = createTheme({
tokens: {
color: {
primary: palette.blue,
l1: palette.white,
l2: palette.gray1,
l3: palette.gray2,
l4: palette.gray3,
l5: palette.gray4,
l6: palette.gray5,
l7: palette.gray6,
l8: palette.black,
},
fontSize: {
xxs: 10,
xs: 12,
s: 14,
m: 16,
l: 18,
xl: 22,
xxl: 26,
},
lineHeight: {
xs: 12,
s: 14,
m: 16,
l: 18,
xl: 22,
},
},
properties: {
w: ['width'],
h: ['height'],
c: ['color'],
bg: ['backgroundColor'],
ma: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
mt: ['marginTop'],
mb: ['marginBottom'],
ml: ['marginLeft'],
mr: ['marginRight'],
my: ['marginTop', 'marginBottom'],
mx: ['marginLeft', 'marginRight'],
/**
* Alias for `padding`, maps to all padding properties e.g. `paddingTop`,
* `paddingBottom`, etc.
*/
pa: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
pt: ['paddingTop'],
pb: ['paddingBottom'],
pl: ['paddingLeft'],
pr: ['paddingRight'],
py: ['paddingTop', 'paddingBottom'],
px: ['paddingLeft', 'paddingRight'],
z: ['zIndex'],
radius: ['borderRadius'],
},
macros: {
row: (_: boolean) => ({flexDirection: 'row'}),
column: (_: boolean) => ({flex: 1}),
abs: (_: boolean) => ({position: 'absolute'}),
cover: (_: boolean) => ({
top: 0,
bottom: 0,
left: 0,
right: 0,
}),
caps: (_: boolean) => ({textTransform: 'uppercase'}),
fontSize(value: 'xs' | 's' | 'm' | 'l' | 'xl', tokens) {
return {
fontSize: tokens.fontSize[value],
lineHeight: tokens.lineHeight[value],
}
},
},
breakpoints: {
gtMobile: 800,
gtTablet: 1300,
},
})
export const dark = createTheme({
...light.config,
tokens: {
...light.config.tokens,
color: {
...light.config.tokens.color,
l1: palette.black,
l2: palette.gray6,
l3: palette.gray5,
l4: palette.gray4,
l5: palette.gray3,
l6: palette.gray2,
l7: palette.gray1,
l8: palette.white,
},
},
})