Add a design system (#34)

* Add theming system

* Add standard Button control and update RadioButtons

* Unify radiobutton with design system

* Update debug screen to have multiple views

* Add ToggleButton

* Update error controls to use design system

* Add typography to <Text> element

* Move DropdownButton into the design system

* Clean out old code

* Move Text into design system

* Add 'inverted' color palette

* Move LoadingPlaceholder into the design system
This commit is contained in:
Paul Frazee 2022-12-28 14:06:01 -06:00 committed by GitHub
parent cc63660982
commit 7e31645e9a
78 changed files with 1431 additions and 375 deletions

View file

@ -5,8 +5,8 @@ import {
StyleSheet,
useWindowDimensions,
} from 'react-native'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {Text} from '../util/Text'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import {Text} from '../util/text/Text'
import {colors} from '../../lib/styles'
interface AutocompleteItem {

View file

@ -15,7 +15,7 @@ import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
import {Autocomplete} from './Autocomplete'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
// @ts-ignore no type definition -prf
import ProgressCircle from 'react-native-progress/Circle'

View file

@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {colors} from '../../lib/styles'
import {useStores} from '../../../state'
import {UserAvatar} from '../util/UserAvatar'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
export function ComposePrompt({
noAvi = false,

View file

@ -10,9 +10,9 @@ import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {observer} from 'mobx-react-lite'
import _omit from 'lodash.omit'
import {ErrorScreen} from '../util/ErrorScreen'
import {ErrorScreen} from '../util/error/ErrorScreen'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import * as Toast from '../util/Toast'
import {useStores} from '../../../state'

View file

@ -10,7 +10,7 @@ import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {SwipeAndZoom, Dir} from '../util/gestures/SwipeAndZoom'
import {useStores} from '../../../state'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import * as models from '../../../state/models/shell-ui'

View file

@ -15,7 +15,7 @@ import * as EmailValidator from 'email-validator'
import {Logo} from './Logo'
import {Picker} from '../util/Picker'
import {TextLink} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {s, colors} from '../../lib/styles'
import {
makeValidHandle,

View file

@ -12,7 +12,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import * as EmailValidator from 'email-validator'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {Logo} from './Logo'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {s, colors} from '../../lib/styles'
import {createFullHandle, toNiceDomain} from '../../../lib/strings'
import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state'

View file

@ -6,10 +6,10 @@ import {
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
export const snapPoints = ['50%']

View file

@ -9,8 +9,8 @@ import {
import LinearGradient from 'react-native-linear-gradient'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
import {AppBskyActorCreateScene} from '@atproto/api'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles'
import {

View file

@ -9,8 +9,8 @@ import {
import LinearGradient from 'react-native-linear-gradient'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
import {Image as PickedImage} from 'react-native-image-crop-picker'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from '../../../state'
import {ProfileViewModel} from '../../../state/models/profile-view'
import {s, colors, gradients} from '../../lib/styles'

View file

@ -19,8 +19,8 @@ import {
import _omit from 'lodash.omit'
import {AtUri} from '../../../third-party/uri'
import {ProfileCard} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api'
import {ProfileViewModel} from '../../../state/models/profile-view'

View file

@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient'
import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles'
import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
const ITEMS: RadioGroupItem[] = [
{key: 'spam', label: 'Spam or excessive repeat posts'},

View file

@ -9,8 +9,8 @@ import LinearGradient from 'react-native-linear-gradient'
import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles'
import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
const ITEMS: RadioGroupItem[] = [
{key: 'spam', label: 'Spam or excessive repeat posts'},

View file

@ -2,7 +2,7 @@ import React, {useState} from 'react'
import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
import {

View file

@ -4,9 +4,9 @@ import {View, FlatList} from 'react-native'
import {NotificationsViewModel} from '../../../state/models/notifications-view'
import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {EmptyState} from '../util/EmptyState'
import {OnScrollCb} from '../../lib/useOnMainScroll'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -54,7 +54,6 @@ export const Feed = observer(function Feed({
{view.isLoading && !data && <NotificationFeedLoadingPlaceholder />}
{view.hasError && (
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}

View file

@ -8,9 +8,9 @@ import {PostThreadViewModel} from '../../../state/models/post-thread-view'
import {s, colors} from '../../lib/styles'
import {ago, pluralize} from '../../../lib/strings'
import {UpIconSolid} from '../../lib/icons'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Post} from '../post/Post'
import {Link} from '../util/Link'
import {InviteAccepter} from './InviteAccepter'

View file

@ -8,7 +8,7 @@ import {ConfirmModal} from '../../../state/models/shell-ui'
import {useStores} from '../../../state'
import {ProfileCard} from '../profile/ProfileCard'
import * as Toast from '../util/Toast'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {s, colors, gradients} from '../../lib/styles'
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {

View file

@ -10,7 +10,7 @@ import {
} from 'react-native'
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {UserGroupIcon} from '../../lib/icons'
import {useStores} from '../../../state'
import {s} from '../../lib/styles'

View file

@ -2,7 +2,7 @@ import React from 'react'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {SuggestedFollows} from '../discover/SuggestedFollows'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import {s} from '../../lib/styles'

View file

@ -6,9 +6,9 @@ import {
RepostedByViewItemModel,
} from '../../../state/models/reposted-by-view'
import {UserAvatar} from '../util/UserAvatar'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
@ -57,7 +57,6 @@ export const PostRepostedBy = observer(function PostRepostedBy({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -6,7 +6,7 @@ import {
PostThreadViewPostModel,
} from '../../../state/models/post-thread-view'
import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
export const PostThread = observer(function PostThread({
uri,
@ -57,7 +57,6 @@ export const PostThread = observer(function PostThread({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -7,9 +7,9 @@ import {AppBskyFeedPost} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
import {Link} from '../util/Link'
import {RichText} from '../util/RichText'
import {Text} from '../util/Text'
import {PostDropdownBtn} from '../util/DropdownBtn'
import {RichText} from '../util/text/RichText'
import {Text} from '../util/text/Text'
import {PostDropdownBtn} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'

View file

@ -6,8 +6,8 @@ import {
VotesViewItemModel,
} from '../../../state/models/votes-view'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
@ -57,7 +57,6 @@ export const PostVotedBy = observer(function PostVotedBy({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -17,8 +17,8 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls'
import {Text} from '../util/Text'
import {RichText} from '../util/RichText'
import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'

View file

@ -2,8 +2,8 @@ import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {View} from 'react-native'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {PostModel} from '../../../state/models/post'
import {useStores} from '../../../state'

View file

@ -9,11 +9,11 @@ import {
} from 'react-native'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {EmptyState} from '../util/EmptyState'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {FeedModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem'
import {ComposePrompt} from '../composer/Prompt'
import {OnScrollCb} from '../../lib/useOnMainScroll'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -80,7 +80,6 @@ export const Feed = observer(function Feed({
{feed.isLoading && !data && <PostFeedLoadingPlaceholder />}
{feed.hasError && (
<ErrorMessage
dark
message={feed.error}
style={{margin: 6}}
onPressTryAgain={onPressTryAgain}

View file

@ -8,12 +8,12 @@ import {AppBskyFeedPost} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FeedItemModel} from '../../../state/models/feed-view'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds'
import {RichText} from '../util/RichText'
import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'

View file

@ -1,7 +1,7 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'

View file

@ -6,8 +6,8 @@ import {
FollowerItem,
} from '../../../state/models/user-followers-view'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
@ -57,7 +57,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -7,8 +7,8 @@ import {
} from '../../../state/models/user-follows-view'
import {useStores} from '../../../state'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {ErrorMessage} from '../util/ErrorMessage'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'
@ -57,7 +57,6 @@ export const ProfileFollows = observer(function ProfileFollows({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -16,11 +16,11 @@ import {
import {pluralize} from '../../../lib/strings'
import {s, colors} from '../../lib/styles'
import {getGradient} from '../../lib/asset-gen'
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/Text'
import {RichText} from '../util/RichText'
import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
import {UserInfoText} from '../util/UserInfoText'
@ -195,11 +195,11 @@ export const ProfileHeader = observer(function ProfileHeader({
</>
)}
{dropdownItems?.length ? (
<DropdownBtn
<DropdownButton
items={dropdownItems}
style={[styles.btn, styles.secondaryBtn]}>
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
</DropdownBtn>
</DropdownButton>
) : undefined}
</View>
<View style={styles.displayNameLine}>

View file

@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
import {ActivityIndicator, FlatList, View} from 'react-native'
import {MembersViewModel, MemberItem} from '../../../state/models/members-view'
import {ProfileCard} from './ProfileCard'
import {ErrorMessage} from '../util/ErrorMessage'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from '../../../state'
export const ProfileMembers = observer(function ProfileMembers({
@ -49,7 +49,6 @@ export const ProfileMembers = observer(function ProfileMembers({
return (
<View>
<ErrorMessage
dark
message={view.error}
style={{margin: 6}}
onPressTryAgain={onRefresh}

View file

@ -2,9 +2,9 @@ import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from './Text'
import {Text} from './text/Text'
import {UserGroupIcon} from '../../lib/icons'
import {colors} from '../../lib/styles'
import {usePalette} from '../../lib/hooks/usePalette'
export function EmptyState({
icon,
@ -15,16 +15,23 @@ export function EmptyState({
message: string
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
return (
<View style={[styles.container, style]}>
<View style={styles.iconContainer}>
{icon === 'user-group' ? (
<UserGroupIcon size="64" style={styles.icon} />
) : (
<FontAwesomeIcon icon={icon} size={64} style={styles.icon} />
<FontAwesomeIcon
icon={icon}
size={64}
style={[styles.icon, pal.textLight]}
/>
)}
</View>
<Text style={styles.text}>{message}</Text>
<Text type="body1" style={[pal.textLight, styles.text]}>
{message}
</Text>
</View>
)
}
@ -40,12 +47,9 @@ const styles = StyleSheet.create({
icon: {
marginLeft: 'auto',
marginRight: 'auto',
color: colors.gray3,
},
text: {
textAlign: 'center',
color: colors.gray5,
paddingTop: 16,
fontSize: 16,
},
})

View file

@ -1,102 +0,0 @@
import React from 'react'
import {
StyleSheet,
TouchableOpacity,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import LinearGradient from 'react-native-linear-gradient'
import {Text} from './Text'
import {colors, gradients} from '../../lib/styles'
export function ErrorMessage({
message,
numberOfLines,
dark,
style,
onPressTryAgain,
}: {
message: string
numberOfLines?: number
dark?: boolean
style?: StyleProp<ViewStyle>
onPressTryAgain?: () => void
}) {
const inner = (
<>
<View style={[styles.errorIcon, dark ? styles.darkErrorIcon : undefined]}>
<FontAwesomeIcon
icon="exclamation"
style={{color: dark ? colors.red3 : colors.white}}
size={16}
/>
</View>
<Text
style={[styles.message, dark ? styles.darkMessage : undefined]}
numberOfLines={numberOfLines}>
{message}
</Text>
{onPressTryAgain && (
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: dark ? colors.white : colors.red4}}
size={16}
/>
</TouchableOpacity>
)}
</>
)
if (dark) {
return (
<LinearGradient
colors={[gradients.error.start, gradients.error.end]}
start={{x: 0.5, y: 0}}
end={{x: 1, y: 1}}
style={[styles.outer, style]}>
{inner}
</LinearGradient>
)
}
return <View style={[styles.outer, style]}>{inner}</View>
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.red1,
borderWidth: 1,
borderColor: colors.red3,
borderRadius: 6,
paddingVertical: 8,
paddingHorizontal: 8,
},
errorIcon: {
backgroundColor: colors.red4,
borderRadius: 12,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
darkErrorIcon: {
backgroundColor: colors.white,
},
message: {
flex: 1,
color: colors.red4,
paddingRight: 10,
},
darkMessage: {
color: colors.white,
fontWeight: '600',
},
btn: {
paddingHorizontal: 4,
paddingVertical: 4,
},
})

View file

@ -1,57 +0,0 @@
import React from 'react'
import {
GestureResponderEvent,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {colors, gradients} from '../../lib/styles'
import * as zIndex from '../../lib/z-index'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export function FAB({icon, onPress}: {icon: IconProp; onPress: OnPress}) {
return (
<TouchableWithoutFeedback onPress={onPress}>
<View style={styles.outer}>
<LinearGradient
colors={[gradients.purple.start, gradients.purple.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.inner}>
<FontAwesomeIcon
size={24}
icon={icon}
color={colors.white}
style={styles.icon}
/>
</LinearGradient>
</View>
</TouchableWithoutFeedback>
)
}
const styles = StyleSheet.create({
outer: {
position: 'absolute',
zIndex: zIndex.FAB,
right: 22,
bottom: 14,
width: 60,
height: 60,
borderRadius: 30,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowOffset: {width: 0, height: 1},
},
inner: {
width: 60,
height: 60,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
},
icon: {},
})

View file

@ -9,7 +9,8 @@ import {
View,
ViewStyle,
} from 'react-native'
import {Text} from './Text'
import {Text} from './text/Text'
import {TypographyVariant} from '../../lib/ThemeContext'
import {useStores, RootStoreModel} from '../../../state'
import {convertBskyAppUrlIfNeeded} from '../../../lib/strings'
@ -57,14 +58,14 @@ export const Link = observer(function Link({
})
export const TextLink = observer(function Link({
type = 'body1',
style,
href,
title,
text,
}: {
type: TypographyVariant
style?: StyleProp<TextStyle>
href: string
title?: string
text: string
}) {
const store = useStores()
@ -75,7 +76,7 @@ export const TextLink = observer(function Link({
handleLink(store, href, true)
}
return (
<Text style={style} onPress={onPress} onLongPress={onLongPress}>
<Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
{text}
</Text>
)

View file

@ -3,6 +3,7 @@ import {StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UpIcon} from '../../lib/icons'
import {s, colors} from '../../lib/styles'
import {useTheme} from '../../lib/ThemeContext'
export function LoadingPlaceholder({
width,
@ -13,13 +14,14 @@ export function LoadingPlaceholder({
height: string | number
style?: StyleProp<ViewStyle>
}) {
const theme = useTheme()
return (
<View
style={[
{
width,
height,
backgroundColor: '#e7e9ea',
backgroundColor: theme.palette.default.backgroundLight,
borderRadius: 6,
overflow: 'hidden',
},
@ -29,7 +31,7 @@ export function LoadingPlaceholder({
style={{
width,
height,
backgroundColor: '#e7e9ea',
backgroundColor: theme.palette.default.backgroundLight,
}}
/>
</View>
@ -41,6 +43,7 @@ export function PostLoadingPlaceholder({
}: {
style?: StyleProp<ViewStyle>
}) {
const theme = useTheme()
return (
<View style={[styles.post, style]}>
<LoadingPlaceholder width={50} height={50} style={styles.avatar} />
@ -52,16 +55,24 @@ export function PostLoadingPlaceholder({
<View style={s.flexRow}>
<View style={s.flex1}>
<FontAwesomeIcon
style={s.gray3}
style={{color: theme.palette.default.icon}}
icon={['far', 'comment']}
size={14}
/>
</View>
<View style={s.flex1}>
<FontAwesomeIcon style={s.gray3} icon="retweet" size={18} />
<FontAwesomeIcon
style={{color: theme.palette.default.icon}}
icon="retweet"
size={18}
/>
</View>
<View style={s.flex1}>
<UpIcon style={s.gray3} size={17} strokeWidth={1.7} />
<UpIcon
style={{color: theme.palette.default.icon}}
size={17}
strokeWidth={1.7}
/>
</View>
<View style={s.flex1}></View>
</View>
@ -125,8 +136,6 @@ export function NotificationFeedLoadingPlaceholder() {
const styles = StyleSheet.create({
post: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 6,
padding: 10,
margin: 1,
},
@ -135,8 +144,6 @@ const styles = StyleSheet.create({
marginRight: 10,
},
notification: {
backgroundColor: colors.white,
borderRadius: 6,
padding: 10,
paddingLeft: 46,
margin: 1,

View file

@ -1,3 +1,5 @@
// TODO: replaceme with something in the design system
import React, {useRef} from 'react'
import {
StyleProp,
@ -13,7 +15,7 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import RootSiblings from 'react-native-root-siblings'
import {Text} from './Text'
import {Text} from './text/Text'
import {colors} from '../../lib/styles'
interface PickerItem {

View file

@ -2,10 +2,10 @@ import React from 'react'
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'
import {Text} from './Text'
import {Text} from './text/Text'
import {UpIcon, UpIconSolid} from '../../lib/icons'
import {s, colors} from '../../lib/styles'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
interface PostCtrlsOpts {
big?: boolean

View file

@ -2,7 +2,7 @@ import React from 'react'
import {ImageStyle, StyleSheet, StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {Text} from './text/Text'
import {colors} from '../../lib/styles'
import {AutoSizedImage} from './images/AutoSizedImage'
import {ImagesLightbox} from '../../../state/models/shell-ui'

View file

@ -2,8 +2,8 @@ import React from 'react'
import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Link} from '../util/Link'
import {Text} from '../util/Text'
import {PostDropdownBtn} from '../util/DropdownBtn'
import {Text} from './text/Text'
import {PostDropdownBtn} from './forms/DropdownButton'
import {s} from '../../lib/styles'
import {ago} from '../../../lib/strings'

View file

@ -5,7 +5,7 @@ import {
TouchableWithoutFeedback,
View,
} from 'react-native'
import {Text} from './Text'
import {Text} from './text/Text'
import {colors} from '../../lib/styles'
interface Layout {

View file

@ -1,15 +0,0 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {s} from '../../lib/styles'
export function Text({
children,
style,
...props
}: React.PropsWithChildren<TextProps>) {
return (
<RNText style={[s.black, style]} {...props}>
{children}
</RNText>
)
}

View file

@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
import {StyleProp, TextStyle} from 'react-native'
import {Link} from './Link'
import {Text} from './Text'
import {Text} from './text/Text'
import {LoadingPlaceholder} from './LoadingPlaceholder'
import {useStores} from '../../../state'

View file

@ -8,7 +8,7 @@ import {
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAvatar} from './UserAvatar'
import {Text} from './Text'
import {Text} from './text/Text'
import {s, colors} from '../../lib/styles'
import {MagnifyingGlassIcon} from '../../lib/icons'
import {useStores} from '../../../state'

View file

@ -7,8 +7,8 @@ import {
} from 'react-native'
import {Selector} from './Selector'
import {HorzSwipe} from './gestures/HorzSwipe'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {OnScrollCb} from '../../lib/useOnMainScroll'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
import {OnScrollCb} from '../../lib/hooks/useOnMainScroll'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}

View file

@ -0,0 +1,76 @@
import React from 'react'
import {
StyleSheet,
TouchableOpacity,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {colors} from '../../../lib/styles'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
export function ErrorMessage({
message,
numberOfLines,
style,
onPressTryAgain,
}: {
message: string
numberOfLines?: number
style?: StyleProp<ViewStyle>
onPressTryAgain?: () => void
}) {
const theme = useTheme()
const pal = usePalette('error')
return (
<View style={[styles.outer, pal.view, style]}>
<View
style={[styles.errorIcon, {backgroundColor: theme.palette.error.icon}]}>
<FontAwesomeIcon icon="exclamation" style={pal.text} size={16} />
</View>
<Text
type="body2"
style={[styles.message, pal.text]}
numberOfLines={numberOfLines}>
{message}
</Text>
{onPressTryAgain && (
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: theme.palette.error.icon}}
size={18}
/>
</TouchableOpacity>
)}
</View>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 8,
},
errorIcon: {
borderRadius: 12,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
message: {
flex: 1,
paddingRight: 10,
},
btn: {
paddingHorizontal: 4,
paddingVertical: 4,
},
})

View file

@ -1,8 +1,10 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from './Text'
import {colors} from '../../lib/styles'
import {Text} from '../text/Text'
import {colors} from '../../../lib/styles'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
export function ErrorScreen({
title,
@ -15,10 +17,16 @@ export function ErrorScreen({
details?: string
onPressTryAgain?: () => void
}) {
const theme = useTheme()
const pal = usePalette('error')
return (
<View style={styles.outer}>
<View style={[styles.outer, pal.view]}>
<View style={styles.errorIconContainer}>
<View style={styles.errorIcon}>
<View
style={[
styles.errorIcon,
{backgroundColor: theme.palette.error.icon},
]}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.white}}
@ -26,18 +34,30 @@ export function ErrorScreen({
/>
</View>
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
{details && <Text style={styles.details}>{details}</Text>}
<Text type="h3" style={[styles.title, pal.text]}>
{title}
</Text>
<Text style={[styles.message, pal.textLight]}>{message}</Text>
{details && (
<Text
type="body2"
style={[
styles.details,
pal.textInverted,
{backgroundColor: theme.palette.default.background},
]}>
{details}
</Text>
)}
{onPressTryAgain && (
<View style={styles.btnContainer}>
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: colors.white}}
size={16}
/>
<Text style={styles.btnText}>Try again</Text>
<TouchableOpacity
style={[styles.btn, {backgroundColor: theme.palette.error.icon}]}
onPress={onPressTryAgain}>
<FontAwesomeIcon icon="arrows-rotate" style={pal.text} size={16} />
<Text type="button" style={[styles.btnText, pal.text]}>
Try again
</Text>
</TouchableOpacity>
</View>
)}
@ -48,32 +68,19 @@ export function ErrorScreen({
const styles = StyleSheet.create({
outer: {
flex: 1,
backgroundColor: colors.red1,
borderWidth: 1,
borderColor: colors.red3,
borderRadius: 6,
paddingVertical: 30,
paddingHorizontal: 14,
margin: 10,
},
title: {
textAlign: 'center',
color: colors.red4,
fontSize: 24,
marginBottom: 10,
},
message: {
textAlign: 'center',
color: colors.red4,
marginBottom: 20,
},
details: {
textAlign: 'center',
color: colors.black,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.gray5,
borderRadius: 6,
paddingVertical: 10,
paddingHorizontal: 14,
overflow: 'hidden',
@ -85,23 +92,17 @@ const styles = StyleSheet.create({
btn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.red4,
borderRadius: 6,
paddingHorizontal: 16,
paddingVertical: 10,
},
btnText: {
marginLeft: 5,
color: colors.white,
fontSize: 16,
fontWeight: 'bold',
},
errorIconContainer: {
alignItems: 'center',
marginBottom: 10,
},
errorIcon: {
backgroundColor: colors.red4,
borderRadius: 30,
width: 50,
height: 50,

View file

@ -0,0 +1,120 @@
import React from 'react'
import {
StyleProp,
StyleSheet,
TextStyle,
TouchableOpacity,
ViewStyle,
} from 'react-native'
import {Text} from '../text/Text'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
export type ButtonType =
| 'primary'
| 'secondary'
| 'inverted'
| 'primary-outline'
| 'secondary-outline'
| 'primary-light'
| 'secondary-light'
| 'default-light'
export function Button({
type = 'primary',
label,
style,
onPress,
children,
}: React.PropsWithChildren<{
type?: ButtonType
label?: string
style?: StyleProp<ViewStyle>
onPress?: () => void
}>) {
const theme = useTheme()
const outerStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(type, {
primary: {
backgroundColor: theme.palette.primary.background,
},
secondary: {
backgroundColor: theme.palette.secondary.background,
},
inverted: {
backgroundColor: theme.palette.inverted.background,
},
'primary-outline': {
backgroundColor: theme.palette.default.background,
borderWidth: 1,
borderColor: theme.palette.primary.border,
},
'secondary-outline': {
backgroundColor: theme.palette.default.background,
borderWidth: 1,
borderColor: theme.palette.secondary.border,
},
'primary-light': {
backgroundColor: theme.palette.default.background,
},
'secondary-light': {
backgroundColor: theme.palette.default.background,
},
'default-light': {
backgroundColor: theme.palette.default.background,
},
})
const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: {
color: theme.palette.primary.text,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
})
return (
<TouchableOpacity
style={[outerStyle, styles.outer, style]}
onPress={onPress}>
{label ? (
<Text type="button" style={[labelStyle]}>
{label}
</Text>
) : (
children
)}
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
outer: {
paddingHorizontal: 10,
paddingVertical: 8,
},
})

View file

@ -11,12 +11,13 @@ import {
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import RootSiblings from 'react-native-root-siblings'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from './Text'
import {colors} from '../../lib/styles'
import {toShareUrl} from '../../../lib/strings'
import {useStores} from '../../../state'
import {ReportPostModal, ConfirmModal} from '../../../state/models/shell-ui'
import {TABS_ENABLED} from '../../../build-flags'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {colors} from '../../../lib/styles'
import {toShareUrl} from '../../../../lib/strings'
import {useStores} from '../../../../state'
import {ReportPostModal, ConfirmModal} from '../../../../state/models/shell-ui'
import {TABS_ENABLED} from '../../../../build-flags'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
@ -26,14 +27,20 @@ export interface DropdownItem {
onPress: () => void
}
export function DropdownBtn({
export type DropdownButtonType = ButtonType | 'bare'
export function DropdownButton({
type = 'bare',
style,
items,
label,
menuWidth,
children,
}: {
type: DropdownButtonType
style?: StyleProp<ViewStyle>
items: DropdownItem[]
label?: string
menuWidth?: number
children?: React.ReactNode
}) {
@ -62,14 +69,23 @@ export function DropdownBtn({
)
}
if (type === 'bare') {
return (
<TouchableOpacity
style={style}
onPress={onPress}
hitSlop={HITSLOP}
ref={ref}>
{children}
</TouchableOpacity>
)
}
return (
<TouchableOpacity
style={style}
onPress={onPress}
hitSlop={HITSLOP}
ref={ref}>
{children}
</TouchableOpacity>
<View ref={ref}>
<Button onPress={onPress} style={style} label={label}>
{children}
</Button>
</View>
)
}
@ -77,7 +93,6 @@ export function PostDropdownBtn({
style,
children,
itemHref,
itemTitle,
isAuthor,
onCopyPostText,
onDeletePost,
@ -141,9 +156,9 @@ export function PostDropdownBtn({
].filter(Boolean) as DropdownItem[]
return (
<DropdownBtn style={style} items={dropdownItems} menuWidth={200}>
<DropdownButton style={style} items={dropdownItems} menuWidth={200}>
{children}
</DropdownBtn>
</DropdownButton>
)
}

View file

@ -1,24 +1,126 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {Text} from '../Text'
import {colors} from '../../../lib/styles'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
export function RadioButton({
type = 'default-light',
label,
isSelected,
style,
onPress,
}: {
type?: ButtonType
label: string
isSelected: boolean
style?: StyleProp<ViewStyle>
onPress: () => void
}) {
const theme = useTheme()
const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: {
borderColor: theme.palette.primary.text,
},
secondary: {
borderColor: theme.palette.secondary.text,
},
inverted: {
borderColor: theme.palette.inverted.text,
},
'primary-outline': {
borderColor: theme.palette.primary.border,
},
'secondary-outline': {
borderColor: theme.palette.secondary.border,
},
'primary-light': {
borderColor: theme.palette.primary.border,
},
'secondary-light': {
borderColor: theme.palette.secondary.border,
},
'default-light': {
borderColor: theme.palette.default.border,
},
})
const circleFillStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(
type,
{
primary: {
backgroundColor: theme.palette.primary.text,
},
secondary: {
backgroundColor: theme.palette.secondary.text,
},
inverted: {
backgroundColor: theme.palette.inverted.text,
},
'primary-outline': {
backgroundColor: theme.palette.primary.background,
},
'secondary-outline': {
backgroundColor: theme.palette.secondary.background,
},
'primary-light': {
backgroundColor: theme.palette.primary.background,
},
'secondary-light': {
backgroundColor: theme.palette.secondary.background,
},
'default-light': {
backgroundColor: theme.palette.primary.background,
},
},
)
const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: {
color: theme.palette.primary.text,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
})
return (
<TouchableOpacity style={styles.outer} onPress={onPress}>
<View style={styles.circle}>
{isSelected ? <View style={styles.circleFill} /> : undefined}
<Button type={type} onPress={onPress} style={style}>
<View style={styles.outer}>
<View style={[circleStyle, styles.circle]}>
{isSelected ? (
<View style={[circleFillStyle, styles.circleFill]} />
) : undefined}
</View>
<Text type="button" style={[labelStyle, styles.label]}>
{label}
</Text>
</View>
<Text style={styles.label}>{label}</Text>
</TouchableOpacity>
</Button>
)
}
@ -26,30 +128,21 @@ const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.gray2,
paddingHorizontal: 10,
paddingVertical: 8,
},
circle: {
width: 30,
height: 30,
width: 26,
height: 26,
borderRadius: 15,
padding: 4,
borderWidth: 1,
borderColor: colors.gray3,
marginRight: 10,
},
circleFill: {
width: 20,
height: 20,
width: 16,
height: 16,
borderRadius: 10,
backgroundColor: colors.blue3,
},
label: {
flex: 1,
fontSize: 17,
},
})

View file

@ -1,6 +1,7 @@
import React, {useState} from 'react'
import {View} from 'react-native'
import {RadioButton} from './RadioButton'
import {ButtonType} from './Button'
export interface RadioGroupItem {
label: string
@ -8,22 +9,28 @@ export interface RadioGroupItem {
}
export function RadioGroup({
type,
items,
initialSelection = '',
onSelect,
}: {
type?: ButtonType
items: RadioGroupItem[]
initialSelection?: string
onSelect: (key: string) => void
}) {
const [selection, setSelection] = useState<string>('')
const [selection, setSelection] = useState<string>(initialSelection)
const onSelectInner = (key: string) => {
setSelection(key)
onSelect(key)
}
return (
<View>
{items.map(item => (
{items.map((item, i) => (
<RadioButton
key={item.key}
style={i !== 0 ? {marginTop: 2} : undefined}
type={type}
label={item.label}
isSelected={item.key === selection}
onPress={() => onSelectInner(item.key)}

View file

@ -0,0 +1,165 @@
import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {useTheme} from '../../../lib/ThemeContext'
import {choose} from '../../../../lib/functions'
import {colors} from '../../../lib/styles'
export function ToggleButton({
type = 'default-light',
label,
isSelected,
style,
onPress,
}: {
type?: ButtonType
label: string
isSelected: boolean
style?: StyleProp<ViewStyle>
onPress: () => void
}) {
const theme = useTheme()
const circleStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: {
borderColor: theme.palette.primary.text,
},
secondary: {
borderColor: theme.palette.secondary.text,
},
inverted: {
borderColor: theme.palette.inverted.text,
},
'primary-outline': {
borderColor: theme.palette.primary.border,
},
'secondary-outline': {
borderColor: theme.palette.secondary.border,
},
'primary-light': {
borderColor: theme.palette.primary.border,
},
'secondary-light': {
borderColor: theme.palette.secondary.border,
},
'default-light': {
borderColor: theme.palette.default.border,
},
})
const circleFillStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(
type,
{
primary: {
backgroundColor: theme.palette.primary.text,
opacity: isSelected ? 1 : 0.33,
},
secondary: {
backgroundColor: theme.palette.secondary.text,
opacity: isSelected ? 1 : 0.33,
},
inverted: {
backgroundColor: theme.palette.inverted.text,
opacity: isSelected ? 1 : 0.33,
},
'primary-outline': {
backgroundColor: theme.palette.primary.background,
opacity: isSelected ? 1 : 0.5,
},
'secondary-outline': {
backgroundColor: theme.palette.secondary.background,
opacity: isSelected ? 1 : 0.5,
},
'primary-light': {
backgroundColor: theme.palette.primary.background,
opacity: isSelected ? 1 : 0.5,
},
'secondary-light': {
backgroundColor: theme.palette.secondary.background,
opacity: isSelected ? 1 : 0.5,
},
'default-light': {
backgroundColor: isSelected
? theme.palette.primary.background
: colors.gray3,
},
},
)
const labelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(type, {
primary: {
color: theme.palette.primary.text,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
})
return (
<Button type={type} onPress={onPress} style={style}>
<View style={styles.outer}>
<View style={[circleStyle, styles.circle]}>
<View
style={[
circleFillStyle,
styles.circleFill,
isSelected ? styles.circleFillSelected : undefined,
]}
/>
</View>
<Text type="button" style={[labelStyle, styles.label]}>
{label}
</Text>
</View>
</Button>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
},
circle: {
width: 42,
height: 26,
borderRadius: 15,
padding: 4,
borderWidth: 1,
marginRight: 10,
},
circleFill: {
width: 16,
height: 16,
borderRadius: 10,
},
circleFillSelected: {
marginLeft: 16,
},
label: {
flex: 1,
},
})

View file

@ -9,7 +9,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {Text} from '../Text'
import {Text} from '../text/Text'
import {colors} from '../../../lib/styles'
const MAX_HEIGHT = 300

View file

@ -1,9 +1,11 @@
import React from 'react'
import {TextStyle, StyleProp} from 'react-native'
import {TextLink} from './Link'
import {TextLink} from '../Link'
import {Text} from './Text'
import {s} from '../../lib/styles'
import {toShortUrl} from '../../../lib/strings'
import {s} from '../../../lib/styles'
import {toShortUrl} from '../../../../lib/strings'
import {TypographyVariant} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
type TextSlice = {start: number; end: number}
type Entity = {
@ -13,16 +15,19 @@ type Entity = {
}
export function RichText({
type = 'body1',
text,
entities,
style,
numberOfLines,
}: {
type: TypographyVariant
text: string
entities?: Entity[]
style?: StyleProp<TextStyle>
numberOfLines?: number
}) {
const pal = usePalette('default')
if (!entities?.length) {
if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
style = {
@ -47,18 +52,20 @@ export function RichText({
els.push(
<TextLink
key={key}
type={type}
text={segment.text}
href={`/profile/${segment.entity.value}`}
style={[style, s.blue3]}
style={[style, pal.link]}
/>,
)
} else if (segment.entity.type === 'link') {
els.push(
<TextLink
key={key}
type={type}
text={toShortUrl(segment.text)}
href={segment.entity.value}
style={[style, s.blue3]}
style={[style, pal.link]}
/>,
)
}
@ -66,7 +73,7 @@ export function RichText({
key++
}
return (
<Text style={style} numberOfLines={numberOfLines}>
<Text type={type} style={[pal.text, style]} numberOfLines={numberOfLines}>
{els}
</Text>
)

View file

@ -0,0 +1,23 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {s} from '../../../lib/styles'
import {useTheme, TypographyVariant} from '../../../lib/ThemeContext'
export type CustomTextProps = TextProps & {
type?: TypographyVariant
}
export function Text({
type = 'body1',
children,
style,
...props
}: React.PropsWithChildren<CustomTextProps>) {
const theme = useTheme()
const typography = theme.typography[type]
return (
<RNText style={[s.black, typography, style]} {...props}>
{children}
</RNText>
)
}