Compare commits

...

10 Commits

Author SHA1 Message Date
Eric Bailey 7f5456d0a8 comment 2023-09-14 20:30:10 -05:00
Eric Bailey ca50055118 refactor toggle button 2023-09-14 20:19:47 -05:00
Eric Bailey 2a6bea462c add some theme values 2023-09-14 20:18:54 -05:00
Eric Bailey bf8c1bb947 useTheme 2023-09-14 19:45:26 -05:00
Eric Bailey c0c4722b2f multi-theme 2023-09-14 19:43:53 -05:00
Eric Bailey 737e1d6453 cleanup and better naming 2023-09-14 19:34:50 -05:00
Eric Bailey 9a2fc9d9bd add comments 2023-09-14 10:26:20 -05:00
Eric Bailey 10702e9733 add useMultiStyleExample 2023-09-13 16:56:40 -05:00
Ansh Nanda c827c226ef fix unnamed component lint error 2023-09-13 09:59:59 +05:30
Eric Bailey 3ec6701b8e bop it
(cherry picked from commit a5ea4d866241ce9794f1087a8218b71d95cdf7b7)
2023-09-12 11:45:18 -05:00
12 changed files with 1002 additions and 4 deletions
+2
View File
@@ -97,6 +97,7 @@
"lodash.chunk": "^4.2.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.once": "^4.1.1",
"lodash.samplesize": "^4.2.0",
@@ -163,6 +164,7 @@
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.merge": "^4.6.7",
"@types/lodash.omit": "^4.5.7",
"@types/lodash.once": "^4.1.7",
"@types/lodash.samplesize": "^4.2.7",
+6
View File
@@ -68,6 +68,7 @@ import {bskyTitle} from 'lib/strings/headings'
import {JSX} from 'react/jsx-runtime'
import {timeout} from 'lib/async/timeout'
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {DesignSystemScreen} from 'view/screens/DesignSystem'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -225,6 +226,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
component={PreferencesHomeFeed}
options={{title: title('Home Feed Preferences')}}
/>
<Stack.Screen
name="DesignSystem"
component={DesignSystemScreen}
options={{title: title('Design System')}}
/>
</>
)
}
+109
View File
@@ -0,0 +1,109 @@
import {describe, it, expect} from '@jest/globals'
import {create} from '../lib'
const theme = create({
tokens: {
space: {
s: 10,
m: 16,
l: 24,
},
color: {
primary: 'tomato',
},
fontSize: {
s: 14,
m: 16,
l: 18,
},
},
properties: {
c: ['color'],
px: ['paddingLeft', 'paddingRight'],
fs: ['fontSize'],
},
macros: {
font(value: 'inter' | 'mono') {
return {
fontFamily: value === 'inter' ? 'Inter' : 'Roboto Mono',
}
},
},
breakpoints: {
gtPhone: 640,
},
})
describe('style: basic', () => {
it('works with configured properties', () => {
const styles = theme.style({
color: 'primary',
paddingVertical: 's',
fontSize: 'm',
})
expect(styles).toEqual({
color: theme.config.tokens.color.primary,
paddingTop: theme.config.tokens.space.s,
paddingBottom: theme.config.tokens.space.s,
fontSize: theme.config.tokens.fontSize.m,
})
})
it('works with user-defined properties', () => {
const styles = theme.style({
c: 'primary',
px: 's',
fontSize: 'm',
})
expect(styles).toEqual({
color: theme.config.tokens.color.primary,
paddingLeft: theme.config.tokens.space.s,
paddingRight: theme.config.tokens.space.s,
fontSize: theme.config.tokens.fontSize.m,
})
})
})
describe('pick', () => {
it('works', () => {
const {styles, props} = theme.pick({
c: 'primary',
gtPhone: {
px: 'm',
},
accessibilityLabel: 'hello',
})
expect(styles).toEqual({
default: {
c: 'primary',
},
gtPhone: {
px: 'm',
},
})
expect(props).toEqual({
accessibilityLabel: 'hello',
})
})
})
describe('getActiveBreakpoints', () => {
it('works', () => {
const activeBreakpoints = theme.getActiveBreakpoints({
width: 1000,
})
expect(activeBreakpoints).toEqual({
active: ['default', 'gtPhone'],
current: 'gtPhone',
})
})
})
describe('applyBreakpoints', () => {
it('works', () => {})
})
+245
View File
@@ -0,0 +1,245 @@
import React from 'react'
import {
View,
Text as RNText,
Dimensions,
Platform,
ViewProps,
TextProps,
ImageProps,
} from 'react-native'
import merge from 'lodash.merge'
import {Theme, light, dark} from './themes'
type NativeProps = Partial<ViewProps & TextProps & ImageProps>
export type StyleProps = Parameters<typeof light.style>[0] &
Record<string, unknown>
export type ComponentProps<T = NativeProps> = Parameters<
typeof light.pick<T>
>[0] & {
/**
* Debug mode will log the styles and props to the console
*/
debug?: boolean
}
type HeadingElements = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
type TypeProps = ComponentProps<TextProps> & {
as?: HeadingElements
}
const themes = {
light,
dark,
}
type ThemeName = keyof typeof themes
const Context = React.createContext<{
themeName: ThemeName
theme: Theme
themes: {
[key in ThemeName]: Theme
}
}>({
themeName: 'light',
theme: light,
themes: {
light,
dark,
},
})
export const ThemeProvider = ({
children,
theme,
}: React.PropsWithChildren<{theme: ThemeName}>) => (
<Context.Provider
value={{
themeName: theme,
theme: themes[theme],
themes,
}}>
{children}
</Context.Provider>
)
export function useTheme() {
return React.useContext(Context)
}
export function useBreakpoints() {
const {theme} = useTheme()
const [breakpoints, setBreakpoints] = React.useState(
theme.getActiveBreakpoints({width: Dimensions.get('window').width}),
)
React.useEffect(() => {
const listener = Dimensions.addEventListener('change', ({window}) => {
const bp = theme.getActiveBreakpoints({width: window.width})
if (bp.current !== breakpoints.current) setBreakpoints(bp)
})
return () => {
listener.remove()
}
}, [breakpoints, theme])
return breakpoints
}
export function usePick<T = NativeProps>(props: ComponentProps<T>) {
const {theme} = useTheme()
return React.useMemo(() => theme.pick(props), [props, theme])
}
export function useStyle(props: StyleProps) {
const {theme} = useTheme()
const breakpoints = useBreakpoints()
const {styles: responsiveStyles} = usePick(props)
return React.useMemo(() => {
return theme.style(
theme.applyBreakpoints(responsiveStyles, breakpoints.active),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [responsiveStyles, breakpoints.current, theme])
}
export function useStyles<O extends Record<string, StyleProps>>(
styles: O,
): {
[Name in keyof O]: ReturnType<typeof light.style>
} {
const {theme} = useTheme()
const breakpoints = useBreakpoints()
return React.useMemo(() => {
return Object.entries(styles).reduce((acc, [key, style]) => {
const responsiveStyles = theme.pick(style).styles
acc[key as keyof O] = theme.style(
theme.applyBreakpoints(responsiveStyles, breakpoints.active),
)
return acc
}, {} as {[Name in keyof O]: ReturnType<typeof light.style>})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [styles, breakpoints.current, theme])
}
export const Box = React.forwardRef<View, ComponentProps<ViewProps>>(
function BoxThemed({children, style, ...props}, ref) {
const {styles: pickedStyles, props: rest} = usePick<ViewProps>(props)
const styles = useStyle(pickedStyles)
if (props.debug) console.log({styles: pickedStyles, props: rest})
return (
<View {...rest} style={[styles, style]} ref={ref}>
{children}
</View>
)
},
)
export const Text = React.forwardRef<RNText, ComponentProps<TextProps>>(
function TextThemed({children, style, ...props}, ref) {
const {styles: pickedStyles, props: rest} = usePick<TextProps>(props)
const styles = useStyle({
color: 'text',
...pickedStyles,
})
if (props.debug) console.log({styles, props: rest})
return (
<RNText {...rest} style={[styles, style]} ref={ref}>
{children}
</RNText>
)
},
)
const asToAriaLevel = {
h1: 1,
h2: 2,
h3: 3,
h4: 4,
h5: 5,
h6: 6,
}
const asToTypeStyles: {
[key in HeadingElements]: ComponentProps
} = {
h1: {
fontSize: 'l',
lineHeight: 'l',
gtPhone: {
fontSize: 'xl',
lineHeight: 'xl',
},
},
h2: {
fontSize: 24,
lineHeight: 32,
},
h3: {
fontSize: 20,
lineHeight: 28,
},
h4: {
fontSize: 16,
lineHeight: 24,
},
h5: {
fontSize: 14,
lineHeight: 20,
},
h6: {
fontSize: 12,
lineHeight: 16,
},
}
export const P = React.forwardRef<RNText, TypeProps>(function PThemed(
props,
ref,
) {
// @ts-expect-error role is web only
return <Text role="paragraph" c="text" {...props} ref={ref} />
})
/**
* @see https://necolas.github.io/react-native-web/docs/accessibility/#semantic-html
* @see https://docs.expo.dev/develop/user-interface/fonts/
*/
function createHeadingComponent(element: HeadingElements) {
return React.forwardRef<RNText, TypeProps>(function HeadingThemed(
{children, style, as, ...props},
ref,
) {
const asEl = as || element
const extra = Platform.select({
web: {
'aria-level': asToAriaLevel[element],
},
default: {},
})
const {styles: pickedStyles, props: rest} = usePick<TextProps>(props)
const styles = useStyle({
color: 'text',
...merge(asToTypeStyles[asEl], pickedStyles),
})
if (props.debug) console.debug({styles, props: rest})
return (
<RNText
role="heading"
{...extra}
{...rest}
style={[styles, style]}
ref={ref}>
{children}
</RNText>
)
})
}
export const H1 = createHeadingComponent('h1')
export const H2 = createHeadingComponent('h2')
export const H3 = createHeadingComponent('h3')
export const H4 = createHeadingComponent('h4')
export const H5 = createHeadingComponent('h5')
export const H6 = createHeadingComponent('h6')
+384
View File
@@ -0,0 +1,384 @@
import type {
ViewStyle,
TextStyle,
ImageStyle,
DimensionValue,
ViewProps,
TextProps,
ImageProps,
} 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
}
type Styles<T extends Tokens, P extends Properties, C extends Macros<T>> = {
[Property in ColorProperties]?: 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, {}, C>[P[Property][0]]
} & {
[Property in keyof C]?: C[Property] extends (
value: infer Value,
tokens: T,
) => StyleObject
? Value
: never
}
type ResponsiveStyles<
B extends Breakpoints,
S extends Styles<any, any, any>,
> = S & {
[Breakpoint in keyof B]?: S
}
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 create = <
T extends Tokens,
P extends Properties,
C extends Macros<T>,
B extends Breakpoints,
>({
tokens,
properties: userProperties = {},
macros: userMacros = {},
breakpoints: userBreakpoints = {},
}: {
tokens: T
properties?: Partial<P>
macros?: Partial<C>
breakpoints?: Partial<B>
}) => {
const properties = Object.assign({}, propertyMapping, userProperties) as P
const macros = userMacros as C
const breakpoints = userBreakpoints as B
const keyofProperties = Object.keys(properties) as (keyof P)[]
const keyofMacros = Object.keys(macros) as (keyof C)[]
const keyofBreakpoints = Object.keys(breakpoints) as (keyof B)[]
const allPropertyKeys = [...keyofProperties, ...keyofMacros]
type InnerStyles = Styles<typeof tokens, typeof properties, typeof macros>
type InnerResponsiveStyles = ResponsiveStyles<typeof breakpoints, InnerStyles>
function pick<Props = Partial<ViewProps & TextProps & ImageProps>>(
props: InnerResponsiveStyles & Props,
) {
const res = {styles: {}, props: {}} as {
styles: InnerResponsiveStyles
props: Props
}
for (const prop of Object.keys(props)) {
const value = props[prop]
if (value === undefined) continue
if (allPropertyKeys.includes(prop)) {
// @ts-ignore no index sig, it's fine
res.styles[prop] = value
} else if (keyofBreakpoints.includes(prop)) {
// @ts-ignore no index sig, it's fine
res.styles[prop] = value
} else {
// @ts-ignore no index sig, it's fine
res.props[prop] = value
}
}
return res
}
function style(styles: InnerStyles): StyleObject {
const s: StyleObject = {}
for (const prop of Object.keys(styles)) {
const value = styles[prop]
if (value === undefined) continue
if (keyofProperties.includes(prop)) {
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]
s[_prop] = t?.[value] || value
}
} else if (keyofMacros.includes(prop)) {
const property = macros[prop]
if (property) {
Object.assign(s, property(value, tokens))
}
} else {
// @ts-ignore no index sig, it's fine
const t = specialTokenMapping[prop]
? // @ts-ignore no index sig, it's fine
tokens[specialTokenMapping[prop]]
: // @ts-ignore no index sig, it's fine
tokens[prop]
s[prop as StyleObjectProperties] = t?.[value] || value
}
}
return StyleSheet.create({sheet: s}).sheet
}
function applyBreakpoints(
styles: InnerResponsiveStyles,
bp: (keyof typeof breakpoints)[],
) {
let s = styles
for (const breakpoint of bp) {
const o = styles[breakpoint] || {}
s = {...s, ...o}
}
return s
}
function getActiveBreakpoints({width}: {width: number}) {
const active: (keyof typeof breakpoints)[] = []
for (const breakpoint in breakpoints) {
if (width >= breakpoints[breakpoint]) {
active.push(breakpoint)
}
}
return {
active,
current: active[active.length - 1],
}
}
return {
config: {
tokens,
properties,
macros,
breakpoints,
},
style,
pick,
applyBreakpoints,
getActiveBreakpoints,
}
}
+135
View File
@@ -0,0 +1,135 @@
import {create} from './lib'
export type Theme = typeof light
export const light = create({
tokens: {
space: {
s: 10,
m: 16,
l: 24,
},
// naming inspo https://polaris.shopify.com/tokens/color
color: {
surface: '#fff',
// Text
text: '#000',
textInverted: '#fff',
// Interactive elements
textLink: '#0085ff',
// UI
border: '#f0e9e9',
},
fontSize: {
xxs: 10,
xs: 12,
s: 14,
m: 16,
l: 24,
xl: 32,
xxl: 48,
},
lineHeight: {
xxs: 10,
xs: 12,
s: 14,
m: 16,
l: 24,
xl: 32,
xxl: 48,
},
fontFamily: {
inter: 'sans-serif',
roboto: 'monospace',
},
},
properties: {
d: ['display'],
w: ['width'],
h: ['height'],
c: ['color'],
bg: ['backgroundColor'],
ma: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
mt: ['marginTop'],
mb: ['marginBottom'],
ml: ['marginLeft'],
mr: ['marginRight'],
my: ['marginTop', 'marginBottom'],
mx: ['marginLeft', 'marginRight'],
/**
* Alias for `padding`, maps to all padding properties e.g. `paddingTop`,
* `paddingBottom`, etc.
*/
pa: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
pt: ['paddingTop'],
pb: ['paddingBottom'],
pl: ['paddingLeft'],
pr: ['paddingRight'],
py: ['paddingTop', 'paddingBottom'],
px: ['paddingLeft', 'paddingRight'],
z: ['zIndex'],
fs: ['fontSize'],
ff: ['fontFamily'],
fw: ['fontWeight'],
lh: ['lineHeight'],
ta: ['textAlign'],
radius: ['borderRadius'],
},
macros: {
inline: (_: boolean) => ({flexDirection: 'row'}),
aic: (_: boolean) => ({alignItems: 'center'}),
aie: (_: boolean) => ({alignItems: 'flex-end'}),
jcs: (_: boolean) => ({justifyContent: 'flex-start'}),
jcc: (_: boolean) => ({justifyContent: 'center'}),
jce: (_: boolean) => ({justifyContent: 'flex-end'}),
jcb: (_: boolean) => ({justifyContent: 'space-between'}),
rel: (_: boolean) => ({position: 'relative'}),
abs: (_: boolean) => ({position: 'absolute'}),
cover: (_: boolean) => ({
top: 0,
bottom: 0,
left: 0,
right: 0,
}),
tac: (_: boolean) => ({textAlign: 'center'}),
tar: (_: boolean) => ({textAlign: 'right'}),
mxa: (_: boolean) => ({marginLeft: 'auto', marginRight: 'auto'}),
mya: (_: boolean) => ({marginTop: 'auto', marginBottom: 'auto'}),
/**
* Macro for applying `{ textTransform: 'uppercase' }` to a style.
*/
caps: (_: boolean) => ({textTransform: 'uppercase'}),
font(value: 'sans' | 'mono', tokens) {
return {
fontFamily:
value === 'sans' ? tokens.fontFamily.inter : tokens.fontFamily.roboto,
}
},
},
breakpoints: {
gtPhone: 640,
},
})
export const dark: typeof light = create({
...light.config,
tokens: {
...light.config.tokens,
color: {
surface: '#000',
// Text
text: '#fff',
textInverted: '#000',
// Interactive elements
textLink: '#0085ff',
// UI
border: '#f0e9e9',
},
},
})
+1
View File
@@ -30,6 +30,7 @@ export type CommonNavigatorParams = {
AppPasswords: undefined
SavedFeeds: undefined
PreferencesHomeFeed: undefined
DesignSystem: undefined
}
export type BottomTabNavigatorParams = CommonNavigatorParams & {
+1
View File
@@ -21,6 +21,7 @@ export const router = new Router({
CustomFeed: '/profile/:name/feed/:rkey',
CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
Debug: '/sys/debug',
DesignSystem: '/sys/ds',
Log: '/sys/log',
AppPasswords: '/settings/app-passwords',
PreferencesHomeFeed: '/settings/home-feed',
+49 -4
View File
@@ -1,12 +1,55 @@
import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from '../text/Text'
import {
StyleProp,
StyleSheet,
TextStyle,
View,
ViewStyle,
Pressable,
} from 'react-native'
import {Text as RNText} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from 'lib/ThemeContext'
import {choose} from 'lib/functions'
import {colors} from 'lib/styles'
import {TypographyVariant} from 'lib/ThemeContext'
import {ComponentProps, Box, Text} from 'lib/design-system'
export function Toggle({
children,
selected,
...props
}: React.PropsWithChildren<
ComponentProps & {
selected: boolean
}
>) {
return (
<Pressable {...props}>
<Box inline aic gap="s" py="s" radius={24}>
<Box rel w={42} h={26} radius={15} borderWidth={2} borderColor="border">
<Box
abs
w={16}
h={16}
radius={10}
bg={selected ? 'textLink' : 'border'}
top={3}
left={selected ? undefined : 3}
right={selected ? 3 : undefined}
/>
</Box>
<Box>
{/* @ts-ignore userSelect is a web property */}
<Text style={{userSelect: 'none'}}>{children}</Text>
</Box>
</Box>
</Pressable>
)
}
export function ToggleButton({
type = 'default-light',
label,
@@ -146,9 +189,11 @@ export function ToggleButton({
/>
</View>
{label === '' ? null : (
<Text type={labelType || 'button'} style={[labelStyle, styles.label]}>
<RNText
type={labelType || 'button'}
style={[labelStyle, styles.label]}>
{label}
</Text>
</RNText>
)}
</View>
</Button>
+49
View File
@@ -0,0 +1,49 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ThemeProvider, Box, Text, H1, H2, H3, P} from 'lib/design-system'
import {Toggle} from 'view/com/util/forms/ToggleButton'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'DesignSystem'>
function ToggleButtons() {
const [selected, setSelected] = React.useState(false)
return (
<>
<Toggle selected={selected} onPress={() => setSelected(!selected)}>
Toggle button
</Toggle>
</>
)
}
export const DesignSystemScreen = withAuthRequired(
observer(function DesignSystem({}: Props) {
return (
<ThemeProvider theme="dark">
<Box pa="m" gtPhone={{padding: 'l'}} debug>
<H2 as="h1" c="text" gtPhone={{mb: 'm', marginTop: 'l'}}>
Heading 1
</H2>
<H1 as="h2" c="text" style={{color: 'tomato'}}>
Heading 2
</H1>
<H3 lh="xl">Heading 3</H3>
<P caps>Paragraph</P>
<Box inline aic gap="m" my="l">
<Box w={100} h={100} bg="textLink" />
<Box w={50} h={50} bg="text" />
<Text font="mono">Monospace</Text>
</Box>
<Box py="l">
<ToggleButtons />
</Box>
</Box>
</ThemeProvider>
)
}),
)
+14
View File
@@ -192,6 +192,10 @@ export const SettingsScreen = withAuthRequired(
navigation.navigate('Debug')
}, [navigation])
const onPressDesignSystem = React.useCallback(() => {
navigation.navigate('DesignSystem')
}, [navigation])
const onPressSavedFeeds = React.useCallback(() => {
navigation.navigate('SavedFeeds')
}, [navigation])
@@ -564,6 +568,16 @@ export const SettingsScreen = withAuthRequired(
Storybook
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressDesignSystem}
accessibilityRole="button"
accessibilityHint="Open storybook page"
accessibilityLabel="Opens the storybook page">
<Text type="lg" style={pal.text}>
Design System
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressResetPreferences}
+7
View File
@@ -6391,6 +6391,13 @@
dependencies:
"@types/lodash" "*"
"@types/lodash.merge@^4.6.7":
version "4.6.7"
resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.7.tgz#0af6555dd8bc6568ef73e5e0d820a027362946b1"
integrity sha512-OwxUJ9E50gw3LnAefSHJPHaBLGEKmQBQ7CZe/xflHkyy/wH2zVyEIAKReHvVrrn7zKdF58p16We9kMfh7v0RRQ==
dependencies:
"@types/lodash" "*"
"@types/lodash.omit@^4.5.7":
version "4.5.7"
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.7.tgz#2357ed2412b4164344e8ee41f85bb0b2920304ba"