Alf go brrrr
This commit is contained in:
+21
-298
@@ -1,312 +1,35 @@
|
||||
# Application Layout Framework (ALF)
|
||||
|
||||
ALF is a low-level styling system inspired by prior art like
|
||||
[styled-system](https://github.com/styled-system/styled-system) and others.
|
||||
|
||||
It consists of two core parts: one or more **themes** and a single **system**.
|
||||
Although a theme really comes first, you'll most often interact with the system,
|
||||
so we'll start there.
|
||||
|
||||
## System
|
||||
|
||||
The _system_ is just the "layout system", and it's created from a set of themes.
|
||||
|
||||
```typescript
|
||||
import {createSystem} from '#/alf/lib/system'
|
||||
import {light, dark} from '#/alf/themes'
|
||||
|
||||
const {
|
||||
ThemeProvider,
|
||||
styled,
|
||||
useStyle,
|
||||
useStyles,
|
||||
useTheme,
|
||||
useTokens,
|
||||
useBreakpoints,
|
||||
} = createSystem({
|
||||
light,
|
||||
dark,
|
||||
})
|
||||
```
|
||||
|
||||
### ThemeProvider
|
||||
|
||||
The `ThemeProvider` expects a single prop `theme`, which corresponds to the
|
||||
_keys_ of the object passed to `createSystem`, which should correspond to your
|
||||
theme names. Using the above, `theme` should be `light` or `dark`.
|
||||
|
||||
|
||||
```typescript
|
||||
<ThemeProvider theme='light'>...</ThemeProvider>
|
||||
```
|
||||
|
||||
### styled
|
||||
|
||||
Convenience method to create a themeable component from another primitive.
|
||||
Accepts a component and an object of default styles. Typically only be used in a
|
||||
few places, like for creating primitive `Box` and `Text` components.
|
||||
|
||||
**Do not get carried away with this. Multiple levels of nesting is performance
|
||||
intensive, and is hard to debug.**
|
||||
|
||||
```tsx
|
||||
import {styled} from '#/alf/system'
|
||||
import { View } from 'react-native'
|
||||
import { useAlf } from '#/alf'
|
||||
import { H3 } from '#/view/com/Typography'
|
||||
|
||||
const Box = styled(View, {})
|
||||
function App() {
|
||||
const { styles, breakpoints } = useAlf()
|
||||
|
||||
<Box c='primary' gtMobile={{ pa: 'm' }} />
|
||||
```
|
||||
|
||||
> An additional feature of `styled` components is a boolean `debug` prop that
|
||||
> will print the processed styles and properties to the console.
|
||||
|
||||
### useStyle
|
||||
|
||||
Mid-level hook to create themed styles with the full theme config at your
|
||||
disposal. Most often helpful when styling 3rd party libraries, such as a
|
||||
dropdown.
|
||||
|
||||
```tsx
|
||||
const styles = useStyle({
|
||||
c: 'primary',
|
||||
gtMobile: {
|
||||
c: 'secondary',
|
||||
},
|
||||
})
|
||||
|
||||
<DropdownItem style={styles} />
|
||||
```
|
||||
|
||||
### useStyles
|
||||
|
||||
Mid-level hook to create _named_ and themed styles with the full theme config at
|
||||
your disposal. Most often helpful when styling 3rd party libraries, such as a
|
||||
dropdown. Think of this as similar to `StyleSheet.create`.
|
||||
|
||||
```typescript
|
||||
const { outer, header } = useStyles({
|
||||
outer: {
|
||||
px: 'm',
|
||||
gtMobile: {
|
||||
px: 'l',
|
||||
},
|
||||
},
|
||||
header: {
|
||||
fontSize: 'xl',
|
||||
}
|
||||
})
|
||||
|
||||
<View style={outer}>
|
||||
<Text style={header}>Hello</Text>
|
||||
</View>
|
||||
```
|
||||
|
||||
### useTheme
|
||||
|
||||
Returns the full currently-active theme object e.g. `light` or `dark`, and all utils attached.
|
||||
Really only used when you need low-level access.
|
||||
|
||||
```typescript
|
||||
const theme = useTheme()
|
||||
const styles = theme.style({
|
||||
c: 'blue',
|
||||
})
|
||||
```
|
||||
|
||||
### useTokens
|
||||
|
||||
Returns just the design tokens of the currently-active theme.
|
||||
|
||||
```tsx
|
||||
const tokens = useTokens()
|
||||
|
||||
<View style={{ color: tokens.color.blue }} />
|
||||
```
|
||||
|
||||
### useBreakpoints
|
||||
|
||||
Returns the current and active breakpoints, which are stored on the theme
|
||||
context.
|
||||
|
||||
```typescript
|
||||
const { current, active } = useBreakpoints()
|
||||
|
||||
// =>
|
||||
{
|
||||
current: 'gtTablet',
|
||||
active: [
|
||||
'gtTablet',
|
||||
'gtMobile',
|
||||
]
|
||||
return (
|
||||
<View style={[styles.flex.row, styles.padding.pa.xl]}>
|
||||
<H3 style={[styles.padding.pb.m, styles.color.primary]}>I'm the blue color</H3>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Themes
|
||||
|
||||
Themes are made up of a collection of utilities and created from a set of design
|
||||
tokens and other configuration. They are external to React, and can be used
|
||||
directly if low-level access is needed.
|
||||
|
||||
### Creating a theme
|
||||
|
||||
```typescript
|
||||
import {createTheme} from '#/alf/lib/theme'
|
||||
|
||||
const light = createTheme(config)
|
||||
```
|
||||
|
||||
Config consists of:
|
||||
|
||||
#### Tokens
|
||||
|
||||
Tokens a.k.a. "design tokens", are the smallest building block of the design
|
||||
system. The name of each token directly corresponds to the name of the CSS
|
||||
property, with the exception of `space`, which is used as a value source for
|
||||
properties like `width` unless a specific `width` scale is configured.
|
||||
|
||||
```typescript
|
||||
createTheme({
|
||||
tokens: {
|
||||
// special token in ALF
|
||||
space: {
|
||||
s: 8,
|
||||
m: 12,
|
||||
l: 18,
|
||||
},
|
||||
// matches CSS prop name exactly
|
||||
color: {
|
||||
blue: '#0000FF',
|
||||
},
|
||||
// matches CSS prop name exactly
|
||||
fontSize: {
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
Properties are a mapping property names to actual CSS properties. Internally,
|
||||
ALF specifies a mapping of all supported CSS properties. When creating a theme
|
||||
is a time to specify "shorthands" or "aliases" a.k.a. syntax sugar. Docblocks
|
||||
will persist and be available for intellisense.
|
||||
|
||||
```typescript
|
||||
createTheme({
|
||||
tokens: {...},
|
||||
properties: {
|
||||
/** Alias for `width` */
|
||||
w: ['width'],
|
||||
/** Alias for `color` */
|
||||
c: ['color'],
|
||||
/** Alias for all directional margin properties */
|
||||
ma: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Breakpoints
|
||||
|
||||
In ALF, breakpoints are applied as the equivalent of `min-width` CSS media
|
||||
queries, meaning your base styles are mobile, and breakpoints are then applied
|
||||
in order. You can name these anything you want, but we recommend the `gt`
|
||||
prefix; basically "greater than N".
|
||||
|
||||
Given the below, at 1000px wide, both the base and the styles applied in
|
||||
`gtMobile` will be applied.
|
||||
|
||||
|
||||
```typescript
|
||||
createTheme({
|
||||
tokens: {...},
|
||||
properties: {...},
|
||||
breakpoints: {
|
||||
/** Greater than 800 */
|
||||
gtMobile: 800,
|
||||
/** Greater than 1300 */
|
||||
gtTablet: 1300,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Macros
|
||||
|
||||
Macros are further syntax sugar. They can be configured as boolean attributes,
|
||||
as allowing a specific set of values, as simple generic methods, or a
|
||||
combination.
|
||||
|
||||
```typescript
|
||||
createTheme({
|
||||
tokens: {...},
|
||||
properties: {...},
|
||||
breakpoints: {...},
|
||||
macros: {
|
||||
/** Shorthand for `flexDirection: 'row'` */
|
||||
row: (_: boolean) => ({flexDirection: 'row', flex: 1}),
|
||||
/**
|
||||
* Shorthand for `flex: 1`. Optionally pass an integer to specify the
|
||||
* col-span.
|
||||
*
|
||||
* Semantically this is helpful as a direct child of `<Box row>`
|
||||
*
|
||||
* @example
|
||||
* <Box row>
|
||||
* <Box column>
|
||||
* <Text>Hello</Text>
|
||||
* </Box>
|
||||
* <Box column={2}>
|
||||
* <Text>Hello</Text>
|
||||
* </Box>
|
||||
* </Box>
|
||||
*/
|
||||
column: (span: boolean | number) => ({
|
||||
flex: typeof span === 'number' ? span : 1,
|
||||
}),
|
||||
/** Shorthand for `alignItems: 'center'` */
|
||||
aic: (_: boolean) => ({alignItems: 'center'}),
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Given the above config, creating a grid with `Box` (see next section) is as
|
||||
simple as:
|
||||
Is a little nicer than:
|
||||
|
||||
```tsx
|
||||
<Box row aic>
|
||||
{/* 1/4 width */}
|
||||
<Box column></Box>
|
||||
import { View } from 'react-native'
|
||||
import { useTheme, styles } from '#/alf'
|
||||
import { H3 } from '#/view/com/Typography'
|
||||
|
||||
{/* 1/2 width */}
|
||||
<Box column={2}></Box>
|
||||
function App() {
|
||||
const theme = useTheme()
|
||||
|
||||
{/* 1/4 width */}
|
||||
<Box column></Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Performance considerations
|
||||
|
||||
### Memoize style objects
|
||||
|
||||
For primitive components or those that render often, it's probably a good idea
|
||||
to memoize your style objects prior to passing them to one of the hooks or
|
||||
`Box`.
|
||||
|
||||
```typescript
|
||||
function Component(props) {
|
||||
const styles = useStyle(React.useMemo(() => ({
|
||||
color: props.prop ? 'blue' : 'red',
|
||||
}), [props.prop]))
|
||||
|
||||
return <View style={styles} />
|
||||
return (
|
||||
<View style={[styles.flex.row, styles.padding.pa.xl]}>
|
||||
<H3 style={[styles.padding.pb.m, theme.color.primary]}>I'm the blue color</H3>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-define styles for list views
|
||||
|
||||
For some simple `FlatList`s, you may be able to pre-define styles for list
|
||||
components, and pass them in as properties, instead of computing new styles for
|
||||
each component.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import {View} from 'react-native'
|
||||
import {styled} from '#/alf/system'
|
||||
export const Box = styled(View)
|
||||
+89
-4
@@ -1,4 +1,89 @@
|
||||
export * from '#/alf/system'
|
||||
export * from '#/alf/components/Box'
|
||||
export * from '#/alf/components/Typography'
|
||||
export * from '#/alf/components/Button'
|
||||
import React from 'react'
|
||||
import {Dimensions} from 'react-native'
|
||||
import * as themes from '#/alf/themes'
|
||||
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export {styles} from '#/alf/styles'
|
||||
export * from '#/alf/util/platform'
|
||||
|
||||
type BreakpointName = keyof typeof breakpoints
|
||||
|
||||
/*
|
||||
* Breakpoints
|
||||
*/
|
||||
const breakpoints: {
|
||||
[key: string]: number
|
||||
} = {
|
||||
gtMobile: 800,
|
||||
gtTablet: 1200,
|
||||
}
|
||||
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],
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<{
|
||||
themeName: themes.ThemeName
|
||||
styles: themes.Theme
|
||||
breakpoints: {
|
||||
current: BreakpointName | undefined
|
||||
active: BreakpointName[]
|
||||
}
|
||||
}>({
|
||||
themeName: 'light',
|
||||
styles: themes.light,
|
||||
breakpoints: {
|
||||
current: undefined,
|
||||
active: [],
|
||||
},
|
||||
})
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
theme: themeName,
|
||||
}: React.PropsWithChildren<{theme: themes.ThemeName}>) {
|
||||
const theme = themes[themeName]
|
||||
const [breakpoints, setBreakpoints] = React.useState(() =>
|
||||
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = Dimensions.addEventListener('change', ({window}) => {
|
||||
const bp = getActiveBreakpoints({width: window.width})
|
||||
if (bp.current !== breakpoints.current) setBreakpoints(bp)
|
||||
})
|
||||
|
||||
return listener.remove
|
||||
}, [breakpoints, setBreakpoints])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={React.useMemo(
|
||||
() => ({
|
||||
themeName: themeName,
|
||||
styles: theme,
|
||||
breakpoints,
|
||||
}),
|
||||
[theme, themeName, breakpoints],
|
||||
)}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAlf() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function useBreakpoints() {
|
||||
return React.useContext(Context).breakpoints
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
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() {
|
||||
return () => {}
|
||||
},
|
||||
},
|
||||
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>,
|
||||
)
|
||||
})
|
||||
@@ -1,292 +0,0 @@
|
||||
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'}),
|
||||
column: (span: boolean | number) => ({
|
||||
flex: typeof span === 'number' ? span : 1,
|
||||
}),
|
||||
},
|
||||
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({})
|
||||
})
|
||||
|
||||
test('custom', () => {
|
||||
const {styles} = theme.style(
|
||||
{
|
||||
column: 2,
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
expect(styles).toEqual({flex: 2})
|
||||
})
|
||||
})
|
||||
|
||||
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'],
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,173 +0,0 @@
|
||||
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
|
||||
}
|
||||
breakpoints: ReturnType<SystemTheme['getActiveBreakpoints']>
|
||||
}>({
|
||||
themeName: Object.keys(themes)[0],
|
||||
theme: defaultTheme,
|
||||
themes,
|
||||
breakpoints: defaultTheme.getActiveBreakpoints({
|
||||
width: Dimensions.get('window').width,
|
||||
}),
|
||||
})
|
||||
|
||||
const ThemeProvider = ({
|
||||
children,
|
||||
theme: themeName,
|
||||
}: React.PropsWithChildren<{theme: ThemeName}>) => {
|
||||
const theme = themes[themeName]
|
||||
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, setBreakpoints])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={React.useMemo(
|
||||
() => ({
|
||||
themeName: themeName,
|
||||
theme,
|
||||
themes,
|
||||
breakpoints,
|
||||
}),
|
||||
[theme, themeName, breakpoints],
|
||||
)}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function useTheme() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
function useTokens() {
|
||||
return useTheme().theme.config.tokens
|
||||
}
|
||||
|
||||
function useBreakpoints() {
|
||||
return useTheme().breakpoints
|
||||
}
|
||||
|
||||
function useStyle(props: ResponsiveStyleProps) {
|
||||
const {theme, breakpoints} = useTheme()
|
||||
return React.useMemo(
|
||||
() => theme.style(props as StylesAndProps, breakpoints.active).styles,
|
||||
[breakpoints.active, props, theme],
|
||||
)
|
||||
}
|
||||
|
||||
// TODO we don't get full intellisense from this hook, find out why
|
||||
function useStyles<O extends Record<string, ResponsiveStyleProps>>(
|
||||
styles: O,
|
||||
): {
|
||||
[Name in keyof O]: ReturnType<SystemTheme['style']>['styles']
|
||||
} {
|
||||
const {theme, breakpoints} = useTheme()
|
||||
return React.useMemo(() => {
|
||||
const acc = {} as {
|
||||
[Name in keyof O]: ReturnType<SystemTheme['style']>['styles']
|
||||
}
|
||||
for (const key in styles) {
|
||||
acc[key as keyof O] = theme.style(
|
||||
styles[key] as StylesAndProps,
|
||||
breakpoints.active,
|
||||
).styles
|
||||
}
|
||||
return acc
|
||||
}, [styles, breakpoints.active, 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, breakpoints} = useTheme()
|
||||
const {styles, props: rest} = React.useMemo(
|
||||
() =>
|
||||
theme.style<Props>(
|
||||
{
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
breakpoints.active,
|
||||
),
|
||||
[breakpoints.active, props, theme],
|
||||
)
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
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]) {
|
||||
Object.assign(
|
||||
styles,
|
||||
rawProps[prop] !== false ? macros[prop](value, tokens) : {},
|
||||
)
|
||||
} else if (breakpoints[prop]) {
|
||||
for (const b in breakpoints) {
|
||||
if (activeBreakpoints.includes(b)) {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
const breakpointStyles = rawProps[b] || {}
|
||||
const r = style(breakpointStyles, activeBreakpoints)
|
||||
Object.assign(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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import {TextStyle} from 'react-native'
|
||||
import * as tokens from '#/alf/tokens'
|
||||
|
||||
const gap = Object.keys(tokens.space).reduce((acc, key) => {
|
||||
const k = key as tokens.Space
|
||||
acc[k] = {
|
||||
gap: tokens.space[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<tokens.Space, {gap: number}>)
|
||||
|
||||
const fontSize = Object.keys(tokens.fontSize).reduce((acc, key) => {
|
||||
const k = key as tokens.FontSize
|
||||
acc[k] = {
|
||||
fontSize: tokens.fontSize[k],
|
||||
lineHeight: tokens.fontSize[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<tokens.FontSize, {fontSize: number; lineHeight: number}>)
|
||||
|
||||
const lineHeight = Object.keys(tokens.lineHeight).reduce((acc, key) => {
|
||||
const k = key as tokens.LineHeight
|
||||
acc[k] = {
|
||||
lineHeight: tokens.lineHeight[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<tokens.LineHeight, {lineHeight: string}>)
|
||||
|
||||
const fontWeight = Object.keys(tokens.fontWeight).reduce((acc, key) => {
|
||||
const k = key as tokens.FontWeight
|
||||
acc[k] = {
|
||||
fontWeight: tokens.fontWeight[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<tokens.FontWeight, {fontWeight: TextStyle['fontWeight']}>)
|
||||
|
||||
const radius = Object.keys(tokens.borderRadius).reduce((acc, key) => {
|
||||
const k = key as tokens.BorderRadius
|
||||
acc[k] = {
|
||||
borderRadius: tokens.borderRadius[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<tokens.BorderRadius, {borderRadius: number}>)
|
||||
|
||||
const padding = Object.keys(tokens.space).reduce(
|
||||
(acc, key) => {
|
||||
const k = key as tokens.Space
|
||||
const value = tokens.space[k]
|
||||
return {
|
||||
pa: {
|
||||
...acc.pa,
|
||||
[k]: {
|
||||
paddingTop: value,
|
||||
paddingBottom: value,
|
||||
paddingLeft: value,
|
||||
paddingRight: value,
|
||||
},
|
||||
},
|
||||
px: {
|
||||
...acc.px,
|
||||
[k]: {
|
||||
paddingLeft: value,
|
||||
paddingRight: value,
|
||||
},
|
||||
},
|
||||
py: {
|
||||
...acc.py,
|
||||
[k]: {
|
||||
paddingTop: value,
|
||||
paddingBottom: value,
|
||||
},
|
||||
},
|
||||
pt: {
|
||||
...acc.pt,
|
||||
[k]: {
|
||||
paddingTop: value,
|
||||
},
|
||||
},
|
||||
pb: {
|
||||
...acc.pb,
|
||||
[k]: {
|
||||
paddingBottom: value,
|
||||
},
|
||||
},
|
||||
pl: {
|
||||
...acc.pl,
|
||||
[k]: {
|
||||
paddingLeft: value,
|
||||
},
|
||||
},
|
||||
pr: {
|
||||
...acc.pr,
|
||||
[k]: {
|
||||
paddingRight: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
{} as {
|
||||
pa: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingTop: number
|
||||
paddingBottom: number
|
||||
paddingLeft: number
|
||||
paddingRight: number
|
||||
}
|
||||
>
|
||||
px: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingLeft: number
|
||||
paddingRight: number
|
||||
}
|
||||
>
|
||||
py: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingTop: number
|
||||
paddingBottom: number
|
||||
}
|
||||
>
|
||||
pt: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingTop: number
|
||||
}
|
||||
>
|
||||
pb: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingBottom: number
|
||||
}
|
||||
>
|
||||
pl: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingLeft: number
|
||||
}
|
||||
>
|
||||
pr: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
paddingRight: number
|
||||
}
|
||||
>
|
||||
},
|
||||
)
|
||||
|
||||
const margin = Object.keys(tokens.space).reduce(
|
||||
(acc, key) => {
|
||||
const k = key as tokens.Space
|
||||
const value = tokens.space[k]
|
||||
return {
|
||||
pa: {
|
||||
...acc.pa,
|
||||
[k]: {
|
||||
marginTop: value,
|
||||
marginBottom: value,
|
||||
marginLeft: value,
|
||||
marginRight: value,
|
||||
},
|
||||
},
|
||||
px: {
|
||||
...acc.px,
|
||||
[k]: {
|
||||
marginLeft: value,
|
||||
marginRight: value,
|
||||
},
|
||||
},
|
||||
py: {
|
||||
...acc.py,
|
||||
[k]: {
|
||||
marginTop: value,
|
||||
marginBottom: value,
|
||||
},
|
||||
},
|
||||
pt: {
|
||||
...acc.pt,
|
||||
[k]: {
|
||||
marginTop: value,
|
||||
},
|
||||
},
|
||||
pb: {
|
||||
...acc.pb,
|
||||
[k]: {
|
||||
marginBottom: value,
|
||||
},
|
||||
},
|
||||
pl: {
|
||||
...acc.pl,
|
||||
[k]: {
|
||||
marginLeft: value,
|
||||
},
|
||||
},
|
||||
pr: {
|
||||
...acc.pr,
|
||||
[k]: {
|
||||
marginRight: value,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
{} as {
|
||||
pa: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginTop: number
|
||||
marginBottom: number
|
||||
marginLeft: number
|
||||
marginRight: number
|
||||
}
|
||||
>
|
||||
px: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginLeft: number
|
||||
marginRight: number
|
||||
}
|
||||
>
|
||||
py: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginTop: number
|
||||
marginBottom: number
|
||||
}
|
||||
>
|
||||
pt: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginTop: number
|
||||
}
|
||||
>
|
||||
pb: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginBottom: number
|
||||
}
|
||||
>
|
||||
pl: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginLeft: number
|
||||
}
|
||||
>
|
||||
pr: Record<
|
||||
tokens.Space,
|
||||
{
|
||||
marginRight: number
|
||||
}
|
||||
>
|
||||
},
|
||||
)
|
||||
|
||||
export const styles = {
|
||||
radius,
|
||||
padding,
|
||||
margin,
|
||||
font: {
|
||||
...fontSize,
|
||||
...fontWeight,
|
||||
...lineHeight,
|
||||
},
|
||||
flex: {
|
||||
gap,
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
wrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
one: {
|
||||
flex: 1,
|
||||
},
|
||||
two: {
|
||||
flex: 2,
|
||||
},
|
||||
three: {
|
||||
flex: 3,
|
||||
},
|
||||
alignCenter: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
justifyCenter: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
justifyBetween: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
justifyEnd: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
},
|
||||
pos: {
|
||||
abs: {
|
||||
position: 'absolute',
|
||||
},
|
||||
rel: {
|
||||
position: 'relative',
|
||||
},
|
||||
cover: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Library imports
|
||||
*/
|
||||
import {createSystem} from './lib/system'
|
||||
import {light, dark} from './themes'
|
||||
|
||||
const {
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
useTokens,
|
||||
useBreakpoints,
|
||||
useStyle,
|
||||
useStyles,
|
||||
styled,
|
||||
} = createSystem({
|
||||
light,
|
||||
dark,
|
||||
})
|
||||
|
||||
export {
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
useTokens,
|
||||
useBreakpoints,
|
||||
useStyle,
|
||||
useStyles,
|
||||
styled,
|
||||
}
|
||||
+78
-190
@@ -1,195 +1,83 @@
|
||||
import {createTheme} from './lib/theme'
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {styles as sharedStyles} from '#/alf/styles'
|
||||
|
||||
const BLUE_HUE = 210
|
||||
const GRAYSCALE_SATURATION = 12
|
||||
export type ThemeName = 'light' | 'dark'
|
||||
export type Theme = typeof light
|
||||
|
||||
export const palette = {
|
||||
white: '#FFFFFF',
|
||||
|
||||
/**
|
||||
* Mathematical scale of grays, from lightest to darkest, all based on the
|
||||
* primary blue color
|
||||
*/
|
||||
gray1: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`,
|
||||
gray2: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`,
|
||||
gray3: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`,
|
||||
gray4: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 30%)`,
|
||||
gray5: `hsl(${BLUE_HUE} ${GRAYSCALE_SATURATION}%, 20%)`,
|
||||
gray6: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 10%)`,
|
||||
black: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`,
|
||||
|
||||
blue: `hsl(${BLUE_HUE}, 100%, 53%)`,
|
||||
green: '#54D469',
|
||||
red: '#FB4566',
|
||||
export type Palette = {
|
||||
primary: string
|
||||
positive: string
|
||||
negative: string
|
||||
l0: string
|
||||
l1: string
|
||||
l2: string
|
||||
l3: string
|
||||
l4: string
|
||||
l5: string
|
||||
l6: string
|
||||
l7: string
|
||||
}
|
||||
|
||||
export const light = createTheme({
|
||||
tokens: {
|
||||
space: {
|
||||
xxs: 2,
|
||||
xs: 4,
|
||||
s: 8,
|
||||
m: 12,
|
||||
l: 18,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
},
|
||||
color: {
|
||||
primary: palette.blue,
|
||||
positive: palette.green,
|
||||
negative: palette.red,
|
||||
l0: palette.white,
|
||||
l1: palette.gray1,
|
||||
l2: palette.gray2,
|
||||
l3: palette.gray3,
|
||||
l4: palette.gray4,
|
||||
l5: palette.gray5,
|
||||
l6: palette.gray6,
|
||||
l7: palette.black,
|
||||
},
|
||||
fontSize: {
|
||||
xxs: 10,
|
||||
xs: 12,
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
xl: 22,
|
||||
xxl: 26,
|
||||
},
|
||||
lineHeight: {
|
||||
xxs: 10,
|
||||
xs: 12,
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
xl: 22,
|
||||
xxl: 26,
|
||||
},
|
||||
borderRadius: {
|
||||
s: 8,
|
||||
m: 12,
|
||||
xl: 36,
|
||||
round: 999,
|
||||
},
|
||||
fontWeight: {
|
||||
normal: '400',
|
||||
semi: '600',
|
||||
bold: '900',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
/** Alias for `width` */
|
||||
w: ['width'],
|
||||
/** Alias for `height` */
|
||||
h: ['height'],
|
||||
/** Alias for `color` */
|
||||
c: ['color'],
|
||||
/** Alias for `backgroundColor` */
|
||||
bg: ['backgroundColor'],
|
||||
/** Alias for all directional margin properties */
|
||||
ma: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
|
||||
/** Alias for `marginTop` */
|
||||
mt: ['marginTop'],
|
||||
/** Alias for `marginBottom` */
|
||||
mb: ['marginBottom'],
|
||||
/** Alias for `marginLeft` */
|
||||
ml: ['marginLeft'],
|
||||
/** Alias for `marginRight` */
|
||||
mr: ['marginRight'],
|
||||
/** Alias for `marginVertical` */
|
||||
my: ['marginTop', 'marginBottom'],
|
||||
/** Alias for `marginHorizontal` */
|
||||
mx: ['marginLeft', 'marginRight'],
|
||||
/** Alias for all directional padding properties */
|
||||
pa: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
|
||||
/** Alias for `paddingTop` */
|
||||
pt: ['paddingTop'],
|
||||
/** Alias for `paddingBottom` */
|
||||
pb: ['paddingBottom'],
|
||||
/** Alias for `paddingLeft` */
|
||||
pl: ['paddingLeft'],
|
||||
/** Alias for `paddingRight` */
|
||||
pr: ['paddingRight'],
|
||||
/** Alias for `paddingVertical` */
|
||||
py: ['paddingTop', 'paddingBottom'],
|
||||
/** Alias for `paddingHorizontal` */
|
||||
px: ['paddingLeft', 'paddingRight'],
|
||||
/** Alias for `zIndex` */
|
||||
z: ['zIndex'],
|
||||
/** Alias for `borderRadius` */
|
||||
radius: ['borderRadius'],
|
||||
},
|
||||
macros: {
|
||||
/** Shorthand for `flexDirection: 'row'` */
|
||||
row: (_: boolean) => ({flexDirection: 'row', flex: 1}),
|
||||
/**
|
||||
* Shorthand for `flex: 1`. Optionally pass an integer to specify the
|
||||
* col-span.
|
||||
*
|
||||
* Semantically this is helpful as a direct child of `<Box row>`
|
||||
*
|
||||
* @example
|
||||
* <Box row>
|
||||
* <Box column>
|
||||
* <Text>Hello</Text>
|
||||
* </Box>
|
||||
* <Box column={2}>
|
||||
* <Text>Hello</Text>
|
||||
* </Box>
|
||||
* </Box>
|
||||
*/
|
||||
column: (span: boolean | number) => ({
|
||||
flex: typeof span === 'number' ? span : 1,
|
||||
}),
|
||||
/** Shorthand for `alignItems: 'center'` */
|
||||
aic: (_: boolean) => ({alignItems: 'center'}),
|
||||
/** Shorthand for `justifyContent: 'center'` */
|
||||
jcc: (_: boolean) => ({justifyContent: 'center'}),
|
||||
/** Shorthand for `justifyContent: 'space-between'` */
|
||||
jcb: (_: boolean) => ({justifyContent: 'space-between'}),
|
||||
/** Shorthand for `position: 'absolute'` */
|
||||
abs: (_: boolean) => ({position: 'absolute'}),
|
||||
/** Shorthand for `StyleSheet.absoluteFillObject` */
|
||||
cover: (_: boolean) => ({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}),
|
||||
/** Shorthand for `textTransform: 'uppercase'` */
|
||||
caps: (_: boolean) => ({textTransform: 'uppercase'}),
|
||||
/**
|
||||
* Shorthand for applying `fontSize` and `fontHeight`, according to our type scale.
|
||||
*/
|
||||
fontSize(value: 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl', tokens) {
|
||||
return {
|
||||
fontSize: tokens.fontSize[value],
|
||||
lineHeight: tokens.lineHeight[value],
|
||||
}
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
/** Greater than 800 */
|
||||
gtMobile: 800,
|
||||
/** Greater than 1300 */
|
||||
gtTablet: 1300,
|
||||
},
|
||||
})
|
||||
export const lightPalette: Palette = {
|
||||
primary: tokens.color.blue,
|
||||
positive: tokens.color.green,
|
||||
negative: tokens.color.red,
|
||||
l0: tokens.color.white,
|
||||
l1: tokens.color.gray1,
|
||||
l2: tokens.color.gray2,
|
||||
l3: tokens.color.gray3,
|
||||
l4: tokens.color.gray4,
|
||||
l5: tokens.color.gray5,
|
||||
l6: tokens.color.gray6,
|
||||
l7: tokens.color.black,
|
||||
} as const
|
||||
|
||||
export const dark = createTheme({
|
||||
...light.config,
|
||||
tokens: {
|
||||
...light.config.tokens,
|
||||
color: {
|
||||
...light.config.tokens.color,
|
||||
l0: palette.black,
|
||||
l1: palette.gray6,
|
||||
l2: palette.gray5,
|
||||
l3: palette.gray4,
|
||||
l4: palette.gray3,
|
||||
l5: palette.gray2,
|
||||
l6: palette.gray1,
|
||||
l7: palette.white,
|
||||
},
|
||||
},
|
||||
})
|
||||
export const darkPalette: Palette = {
|
||||
primary: tokens.color.blue,
|
||||
positive: tokens.color.green,
|
||||
negative: tokens.color.red,
|
||||
l0: tokens.color.black,
|
||||
l1: tokens.color.gray6,
|
||||
l2: tokens.color.gray5,
|
||||
l3: tokens.color.gray4,
|
||||
l4: tokens.color.gray3,
|
||||
l5: tokens.color.gray2,
|
||||
l6: tokens.color.gray1,
|
||||
l7: tokens.color.white,
|
||||
} as const
|
||||
|
||||
export const light = {
|
||||
...sharedStyles,
|
||||
color: Object.keys(lightPalette).reduce((acc, key) => {
|
||||
const k = key as keyof Palette
|
||||
acc[k] = {
|
||||
color: lightPalette[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<keyof Palette, {color: string}>),
|
||||
backgroundColor: Object.keys(lightPalette).reduce((acc, key) => {
|
||||
const k = key as keyof Palette
|
||||
acc[k] = {
|
||||
backgroundColor: lightPalette[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<keyof Palette, {backgroundColor: string}>),
|
||||
} as const
|
||||
|
||||
export const dark = {
|
||||
...sharedStyles,
|
||||
color: Object.keys(darkPalette).reduce((acc, key) => {
|
||||
const k = key as keyof Palette
|
||||
acc[k] = {
|
||||
color: darkPalette[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<keyof Palette, {color: string}>),
|
||||
backgroundColor: Object.keys(darkPalette).reduce((acc, key) => {
|
||||
const k = key as keyof Palette
|
||||
acc[k] = {
|
||||
backgroundColor: darkPalette[k],
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<keyof Palette, {backgroundColor: string}>),
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const BLUE_HUE = 210
|
||||
const GRAYSCALE_SATURATION = 12
|
||||
|
||||
export const color = {
|
||||
white: '#FFFFFF',
|
||||
|
||||
/**
|
||||
* Mathematical scale of grays, from lightest to darkest, all based on the
|
||||
* primary blue color
|
||||
*/
|
||||
gray1: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`,
|
||||
gray2: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`,
|
||||
gray3: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`,
|
||||
gray4: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 30%)`,
|
||||
gray5: `hsl(${BLUE_HUE} ${GRAYSCALE_SATURATION}%, 20%)`,
|
||||
gray6: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 10%)`,
|
||||
black: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`,
|
||||
|
||||
blue: `hsl(${BLUE_HUE}, 100%, 53%)`,
|
||||
green: '#54D469',
|
||||
red: '#FB4566',
|
||||
} as const
|
||||
|
||||
export const space = {
|
||||
xxs: 2,
|
||||
xs: 4,
|
||||
s: 8,
|
||||
m: 12,
|
||||
l: 18,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
} as const
|
||||
|
||||
export const fontSize = {
|
||||
xxs: 10,
|
||||
xs: 12,
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
xl: 22,
|
||||
xxl: 26,
|
||||
} as const
|
||||
|
||||
// TODO test
|
||||
export const lineHeight = {
|
||||
tight: '1.0', // default
|
||||
normal: '1.5',
|
||||
relaxed: '1.625',
|
||||
} as const
|
||||
|
||||
export const borderRadius = {
|
||||
s: 8,
|
||||
m: 12,
|
||||
xl: 36,
|
||||
round: 999,
|
||||
} as const
|
||||
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
semi: '600',
|
||||
bold: '900',
|
||||
} as const
|
||||
|
||||
export type Color = keyof typeof color
|
||||
export type Space = keyof typeof space
|
||||
export type FontSize = keyof typeof fontSize
|
||||
export type LineHeight = keyof typeof lineHeight
|
||||
export type BorderRadius = keyof typeof borderRadius
|
||||
export type FontWeight = keyof typeof fontWeight
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import {Pressable, Text} from 'react-native'
|
||||
import {useAlf, tokens} from '#/alf'
|
||||
|
||||
export function Button({children, ...rest}: React.PropsWithChildren<any>) {
|
||||
const {styles} = useAlf()
|
||||
return (
|
||||
<Pressable
|
||||
{...rest}
|
||||
style={[
|
||||
styles.flex.row,
|
||||
styles.flex.gap.m,
|
||||
styles.padding.px.m,
|
||||
styles.padding.py.s,
|
||||
styles.radius.m,
|
||||
styles.backgroundColor.primary,
|
||||
]}>
|
||||
{typeof children === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.font.s,
|
||||
styles.font.semi,
|
||||
{color: tokens.color.white},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react'
|
||||
import {Text as RNText, TextProps} from 'react-native'
|
||||
import {useAlf, web} from '#/alf'
|
||||
|
||||
export function Text({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
return <RNText style={[styles.font.s, styles.color.l7, style]} {...rest} />
|
||||
}
|
||||
|
||||
export function H1({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 1,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.xxl, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function H2({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 2,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.l, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function H3({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 3,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.m, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function H4({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 4,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.s, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function H5({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 5,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.xs, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function H6({style, ...rest}: TextProps) {
|
||||
const {styles} = useAlf()
|
||||
const attr =
|
||||
web({
|
||||
role: 'heading',
|
||||
'aria-level': 6,
|
||||
}) || {}
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[styles.font.xxs, styles.font.bold, styles.color.l7, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+218
-203
@@ -1,251 +1,266 @@
|
||||
import React from 'react'
|
||||
import {View, Text as RNText} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||
|
||||
import {useSetColorMode} from '#/state/shell'
|
||||
import {
|
||||
ThemeProvider as Alf,
|
||||
useBreakpoints,
|
||||
useStyle,
|
||||
useStyles,
|
||||
Box,
|
||||
Text,
|
||||
Button,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
H4,
|
||||
H5,
|
||||
H6,
|
||||
} from '#/alf'
|
||||
import {useAlf, ThemeProvider as Alf} from '#/alf'
|
||||
import {Button} from '#/view/com/Button'
|
||||
import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
|
||||
|
||||
function ThemeSelector() {
|
||||
const setColorMode = useSetColorMode()
|
||||
const {styles} = useAlf()
|
||||
|
||||
return (
|
||||
<Box row gap="s">
|
||||
<Button onPress={() => setColorMode('system')}>
|
||||
<Text>System</Text>
|
||||
</Button>
|
||||
<Button onPress={() => setColorMode('light')}>
|
||||
<Text>Light</Text>
|
||||
</Button>
|
||||
<Button onPress={() => setColorMode('dark')}>
|
||||
<Text>Dark</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
<View style={[styles.flex.row, styles.flex.gap.m]}>
|
||||
<Button onPress={() => setColorMode('system')}>System</Button>
|
||||
<Button onPress={() => setColorMode('light')}>Light</Button>
|
||||
<Button onPress={() => setColorMode('dark')}>Dark</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function BreakpointDebugger() {
|
||||
const breakpoints = useBreakpoints()
|
||||
const {styles, breakpoints} = useAlf()
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Breakpoint Debugger
|
||||
</H3>
|
||||
<Text pa="m" bg="l1" fontFamily="monospace">
|
||||
<View>
|
||||
<H3 style={[styles.padding.pb.m]}>Breakpoint Debugger</H3>
|
||||
<Text
|
||||
style={[
|
||||
styles.padding.pa.m,
|
||||
styles.backgroundColor.l1,
|
||||
{fontFamily: 'monospace'},
|
||||
]}>
|
||||
{JSON.stringify(breakpoints, null, 2)}
|
||||
</Text>
|
||||
</Box>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Hooks() {
|
||||
const outer = useStyle({
|
||||
pa: 'm',
|
||||
bg: 'l1',
|
||||
})
|
||||
const {heading} = useStyles({
|
||||
heading: {
|
||||
color: 'l7',
|
||||
fontSize: 'l',
|
||||
fontWeight: 'bold',
|
||||
gtTablet: {
|
||||
fontSize: 'xl',
|
||||
},
|
||||
},
|
||||
})
|
||||
function ThemedSection() {
|
||||
const {styles} = useAlf()
|
||||
|
||||
return (
|
||||
<View style={outer}>
|
||||
<RNText style={heading}>Hooks</RNText>
|
||||
<View style={[styles.backgroundColor.l0, styles.padding.pa.m]}>
|
||||
<H3 style={[styles.padding.pb.m, styles.font.bold]}>
|
||||
Colors (this theme is always light)
|
||||
</H3>
|
||||
<View style={[styles.flex.row, styles.flex.gap.m]}>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l0, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l1, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l2, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l3, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l4, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l5, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l6, {height: 60}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.flex.one, styles.backgroundColor.l7, {height: 60}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function DebugScreen() {
|
||||
const backgroundStyles = useStyle({
|
||||
bg: 'l0',
|
||||
})
|
||||
const {styles} = useAlf()
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<CenteredView style={backgroundStyles}>
|
||||
<Box pa="xl" pb={200} gap="xxl">
|
||||
<CenteredView style={[styles.backgroundColor.l0]}>
|
||||
<View
|
||||
style={[
|
||||
styles.padding.pa.xl,
|
||||
styles.flex.gap.xxl,
|
||||
{paddingBottom: 200},
|
||||
]}>
|
||||
<ThemeSelector />
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Colors
|
||||
</H3>
|
||||
<Box row gap="m">
|
||||
<Box column bg="l0" h={60} />
|
||||
<Box column bg="l1" h={60} />
|
||||
<Box column bg="l2" h={60} />
|
||||
<Box column bg="l3" h={60} />
|
||||
<Box column bg="l4" h={60} />
|
||||
<Box column bg="l5" h={60} />
|
||||
<Box column bg="l6" h={60} />
|
||||
<Box column bg="l7" h={60} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Spacing
|
||||
</H3>
|
||||
|
||||
<Box gap="m">
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>xxs (2px)</Text>
|
||||
<Box flex={1} w="100%" pt="xxs" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>xs (4px)</Text>
|
||||
<Box flex={1} w="100%" pt="xs" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>s (8px)</Text>
|
||||
<Box flex={1} w="100%" pt="s" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>m (12px)</Text>
|
||||
<Box flex={1} w="100%" pt="m" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>l (18px)</Text>
|
||||
<Box flex={1} w="100%" pt="l" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>xl (24px)</Text>
|
||||
<Box flex={1} w="100%" pt="xl" bg="l3" />
|
||||
</Box>
|
||||
<Box row alignItems="center">
|
||||
<Text w={80}>xxl (32px)</Text>
|
||||
<Box flex={1} w="100%" pt="xxl" bg="l3" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<BreakpointDebugger />
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Typography
|
||||
</H3>
|
||||
<View>
|
||||
<H3 style={[styles.padding.pb.m, styles.font.bold]}>Colors</H3>
|
||||
<View style={[styles.flex.row, styles.flex.gap.m]}>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l0,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l1,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l2,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l3,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l4,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l5,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l6,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.backgroundColor.l7,
|
||||
{height: 60},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Box gap="m" pa="m" bg="l1">
|
||||
<View>
|
||||
<H3 style={[styles.padding.pb.m, styles.font.bold]}>Spacing</H3>
|
||||
|
||||
<View style={[styles.flex.gap.m]}>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>xxs (2px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.xxs,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>xs (4px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.xs,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>s (8px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.s,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>m (12px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.m,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>l (18px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.l,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>xl (24px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.xl,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex.row, styles.flex.alignCenter]}>
|
||||
<Text style={{width: 80}}>xxl (32px)</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.flex.one,
|
||||
styles.padding.pt.xxl,
|
||||
styles.backgroundColor.l3,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<H3 style={[styles.padding.pb.m, styles.font.bold]}>Typography</H3>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.flex.gap.m,
|
||||
styles.padding.pa.m,
|
||||
styles.backgroundColor.l1,
|
||||
]}>
|
||||
<H1>Heading 1</H1>
|
||||
<H2>Heading 2</H2>
|
||||
<H3>Heading 3</H3>
|
||||
<H4>Heading 4</H4>
|
||||
<H5>Heading 5</H5>
|
||||
<H6>Heading 6</H6>
|
||||
<Text fontSize="xl" fontWeight="bold">
|
||||
H1 Size Text
|
||||
</Text>
|
||||
<Text fontSize="l">H2 Size Text</Text>
|
||||
<Text fontSize="m">H3 Size Text</Text>
|
||||
<Text fontSize="s">H4 Size Text</Text>
|
||||
<Text fontSize="xs">H5 Size Text</Text>
|
||||
<Text fontSize="xxs">H6 Size Text</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Grid
|
||||
</H3>
|
||||
<Box pa="m" gap="m" bg="l1">
|
||||
<Box row gap="s">
|
||||
<Box pa="s" bg="l6" />
|
||||
<Box pa="s" bg="l6" />
|
||||
<Box pa="s" bg="l6" />
|
||||
</Box>
|
||||
|
||||
<Box row gap="s">
|
||||
<Box column pa="s" bg="l6" />
|
||||
<Box column pa="s" bg="l6" />
|
||||
<Box column pa="s" bg="l6" />
|
||||
</Box>
|
||||
|
||||
<Box row gap="s">
|
||||
<Box column pa="s" bg="l6" debug />
|
||||
<Box column={2} pa="s" bg="l6" debug />
|
||||
<Box column pa="s" bg="l6" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Breakpoint styles
|
||||
</H3>
|
||||
|
||||
<Box
|
||||
row
|
||||
justifyContent="center"
|
||||
px="l"
|
||||
py="l"
|
||||
bg="l2"
|
||||
gtMobile={{
|
||||
py: 'xl',
|
||||
bg: 'l4',
|
||||
}}
|
||||
gtTablet={{
|
||||
py: 64,
|
||||
bg: 'l6',
|
||||
}}>
|
||||
<Box w={64} h={64} bg="primary" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Hooks />
|
||||
<Text style={[styles.font.xxl]}>H1 Size Text</Text>
|
||||
<Text style={[styles.font.xl]}>H2 Size Text</Text>
|
||||
<Text style={[styles.font.l]}>H3 Size Text</Text>
|
||||
<Text style={[styles.font.m]}>H4 Size Text</Text>
|
||||
<Text style={[styles.font.s]}>H5 Size Text</Text>
|
||||
<Text style={[styles.font.xs]}>H6 Size Text</Text>
|
||||
<Text style={[styles.font.xxs]}>Very Small Size Text</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Alf theme="light">
|
||||
<Box bg="l0" pa="m">
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Nested theme context.
|
||||
</H3>
|
||||
<Text pb="s">This theme is always light.</Text>
|
||||
<Box row gap="m">
|
||||
<Box column bg="l0" h={60} />
|
||||
<Box column bg="l1" h={60} />
|
||||
<Box column bg="l2" h={60} />
|
||||
<Box column bg="l3" h={60} />
|
||||
<Box column bg="l4" h={60} />
|
||||
<Box column bg="l5" h={60} />
|
||||
<Box column bg="l6" h={60} />
|
||||
<Box column bg="l7" h={60} />
|
||||
</Box>
|
||||
</Box>
|
||||
<ThemedSection />
|
||||
</Alf>
|
||||
|
||||
<Box>
|
||||
<H3 pb="s" fontWeight="bold">
|
||||
Breakpoint styles
|
||||
</H3>
|
||||
|
||||
<Box gap="m">
|
||||
<Button>Primary</Button>
|
||||
<Button type="secondary">Secondary</Button>
|
||||
<Button type="positive">Positive</Button>
|
||||
<Button type="negative">Negative</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</View>
|
||||
</CenteredView>
|
||||
</ScrollView>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user