about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/DebugNew.tsx541
-rw-r--r--src/view/screens/Home.tsx3
-rw-r--r--src/view/screens/PostThread.tsx4
-rw-r--r--src/view/screens/Search/Search.tsx226
-rw-r--r--src/view/screens/Settings.tsx30
-rw-r--r--src/view/screens/Storybook/Breakpoints.tsx25
-rw-r--r--src/view/screens/Storybook/Buttons.tsx124
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx90
-rw-r--r--src/view/screens/Storybook/Forms.tsx215
-rw-r--r--src/view/screens/Storybook/Icons.tsx41
-rw-r--r--src/view/screens/Storybook/Links.tsx48
-rw-r--r--src/view/screens/Storybook/Palette.tsx336
-rw-r--r--src/view/screens/Storybook/Shadows.tsx53
-rw-r--r--src/view/screens/Storybook/Spacing.tsx64
-rw-r--r--src/view/screens/Storybook/Theming.tsx56
-rw-r--r--src/view/screens/Storybook/Typography.tsx30
-rw-r--r--src/view/screens/Storybook/index.tsx78
17 files changed, 1391 insertions, 573 deletions
diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx
deleted file mode 100644
index 0b7c5f03b..000000000
--- a/src/view/screens/DebugNew.tsx
+++ /dev/null
@@ -1,541 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-
-import {useSetColorMode} from '#/state/shell'
-import * as tokens from '#/alf/tokens'
-import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf'
-import {Button, ButtonText} from '#/view/com/Button'
-import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
-
-function ThemeSelector() {
-  const setColorMode = useSetColorMode()
-
-  return (
-    <View style={[a.flex_row, a.gap_md]}>
-      <Button
-        type="secondary"
-        size="small"
-        onPress={() => setColorMode('system')}>
-        System
-      </Button>
-      <Button
-        type="secondary"
-        size="small"
-        onPress={() => setColorMode('light')}>
-        Light
-      </Button>
-      <Button
-        type="secondary"
-        size="small"
-        onPress={() => setColorMode('dark')}>
-        Dark
-      </Button>
-    </View>
-  )
-}
-
-function BreakpointDebugger() {
-  const t = useTheme()
-  const breakpoints = useBreakpoints()
-
-  return (
-    <View>
-      <H3 style={[a.pb_md]}>Breakpoint Debugger</H3>
-      <Text style={[a.pb_md]}>
-        Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>}
-        {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>}
-        {breakpoints.gtTablet && <Text>desktop</Text>}
-      </Text>
-      <Text
-        style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}>
-        {JSON.stringify(breakpoints, null, 2)}
-      </Text>
-    </View>
-  )
-}
-
-function ThemedSection() {
-  const t = useTheme()
-
-  return (
-    <View style={[t.atoms.bg, a.gap_md, a.p_xl]}>
-      <H3 style={[a.font_bold]}>theme.atoms.text</H3>
-      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
-      <H3 style={[a.font_bold, t.atoms.text_contrast_700]}>
-        theme.atoms.text_contrast_700
-      </H3>
-      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
-      <H3 style={[a.font_bold, t.atoms.text_contrast_500]}>
-        theme.atoms.text_contrast_500
-      </H3>
-      <View style={[a.flex_1, t.atoms.border_contrast_500, a.border_t]} />
-
-      <View style={[a.flex_row, a.gap_md]}>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg</Text>
-        </View>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg_contrast_100,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg_contrast_100</Text>
-        </View>
-      </View>
-      <View style={[a.flex_row, a.gap_md]}>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg_contrast_200,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg_contrast_200</Text>
-        </View>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg_contrast_300,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg_contrast_300</Text>
-        </View>
-      </View>
-      <View style={[a.flex_row, a.gap_md]}>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg_positive,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg_positive</Text>
-        </View>
-        <View
-          style={[
-            a.flex_1,
-            t.atoms.bg_negative,
-            a.align_center,
-            a.justify_center,
-            {height: 60},
-          ]}>
-          <Text>theme.bg_negative</Text>
-        </View>
-      </View>
-    </View>
-  )
-}
-
-export function DebugScreen() {
-  const t = useTheme()
-
-  return (
-    <ScrollView>
-      <CenteredView style={[t.atoms.bg]}>
-        <View style={[a.p_xl, a.gap_xxl, {paddingBottom: 200}]}>
-          <ThemeSelector />
-
-          <Alf theme="light">
-            <ThemedSection />
-          </Alf>
-          <Alf theme="dark">
-            <ThemedSection />
-          </Alf>
-
-          <H1>Heading 1</H1>
-          <H2>Heading 2</H2>
-          <H3>Heading 3</H3>
-          <H4>Heading 4</H4>
-          <H5>Heading 5</H5>
-          <H6>Heading 6</H6>
-
-          <Text style={[a.text_xxl]}>atoms.text_xxl</Text>
-          <Text style={[a.text_xl]}>atoms.text_xl</Text>
-          <Text style={[a.text_lg]}>atoms.text_lg</Text>
-          <Text style={[a.text_md]}>atoms.text_md</Text>
-          <Text style={[a.text_sm]}>atoms.text_sm</Text>
-          <Text style={[a.text_xs]}>atoms.text_xs</Text>
-          <Text style={[a.text_xxs]}>atoms.text_xxs</Text>
-
-          <View style={[a.gap_md, a.align_start]}>
-            <Button>
-              {({state}) => (
-                <View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}>
-                  <Text>Unstyled button, state: {JSON.stringify(state)}</Text>
-                </View>
-              )}
-            </Button>
-
-            <Button type="primary" size="small">
-              Button
-            </Button>
-            <Button type="secondary" size="small">
-              Button
-            </Button>
-
-            <Button type="primary" size="large">
-              Button
-            </Button>
-            <Button type="secondary" size="large">
-              Button
-            </Button>
-
-            <Button type="secondary" size="small">
-              {({type, size}) => (
-                <>
-                  <FontAwesomeIcon icon={['fas', 'plus']} size={12} />
-                  <ButtonText type={type} size={size}>
-                    With an icon
-                  </ButtonText>
-                </>
-              )}
-            </Button>
-            <Button type="primary" size="large">
-              {({state: _state, ...rest}) => (
-                <>
-                  <FontAwesomeIcon icon={['fas', 'plus']} />
-                  <ButtonText {...rest}>With an icon</ButtonText>
-                </>
-              )}
-            </Button>
-          </View>
-
-          <View style={[a.gap_md]}>
-            <View style={[a.flex_row, a.gap_md]}>
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_0},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_100},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_200},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_300},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_400},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_500},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_600},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_700},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_800},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_900},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.gray_1000},
-                ]}
-              />
-            </View>
-
-            <View style={[a.flex_row, a.gap_md]}>
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_0},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_100},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_200},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_300},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_400},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_500},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_600},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_700},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_800},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_900},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.blue_1000},
-                ]}
-              />
-            </View>
-            <View style={[a.flex_row, a.gap_md]}>
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_0},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_100},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_200},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_300},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_400},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_500},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_600},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_700},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_800},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_900},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.green_1000},
-                ]}
-              />
-            </View>
-            <View style={[a.flex_row, a.gap_md]}>
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_0},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_100},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_200},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_300},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_400},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_500},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_600},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_700},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_800},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_900},
-                ]}
-              />
-              <View
-                style={[
-                  a.flex_1,
-                  {height: 60, backgroundColor: tokens.color.red_1000},
-                ]}
-              />
-            </View>
-          </View>
-
-          <View>
-            <H3 style={[a.pb_md, a.font_bold]}>Spacing</H3>
-
-            <View style={[a.gap_md]}>
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>xxs (2px)</Text>
-                <View style={[a.flex_1, a.pt_xxs, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>xs (4px)</Text>
-                <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>sm (8px)</Text>
-                <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>md (12px)</Text>
-                <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>lg (18px)</Text>
-                <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>xl (24px)</Text>
-                <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
-              </View>
-
-              <View style={[a.flex_row, a.align_center]}>
-                <Text style={{width: 80}}>xxl (32px)</Text>
-                <View style={[a.flex_1, a.pt_xxl, t.atoms.bg_contrast_300]} />
-              </View>
-            </View>
-          </View>
-
-          <BreakpointDebugger />
-        </View>
-      </CenteredView>
-    </ScrollView>
-  )
-}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 0e20a9cf7..7d6a40f02 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -19,7 +19,6 @@ import {useSession} from '#/state/session'
 import {loadString, saveString} from '#/lib/storage'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {clamp} from '#/lib/numbers'
