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
zio/stable
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,6 +5,7 @@ import {RootSiblingParent} from 'react-native-root-siblings'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import SplashScreen from 'react-native-splash-screen'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {ThemeProvider} from './view/lib/ThemeContext'
import * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {MobileShell} from './view/shell/mobile'
@ -40,9 +41,11 @@ function App() {
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<MobileShell />
</SafeAreaProvider>
<ThemeProvider>
<SafeAreaProvider>
<MobileShell />
</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>

View File

@ -0,0 +1,6 @@
export function choose<U, T extends Record<string, U>>(
value: keyof T,
choices: T,
): U {
return choices[value]
}

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>
)
}

View File

@ -0,0 +1,70 @@
import React, {createContext, useContext, useMemo} from 'react'
import {TextStyle, useColorScheme, ViewStyle} from 'react-native'
import {darkTheme, defaultTheme} from './themes'
export type ColorScheme = 'light' | 'dark'
export type PaletteColorName =
| 'default'
| 'primary'
| 'secondary'
| 'inverted'
| 'error'
export type PaletteColor = {
isLowContrast: boolean
background: string
backgroundLight: string
text: string
textLight: string
textInverted: string
link: string
border: string
icon: string
}
export type Palette = Record<PaletteColorName, PaletteColor>
export type ShapeName = 'button' | 'bigButton' | 'smallButton'
export type Shapes = Record<ShapeName, ViewStyle>
export type TypographyVariant =
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'subtitle1'
| 'subtitle2'
| 'body1'
| 'body2'
| 'button'
| 'caption'
| 'overline'
export type Typography = Record<TypographyVariant, TextStyle>
export interface Theme {
colorScheme: ColorScheme
palette: Palette
shapes: Shapes
typography: Typography
}
export interface ThemeProviderProps {
theme?: ColorScheme
}
export const ThemeContext = createContext<Theme>(defaultTheme)
export const useTheme = () => useContext(ThemeContext)
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
theme,
children,
}) => {
const colorScheme = useColorScheme()
const value = useMemo(
() => ((theme || colorScheme) === 'dark' ? darkTheme : defaultTheme),
[colorScheme, theme],
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

View File

@ -1,6 +1,6 @@
import {useState} from 'react'
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
import {RootStoreModel} from '../../state'
import {RootStoreModel} from '../../../state'
export type OnScrollCb = (
event: NativeSyntheticEvent<NativeScrollEvent>,

View File

@ -0,0 +1,41 @@
import {TextStyle, ViewStyle} from 'react-native'
import {useTheme, PaletteColorName, PaletteColor} from '../ThemeContext'
export interface UsePaletteValue {
colors: PaletteColor
view: ViewStyle
border: ViewStyle
text: TextStyle
textLight: TextStyle
textInverted: TextStyle
link: TextStyle
}
export function usePalette(color: PaletteColorName): UsePaletteValue {
const palette = useTheme().palette[color]
return {
colors: palette,
view: {
backgroundColor: palette.background,
},
border: {
borderWidth: 1,
borderColor: palette.border,
},
text: {
color: palette.text,
fontWeight: palette.isLowContrast ? '500' : undefined,
},
textLight: {
color: palette.textLight,
fontWeight: palette.isLowContrast ? '500' : undefined,
},
textInverted: {
color: palette.textInverted,
fontWeight: palette.isLowContrast ? '500' : undefined,
},
link: {
color: palette.link,
fontWeight: palette.isLowContrast ? '500' : undefined,
},
}
}

View File

@ -0,0 +1,163 @@
import type {Theme} from './ThemeContext'
import {colors} from './styles'
export const defaultTheme: Theme = {
colorScheme: 'light',
palette: {
default: {
isLowContrast: false,
background: colors.white,
backgroundLight: colors.gray2,
text: colors.black,
textLight: colors.gray5,
textInverted: colors.white,
link: colors.blue3,
border: colors.gray3,
icon: colors.gray2,
},
primary: {
isLowContrast: true,
background: colors.blue3,
backgroundLight: colors.blue2,
text: colors.white,
textLight: colors.blue0,
textInverted: colors.blue3,
link: colors.blue0,
border: colors.blue4,
icon: colors.blue4,
},
secondary: {
isLowContrast: true,
background: colors.green3,
backgroundLight: colors.green2,
text: colors.white,
textLight: colors.green1,
textInverted: colors.green4,
link: colors.green1,
border: colors.green4,
icon: colors.green4,
},
inverted: {
isLowContrast: true,
background: colors.black,
backgroundLight: colors.gray6,
text: colors.white,
textLight: colors.gray3,
textInverted: colors.black,
link: colors.blue2,
border: colors.gray3,
icon: colors.gray5,
},
error: {
isLowContrast: true,
background: colors.red3,
backgroundLight: colors.red2,
text: colors.white,
textLight: colors.red1,
textInverted: colors.red3,
link: colors.red1,
border: colors.red4,
icon: colors.red4,
},
},
shapes: {
button: {
// TODO
},
bigButton: {
// TODO
},
smallButton: {
// TODO
},
},
typography: {
h1: {
fontSize: 48,
fontWeight: '500',
},
h2: {
fontSize: 34,
letterSpacing: 0.25,
fontWeight: '500',
},
h3: {
fontSize: 24,
fontWeight: '500',
},
h4: {
fontWeight: '500',
fontSize: 20,
letterSpacing: 0.15,
},
subtitle1: {
fontSize: 16,
letterSpacing: 0.15,
},
subtitle2: {
fontWeight: '500',
fontSize: 14,
letterSpacing: 0.1,
},
body1: {
fontSize: 16,
letterSpacing: 0.5,
},
body2: {
fontSize: 14,
letterSpacing: 0.25,
},
button: {
fontWeight: '500',
fontSize: 14,
letterSpacing: 0.5,
},
caption: {
fontSize: 12,
letterSpacing: 0.4,
},
overline: {
fontSize: 10,
letterSpacing: 1.5,
textTransform: 'uppercase',
},
},
}
export const darkTheme: Theme = {
...defaultTheme,
colorScheme: 'dark',
palette: {
...defaultTheme.palette,
default: {
isLowContrast: true,
background: colors.black,
backgroundLight: colors.gray6,
text: colors.white,
textLight: colors.gray3,
textInverted: colors.black,
link: colors.blue2,
border: colors.gray3,
icon: colors.gray5,
},
primary: {
...defaultTheme.palette.primary,
textInverted: colors.blue2,
},
secondary: {
...defaultTheme.palette.secondary,
textInverted: colors.green2,
},
inverted: {
isLowContrast: false,
background: colors.white,
backgroundLight: colors.gray2,
text: colors.black,
textLight: colors.gray5,
textInverted: colors.white,
link: colors.blue3,
border: colors.gray3,
icon: colors.gray1,
},
},
}

View File

@ -1,2 +0,0 @@
export const FAB = 1
export const BASE = 0

View File

@ -15,6 +15,7 @@ import {ProfileFollowers} from './screens/ProfileFollowers'
import {ProfileFollows} from './screens/ProfileFollows'
import {ProfileMembers} from './screens/ProfileMembers'
import {Settings} from './screens/Settings'
import {Debug} from './screens/Debug'
export type ScreenParams = {
navIdx: [number, number]
@ -71,6 +72,7 @@ export const routes: Route[] = [
'retweet',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/reposted-by'),
],
[Debug, 'Debug', 'house', r('/debug')],
]
export function match(url: string): MatchResult {

View File

@ -3,11 +3,11 @@ import {StyleSheet, TextInput, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
import {Selector} from '../com/util/Selector'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {colors} from '../lib/styles'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
import {useAnimatedValue} from '../lib/useAnimatedValue'
import {useAnimatedValue} from '../lib/hooks/useAnimatedValue'
export const Contacts = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores()

View File

@ -0,0 +1,432 @@
import React from 'react'
import {ScrollView, View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader'
import {ThemeProvider} from '../lib/ThemeContext'
import {PaletteColorName} from '../lib/ThemeContext'
import {usePalette} from '../lib/hooks/usePalette'
import {Text} from '../com/util/text/Text'
import {ViewSelector} from '../com/util/ViewSelector'
import {EmptyState} from '../com/util/EmptyState'
import * as LoadingPlaceholder from '../com/util/LoadingPlaceholder'
import {Button} from '../com/util/forms/Button'
import {DropdownButton, DropdownItem} from '../com/util/forms/DropdownButton'
import {ToggleButton} from '../com/util/forms/ToggleButton'
import {RadioGroup} from '../com/util/forms/RadioGroup'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
const MAIN_VIEWS = ['Base', 'Controls', 'Error']
export const Debug = () => {
const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>(
'light',
)
const onToggleColorScheme = () => {
setColorScheme(colorScheme === 'light' ? 'dark' : 'light')
}
return (
<ThemeProvider theme={colorScheme}>
<DebugInner
colorScheme={colorScheme}
onToggleColorScheme={onToggleColorScheme}
/>
</ThemeProvider>
)
}
function DebugInner({
colorScheme,
onToggleColorScheme,
}: {
colorScheme: 'light' | 'dark'
onToggleColorScheme: () => void
}) {
const [currentView, setCurrentView] = React.useState<number>(0)
const pal = usePalette('default')
const renderItem = item => {
return (
<View>
<View style={{paddingTop: 10, paddingHorizontal: 10}}>
<ToggleButton
type="default-light"
onPress={onToggleColorScheme}
isSelected={colorScheme === 'dark'}
label="Dark mode"
/>
</View>
{item.currentView === 2 ? (
<ErrorView key="error" />
) : item.currentView === 1 ? (
<ControlsView key="controls" />
) : (
<BaseView key="base" />
)}
</View>
)
}
const items = [{currentView}]
return (
<View style={[{flex: 1}, pal.view]}>
<ViewHeader title="Debug panel" />
<ViewSelector
swipeEnabled
sections={MAIN_VIEWS}
items={items}
renderItem={renderItem}
onSelectView={setCurrentView}
/>
</View>
)
}
function Heading({label}: {label: string}) {
const pal = usePalette('default')
return (
<View style={{paddingTop: 10, paddingBottom: 5}}>
<Text type="h3" style={pal.text}>
{label}
</Text>
</View>
)
}
function BaseView() {
return (
<View style={{paddingHorizontal: 10}}>
<Heading label="Palettes" />
<PaletteView palette="default" />
<PaletteView palette="primary" />
<PaletteView palette="secondary" />
<PaletteView palette="inverted" />
<PaletteView palette="error" />
<Heading label="Typography" />
<TypographyView />
<Heading label="Empty state" />
<EmptyStateView />
<Heading label="Loading placeholders" />
<LoadingPlaceholderView />
<View style={{height: 200}} />
</View>
)
}
function ControlsView() {
return (
<ScrollView style={{paddingHorizontal: 10}}>
<Heading label="Buttons" />
<ButtonsView />
<Heading label="Dropdown Buttons" />
<DropdownButtonsView />
<Heading label="Toggle Buttons" />
<ToggleButtonsView />
<Heading label="Radio Buttons" />
<RadioButtonsView />
<View style={{height: 200}} />
</ScrollView>
)
}
function ErrorView() {
return (
<View style={{padding: 10}}>
<View style={{marginBottom: 5}}>
<ErrorScreen
title="Error screen"
message="A major error occurred that led the entire screen to fail"
details="Here are some details"
onPressTryAgain={() => {}}
/>
</View>
<View style={{marginBottom: 5}}>
<ErrorMessage message="This is an error that occurred while things were being done" />
</View>
<View style={{marginBottom: 5}}>
<ErrorMessage
message="This is an error that occurred while things were being done"
numberOfLines={1}
/>
</View>
<View style={{marginBottom: 5}}>
<ErrorMessage
message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}}
/>
</View>
<View style={{marginBottom: 5}}>
<ErrorMessage
message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}}
numberOfLines={1}
/>
</View>
</View>
)
}
function PaletteView({palette}: {palette: PaletteColorName}) {
const defaultPal = usePalette('default')
const pal = usePalette(palette)
return (
<View
style={[
pal.view,
pal.border,
{
padding: 10,
marginBottom: 5,
},
]}>
<Text style={[pal.text]}>{palette} colors</Text>
<Text style={[pal.textLight]}>Light text</Text>
<Text style={[pal.link]}>Link text</Text>
{palette !== 'default' && (
<View style={[defaultPal.view]}>
<Text style={[pal.textInverted]}>Inverted text</Text>
</View>
)}
</View>
)
}
function TypographyView() {
const pal = usePalette('default')
return (
<View style={[pal.view]}>
<Text type="h1" style={[pal.text]}>
Heading 1
</Text>
<Text type="h2" style={[pal.text]}>
Heading 2
</Text>
<Text type="h3" style={[pal.text]}>
Heading 3
</Text>
<Text type="h4" style={[pal.text]}>
Heading 4
</Text>
<Text type="subtitle1" style={[pal.text]}>
Subtitle 1
</Text>
<Text type="subtitle2" style={[pal.text]}>
Subtitle 2
</Text>
<Text type="body1" style={[pal.text]}>
Body 1
</Text>
<Text type="body2" style={[pal.text]}>
Body 2
</Text>
<Text type="button" style={[pal.text]}>
Button
</Text>
<Text type="caption" style={[pal.text]}>
Caption
</Text>
<Text type="overline" style={[pal.text]}>
Overline
</Text>
</View>
)
}
function EmptyStateView() {
return <EmptyState icon="bars" message="This is an empty state" />
}
function LoadingPlaceholderView() {
return (
<>
<LoadingPlaceholder.PostLoadingPlaceholder />
<LoadingPlaceholder.NotificationLoadingPlaceholder />
</>
)
}
function ButtonsView() {
const defaultPal = usePalette('default')
const buttonStyles = {marginRight: 5}
return (
<View style={[defaultPal.view]}>
<View
style={{
flexDirection: 'row',
marginBottom: 5,
}}>
<Button type="primary" label="Primary solid" style={buttonStyles} />
<Button type="secondary" label="Secondary solid" style={buttonStyles} />
<Button type="inverted" label="Inverted solid" style={buttonStyles} />
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-outline"
label="Primary outline"
style={buttonStyles}
/>
<Button
type="secondary-outline"
label="Secondary outline"
style={buttonStyles}
/>
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-light"
label="Primary light"
style={buttonStyles}
/>
<Button
type="secondary-light"
label="Secondary light"
style={buttonStyles}
/>
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="default-light"
label="Default light"
style={buttonStyles}
/>
</View>
</View>
)
}
const DROPDOWN_ITEMS: DropdownItem[] = [
{
icon: ['far', 'paste'],
label: 'Copy post text',
onPress() {},
},
{
icon: 'share',
label: 'Share...',
onPress() {},
},
{
icon: 'circle-exclamation',
label: 'Report post',
onPress() {},
},
]
function DropdownButtonsView() {
const defaultPal = usePalette('default')
return (
<View style={[defaultPal.view]}>
<View
style={{
marginBottom: 5,
}}>
<DropdownButton
type="primary"
items={DROPDOWN_ITEMS}
menuWidth={200}
label="Primary button"
/>
</View>
<View
style={{
marginBottom: 5,
}}>
<DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}>
<Text>Bare</Text>
</DropdownButton>
</View>
</View>
)
}
function ToggleButtonsView() {
const defaultPal = usePalette('default')
const buttonStyles = {marginBottom: 5}
const [isSelected, setIsSelected] = React.useState(false)
const onToggle = () => setIsSelected(!isSelected)
return (
<View style={[defaultPal.view]}>
<ToggleButton
type="primary"
label="Primary solid"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="secondary"
label="Secondary solid"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="inverted"
label="Inverted solid"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="primary-outline"
label="Primary outline"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="secondary-outline"
label="Secondary outline"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="primary-light"
label="Primary light"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="secondary-light"
label="Secondary light"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
<ToggleButton
type="default-light"
label="Default light"
style={buttonStyles}
isSelected={isSelected}
onPress={onToggle}
/>
</View>
)
}
const RADIO_BUTTON_ITEMS = [
{key: 'default-light', label: 'Default Light'},
{key: 'primary', label: 'Primary'},
{key: 'secondary', label: 'Secondary'},
{key: 'inverted', label: 'Inverted'},
{key: 'primary-outline', label: 'Primary Outline'},
{key: 'secondary-outline', label: 'Secondary Outline'},
{key: 'primary-light', label: 'Primary Light'},
{key: 'secondary-light', label: 'Secondary Light'},
]
function RadioButtonsView() {
const defaultPal = usePalette('default')
const [rgType, setRgType] = React.useState('default-light')
return (
<View style={[defaultPal.view]}>
<RadioGroup
type={rgType}
items={RADIO_BUTTON_ITEMS}
initialSelection="default-light"
onSelect={setRgType}
/>
</View>
)
}

View File

@ -6,11 +6,11 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/posts/Feed'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {useStores} from '../../state'
import {ScreenParams} from '../routes'
import {s, colors} from '../lib/styles'
import {useOnMainScroll} from '../lib/useOnMainScroll'
import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
import {clamp} from 'lodash'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}

