diff --git a/src/App.native.tsx b/src/App.native.tsx index a532a08d..fa523cd8 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -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() { - - - + + + + + diff --git a/src/lib/functions.ts b/src/lib/functions.ts new file mode 100644 index 00000000..d6fbf5b9 --- /dev/null +++ b/src/lib/functions.ts @@ -0,0 +1,6 @@ +export function choose>( + value: keyof T, + choices: T, +): U { + return choices[value] +} diff --git a/src/view/com/composer/Autocomplete.tsx b/src/view/com/composer/Autocomplete.tsx index 1637108f..b151e0d9 100644 --- a/src/view/com/composer/Autocomplete.tsx +++ b/src/view/com/composer/Autocomplete.tsx @@ -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 { diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index baa93110..fe310f19 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -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' diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index 2b1559df..ec63d950 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -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, diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index b78bae88..77bd94d5 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -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' diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 36c51764..849354ae 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -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' diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx index f97eb7a0..689a4f38 100644 --- a/src/view/com/login/CreateAccount.tsx +++ b/src/view/com/login/CreateAccount.tsx @@ -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, diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx index 45728d3b..f76507d7 100644 --- a/src/view/com/login/Signin.tsx +++ b/src/view/com/login/Signin.tsx @@ -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' diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index a18043f1..7545e36a 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -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%'] diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx index 0d47aa4b..60c24054 100644 --- a/src/view/com/modals/CreateScene.tsx +++ b/src/view/com/modals/CreateScene.tsx @@ -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 { diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index bd97ced5..8a3f016a 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -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' diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx index 28380b6a..a7344017 100644 --- a/src/view/com/modals/InviteToScene.tsx +++ b/src/view/com/modals/InviteToScene.tsx @@ -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' diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx index 582e2423..bf4d5f5a 100644 --- a/src/view/com/modals/ReportAccount.tsx +++ b/src/view/com/modals/ReportAccount.tsx @@ -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'}, diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 6f134032..d4684069 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -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'}, diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index 0d1e0e91..884fb91e 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -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 { diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index c986bca5..91a01db4 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -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 && } {view.hasError && ( } {feed.hasError && ( )} {dropdownItems?.length ? ( - - + ) : undefined} diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx index 251ece41..0e34865b 100644 --- a/src/view/com/profile/ProfileMembers.tsx +++ b/src/view/com/profile/ProfileMembers.tsx @@ -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 ( }) { + const pal = usePalette('default') return ( {icon === 'user-group' ? ( ) : ( - + )} - {message} + + {message} + ) } @@ -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, }, }) diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx deleted file mode 100644 index b87b77ba..00000000 --- a/src/view/com/util/ErrorMessage.tsx +++ /dev/null @@ -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 - onPressTryAgain?: () => void -}) { - const inner = ( - <> - - - - - {message} - - {onPressTryAgain && ( - - - - )} - - ) - if (dark) { - return ( - - {inner} - - ) - } - return {inner} -} - -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, - }, -}) diff --git a/src/view/com/util/FloatingActionButton.tsx b/src/view/com/util/FloatingActionButton.tsx deleted file mode 100644 index 21c4fba6..00000000 --- a/src/view/com/util/FloatingActionButton.tsx +++ /dev/null @@ -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 ( - - - - - - - - ) -} - -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: {}, -}) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 2bb55357..05573d99 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -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 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} ) diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 9c2d0398..15488167 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -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 }) { + const theme = useTheme() return ( @@ -41,6 +43,7 @@ export function PostLoadingPlaceholder({ }: { style?: StyleProp }) { + const theme = useTheme() return ( @@ -52,16 +55,24 @@ export function PostLoadingPlaceholder({ - + - + @@ -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, diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx index 84a627b6..208ec049 100644 --- a/src/view/com/util/Picker.tsx +++ b/src/view/com/util/Picker.tsx @@ -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 { diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 26421076..ac10e92f 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -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 diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx index 1c980465..839110a2 100644 --- a/src/view/com/util/PostEmbeds.tsx +++ b/src/view/com/util/PostEmbeds.tsx @@ -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' diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 77dfbb48..fae3a4c8 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -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' diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx index 954360b3..211c5b90 100644 --- a/src/view/com/util/Selector.tsx +++ b/src/view/com/util/Selector.tsx @@ -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 { diff --git a/src/view/com/util/Text.tsx b/src/view/com/util/Text.tsx deleted file mode 100644 index acf7589e..00000000 --- a/src/view/com/util/Text.tsx +++ /dev/null @@ -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) { - return ( - - {children} - - ) -} diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index cdd1f4d9..f5ed07d6 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -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' diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index e14c2412..c6eaba5d 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -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' diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index e436e41b..a29ee9d2 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -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__'} diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx new file mode 100644 index 00000000..905268d3 --- /dev/null +++ b/src/view/com/util/error/ErrorMessage.tsx @@ -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 + onPressTryAgain?: () => void +}) { + const theme = useTheme() + const pal = usePalette('error') + return ( + + + + + + {message} + + {onPressTryAgain && ( + + + + )} + + ) +} + +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, + }, +}) diff --git a/src/view/com/util/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx similarity index 56% rename from src/view/com/util/ErrorScreen.tsx rename to src/view/com/util/error/ErrorScreen.tsx index d0e1e275..6db54a9f 100644 --- a/src/view/com/util/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -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 ( - + - + - {title} - {message} - {details && {details}} + + {title} + + {message} + {details && ( + + {details} + + )} {onPressTryAgain && ( - - - Try again + + + + Try again + )} @@ -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, diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx new file mode 100644 index 00000000..b5c4da19 --- /dev/null +++ b/src/view/com/util/forms/Button.tsx @@ -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 + onPress?: () => void +}>) { + const theme = useTheme() + const outerStyle = choose>(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>(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 ( + + {label ? ( + + {label} + + ) : ( + children + )} + + ) +} + +const styles = StyleSheet.create({ + outer: { + paddingHorizontal: 10, + paddingVertical: 8, + }, +}) diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/forms/DropdownButton.tsx similarity index 81% rename from src/view/com/util/DropdownBtn.tsx rename to src/view/com/util/forms/DropdownButton.tsx index 3c642193..c81ccf6c 100644 --- a/src/view/com/util/DropdownBtn.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -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 items: DropdownItem[] + label?: string menuWidth?: number children?: React.ReactNode }) { @@ -62,14 +69,23 @@ export function DropdownBtn({ ) } + if (type === 'bare') { + return ( + + {children} + + ) + } return ( - - {children} - + + + ) } @@ -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 ( - + {children} - + ) } diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx index 9da404be..81489c44 100644 --- a/src/view/com/util/forms/RadioButton.tsx +++ b/src/view/com/util/forms/RadioButton.tsx @@ -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 onPress: () => void }) { + const theme = useTheme() + const circleStyle = choose>(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>( + 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>(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 ( - - - {isSelected ? : undefined} + ) } @@ -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, }, }) diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx index 6684cde5..9abc2345 100644 --- a/src/view/com/util/forms/RadioGroup.tsx +++ b/src/view/com/util/forms/RadioGroup.tsx @@ -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('') + const [selection, setSelection] = useState(initialSelection) const onSelectInner = (key: string) => { setSelection(key) onSelect(key) } return ( - {items.map(item => ( + {items.map((item, i) => ( onSelectInner(item.key)} diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx new file mode 100644 index 00000000..77e8fa20 --- /dev/null +++ b/src/view/com/util/forms/ToggleButton.tsx @@ -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 + onPress: () => void +}) { + const theme = useTheme() + const circleStyle = choose>(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>( + 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>(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 ( + + ) +} + +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, + }, +}) diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 05425eb3..9de443b7 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -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 diff --git a/src/view/com/util/RichText.tsx b/src/view/com/util/text/RichText.tsx similarity index 81% rename from src/view/com/util/RichText.tsx rename to src/view/com/util/text/RichText.tsx index d6f193f9..c9ed4b58 100644 --- a/src/view/com/util/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -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 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( , ) } else if (segment.entity.type === 'link') { els.push( , ) } @@ -66,7 +73,7 @@ export function RichText({ key++ } return ( - + {els} ) diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx new file mode 100644 index 00000000..549eb290 --- /dev/null +++ b/src/view/com/util/text/Text.tsx @@ -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) { + const theme = useTheme() + const typography = theme.typography[type] + return ( + + {children} + + ) +} diff --git a/src/view/lib/ThemeContext.tsx b/src/view/lib/ThemeContext.tsx new file mode 100644 index 00000000..57f758c5 --- /dev/null +++ b/src/view/lib/ThemeContext.tsx @@ -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 + +export type ShapeName = 'button' | 'bigButton' | 'smallButton' +export type Shapes = Record + +export type TypographyVariant = + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'subtitle1' + | 'subtitle2' + | 'body1' + | 'body2' + | 'button' + | 'caption' + | 'overline' +export type Typography = Record + +export interface Theme { + colorScheme: ColorScheme + palette: Palette + shapes: Shapes + typography: Typography +} + +export interface ThemeProviderProps { + theme?: ColorScheme +} + +export const ThemeContext = createContext(defaultTheme) + +export const useTheme = () => useContext(ThemeContext) + +export const ThemeProvider: React.FC = ({ + theme, + children, +}) => { + const colorScheme = useColorScheme() + + const value = useMemo( + () => ((theme || colorScheme) === 'dark' ? darkTheme : defaultTheme), + [colorScheme, theme], + ) + + return {children} +} diff --git a/src/view/lib/useAnimatedValue.ts b/src/view/lib/hooks/useAnimatedValue.ts similarity index 100% rename from src/view/lib/useAnimatedValue.ts rename to src/view/lib/hooks/useAnimatedValue.ts diff --git a/src/view/lib/useOnMainScroll.ts b/src/view/lib/hooks/useOnMainScroll.ts similarity index 94% rename from src/view/lib/useOnMainScroll.ts rename to src/view/lib/hooks/useOnMainScroll.ts index ee008122..c3c16ff8 100644 --- a/src/view/lib/useOnMainScroll.ts +++ b/src/view/lib/hooks/useOnMainScroll.ts @@ -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, diff --git a/src/view/lib/hooks/usePalette.ts b/src/view/lib/hooks/usePalette.ts new file mode 100644 index 00000000..e9af4ae1 --- /dev/null +++ b/src/view/lib/hooks/usePalette.ts @@ -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, + }, + } +} diff --git a/src/view/lib/themes.ts b/src/view/lib/themes.ts new file mode 100644 index 00000000..3851ee9d --- /dev/null +++ b/src/view/lib/themes.ts @@ -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, + }, + }, +} diff --git a/src/view/lib/z-index.ts b/src/view/lib/z-index.ts deleted file mode 100644 index 872027d3..00000000 --- a/src/view/lib/z-index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const FAB = 1 -export const BASE = 0 diff --git a/src/view/routes.ts b/src/view/routes.ts index 272a1b09..3717e0f0 100644 --- a/src/view/routes.ts +++ b/src/view/routes.ts @@ -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/(?[^/]+)/post/(?[^/]+)/reposted-by'), ], + [Debug, 'Debug', 'house', r('/debug')], ] export function match(url: string): MatchResult { diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index bcfb4778..8de56d79 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -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() diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx new file mode 100644 index 00000000..f34bcc17 --- /dev/null +++ b/src/view/screens/Debug.tsx @@ -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 ( + + + + ) +} + +function DebugInner({ + colorScheme, + onToggleColorScheme, +}: { + colorScheme: 'light' | 'dark' + onToggleColorScheme: () => void +}) { + const [currentView, setCurrentView] = React.useState(0) + const pal = usePalette('default') + + const renderItem = item => { + return ( + + + + + {item.currentView === 2 ? ( + + ) : item.currentView === 1 ? ( + + ) : ( + + )} + + ) + } + + const items = [{currentView}] + + return ( + + + + + ) +} + +function Heading({label}: {label: string}) { + const pal = usePalette('default') + return ( + + + {label} + + + ) +} + +function BaseView() { + return ( + + + + + + + + + + + + + + + + ) +} + +function ControlsView() { + return ( + + + + + + + + + + + + ) +} + +function ErrorView() { + return ( + + + {}} + /> + + + + + + + + + {}} + /> + + + {}} + numberOfLines={1} + /> + + + ) +} + +function PaletteView({palette}: {palette: PaletteColorName}) { + const defaultPal = usePalette('default') + const pal = usePalette(palette) + return ( + + {palette} colors + Light text + Link text + {palette !== 'default' && ( + + Inverted text + + )} + + ) +} + +function TypographyView() { + const pal = usePalette('default') + return ( + + + Heading 1 + + + Heading 2 + + + Heading 3 + + + Heading 4 + + + Subtitle 1 + + + Subtitle 2 + + + Body 1 + + + Body 2 + + + Button + + + Caption + + + Overline + + + ) +} + +function EmptyStateView() { + return +} + +function LoadingPlaceholderView() { + return ( + <> + + + + ) +} + +function ButtonsView() { + const defaultPal = usePalette('default') + const buttonStyles = {marginRight: 5} + return ( + + +