bop it
(cherry picked from commit a5ea4d866241ce9794f1087a8218b71d95cdf7b7)
This commit is contained in:
@@ -97,6 +97,7 @@
|
|||||||
"lodash.chunk": "^4.2.0",
|
"lodash.chunk": "^4.2.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.omit": "^4.5.0",
|
"lodash.omit": "^4.5.0",
|
||||||
"lodash.once": "^4.1.1",
|
"lodash.once": "^4.1.1",
|
||||||
"lodash.samplesize": "^4.2.0",
|
"lodash.samplesize": "^4.2.0",
|
||||||
@@ -163,6 +164,7 @@
|
|||||||
"@types/lodash.chunk": "^4.2.7",
|
"@types/lodash.chunk": "^4.2.7",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
|
"@types/lodash.merge": "^4.6.7",
|
||||||
"@types/lodash.omit": "^4.5.7",
|
"@types/lodash.omit": "^4.5.7",
|
||||||
"@types/lodash.once": "^4.1.7",
|
"@types/lodash.once": "^4.1.7",
|
||||||
"@types/lodash.samplesize": "^4.2.7",
|
"@types/lodash.samplesize": "^4.2.7",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {bskyTitle} from 'lib/strings/headings'
|
|||||||
import {JSX} from 'react/jsx-runtime'
|
import {JSX} from 'react/jsx-runtime'
|
||||||
import {timeout} from 'lib/async/timeout'
|
import {timeout} from 'lib/async/timeout'
|
||||||
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
||||||
|
import {DesignSystemScreen} from 'view/screens/DesignSystem'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -225,6 +226,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
component={PreferencesHomeFeed}
|
component={PreferencesHomeFeed}
|
||||||
options={{title: title('Home Feed Preferences')}}
|
options={{title: title('Home Feed Preferences')}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="DesignSystem"
|
||||||
|
component={DesignSystemScreen}
|
||||||
|
options={{title: title('Design System')}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', () => {})
|
||||||
|
})
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
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} from './themes'
|
||||||
|
|
||||||
|
type ComponentProps = Partial<ViewProps & TextProps & ImageProps>
|
||||||
|
export type Props<T = ComponentProps> = 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 = Props<TextProps> & {
|
||||||
|
as?: HeadingElements
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = React.createContext({theme: light})
|
||||||
|
|
||||||
|
export const ThemeProvider = ({
|
||||||
|
children,
|
||||||
|
theme,
|
||||||
|
}: React.PropsWithChildren<{theme: Theme}>) => (
|
||||||
|
<Context.Provider value={{theme}}>{children}</Context.Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
export function useBreakpoints() {
|
||||||
|
const {theme} = React.useContext(Context)
|
||||||
|
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 useStyles<T = ComponentProps>(props: Props<T> & T) {
|
||||||
|
const {theme} = React.useContext(Context)
|
||||||
|
const breakpoints = useBreakpoints()
|
||||||
|
const {
|
||||||
|
styles: responsiveStyles,
|
||||||
|
props: {debug, ...rest},
|
||||||
|
} = React.useMemo(
|
||||||
|
() => theme.pick<T & Pick<Props, 'debug'>>(props),
|
||||||
|
[props, theme],
|
||||||
|
)
|
||||||
|
const styles = React.useMemo(() => {
|
||||||
|
return theme.style(
|
||||||
|
theme.applyBreakpoints(responsiveStyles, breakpoints.active),
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [responsiveStyles, breakpoints.current, theme])
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.debug({styles, props: rest, breakpoints})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {styles, props: rest}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Box = React.forwardRef<View, Props<ViewProps>>(
|
||||||
|
({children, style, ...props}, ref) => {
|
||||||
|
const {styles, props: rest} = useStyles<ViewProps>(props)
|
||||||
|
return (
|
||||||
|
<View {...rest} style={[styles, style]} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Text = React.forwardRef<RNText, Props<TextProps>>(
|
||||||
|
({children, style, ...props}, ref) => {
|
||||||
|
const {styles, props: rest} = useStyles<TextProps>({
|
||||||
|
color: 'text',
|
||||||
|
...props,
|
||||||
|
})
|
||||||
|
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]: Props
|
||||||
|
} = {
|
||||||
|
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>((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>(
|
||||||
|
({children, style, as, ...props}, ref) => {
|
||||||
|
const asEl = as || element
|
||||||
|
const extra = Platform.select({
|
||||||
|
web: {
|
||||||
|
'aria-level': asToAriaLevel[element],
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
})
|
||||||
|
const {styles, props: rest} = useStyles({
|
||||||
|
color: 'text',
|
||||||
|
...merge(asToTypeStyles[asEl], props),
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
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>,
|
||||||
|
> = {
|
||||||
|
[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: InnerStyles & InnerResponsiveStyles & Props,
|
||||||
|
) {
|
||||||
|
const res = {styles: {default: {}}, props: {}} as {
|
||||||
|
styles: InnerResponsiveStyles & {default: InnerStyles}
|
||||||
|
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.default[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 | 'default')[],
|
||||||
|
) {
|
||||||
|
let s = styles.default as InnerStyles
|
||||||
|
|
||||||
|
for (const breakpoint of bp) {
|
||||||
|
s = {...s, ...styles[breakpoint]}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveBreakpoints({width}: {width: number}) {
|
||||||
|
const active: (keyof typeof breakpoints | 'default')[] = ['default']
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import {create} from './lib'
|
||||||
|
|
||||||
|
export type Theme = typeof light
|
||||||
|
|
||||||
|
export const light = create({
|
||||||
|
tokens: {
|
||||||
|
space: {
|
||||||
|
s: 10,
|
||||||
|
m: 16,
|
||||||
|
l: 24,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
theme: 'blue',
|
||||||
|
text: '#000',
|
||||||
|
},
|
||||||
|
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'],
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
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'}),
|
||||||
|
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: {
|
||||||
|
theme: 'blue',
|
||||||
|
text: '#333',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -30,6 +30,7 @@ export type CommonNavigatorParams = {
|
|||||||
AppPasswords: undefined
|
AppPasswords: undefined
|
||||||
SavedFeeds: undefined
|
SavedFeeds: undefined
|
||||||
PreferencesHomeFeed: undefined
|
PreferencesHomeFeed: undefined
|
||||||
|
DesignSystem: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const router = new Router({
|
|||||||
CustomFeed: '/profile/:name/feed/:rkey',
|
CustomFeed: '/profile/:name/feed/:rkey',
|
||||||
CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
||||||
Debug: '/sys/debug',
|
Debug: '/sys/debug',
|
||||||
|
DesignSystem: '/sys/ds',
|
||||||
Log: '/sys/log',
|
Log: '/sys/log',
|
||||||
AppPasswords: '/settings/app-passwords',
|
AppPasswords: '/settings/app-passwords',
|
||||||
PreferencesHomeFeed: '/settings/home-feed',
|
PreferencesHomeFeed: '/settings/home-feed',
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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 * as themes from 'lib/design-system/themes'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'DesignSystem'>
|
||||||
|
|
||||||
|
export const DesignSystemScreen = withAuthRequired(
|
||||||
|
observer(function DesignSystem({}: Props) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={themes.dark}>
|
||||||
|
<Box pa="m" gtPhone={{padding: 'l'}}>
|
||||||
|
<H2 as="h1" c="theme" gtPhone={{mb: 'm', marginTop: 'l'}} debug>
|
||||||
|
Heading 1
|
||||||
|
</H2>
|
||||||
|
<H1 as="h2" c="theme" 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="theme" />
|
||||||
|
<Box w={50} h={50} bg="text" />
|
||||||
|
<Text font="mono">Monospace</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -192,6 +192,10 @@ export const SettingsScreen = withAuthRequired(
|
|||||||
navigation.navigate('Debug')
|
navigation.navigate('Debug')
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
|
const onPressDesignSystem = React.useCallback(() => {
|
||||||
|
navigation.navigate('DesignSystem')
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
const onPressSavedFeeds = React.useCallback(() => {
|
const onPressSavedFeeds = React.useCallback(() => {
|
||||||
navigation.navigate('SavedFeeds')
|
navigation.navigate('SavedFeeds')
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
@@ -564,6 +568,16 @@ export const SettingsScreen = withAuthRequired(
|
|||||||
Storybook
|
Storybook
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</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
|
<TouchableOpacity
|
||||||
style={[pal.view, styles.linkCardNoIcon]}
|
style={[pal.view, styles.linkCardNoIcon]}
|
||||||
onPress={onPressResetPreferences}
|
onPress={onPressResetPreferences}
|
||||||
|
|||||||
@@ -6391,6 +6391,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/lodash" "*"
|
"@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":
|
"@types/lodash.omit@^4.5.7":
|
||||||
version "4.5.7"
|
version "4.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.7.tgz#2357ed2412b4164344e8ee41f85bb0b2920304ba"
|
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.7.tgz#2357ed2412b4164344e8ee41f85bb0b2920304ba"
|
||||||
|
|||||||
Reference in New Issue
Block a user