View File

@ -10,7 +10,7 @@ import {observer} from 'mobx-react-lite'
import {Signin} from '../com/login/Signin'
import {Logo} from '../com/login/Logo'
import {CreateAccount} from '../com/login/CreateAccount'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {s, colors} from '../lib/styles'
enum ScreenState {

View File

@ -1,7 +1,7 @@
import React from 'react'
import {Button, View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {useStores} from '../../state'
export const NotFound = () => {

View File

@ -4,7 +4,7 @@ import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed'
import {useStores} from '../../state'
import {ScreenParams} from '../routes'
import {useOnMainScroll} from '../lib/useOnMainScroll'
import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
export const Notifications = ({navIdx, visible}: ScreenParams) => {
const store = useStores()

View File

@ -12,14 +12,14 @@ import {ProfileHeader} from '../com/profile/ProfileHeader'
import {FeedItem} from '../com/posts/FeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
import {ErrorScreen} from '../com/util/ErrorScreen'
import {ErrorMessage} from '../com/util/ErrorMessage'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {EmptyState} from '../com/util/EmptyState'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {ViewHeader} from '../com/util/ViewHeader'
import * as Toast from '../com/util/Toast'
import {s, colors} from '../lib/styles'
import {useOnMainScroll} from '../lib/useOnMainScroll'
import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
const LOADING_ITEM = {_reactKey: '__loading__'}
const END_ITEM = {_reactKey: '__end__'}
@ -116,7 +116,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
renderItem = (item: any) => (
<View style={s.p5}>
<ErrorMessage
dark
message={item.error}
onPressTryAgain={onPressTryAgain}
/>

View File

@ -10,7 +10,7 @@ import {
import {ViewHeader} from '../com/util/ViewHeader'
import {SuggestedFollows} from '../com/discover/SuggestedFollows'
import {UserAvatar} from '../com/util/UserAvatar'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view'

View File

@ -6,7 +6,7 @@ import {ScreenParams} from '../routes'
import {s, colors} from '../lib/styles'
import {ViewHeader} from '../com/util/ViewHeader'
import {Link} from '../com/util/Link'
import {Text} from '../com/util/Text'
import {Text} from '../com/util/text/Text'
import {UserAvatar} from '../com/util/UserAvatar'
export const Settings = observer(function Settings({
@ -57,6 +57,9 @@ export const Settings = observer(function Settings({
</View>
</View>
</Link>
<Link href="/debug" title="Debug tools">
<Text style={s.blue3}>Debug tools</Text>
</Link>
</View>
</View>
)

View File

@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
import {ComposePost} from '../../com/composer/ComposePost'
import {ComposerOpts} from '../../../state/models/shell-ui'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
export const Composer = observer(
({

View File

@ -17,7 +17,7 @@ import {
MagnifyingGlassIcon,
} from '../../lib/icons'
import {UserAvatar} from '../../com/util/UserAvatar'
import {Text} from '../../com/util/Text'
import {Text} from '../../com/util/text/Text'
import {CreateSceneModal} from '../../../state/models/shell-ui'
export const Menu = ({

View File

@ -10,13 +10,13 @@ import {
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../../com/util/Text'
import {Text} from '../../com/util/text/Text'
import Swipeable from 'react-native-gesture-handler/Swipeable'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
import {toShareUrl} from '../../../lib/strings'
import {match} from '../../routes'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
const TAB_HEIGHT = 42

View File

@ -29,7 +29,7 @@ import {Onboard} from '../../screens/Onboard'
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
import {Modal} from '../../com/modals/Modal'
import {Lightbox} from '../../com/lightbox/Lightbox'
import {Text} from '../../com/util/Text'
import {Text} from '../../com/util/text/Text'
import {TabsSelector} from './TabsSelector'
import {Composer} from './Composer'
import {s, colors} from '../../lib/styles'
@ -42,7 +42,7 @@ import {
BellIcon,
BellIconSolid,
} from '../../lib/icons'
import {useAnimatedValue} from '../../lib/useAnimatedValue'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
const Btn = ({
icon,