Application Layout Framework (#1732)

* Initial library setup

* Add docblocks

* Some cleanup

* New storybook

* Playing around

* Remove silly test, use for...in

* Memo

* Memo

* Add hooks example

* Tweak colors, bit of cleanup

* Improve macro handling

* Add some more examples

* Rename for better diff

* Cleanup

* Add nested context example

* Add todo

* Less break more perf

* Buttons, you get the idea

* Fix test

* Remove temp colors

* Add a few more common macros

* Docs

* Perf improvements

* Alf go brrrr

* Update breakpoint handling

* I think it'll work

* Better naming, better code

* Fix typo

* Some renaming

* More complete pass at Tailwind naming

* Build out storybook

* Playing around with curves

* Revert "Playing around with curves"

This reverts commit 6b0e0e5c9d842a2d9af31b53affe2f6291c3fa0d.

* Smooth brain

* Remove outdated docs

* Some docs, fix line-height values, export tokens
This commit is contained in:
Eric Bailey 2024-01-08 19:43:56 -06:00 committed by GitHub
parent 0ee0554b86
commit a5b474895a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1793 additions and 18 deletions

204
src/view/com/Button.tsx Normal file
View file

@ -0,0 +1,204 @@
import React from 'react'
import {Pressable, Text, PressableProps, TextProps} from 'react-native'
import * as tokens from '#/alf/tokens'
import {atoms} from '#/alf'
export type ButtonType =
| 'primary'
| 'secondary'
| 'tertiary'
| 'positive'
| 'negative'
export type ButtonSize = 'small' | 'large'
export type VariantProps = {
type?: ButtonType
size?: ButtonSize
}
type ButtonState = {
pressed: boolean
hovered: boolean
focused: boolean
}
export type ButtonProps = Omit<PressableProps, 'children'> &
VariantProps & {
children:
| ((props: {
state: ButtonState
type?: ButtonType
size?: ButtonSize
}) => React.ReactNode)
| React.ReactNode
| string
}
export type ButtonTextProps = TextProps & VariantProps
export function Button({children, style, type, size, ...rest}: ButtonProps) {
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles = []
const hoverStyles = []
switch (type) {
case 'primary':
baseStyles.push({
backgroundColor: tokens.color.blue_500,
})
break
case 'secondary':
baseStyles.push({
backgroundColor: tokens.color.gray_200,
})
hoverStyles.push({
backgroundColor: tokens.color.gray_100,
})
break
default:
}
switch (size) {
case 'large':
baseStyles.push(
atoms.py_md,
atoms.px_xl,
atoms.rounded_md,
atoms.gap_sm,
)
break
case 'small':
baseStyles.push(
atoms.py_sm,
atoms.px_md,
atoms.rounded_sm,
atoms.gap_xs,
)
break
default:
}
return {
baseStyles,
hoverStyles,
}
}, [type, size])
const [state, setState] = React.useState({
pressed: false,
hovered: false,
focused: false,
})
const onPressIn = React.useCallback(() => {
setState(s => ({
...s,
pressed: true,
}))
}, [setState])
const onPressOut = React.useCallback(() => {
setState(s => ({
...s,
pressed: false,
}))
}, [setState])
const onHoverIn = React.useCallback(() => {
setState(s => ({
...s,
hovered: true,
}))
}, [setState])
const onHoverOut = React.useCallback(() => {
setState(s => ({
...s,
hovered: false,
}))
}, [setState])
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
return (
<Pressable
{...rest}
style={state => [
atoms.flex_row,
atoms.align_center,
...baseStyles,
...(state.hovered ? hoverStyles : []),
typeof style === 'function' ? style(state) : style,
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{typeof children === 'string' ? (
<ButtonText type={type} size={size}>
{children}
</ButtonText>
) : typeof children === 'function' ? (
children({state, type, size})
) : (
children
)}
</Pressable>
)
}
export function ButtonText({
children,
style,
type,
size,
...rest
}: ButtonTextProps) {
const textStyles = React.useMemo(() => {
const base = []
switch (type) {
case 'primary':
base.push({color: tokens.color.white})
break
case 'secondary':
base.push({
color: tokens.color.gray_700,
})
break
default:
}
switch (size) {
case 'small':
base.push(atoms.text_sm, {paddingBottom: 1})
break
case 'large':
base.push(atoms.text_md, {paddingBottom: 1})
break
default:
}
return base
}, [type, size])
return (
<Text
{...rest}
style={[
atoms.flex_1,
atoms.font_semibold,
atoms.text_center,
...textStyles,
style,
]}>
{children}
</Text>
)
}

104
src/view/com/Typography.tsx Normal file
View file

@ -0,0 +1,104 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {useTheme, atoms, web} from '#/alf'
export function Text({style, ...rest}: TextProps) {
const t = useTheme()
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
}
export function H1({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 1,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]}
/>
)
}
export function H2({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 2,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]}
/>
)
}
export function H3({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 3,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]}
/>
)
}
export function H4({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 4,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]}
/>
)
}
export function H5({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 5,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]}
/>
)
}
export function H6({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 6,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]}
/>
)
}

