about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-12-28 14:06:01 -0600
committerGitHub <noreply@github.com>2022-12-28 14:06:01 -0600
commit7e31645e9a355f2a0b3c8d62430a53dbb4714674 (patch)
tree24db1b09b9065472f5c7e08f9e2798d63fee8b1a
parentcc63660982199a989859d3b5328ba43a4edec755 (diff)
downloadvoidsky-7e31645e9a355f2a0b3c8d62430a53dbb4714674.tar.zst
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
-rw-r--r--src/App.native.tsx9
-rw-r--r--src/lib/functions.ts6
-rw-r--r--src/view/com/composer/Autocomplete.tsx4
-rw-r--r--src/view/com/composer/ComposePost.tsx2
-rw-r--r--src/view/com/composer/Prompt.tsx2
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx4
-rw-r--r--src/view/com/lightbox/Lightbox.tsx2
-rw-r--r--src/view/com/login/CreateAccount.tsx2
-rw-r--r--src/view/com/login/Signin.tsx2
-rw-r--r--src/view/com/modals/Confirm.tsx4
-rw-r--r--src/view/com/modals/CreateScene.tsx4
-rw-r--r--src/view/com/modals/EditProfile.tsx4
-rw-r--r--src/view/com/modals/InviteToScene.tsx4
-rw-r--r--src/view/com/modals/ReportAccount.tsx4
-rw-r--r--src/view/com/modals/ReportPost.tsx4
-rw-r--r--src/view/com/modals/ServerInput.tsx2
-rw-r--r--src/view/com/notifications/Feed.tsx5
-rw-r--r--src/view/com/notifications/FeedItem.tsx4
-rw-r--r--src/view/com/notifications/InviteAccepter.tsx2
-rw-r--r--src/view/com/onboard/FeatureExplainer.tsx2
-rw-r--r--src/view/com/onboard/Follows.tsx2
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx5
-rw-r--r--src/view/com/post-thread/PostThread.tsx3
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx6
-rw-r--r--src/view/com/post-thread/PostVotedBy.tsx5
-rw-r--r--src/view/com/post/Post.tsx4
-rw-r--r--src/view/com/post/PostText.tsx4
-rw-r--r--src/view/com/posts/Feed.tsx5
-rw-r--r--src/view/com/posts/FeedItem.tsx4
-rw-r--r--src/view/com/profile/ProfileCard.tsx2
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx5
-rw-r--r--src/view/com/profile/ProfileFollows.tsx5
-rw-r--r--src/view/com/profile/ProfileHeader.tsx10
-rw-r--r--src/view/com/profile/ProfileMembers.tsx3
-rw-r--r--src/view/com/util/EmptyState.tsx18
-rw-r--r--src/view/com/util/ErrorMessage.tsx102
-rw-r--r--src/view/com/util/FloatingActionButton.tsx57
-rw-r--r--src/view/com/util/Link.tsx9
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx25
-rw-r--r--src/view/com/util/Picker.tsx4
-rw-r--r--src/view/com/util/PostCtrls.tsx4
-rw-r--r--src/view/com/util/PostEmbeds.tsx2
-rw-r--r--src/view/com/util/PostMeta.tsx4
-rw-r--r--src/view/com/util/Selector.tsx2
-rw-r--r--src/view/com/util/Text.tsx15
-rw-r--r--src/view/com/util/UserInfoText.tsx2
-rw-r--r--src/view/com/util/ViewHeader.tsx2
-rw-r--r--src/view/com/util/ViewSelector.tsx4
-rw-r--r--src/view/com/util/error/ErrorMessage.tsx76
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx (renamed from src/view/com/util/ErrorScreen.tsx)67
-rw-r--r--src/view/com/util/forms/Button.tsx120
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx (renamed from src/view/com/util/DropdownBtn.tsx)49
-rw-r--r--src/view/com/util/forms/RadioButton.tsx135
-rw-r--r--src/view/com/util/forms/RadioGroup.tsx11
-rw-r--r--src/view/com/util/forms/ToggleButton.tsx165
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx2
-rw-r--r--src/view/com/util/text/RichText.tsx (renamed from src/view/com/util/RichText.tsx)19
-rw-r--r--src/view/com/util/text/Text.tsx23
-rw-r--r--src/view/lib/ThemeContext.tsx70
-rw-r--r--src/view/lib/hooks/useAnimatedValue.ts (renamed from src/view/lib/useAnimatedValue.ts)0
-rw-r--r--src/view/lib/hooks/useOnMainScroll.ts (renamed from src/view/lib/useOnMainScroll.ts)2
-rw-r--r--src/view/lib/hooks/usePalette.ts41
-rw-r--r--src/view/lib/themes.ts163
-rw-r--r--src/view/lib/z-index.ts2
-rw-r--r--src/view/routes.ts2
-rw-r--r--src/view/screens/Contacts.tsx4
-rw-r--r--src/view/screens/Debug.tsx432
-rw-r--r--src/view/screens/Home.tsx4
-rw-r--r--src/view/screens/Login.tsx2
-rw-r--r--src/view/screens/NotFound.tsx2
-rw-r--r--src/view/screens/Notifications.tsx2
-rw-r--r--src/view/screens/Profile.tsx9
-rw-r--r--src/view/screens/Search.tsx2
-rw-r--r--src/view/screens/Settings.tsx5
-rw-r--r--src/view/shell/mobile/Composer.tsx2
-rw-r--r--src/view/shell/mobile/Menu.tsx2
-rw-r--r--src/view/shell/mobile/TabsSelector.tsx4
-rw-r--r--src/view/shell/mobile/index.tsx4
78 files changed, 1431 insertions, 375 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index a532a08d4..fa523cd81 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() {
     <GestureHandlerRootView style={{flex: 1}}>
       <RootSiblingParent>
         <RootStoreProvider value={rootStore}>
-          <SafeAreaProvider>
-            <MobileShell />
-          </SafeAreaProvider>
+          <ThemeProvider>
+            <SafeAreaProvider>
+              <MobileShell />
+            </SafeAreaProvider>
+          </ThemeProvider>
         </RootStoreProvider>
       </RootSiblingParent>
     </GestureHandlerRootView>
diff --git a/src/lib/functions.ts b/src/lib/functions.ts
new file mode 100644
index 000000000..d6fbf5b92
--- /dev/null
+++ b/src/lib/functions.ts
@@ -0,0 +1,6 @@
+export function choose<U, T extends Record<string, U>>(
+  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 1637108f8..b151e0d9e 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 baa931105..fe310f19b 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 2b1559df4..ec63d9501 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 b78bae889..77bd94d5a 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 36c51764f..849354aea 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 f97eb7a0f..689a4f384 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 45728d3b3..f76507d71 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 a18043f1a..7545e36a6 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 0d47aa4bd..60c240546 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 bd97ced53..8a3f016ad 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 28380b6a8..a73440179 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 582e24238..bf4d5f5a0 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 6f134032a..d4684069a 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 0d1e0e911..884fb91e6 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 c986bca57..91a01db4d 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 && <NotificationFeedLoadingPlaceholder />}
       {view.hasError && (
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onPressTryAgain}
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 16374a9f9..c6b286cdb 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -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'
diff --git a/src/view/com/notifications/InviteAccepter.tsx b/src/view/com/notifications/InviteAccepter.tsx
index 4df8b2e4f..eefe7a273 100644
--- a/src/view/com/notifications/InviteAccepter.tsx
+++ b/src/view/com/notifications/InviteAccepter.tsx
@@ -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}) {
diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx
index 31863ad50..ecc1b9692 100644
--- a/src/view/com/onboard/FeatureExplainer.tsx
+++ b/src/view/com/onboard/FeatureExplainer.tsx
@@ -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'
diff --git a/src/view/com/onboard/Follows.tsx b/src/view/com/onboard/Follows.tsx
index 4026c879a..76eff3f4b 100644
--- a/src/view/com/onboard/Follows.tsx
+++ b/src/view/com/onboard/Follows.tsx
@@ -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'
 
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 6328b34e7..0efdfe2e4 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -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}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index ecc0d48f5..8c22cc8b7 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -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}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 1a0b0ff87..c39bebbbe 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -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'
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx
index f3773e47b..96a335919 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostVotedBy.tsx
@@ -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}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index d9cc94315..a058acf8e 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -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'
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 90765a836..436768292 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -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'
 
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index e34513794..02141acef 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -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}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 9fd9d46b2..8670ed9a3 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -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'
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 1d5b93a4c..6ce5f29e5 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -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'
 
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 280173f9e..0d088d7a3 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -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}
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 5f8cdd68c..2cd471b05 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -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}
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index c9da0d96c..131c82b75 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -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}>
diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx
index 251ece41a..0e34865b9 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 (
       <View>
         <ErrorMessage
-          dark
           message={view.error}
           style={{margin: 6}}
           onPressTryAgain={onRefresh}
diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx
index 8d98807e3..d9a317fae 100644
--- a/src/view/com/util/EmptyState.tsx
+++ b/src/view/com/util/EmptyState.tsx
@@ -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,
   },
 })
diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx
deleted file mode 100644
index b87b77baa..000000000
--- 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<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,
-  },
-})
diff --git a/src/view/com/util/FloatingActionButton.tsx b/src/view/com/util/FloatingActionButton.tsx
deleted file mode 100644
index 21c4fba6c..000000000
--- 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 (
-    <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: {},
-})
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 2bb553575..05573d999 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<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>
   )
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 9c2d0398f..15488167f 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<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,
diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx
index 84a627b6d..208ec0492 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 264210768..ac10e92fe 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 1c980465a..839110a21 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 77dfbb485..fae3a4c83 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 954360b32..211c5b902 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 acf7589e0..000000000
--- 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<TextProps>) {
-  return (
-    <RNText style={[s.black, style]} {...props}>
-      {children}
-    </RNText>
-  )
-}
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index cdd1f4d91..f5ed07d63 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 e14c2412d..c6eaba5dd 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 e436e41b2..a29ee9d26 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 000000000..905268d3e
--- /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<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,
+  },
+})
diff --git a/src/view/com/util/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index d0e1e2755..6db54a9f2 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 (
-    <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,
diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx
new file mode 100644
index 000000000..b5c4da19d
--- /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<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,
+  },
+})
diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/forms/DropdownButton.tsx
index 3c6421934..c81ccf6c5 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<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>
   )
 }
 
diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx
index 9da404bea..81489c447 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<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,
   },
 })
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 6684cde5c..9abc2345f 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<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)}
diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx
new file mode 100644
index 000000000..77e8fa203
--- /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<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,
+  },
+})
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 05425eb31..9de443b7f 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
index d6f193f9d..c9ed4b58e 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<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>
   )
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
new file mode 100644
index 000000000..549eb2901
--- /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<CustomTextProps>) {
+  const theme = useTheme()
+  const typography = theme.typography[type]
+  return (
+    <RNText style={[s.black, typography, style]} {...props}>
+      {children}
+    </RNText>
+  )
+}
diff --git a/src/view/lib/ThemeContext.tsx b/src/view/lib/ThemeContext.tsx
new file mode 100644
index 000000000..57f758c53
--- /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<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>
+}
diff --git a/src/view/lib/useAnimatedValue.ts b/src/view/lib/hooks/useAnimatedValue.ts
index 1307ef952..1307ef952 100644
--- a/src/view/lib/useAnimatedValue.ts
+++ b/src/view/lib/hooks/useAnimatedValue.ts
diff --git a/src/view/lib/useOnMainScroll.ts b/src/view/lib/hooks/useOnMainScroll.ts
index ee0081226..c3c16ff83 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<NativeScrollEvent>,
diff --git a/src/view/lib/hooks/usePalette.ts b/src/view/lib/hooks/usePalette.ts
new file mode 100644
index 000000000..e9af4ae16
--- /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 000000000..3851ee9d0
--- /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 872027d3f..000000000
--- 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 272a1b096..3717e0f05 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/(?<name>[^/]+)/post/(?<rkey>[^/]+)/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 bcfb47782..8de56d79a 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 000000000..f34bcc17b
--- /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 (
+    <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>
+  )
+}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 834010b0a..118ba9ed8 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -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}
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index 734903d7e..0315e287e 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -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 {
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index 16d75c386..3591b696c 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -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 = () => {
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index fe4a78721..2257dd221 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -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()
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 8d41d9ad1..437f5f4a7 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -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}
           />
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index c909f50e6..4ab1436a6 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -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'
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 1656d3b68..d3fcdfdff 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -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>
   )
diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx
index c586bc871..1a2d2d24d 100644
--- a/src/view/shell/mobile/Composer.tsx
+++ b/src/view/shell/mobile/Composer.tsx
@@ -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(
   ({
diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx
index 8c11e3e8e..a8a81b4dd 100644
--- a/src/view/shell/mobile/Menu.tsx
+++ b/src/view/shell/mobile/Menu.tsx
@@ -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 = ({
diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx
index 41b18a337..71aaa200d 100644
--- a/src/view/shell/mobile/TabsSelector.tsx
+++ b/src/view/shell/mobile/TabsSelector.tsx
@@ -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
 
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 6437d6969..673c0fbe9 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -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,