-import {PROD_DEFAULT_FEED} from '#/lib/constants'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export function HomeScreen(props: Props) {
@@ -112,7 +111,7 @@ function HomeScreenReady({
       mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
       mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
         ? preferences.feeds.saved
-        : [PROD_DEFAULT_FEED('whats-hot')],
+        : [],
     }
   }, [preferences])
 
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index aaadbf399..276dc842c 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -25,6 +25,7 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage'
 import {CenteredView} from '../com/util/Views'
 import {useComposerControls} from '#/state/shell/composer'
 import {useSession} from '#/state/session'
+import {isWeb} from '#/platform/detection'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
 export function PostThreadScreen({route}: Props) {
@@ -112,7 +113,8 @@ export function PostThreadScreen({route}: Props) {
 
 const styles = StyleSheet.create({
   prompt: {
-    position: 'absolute',
+    // @ts-ignore web-only
+    position: isWeb ? 'fixed' : 'absolute',
     left: 0,
     right: 0,
   },
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 28df298e0..df64cc5aa 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -42,11 +42,16 @@ import {useSetDrawerOpen} from '#/state/shell'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {MagnifyingGlassIcon} from '#/lib/icons'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {SearchResultCard} from '#/view/shell/desktop/Search'
+import {
+  MATCH_HANDLE,
+  SearchLinkCard,
+  SearchProfileCard,
+} from '#/view/shell/desktop/Search'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-import {isWeb} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
 import {s} from '#/lib/styles'
+import AsyncStorage from '@react-native-async-storage/async-storage'
 import {augmentSearchQuery} from '#/lib/strings/helpers'
 
 function Loader() {
@@ -84,9 +89,7 @@ function EmptyState({message, error}: {message: string; error?: string}) {
         },
       ]}>
       <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
-        <Text style={[pal.text]}>
-          <Trans>{message}</Trans>
-        </Text>
+        <Text style={[pal.text]}>{message}</Text>
 
         {error && (
           <>
@@ -337,7 +340,9 @@ export function SearchScreenInner({
         tabBarPosition="top"
         onPageSelected={onPageSelected}
         renderTabBar={props => (
-          <CenteredView sideBorders style={pal.border}>
+          <CenteredView
+            sideBorders
+            style={[pal.border, pal.view, styles.tabBarContainer]}>
             <TabBar items={SECTIONS_LOGGEDIN} {...props} />
           </CenteredView>
         )}
@@ -378,7 +383,9 @@ export function SearchScreenInner({
       tabBarPosition="top"
       onPageSelected={onPageSelected}
       renderTabBar={props => (
-        <CenteredView sideBorders style={pal.border}>
+        <CenteredView
+          sideBorders
+          style={[pal.border, pal.view, styles.tabBarContainer]}>
           <TabBar items={SECTIONS_LOGGEDOUT} {...props} />
         </CenteredView>
       )}
@@ -463,32 +470,56 @@ export function SearchScreen(
   const [inputIsFocused, setInputIsFocused] = React.useState(false)
   const [showAutocompleteResults, setShowAutocompleteResults] =
     React.useState(false)
+  const [searchHistory, setSearchHistory] = React.useState<string[]>([])
+
+  React.useEffect(() => {
+    const loadSearchHistory = async () => {
+      try {
+        const history = await AsyncStorage.getItem('searchHistory')
+        if (history !== null) {
+          setSearchHistory(JSON.parse(history))
+        }
+      } catch (e: any) {
+        logger.error('Failed to load search history', e)
+      }
+    }
+
+    loadSearchHistory()
+  }, [])
 
   const onPressMenu = React.useCallback(() => {
     track('ViewHeader:MenuButtonClicked')
     setDrawerOpen(true)
   }, [track, setDrawerOpen])
+
   const onPressCancelSearch = React.useCallback(() => {
+    scrollToTopWeb()
     textInput.current?.blur()
     setQuery('')
     setShowAutocompleteResults(false)
     if (searchDebounceTimeout.current)
       clearTimeout(searchDebounceTimeout.current)
   }, [textInput])
+
   const onPressClearQuery = React.useCallback(() => {
+    scrollToTopWeb()
     setQuery('')
     setShowAutocompleteResults(false)
   }, [setQuery])
+
   const onChangeText = React.useCallback(
     async (text: string) => {
+      scrollToTopWeb()
+
       setQuery(text)
 
       if (text.length > 0) {
         setIsFetching(true)
         setShowAutocompleteResults(true)
 
-        if (searchDebounceTimeout.current)
+        if (searchDebounceTimeout.current) {
           clearTimeout(searchDebounceTimeout.current)
+        }
 
         searchDebounceTimeout.current = setTimeout(async () => {
           const results = await search({query: text, limit: 30})
@@ -499,8 +530,9 @@ export function SearchScreen(
           }
         }, 300)
       } else {
-        if (searchDebounceTimeout.current)
+        if (searchDebounceTimeout.current) {
           clearTimeout(searchDebounceTimeout.current)
+        }
         setSearchResults([])
         setIsFetching(false)
         setShowAutocompleteResults(false)
@@ -508,14 +540,47 @@ export function SearchScreen(
     },
     [setQuery, search, setSearchResults],
   )
+
+  const updateSearchHistory = React.useCallback(
+    async (newQuery: string) => {
+      newQuery = newQuery.trim()
+      if (newQuery && !searchHistory.includes(newQuery)) {
+        let newHistory = [newQuery, ...searchHistory]
+
+        if (newHistory.length > 5) {
+          newHistory = newHistory.slice(0, 5)
+        }
+
+        setSearchHistory(newHistory)
+        try {
+          await AsyncStorage.setItem(
+            'searchHistory',
+            JSON.stringify(newHistory),
+          )
+        } catch (e: any) {
+          logger.error('Failed to save search history', e)
+        }
+      }
+    },
+    [searchHistory, setSearchHistory],
+  )
+
   const onSubmit = React.useCallback(() => {
+    scrollToTopWeb()
     setShowAutocompleteResults(false)
-  }, [setShowAutocompleteResults])
+    updateSearchHistory(query)
+  }, [query, setShowAutocompleteResults, updateSearchHistory])
 
   const onSoftReset = React.useCallback(() => {
+    scrollToTopWeb()
     onPressCancelSearch()
   }, [onPressCancelSearch])
 
+  const queryMaybeHandle = React.useMemo(() => {
+    const match = MATCH_HANDLE.exec(query)
+    return match && match[1]
+  }, [query])
+
   useFocusEffect(
     React.useCallback(() => {
       setMinimalShellMode(false)
@@ -523,12 +588,28 @@ export function SearchScreen(
     }, [onSoftReset, setMinimalShellMode]),
   )
 
+  const handleHistoryItemClick = (item: React.SetStateAction<string>) => {
+    setQuery(item)
+    onSubmit()
+  }
+
+  const handleRemoveHistoryItem = (itemToRemove: string) => {
+    const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
+    setSearchHistory(updatedHistory)
+    AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch(
+      e => {
+        logger.error('Failed to update search history', e)
+      },
+    )
+  }
+
   return (
-    <View style={{flex: 1}}>
+    <View style={isWeb ? null : {flex: 1}}>
       <CenteredView
         style={[
           styles.header,
           pal.border,
+          pal.view,
           isTabletOrDesktop && {paddingTop: 10},
         ]}
         sideBorders={isTabletOrDesktop}>
@@ -569,7 +650,12 @@ export function SearchScreen(
             style={[pal.text, styles.headerSearchInput]}
             keyboardAppearance={theme.colorScheme}
             onFocus={() => setInputIsFocused(true)}
-            onBlur={() => setInputIsFocused(false)}
+            onBlur={() => {
+              // HACK
+              // give 100ms to not stop click handlers in the search history
+              // -prf
+              setTimeout(() => setInputIsFocused(false), 100)
+            }}
             onChangeText={onChangeText}
             onSubmitEditing={onSubmit}
             autoFocus={false}
@@ -611,9 +697,9 @@ export function SearchScreen(
         ) : undefined}
       </CenteredView>
 
-      {showAutocompleteResults && moderationOpts ? (
+      {showAutocompleteResults ? (
         <>
-          {isFetching ? (
+          {isFetching || !moderationOpts ? (
             <Loader />
           ) : (
             <ScrollView
@@ -622,23 +708,72 @@ export function SearchScreen(
               dataSet={{stableGutters: '1'}}
               keyboardShouldPersistTaps="handled"
               keyboardDismissMode="on-drag">
-              {searchResults.length ? (
-                searchResults.map((item, i) => (
-                  <SearchResultCard
-                    key={item.did}
-                    profile={item}
-                    moderation={moderateProfile(item, moderationOpts)}
-                    style={i === 0 ? {borderTopWidth: 0} : {}}
-                  />
-                ))
-              ) : (
-                <EmptyState message={_(msg`No results found for ${query}`)} />
-              )}
+              <SearchLinkCard
+                label={_(msg`Search for "${query}"`)}
+                onPress={isNative ? onSubmit : undefined}
+                to={
+                  isNative
+                    ? undefined
+                    : `/search?q=${encodeURIComponent(query)}`
+                }
+                style={{borderBottomWidth: 1}}
+              />
+
+              {queryMaybeHandle ? (
+                <SearchLinkCard
+                  label={_(msg`Go to @${queryMaybeHandle}`)}
+                  to={`/profile/${queryMaybeHandle}`}
+                />
+              ) : null}
+
+              {searchResults.map(item => (
+                <SearchProfileCard
+                  key={item.did}
+                  profile={item}
+                  moderation={moderateProfile(item, moderationOpts)}
+                />
+              ))}
 
               <View style={{height: 200}} />
             </ScrollView>
           )}
         </>
+      ) : !query && inputIsFocused ? (
+        <CenteredView
+          sideBorders={isTabletOrDesktop}
+          // @ts-ignore web only -prf
+          style={{
+            height: isWeb ? '100vh' : undefined,
+          }}>
+          <View style={styles.searchHistoryContainer}>
+            {searchHistory.length > 0 && (
+              <View style={styles.searchHistoryContent}>
+                <Text style={[pal.text, styles.searchHistoryTitle]}>
+                  Recent Searches
+                </Text>
+                {searchHistory.map((historyItem, index) => (
+                  <View key={index} style={styles.historyItemContainer}>
+                    <Pressable
+                      accessibilityRole="button"
+                      onPress={() => handleHistoryItemClick(historyItem)}
+                      style={styles.historyItem}>
+                      <Text style={pal.text}>{historyItem}</Text>
+                    </Pressable>
+                    <Pressable
+                      accessibilityRole="button"
+                      onPress={() => handleRemoveHistoryItem(historyItem)}>
+                      <FontAwesomeIcon
+                        icon="xmark"
+                        size={16}
+                        style={pal.textLight as FontAwesomeIconStyle}
+                      />
+                    </Pressable>
+                  </View>
+                ))}
+              </View>
+            )}
+          </View>
+        </CenteredView>
       ) : (
         <SearchScreenInner query={query} />
       )}
@@ -646,12 +781,25 @@ export function SearchScreen(
   )
 }
 
+function scrollToTopWeb() {
+  if (isWeb) {
+    window.scrollTo(0, 0)
+  }
+}
+
+const HEADER_HEIGHT = 50
+
 const styles = StyleSheet.create({
   header: {
     flexDirection: 'row',
     alignItems: 'center',
     paddingHorizontal: 12,
     paddingVertical: 4,
+    height: HEADER_HEIGHT,
+    // @ts-ignore web only
+    position: isWeb ? 'sticky' : '',
+    top: 0,
+    zIndex: 1,
   },
   headerMenuBtn: {
     width: 30,
@@ -681,4 +829,30 @@ const styles = StyleSheet.create({
   headerCancelBtn: {
     paddingLeft: 10,
   },
+  tabBarContainer: {
+    // @ts-ignore web only
+    position: isWeb ? 'sticky' : '',
+    top: isWeb ? HEADER_HEIGHT : 0,
+    zIndex: 1,
+  },
+  searchHistoryContainer: {
+    width: '100%',
+    paddingHorizontal: 12,
+  },
+  searchHistoryContent: {
+    padding: 10,
+    borderRadius: 8,
+  },
+  searchHistoryTitle: {
+    fontWeight: 'bold',
+  },
+  historyItem: {
+    paddingVertical: 8,
+  },
+  historyItemContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 8,
+  },
 })
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index c078e7a23..3b50c5449 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -70,6 +70,11 @@ import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useCloseAllActiveElements} from '#/state/util'
+import {
+  useInAppBrowser,
+  useSetInAppBrowser,
+} from '#/state/preferences/in-app-browser'
+import {isNative} from '#/platform/detection'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
@@ -146,6 +151,8 @@ export function SettingsScreen({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const requireAltTextEnabled = useRequireAltTextEnabled()
   const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
+  const inAppBrowserPref = useInAppBrowser()
+  const setUseInAppBrowser = useSetInAppBrowser()
   const onboardingDispatch = useOnboardingDispatch()
   const navigation = useNavigation<NavigationProp>()
   const {isMobile} = useWebMediaQueries()
@@ -284,7 +291,7 @@ export function SettingsScreen({}: Props) {
         ]}>
         <View style={{flex: 1}}>
           <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
-            <Trans>{_(msg`Settings`)}</Trans>
+            <Trans>Settings</Trans>
           </Text>
         </View>
       </SimpleViewHeader>
@@ -313,8 +320,14 @@ export function SettingsScreen({}: Props) {
                   />
                 </>
               )}
-              <Text type="lg" style={pal.text}>
-                {currentAccount.email || '(no email)'}{' '}
+              <Text
+                type="lg"
+                numberOfLines={1}
+                style={[
+                  pal.text,
+                  {overflow: 'hidden', marginRight: 4, flex: 1},
+                ]}>
+                {currentAccount.email || '(no email)'}
               </Text>
               <Link onPress={() => openModal({name: 'change-email'})}>
                 <Text type="lg" style={pal.link}>
@@ -658,6 +671,17 @@ export function SettingsScreen({}: Props) {
             <Trans>Change handle</Trans>
           </Text>
         </TouchableOpacity>
+        {isNative && (
+          <View style={[pal.view, styles.toggleCard]}>
+            <ToggleButton
+              type="default-light"
+              label={_(msg`Open links with in-app browser`)}
+              labelType="lg"
+              isSelected={inAppBrowserPref ?? false}
+              onPress={() => setUseInAppBrowser(!inAppBrowserPref)}
+            />
+          </View>
+        )}
         <View style={styles.spacer20} />
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
           <Trans>Danger Zone</Trans>
diff --git a/src/view/screens/Storybook/Breakpoints.tsx b/src/view/screens/Storybook/Breakpoints.tsx
new file mode 100644
index 000000000..1b846d517
--- /dev/null
+++ b/src/view/screens/Storybook/Breakpoints.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme, useBreakpoints} from '#/alf'
+import {Text, H3} from '#/components/Typography'
+
+export function Breakpoints() {
+  const t = useTheme()
+  const breakpoints = useBreakpoints()
+
+  return (
+    <View>
+      <H3 style={[a.pb_md]}>Breakpoint Debugger</H3>
+      <Text style={[a.pb_md]}>
+        Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>}
+        {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>}
+        {breakpoints.gtTablet && <Text>desktop</Text>}
+      </Text>
+      <Text
+        style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}>
+        {JSON.stringify(breakpoints, null, 2)}
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
new file mode 100644
index 000000000..fbdc84eb4
--- /dev/null
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -0,0 +1,124 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {
+  Button,
+  ButtonVariant,
+  ButtonColor,
+  ButtonIcon,
+  ButtonText,
+} from '#/components/Button'
+import {H1} from '#/components/Typography'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+
+export function Buttons() {
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Buttons</H1>
+
+      <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}>
+        {['primary', 'secondary', 'negative'].map(color => (
+          <View key={color} style={[a.gap_md, a.align_start]}>
+            {['solid', 'outline', 'ghost'].map(variant => (
+              <React.Fragment key={variant}>
+                <Button
+                  variant={variant as ButtonVariant}
+                  color={color as ButtonColor}
+                  size="large"
+                  label="Click here">
+                  Button
+                </Button>
+                <Button
+                  disabled
+                  variant={variant as ButtonVariant}
+                  color={color as ButtonColor}
+                  size="large"
+                  label="Click here">
+                  Button
+                </Button>
+              </React.Fragment>
+            ))}
+          </View>
+        ))}
+
+        <View style={[a.flex_row, a.gap_md, a.align_start]}>
+          <View style={[a.gap_md, a.align_start]}>
+            {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map(
+              name => (
+                <React.Fragment key={name}>
+                  <Button
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                  <Button
+                    disabled
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                </React.Fragment>
+              ),
+            )}
+          </View>
+          <View style={[a.gap_md, a.align_start]}>
+            {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map(
+              name => (
+                <React.Fragment key={name}>
+                  <Button
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                  <Button
+                    disabled
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                </React.Fragment>
+              ),
+            )}
+          </View>
+        </View>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="large"
+          label="Link out">
+          <ButtonText>Link out</ButtonText>
+          <ButtonIcon icon={ArrowTopRight} />
+        </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="small"
+          label="Link out">
+          <ButtonText>Link out</ButtonText>
+          <ButtonIcon icon={ArrowTopRight} />
+        </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="small"
+          label="Link out">
+          <ButtonIcon icon={Globe} />
+          <ButtonText>See the world</ButtonText>
+        </Button>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
new file mode 100644
index 000000000..db568c6bd
--- /dev/null
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {Button} from '#/components/Button'
+import {H3, P} from '#/components/Typography'
+import * as Dialog from '#/components/Dialog'
+import * as Prompt from '#/components/Prompt'
+import {useDialogStateControlContext} from '#/state/dialogs'
+
+export function Dialogs() {
+  const control = Dialog.useDialogControl()
+  const prompt = Prompt.usePromptControl()
+  const {closeAllDialogs} = useDialogStateControlContext()
+
+  return (
+    <View style={[a.gap_md]}>
+      <Button
+        variant="outline"
+        color="secondary"
+        size="small"
+        onPress={() => {
+          control.open()
+          prompt.open()
+        }}
+        label="Open basic dialog">
+        Open basic dialog
+      </Button>
+
+      <Button
+        variant="solid"
+        color="primary"
+        size="small"
+        onPress={() => prompt.open()}
+        label="Open prompt">
+        Open prompt
+      </Button>
+
+      <Prompt.Outer control={prompt}>
+        <Prompt.Title>This is a prompt</Prompt.Title>
+        <Prompt.Description>
+          This is a generic prompt component. It accepts a title and a
+          description, as well as two actions.
+        </Prompt.Description>
+        <Prompt.Actions>
+          <Prompt.Cancel>Cancel</Prompt.Cancel>
+          <Prompt.Action>Confirm</Prompt.Action>
+        </Prompt.Actions>
+      </Prompt.Outer>
+
+      <Dialog.Outer
+        control={control}
+        nativeOptions={{sheet: {snapPoints: ['90%']}}}>
+        <Dialog.Handle />
+
+        <Dialog.ScrollableInner
+          accessibilityDescribedBy="dialog-description"
+          accessibilityLabelledBy="dialog-title">
+          <View style={[a.relative, a.gap_md, a.w_full]}>
+            <H3 nativeID="dialog-title">Dialog</H3>
+            <P nativeID="dialog-description">
+              A scrollable dialog with an input within it.
+            </P>
+            <Dialog.Input value="" onChangeText={() => {}} label="Type here" />
+
+            <Button
+              variant="outline"
+              color="secondary"
+              size="small"
+              onPress={closeAllDialogs}
+              label="Close all dialogs">
+              Close all dialogs
+            </Button>
+            <View style={{height: 1000}} />
+            <View style={[a.flex_row, a.justify_end]}>
+              <Button
+                variant="outline"
+                color="primary"
+                size="small"
+                onPress={() => control.close()}
+                label="Open basic dialog">
+                Close basic dialog
+              </Button>
+            </View>
+          </View>
+        </Dialog.ScrollableInner>
+      </Dialog.Outer>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx
new file mode 100644
index 000000000..9396cca67
--- /dev/null
+++ b/src/view/screens/Storybook/Forms.tsx
@@ -0,0 +1,215 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {H1, H3} from '#/components/Typography'
+import * as TextField from '#/components/forms/TextField'
+import {DateField, Label} from '#/components/forms/DateField'
+import * as Toggle from '#/components/forms/Toggle'
+import * as ToggleButton from '#/components/forms/ToggleButton'
+import {Button} from '#/components/Button'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+
+export function Forms() {
+  const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a'])
+  const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b'])
+  const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b'])
+  const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn'])
+
+  const [value, setValue] = React.useState('')
+  const [date, setDate] = React.useState('2001-01-01')
+
+  return (
+    <View style={[a.gap_4xl, a.align_start]}>
+      <H1>Forms</H1>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>InputText</H3>
+
+        <TextField.Input
+          value={value}
+          onChangeText={setValue}
+          label="Text field"
+        />
+
+        <TextField.Root>
+          <TextField.Icon icon={Globe} />
+          <TextField.Input
+            value={value}
+            onChangeText={setValue}
+            label="Text field"
+          />
+        </TextField.Root>
+
+        <View style={[a.w_full]}>
+          <TextField.Label>Text field</TextField.Label>
+          <TextField.Root>
+            <TextField.Icon icon={Globe} />
+            <TextField.Input
+              value={value}
+              onChangeText={setValue}
+              label="Text field"
+            />
+            <TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix>
+          </TextField.Root>
+        </View>
+
+        <View style={[a.w_full]}>
+          <TextField.Label>Textarea</TextField.Label>
+          <TextField.Input
+            multiline
+            numberOfLines={4}
+            value={value}
+            onChangeText={setValue}
+            label="Text field"
+          />
+        </View>
+
+        <H3>DateField</H3>
+
+        <View style={[a.w_full]}>
+          <Label>Date</Label>
+          <DateField
+            testID="date"
+            value={date}
+            onChangeDate={date => {
+              console.log(date)
+              setDate(date)
+            }}
+            label="Input"
+          />
+        </View>
+      </View>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>Toggles</H3>
+
+        <Toggle.Item name="a" label="Click me">
+          <Toggle.Checkbox />
+          <Toggle.Label>Uncontrolled toggle</Toggle.Label>
+        </Toggle.Item>
+
+        <Toggle.Group
+          label="Toggle"
+          type="checkbox"
+          maxSelections={2}
+          values={toggleGroupAValues}
+          onChange={setToggleGroupAValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+
+        <Toggle.Group
+          label="Toggle"
+          type="checkbox"
+          maxSelections={2}
+          values={toggleGroupBValues}
+          onChange={setToggleGroupBValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+
+        <Toggle.Group
+          label="Toggle"
+          type="radio"
+          values={toggleGroupCValues}
+          onChange={setToggleGroupCValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+      </View>
+
+      <Button
+        variant="gradient"
+        color="gradient_nordic"
+        size="small"
+        label="Reset all toggles"
+        onPress={() => {
+          setToggleGroupAValues(['a'])
+          setToggleGroupBValues(['a', 'b'])
+          setToggleGroupCValues(['a'])
+        }}>
+        Reset all toggles
+      </Button>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>ToggleButton</H3>
+
+        <ToggleButton.Group
+          label="Preferences"
+          values={toggleGroupDValues}
+          onChange={setToggleGroupDValues}>
+          <ToggleButton.Button name="hide" label="Hide">
+            Hide
+          </ToggleButton.Button>
+          <ToggleButton.Button name="warn" label="Warn">
+            Warn
+          </ToggleButton.Button>
+          <ToggleButton.Button name="show" label="Show">
+            Show
+          </ToggleButton.Button>
+        </ToggleButton.Group>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx
new file mode 100644
index 000000000..73466e077
--- /dev/null
+++ b/src/view/screens/Storybook/Icons.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {H1} from '#/components/Typography'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+
+export function Icons() {
+  const t = useTheme()
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Icons</H1>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <Globe size="xs" fill={t.atoms.text.color} />
+        <Globe size="sm" fill={t.atoms.text.color} />
+        <Globe size="md" fill={t.atoms.text.color} />
+        <Globe size="lg" fill={t.atoms.text.color} />
+        <Globe size="xl" fill={t.atoms.text.color} />
+      </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <ArrowTopRight size="xs" fill={t.atoms.text.color} />
+        <ArrowTopRight size="sm" fill={t.atoms.text.color} />
+        <ArrowTopRight size="md" fill={t.atoms.text.color} />
+        <ArrowTopRight size="lg" fill={t.atoms.text.color} />
+        <ArrowTopRight size="xl" fill={t.atoms.text.color} />
+      </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <CalendarDays size="xs" fill={t.atoms.text.color} />
+        <CalendarDays size="sm" fill={t.atoms.text.color} />
+        <CalendarDays size="md" fill={t.atoms.text.color} />
+        <CalendarDays size="lg" fill={t.atoms.text.color} />
+        <CalendarDays size="xl" fill={t.atoms.text.color} />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
new file mode 100644
index 000000000..c3b1c0e0f
--- /dev/null
+++ b/src/view/screens/Storybook/Links.tsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {ButtonText} from '#/components/Button'
+import {Link} from '#/components/Link'
+import {H1, H3} from '#/components/Typography'
+
+export function Links() {
+  return (
+    <View style={[a.gap_md, a.align_start]}>
+      <H1>Links</H1>
+
+      <View style={[a.gap_md, a.align_start]}>
+        <Link
+          to="https://blueskyweb.xyz"
+          warnOnMismatchingTextChild
+          style={[a.text_md]}>
+          External
+        </Link>
+        <Link to="https://blueskyweb.xyz" style={[a.text_md]}>
+          <H3>External with custom children</H3>
+        </Link>
+        <Link
+          to="https://blueskyweb.xyz"
+          warnOnMismatchingTextChild
+          style={[a.text_lg]}>
+          https://blueskyweb.xyz
+        </Link>
+        <Link
+          to="https://bsky.app/profile/bsky.app"
+          warnOnMismatchingTextChild
+          style={[a.text_md]}>
+          Internal
+        </Link>
+
+        <Link
+          variant="solid"
+          color="primary"
+          size="large"
+          label="View @bsky.app's profile"
+          to="https://bsky.app/profile/bsky.app">
+          <ButtonText>Link as a button</ButtonText>
+        </Link>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx
new file mode 100644
index 000000000..b521fe860
--- /dev/null
+++ b/src/view/screens/Storybook/Palette.tsx
@@ -0,0 +1,336 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import * as tokens from '#/alf/tokens'
+import {atoms as a} from '#/alf'
+
+export function Palette() {
+  return (
+    <View style={[a.gap_md]}>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_975},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_1000},
+          ]}
+        />
+      </View>
+
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_975},
+          ]}
+        />
+      </View>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_975},
+          ]}
+        />
+      </View>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]}
+        />
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_975},
+          ]}
+        />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Shadows.tsx b/src/view/screens/Storybook/Shadows.tsx
new file mode 100644
index 000000000..f92112395
--- /dev/null
+++ b/src/view/screens/Storybook/Shadows.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {H1, Text} from '#/components/Typography'
+
+export function Shadows() {
+  const t = useTheme()
+
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Shadows</H1>
+
+      <View style={[a.flex_row, a.gap_5xl]}>
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_sm,
+          ]}>
+          <Text>shadow_sm</Text>
+        </View>
+
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_md,
+          ]}>
+          <Text>shadow_md</Text>
+        </View>
+
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_lg,
+          ]}>
+          <Text>shadow_lg</Text>
+        </View>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Spacing.tsx b/src/view/screens/Storybook/Spacing.tsx
new file mode 100644
index 000000000..d7faf93a8
--- /dev/null
+++ b/src/view/screens/Storybook/Spacing.tsx
@@ -0,0 +1,64 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text, H1} from '#/components/Typography'
+
+export function Spacing() {
+  const t = useTheme()
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Spacing</H1>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>2xs (2px)</Text>
+        <View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>xs (4px)</Text>
+        <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>sm (8px)</Text>
+        <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>md (12px)</Text>
+        <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>lg (16px)</Text>
+        <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>xl (20px)</Text>
+        <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>2xl (24px)</Text>
+        <View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>3xl (28px)</Text>
+        <View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>4xl (32px)</Text>
+        <View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>5xl (40px)</Text>
+        <View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Theming.tsx b/src/view/screens/Storybook/Theming.tsx
new file mode 100644
index 000000000..a05443473
--- /dev/null
+++ b/src/view/screens/Storybook/Theming.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {Palette} from './Palette'
+
+export function Theming() {
+  const t = useTheme()
+
+  return (
+    <View style={[t.atoms.bg, a.gap_lg, a.p_xl]}>
+      <Palette />
+
+      <Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}>
+        theme.atoms.text_contrast_600
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}>
+        theme.atoms.text_contrast_500
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}>
+        theme.atoms.text_contrast_400
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} />
+
+      <View style={[a.w_full, a.gap_md]}>
+        <View style={[t.atoms.bg, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_25, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_25</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_50, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_50</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_100, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_100</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_200, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_200</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_300, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_300</Text>
+        </View>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx
new file mode 100644
index 000000000..2e1f04a66
--- /dev/null
+++ b/src/view/screens/Storybook/Typography.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
+
+export function Typography() {
+  return (
+    <View style={[a.gap_md]}>
+      <H1>H1 Heading</H1>
+      <H2>H2 Heading</H2>
+      <H3>H3 Heading</H3>
+      <H4>H4 Heading</H4>
+      <H5>H5 Heading</H5>
+      <H6>H6 Heading</H6>
+      <P>P Paragraph</P>
+
+      <Text style={[a.text_5xl]}>atoms.text_5xl</Text>
+      <Text style={[a.text_4xl]}>atoms.text_4xl</Text>
+      <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
+      <Text style={[a.text_2xl]}>atoms.text_2xl</Text>
+      <Text style={[a.text_xl]}>atoms.text_xl</Text>
+      <Text style={[a.text_lg]}>atoms.text_lg</Text>
+      <Text style={[a.text_md]}>atoms.text_md</Text>
+      <Text style={[a.text_sm]}>atoms.text_sm</Text>
+      <Text style={[a.text_xs]}>atoms.text_xs</Text>
+      <Text style={[a.text_2xs]}>atoms.text_2xs</Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
new file mode 100644
index 000000000..d8898f20e
--- /dev/null
+++ b/src/view/screens/Storybook/index.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import {View} from 'react-native'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
+
+import {atoms as a, useTheme, ThemeProvider} from '#/alf'
+import {useSetColorMode} from '#/state/shell'
+import {Button} from '#/components/Button'
+
+import {Theming} from './Theming'
+import {Typography} from './Typography'
+import {Spacing} from './Spacing'
+import {Buttons} from './Buttons'
+import {Links} from './Links'
+import {Forms} from './Forms'
+import {Dialogs} from './Dialogs'
+import {Breakpoints} from './Breakpoints'
+import {Shadows} from './Shadows'
+import {Icons} from './Icons'
+
+export function Storybook() {
+  const t = useTheme()
+  const setColorMode = useSetColorMode()
+
+  return (
+    <ScrollView>
+      <CenteredView style={[t.atoms.bg]}>
+        <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}>
+          <View style={[a.flex_row, a.align_start, a.gap_md]}>
+            <Button
+              variant="outline"
+              color="primary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('system')}>
+              System
+            </Button>
+            <Button
+              variant="solid"
+              color="secondary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('light')}>
+              Light
+            </Button>
+            <Button
+              variant="solid"
+              color="secondary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('dark')}>
+              Dark
+            </Button>
+          </View>
+
+          <ThemeProvider theme="light">
+            <Theming />
+          </ThemeProvider>
+          <ThemeProvider theme="dim">
+            <Theming />
+          </ThemeProvider>
+          <ThemeProvider theme="dark">
+            <Theming />
+          </ThemeProvider>
+
+          <Typography />
+          <Spacing />
+          <Shadows />
+          <Buttons />
+          <Icons />
+          <Links />
+          <Forms />
+          <Dialogs />
+          <Breakpoints />
+        </View>
+      </CenteredView>
+    </ScrollView>
+  )
+}