View file

@ -0,0 +1,541 @@
import React from 'react'
import {View} from 'react-native'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSetColorMode} from '#/state/shell'
import * as tokens from '#/alf/tokens'
import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf'
import {Button, ButtonText} from '#/view/com/Button'
import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
function ThemeSelector() {
const setColorMode = useSetColorMode()
return (
<View style={[a.flex_row, a.gap_md]}>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('system')}>
System
</Button>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('light')}>
Light
</Button>
<Button
type="secondary"
size="small"
onPress={() => setColorMode('dark')}>
Dark
</Button>
</View>
)
}
function BreakpointDebugger() {
const t = useTheme()
const breakpoints = useBreakpoints()
return (
<View>
<H3 style={[a.pb_md]}>Breakpoint Debugger</H3>
<Text style={[a.pb_md]}>
Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>}
{breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>}
{breakpoints.gtTablet && <Text>desktop</Text>}
</Text>
<Text
style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}>
{JSON.stringify(breakpoints, null, 2)}
</Text>
</View>
)
}
function ThemedSection() {
const t = useTheme()
return (
<View style={[t.atoms.bg, a.gap_md, a.p_xl]}>
<H3 style={[a.font_bold]}>theme.atoms.text</H3>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<H3 style={[a.font_bold, t.atoms.text_contrast_700]}>
theme.atoms.text_contrast_700
</H3>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<H3 style={[a.font_bold, t.atoms.text_contrast_500]}>
theme.atoms.text_contrast_500
</H3>
<View style={[a.flex_1, t.atoms.border_contrast_500, a.border_t]} />
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_100,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_100</Text>
</View>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_200,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_200</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_300,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_contrast_300</Text>
</View>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
t.atoms.bg_positive,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_positive</Text>
</View>
<View
style={[
a.flex_1,
t.atoms.bg_negative,
a.align_center,
a.justify_center,
{height: 60},
]}>
<Text>theme.bg_negative</Text>
</View>
</View>
</View>
)
}
export function DebugScreen() {
const t = useTheme()
return (
<ScrollView>
<CenteredView style={[t.atoms.bg]}>
<View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
<ThemeSelector />
<Alf theme="light">
<ThemedSection />
</Alf>
<Alf theme="dark">
<ThemedSection />
</Alf>
<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 style={[a.text_xxl]}>atoms.text_xxl</Text>
<Text style={[a.text_xl]}>atoms.text_xl</Text>
<Text style={[a.text_lg]}>atoms.text_lg</Text>
<Text style={[a.text_md]}>atoms.text_md</Text>
<Text style={[a.text_sm]}>atoms.text_sm</Text>
<Text style={[a.text_xs]}>atoms.text_xs</Text>
<Text style={[a.text_xxs]}>atoms.text_xxs</Text>
<View style={[a.gap_md, a.align_start]}>
<Button>
{({state}) => (
<View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}>
<Text>Unstyled button, state: {JSON.stringify(state)}</Text>
</View>
)}
</Button>
<Button type="primary" size="small">
Button
</Button>
<Button type="secondary" size="small">
Button
</Button>
<Button type="primary" size="large">
Button
</Button>
<Button type="secondary" size="large">
Button
</Button>
<Button type="secondary" size="small">
{({type, size}) => (
<>
<FontAwesomeIcon icon={['fas', 'plus']} size={12} />
<ButtonText type={type} size={size}>
With an icon
</ButtonText>
</>
)}
</Button>
<Button type="primary" size="large">
{({state: _state, ...rest}) => (
<>
<FontAwesomeIcon icon={['fas', 'plus']} />
<ButtonText {...rest}>With an icon</ButtonText>
</>
)}
</Button>
</View>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.gray_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.blue_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.green_1000},
]}
/>
</View>
<View style={[a.flex_row, a.gap_md]}>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_0},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_100},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_200},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_300},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_400},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_500},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_600},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_700},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_800},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_900},
]}
/>
<View
style={[
a.flex_1,
{height: 60, backgroundColor: tokens.color.red_1000},
]}
/>
</View>
</View>
<View>
<H3 style={[a.pb_md, a.font_bold]}>Spacing</H3>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xxs (2px)</Text>
<View style={[a.flex_1, a.pt_xxs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xs (4px)</Text>
<View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>sm (8px)</Text>
<View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>md (12px)</Text>
<View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>lg (18px)</Text>
<View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xl (24px)</Text>
<View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xxl (32px)</Text>
<View style={[a.flex_1, a.pt_xxl, t.atoms.bg_contrast_300]} />
</View>
</View>
</View>
<BreakpointDebugger />
</View>
</CenteredView>
</ScrollView>
)
}