Alf go brrrr

This commit is contained in:
Eric Bailey
2023-12-21 13:18:35 -06:00
parent 47587de0b2
commit b73013c619
14 changed files with 920 additions and 1733 deletions
+21 -298
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
import {View} from 'react-native'
import {styled} from '#/alf/system'
export const Box = styled(View)
+89 -4
View File
@@ -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
}
-149
View File
@@ -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>,
)
})
-292
View File
@@ -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'],
)
})
})
-173
View File
@@ -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,
}
}
-393
View File
@@ -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,
}
}
+309
View File
@@ -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
-28
View File
@@ -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
View File
@@ -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
+69
View File
@@ -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
+32
View File
@@ -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>
)
}
+104
View File
@@ -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
View File
@@ -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>
)