about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-12-05 18:59:26 -0600
committerGitHub <noreply@github.com>2024-12-05 18:59:26 -0600
commit143e2c802d1d8d8498e6658c174ed1e657c4ec12 (patch)
treecbe937bec7e0a241774060ade7428180c4fe0aaf /src/view/com
parent8467dfd452b4cb1b62214b3abe87fd90d23a183b (diff)
downloadvoidsky-143e2c802d1d8d8498e6658c174ed1e657c4ec12.tar.zst
[Layout] Base (#6907)
* Add common gutter styles as hook

* Add computed scrollbar gutter CSS vars

* Add new layout components

* Replace layout components in settings screens

* Remove old back button

* Invert web border logic for easier migration

* Clean up Slot API

* Port over FF handling of scrollbar offset

* Trade boilerplate for ease of use

* Limit to one line

* Allow two lines, fix wrapping

* Fix alignment

* sticky headers

* set max with on header and center

* [Layout] Notifications Header (#6910)

* Replace notifications screen header

* fix cropped indicator

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Replace Hashtag header (#6928)

* [Layout] ChatList header (#6929)

* Replace ChatList header

* update chat settings as well

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Add web borders to Chat settings

* Remove unused var

* Move ChatList header outside center

* Replace empty chat layout

* fix breakpoints

* [Layout] Scrollbar gutters (#6908)

* Fix sidebar alignment

* Make sure scrollbars don't hide

* Gift left nav more space

* Use stable one-edge, update logic in RightNav

* Ope

* Increase width

* Reset

* Add transform to sidebars

* Remove bg in sidebars

* Handle shifts in layout components

* Replace scroll-removal handling

* Make react-remove-scroll an explicit dep

* Remove unused script

* use correct scroll insets (#6950)

* [Layout] Feeds headers (#6913)

* Replace ViewHeader internals, duplicate old ViewHeader

* Replace Feeds header

* Replace SavedFeeds header

* Visual alignment

* Uglier but clear

* Use old ViewHeader for SavedFeeds

* use Layout.Center instead of Layout.Content

* use left-aligned header for feed edit

* delete unused old view header

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* [Layout] Every other screen (#6953)

* attempt to fix double borders on every other screen

* delete ListHeaderDesktop

* delete `SimpleViewHeader` and fix screens (#6956)

* Make Layout.Center not full height

* Refactor List to use Layout.Center, remove built-in borders

* Fix Home screen

* Refactor PagerWithHeader to use Layout components

* Replace components in ProfileFeed and ProfileList

* Borders on Profile

* Search screen replacements

* use new header for profile subpage header (#6958)

* Search AutocompleteResults

* use new header for starter pack wizard (#6957)

* Fix post thread

* Enable borders by default

* Moderation muted and blocked accounts

* Fix scrollbar offset on Labeler

* Remove ScrollView from Moderation

* Remove ScrollView from Deactivated

* Remove ScrollView from onboarding

* Remove ScrollView from SignupQueued

* Mark deprecations

* fix lint

* Fix double borders on profile load

* Remove unneeded CenteredView from noty Feed

* Remove double Center layout on Notifications screen

* Remove double Center layout on ChatList screen

* Handle scrollbar offset in chat

* Use new atom for other scrollbar offsets

* Remove borders from old views

* Better doc

* Remove temp migration prop

* Fix new atom usage on native

* Clean up Hashtag screen

* Layout docs

* Clarify usage in Pager

* Handle nested offset contexts

* Clean up Layout

* fix feeds page

* asymmetric header on native (#6969)

* Reusable header const

* Fix up home header

* Add back button to convo

* Add hitslop to header buttons

* Comment

* Better handling on native for new atom

* Format

* Fix nested flatlist on mod screens

* Use react-remove-scroll-bar directly

* Fix notification count overflow on web

* Clarify doc

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/feeds/FeedPage.tsx2
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx126
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx124
-rw-r--r--src/view/com/lightbox/Lightbox.web.tsx16
-rw-r--r--src/view/com/lists/MyLists.tsx7
-rw-r--r--src/view/com/modals/Modal.web.tsx4
-rw-r--r--src/view/com/notifications/Feed.tsx23
-rw-r--r--src/view/com/pager/PagerWithHeader.web.tsx60
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx5
-rw-r--r--src/view/com/post-thread/PostQuotes.tsx3
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx16
-rw-r--r--src/view/com/post-thread/PostThread.tsx5
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx3
-rw-r--r--src/view/com/profile/ProfileFollows.tsx3
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx103
-rw-r--r--src/view/com/util/List.web.tsx115
-rw-r--r--src/view/com/util/LoadingScreen.tsx9
-rw-r--r--src/view/com/util/SimpleViewHeader.tsx114
-rw-r--r--src/view/com/util/ViewHeader.tsx270
-rw-r--r--src/view/com/util/Views.tsx7
-rw-r--r--src/view/com/util/Views.web.tsx31
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx4
22 files changed, 237 insertions, 813 deletions
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 44e90a551..fa5a620bf 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -108,7 +108,7 @@ export function FeedPage({
   }, [scrollToTop, feed, queryClient, setHasNew])
 
   return (
-    <View testID={testID} style={s.h100pct}>
+    <View testID={testID}>
       <MainScrollProvider>
         <FeedFeedbackProvider value={feedFeedback}>
           <Feed
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index bdfc2c7ff..1dc67b6c3 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,26 +1,27 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {View} from 'react-native'
 import Animated from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {useKawaiiMode} from '#/state/preferences/kawaii'
 import {useSession} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
 import {Logo} from '#/view/icons/Logo'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
+import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
-import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
 
 export function HomeHeaderLayout(props: {
   children: React.ReactNode
   tabBarAnchor: JSX.Element | null | undefined
 }) {
-  const {isMobile} = useWebMediaQueries()
-  if (isMobile) {
+  const {gtMobile} = useBreakpoints()
+  if (!gtMobile) {
     return <HomeHeaderLayoutMobile {...props} />
   } else {
     return <HomeHeaderLayoutDesktopAndTablet {...props} />
@@ -40,98 +41,43 @@ function HomeHeaderLayoutDesktopAndTablet({
   const {hasSession} = useSession()
   const {_} = useLingui()
   const kawaii = useKawaiiMode()
+  const gutter = useGutterStyles()
 
   return (
     <>
       {hasSession && (
-        <View
-          style={[
-            a.relative,
-            a.flex_row,
-            a.justify_end,
-            a.align_center,
-            a.pt_lg,
-            a.px_md,
-            a.pb_2xs,
-            t.atoms.bg,
-            t.atoms.border_contrast_low,
-            styles.bar,
-            kawaii && {paddingTop: 22, paddingBottom: 16},
-          ]}>
+        <Layout.Center>
           <View
-            style={[
-              a.absolute,
-              a.inset_0,
-              a.pt_lg,
-              a.m_auto,
-              kawaii && {paddingTop: 4, paddingBottom: 0},
-              {
-                width: kawaii ? 84 : 28,
-              },
-            ]}>
-            <Logo width={kawaii ? 60 : 28} />
+            style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}>
+            <View style={{width: 34}} />
+            <View style={[a.flex_1, a.align_center, a.justify_center]}>
+              <Logo width={kawaii ? 60 : 28} />
+            </View>
+            <Link
+              to="/feeds"
+              hitSlop={10}
+              label={_(msg`View your feeds and explore more`)}
+              size="small"
+              variant="ghost"
+              color="secondary"
+              shape="square"
+              style={[a.justify_center]}>
+              <ButtonIcon icon={FeedsIcon} size="lg" />
+            </Link>
           </View>
-
-          <Link
-            to="/feeds"
-            hitSlop={10}
-            label={_(msg`View your feeds and explore more`)}
-            size="small"
-            variant="ghost"
-            color="secondary"
-            shape="square"
-            style={[
-              a.justify_center,
-              {
-                marginTop: -4,
-              },
-            ]}>
-            <FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} />
-          </Link>
-        </View>
+        </Layout.Center>
       )}
       {tabBarAnchor}
-      <Animated.View
-        onLayout={e => {
-          headerHeight.set(e.nativeEvent.layout.height)
-        }}
-        style={[
-          t.atoms.bg,
-          t.atoms.border_contrast_low,
-          styles.bar,
-          styles.tabBar,
-          headerMinimalShellTransform,
-        ]}>
-        {children}
-      </Animated.View>
+      <Layout.Center
+        style={[a.sticky, a.z_10, a.align_center, t.atoms.bg, {top: 0}]}>
+        <Animated.View
+          onLayout={e => {
+            headerHeight.set(e.nativeEvent.layout.height)
+          }}
+          style={[headerMinimalShellTransform]}>
+          {children}
+        </Animated.View>
+      </Layout.Center>
     </>
   )
 }
-
-const styles = StyleSheet.create({
-  bar: {
-    // @ts-ignore Web only
-    left: 'calc(50% - 300px)',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  topBar: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingHorizontal: 18,
-    paddingTop: 16,
-    paddingBottom: 8,
-  },
-  tabBar: {
-    // @ts-ignore Web only
-    position: 'sticky',
-    top: 0,
-    flexDirection: 'column',
-    alignItems: 'center',
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-    zIndex: 1,
-  },
-})
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index 832396092..e48c2cc89 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -1,25 +1,22 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {View} from 'react-native'
 import Animated from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {HITSLOP_10} from '#/lib/constants'
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {useHaptics} from '#/lib/haptics'
 import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {isWeb} from '#/platform/detection'
+import {emitSoftReset} from '#/state/events'
 import {useSession} from '#/state/session'
-import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {Logo} from '#/view/icons/Logo'
-import {atoms} from '#/alf'
-import {useTheme} from '#/alf'
-import {atoms as a} from '#/alf'
-import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
+import {atoms as a, useTheme} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
-import {IS_DEV} from '#/env'
 
 export function HomeHeaderLayoutMobile({
   children,
@@ -28,58 +25,50 @@ export function HomeHeaderLayoutMobile({
   tabBarAnchor: JSX.Element | null | undefined
 }) {
   const t = useTheme()
-  const pal = usePalette('default')
   const {_} = useLingui()
-  const setDrawerOpen = useSetDrawerOpen()
   const {headerHeight} = useShellLayout()
   const headerMinimalShellTransform = useMinimalShellHeaderTransform()
   const {hasSession} = useSession()
-
-  const onPressAvi = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
+  const playHaptic = useHaptics()
 
   return (
     <Animated.View
-      style={[pal.border, styles.tabBar, headerMinimalShellTransform]}
+      style={[
+        a.fixed,
+        a.z_10,
+        t.atoms.bg,
+        {
+          top: 0,
+          left: 0,
+          right: 0,
+        },
+        headerMinimalShellTransform,
+      ]}
       onLayout={e => {
         headerHeight.set(e.nativeEvent.layout.height)
       }}>
-      <View style={[pal.view, styles.topBar]}>
-        <View style={[{width: 100}]}>
-          <TouchableOpacity
-            testID="viewHeaderDrawerBtn"
-            onPress={onPressAvi}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Open navigation`)}
-            accessibilityHint={_(
-              msg`Access profile and other navigation links`,
-            )}
-            hitSlop={HITSLOP_10}>
-            <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
-          </TouchableOpacity>
-        </View>
-        <View>
-          <Logo width={30} />
+      <Layout.Header.Outer noBottomBorder>
+        <Layout.Header.Slot>
+          <Layout.Header.MenuButton />
+        </Layout.Header.Slot>
+
+        <View style={[a.flex_1, a.align_center]}>
+          <PressableScale
+            targetScale={0.9}
+            onPress={() => {
+              emitSoftReset()
+            }}
+            onPressIn={() => {
+              playHaptic('Heavy')
+            }}
+            onPressOut={() => {
+              playHaptic('Light')
+            }}>
+            <Logo width={30} />
+          </PressableScale>
         </View>
-        <View
-          style={[
-            atoms.flex_row,
-            atoms.justify_end,
-            atoms.align_center,
-            atoms.gap_md,
-            {width: 100},
-          ]}>
-          {IS_DEV && (
-            <>
-              <Link
-                label="View storybook"
-                to="/sys/debug"
-                testID="storybookBtn">
-                <ColorPalette size="md" />
-              </Link>
-            </>
-          )}
+
+        <Layout.Header.Slot>
           {hasSession && (
             <Link
               testID="viewHeaderHomeFeedPrefsBtn"
@@ -93,40 +82,15 @@ export function HomeHeaderLayoutMobile({
               style={[
                 a.justify_center,
                 {
-                  marginTop: 2,
-                  marginRight: -6,
+                  marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET,
                 },
               ]}>
-              <FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
+              <ButtonIcon icon={FeedsIcon} size="lg" />
             </Link>
           )}
-        </View>
-      </View>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
       {children}
     </Animated.View>
   )
 }
-
-const styles = StyleSheet.create({
-  tabBar: {
-    // @ts-ignore web-only
-    position: isWeb ? 'fixed' : 'absolute',
-    zIndex: 1,
-    left: 0,
-    right: 0,
-    top: 0,
-    flexDirection: 'column',
-  },
-  topBar: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingHorizontal: 16,
-    paddingVertical: 5,
-    width: '100%',
-    minHeight: 46,
-  },
-  title: {
-    fontSize: 21,
-  },
-})
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index f9b147b29..f6b6223ce 100644
--- a/src/view/com/lightbox/Lightbox.web.tsx
+++ b/src/view/com/lightbox/Lightbox.web.tsx
@@ -15,8 +15,8 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {colors, s} from '#/lib/styles'
 import {useLightbox, useLightboxControls} from '#/state/lightbox'
@@ -28,7 +28,6 @@ export function Lightbox() {
   const {activeLightbox} = useLightbox()
   const {closeLightbox} = useLightboxControls()
   const isActive = !!activeLightbox
-  useWebBodyScrollLock(isActive)
 
   if (!isActive) {
     return null
@@ -37,11 +36,14 @@ export function Lightbox() {
   const initialIndex = activeLightbox.index
   const imgs = activeLightbox.images
   return (
-    <LightboxInner
-      imgs={imgs}
-      initialIndex={initialIndex}
-      onClose={closeLightbox}
-    />
+    <>
+      <RemoveScrollBar />
+      <LightboxInner
+        imgs={imgs}
+        initialIndex={initialIndex}
+        onClose={closeLightbox}
+      />
+    </>
   )
 }
 
diff --git a/src/view/com/lists/MyLists.tsx b/src/view/com/lists/MyLists.tsx
index 363dd100d..17327fd9a 100644
--- a/src/view/com/lists/MyLists.tsx
+++ b/src/view/com/lists/MyLists.tsx
@@ -15,7 +15,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
 import {cleanError} from '#/lib/strings/errors'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
 import {EmptyState} from '#/view/com/util/EmptyState'
@@ -110,7 +109,7 @@ export function MyLists({
       ) : (
         <View
           style={[
-            (index !== 0 || isWeb) && a.border_t,
+            index !== 0 && a.border_t,
             t.atoms.border_contrast_low,
             a.px_lg,
             a.py_lg,
@@ -141,8 +140,6 @@ export function MyLists({
             }
             contentContainerStyle={[s.contentContainer]}
             removeClippedSubviews={true}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
           />
         )}
       </View>
@@ -160,8 +157,8 @@ export function MyLists({
             onRefresh={onRefresh}
             contentContainerStyle={[s.contentContainer]}
             removeClippedSubviews={true}
-            // @ts-ignore our .web version only -prf
             desktopFixedHeight
+            sideBorders={false}
           />
         )}
       </View>
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 8d93c21b4..0c49c8771 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -1,8 +1,8 @@
 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
+import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import type {Modal as ModalIface} from '#/state/modals'
 import {useModalControls, useModals} from '#/state/modals'
@@ -22,7 +22,6 @@ import * as VerifyEmailModal from './VerifyEmail'
 
 export function ModalsContainer() {
   const {isModalActive, activeModals} = useModals()
-  useWebBodyScrollLock(isModalActive)
 
   if (!isModalActive) {
     return null
@@ -30,6 +29,7 @@ export function ModalsContainer() {
 
   return (
     <>
+      <RemoveScrollBar />
       {activeModals.map((modal, i) => (
         <Modal key={`modal-${i}`} modal={modal} />
       ))}
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index bd39ddd84..9871455a1 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
 
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {cleanError} from '#/lib/strings/errors'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
@@ -22,7 +21,6 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {List, ListRef} from '#/view/com/util/List'
 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
-import {CenteredView} from '#/view/com/util/Views'
 import {FeedItem} from './FeedItem'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -46,7 +44,6 @@ export function Feed({
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const pal = usePalette('default')
-  const {isTabletOrMobile} = useWebMediaQueries()
 
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
@@ -133,11 +130,7 @@ export function Feed({
         )
       } else if (item === LOADING_ITEM) {
         return (
-          <View
-            style={[
-              pal.border,
-              !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth},
-            ]}>
+          <View style={[pal.border]}>
             <NotificationFeedLoadingPlaceholder />
           </View>
         )
@@ -146,11 +139,11 @@ export function Feed({
         <FeedItem
           item={item}
           moderationOpts={moderationOpts!}
-          hideTopBorder={index === 0 && isTabletOrMobile}
+          hideTopBorder={index === 0}
         />
       )
     },
-    [moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border],
+    [moderationOpts, _, onPressRetryLoadMore, pal.border],
   )
 
   const FeedFooter = React.useCallback(
@@ -168,12 +161,10 @@ export function Feed({
   return (
     <View style={s.hContentRegion}>
       {error && (
-        <CenteredView>
-          <ErrorMessage
-            message={cleanError(error)}
-            onPressTryAgain={onPressTryAgain}
-          />
-        </CenteredView>
+        <ErrorMessage
+          message={cleanError(error)}
+          onPressTryAgain={onPressTryAgain}
+        />
       )}
       <List
         testID="notifsFeed"
diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx
index 13c723f47..3335532b3 100644
--- a/src/view/com/pager/PagerWithHeader.web.tsx
+++ b/src/view/com/pager/PagerWithHeader.web.tsx
@@ -1,10 +1,10 @@
 import * as React from 'react'
-import {ScrollView, StyleSheet, View} from 'react-native'
+import {ScrollView, View} from 'react-native'
 import {useAnimatedRef} from 'react-native-reanimated'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {atoms as a, web} from '#/alf'
+import * as Layout from '#/components/Layout'
 import {ListMethods} from '../util/List'
 import {TabBar} from './TabBar'
 
@@ -121,30 +121,19 @@ let PagerTabBar = ({
   onSelect?: (index: number) => void
   tabBarAnchor?: JSX.Element | null | undefined
 }): React.ReactNode => {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
   return (
     <>
-      <View
-        style={[
-          !isMobile && styles.headerContainerDesktop,
-          pal.border,
-          !isHeaderReady && styles.loadingHeader,
-        ]}>
-        {renderHeader?.()}
-      </View>
+      <Layout.Center>{renderHeader?.()}</Layout.Center>
       {tabBarAnchor}
-      <View
-        style={[
-          styles.tabBarContainer,
-          isMobile
-            ? styles.tabBarContainerMobile
-            : styles.tabBarContainerDesktop,
-          pal.border,
+      <Layout.Center
+        style={web([
+          a.sticky,
+          a.z_10,
           {
+            top: 0,
             display: isHeaderReady ? undefined : 'none',
           },
-        ]}>
+        ])}>
         <TabBar
           testID={testID}
           items={items}
@@ -154,7 +143,7 @@ let PagerTabBar = ({
           dragProgress={undefined as any /* native-only */}
           dragState={undefined as any /* native-only */}
         />
-      </View>
+      </Layout.Center>
     </>
   )
 }
@@ -180,33 +169,6 @@ function PagerItem({
   })
 }
 
-const styles = StyleSheet.create({
-  headerContainerDesktop: {
-    marginHorizontal: 'auto',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  tabBarContainer: {
-    // @ts-ignore web-only
-    position: 'sticky',
-    top: 0,
-    zIndex: 1,
-  },
-  tabBarContainerDesktop: {
-    marginHorizontal: 'auto',
-    width: 600,
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  tabBarContainerMobile: {
-    paddingHorizontal: 0,
-  },
-  loadingHeader: {
-    borderColor: 'transparent',
-  },
-})
-
 function toArray<T>(v: T | T[]): T[] {
   if (Array.isArray(v)) {
     return v
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 4c0d973a9..b9051a9c6 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useLikedByQuery} from '#/state/queries/post-liked-by'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
@@ -18,7 +17,7 @@ function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
     <ProfileCardWithFollowBtn
       key={item.actor.did}
       profile={item.actor}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
@@ -88,6 +87,7 @@ export function PostLikedBy({uri}: {uri: string}) {
         )}
         errorMessage={cleanError(resolveError || error)}
         sideBorders={false}
+        topBorder={false}
       />
     )
   }
@@ -108,7 +108,6 @@ export function PostLikedBy({uri}: {uri: string}) {
           onRetry={fetchNextPage}
         />
       }
-      // @ts-ignore our .web version only -prf
       desktopFixedHeight
       initialNumToRender={initialNumToRender}
       windowSize={11}
diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx
index 10a51166c..a22000b96 100644
--- a/src/view/com/post-thread/PostQuotes.tsx
+++ b/src/view/com/post-thread/PostQuotes.tsx
@@ -11,7 +11,6 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePostQuotesQuery} from '#/state/queries/post-quotes'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
@@ -30,7 +29,7 @@ function renderItem({
   }
   index: number
 }) {
-  return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} />
+  return <Post post={item.post} hideTopBorder={index === 0} />
 }
 
 function keyExtractor(item: {
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index dfaa69780..2143bd9c2 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -12,8 +12,20 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
 import {List} from '#/view/com/util/List'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 
-function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
-  return <ProfileCardWithFollowBtn key={item.did} profile={item} />
+function renderItem({
+  item,
+  index,
+}: {
+  item: ActorDefs.ProfileViewBasic
+  index: number
+}) {
+  return (
+    <ProfileCardWithFollowBtn
+      key={item.did}
+      profile={item}
+      noBorder={index === 0}
+    />
+  )
 }
 
 function keyExtractor(item: ActorDefs.ProfileViewBasic) {
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index a10149395..0cdccff59 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -32,7 +32,6 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {CenteredView} from '#/view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
 import {Text} from '#/components/Typography'
@@ -484,7 +483,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
   }
 
   return (
-    <CenteredView style={[a.flex_1]} sideBorders={true}>
+    <>
       {showHeader && (
         <ViewHeader
           title={_(msg({message: `Post`, context: 'description'}))}
@@ -531,7 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
       {isMobile && canReply && hasSession && (
         <MobileComposePrompt onPressReply={onPressReply} />
       )}
-    </CenteredView>
+    </>
   )
 }
 
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 60a7a5e31..3c0476929 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useSession} from '#/state/session'
@@ -25,7 +24,7 @@ function renderItem({
     <ProfileCardWithFollowBtn
       key={item.did}
       profile={item}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 572b0b9f4..1cd65c74c 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useSession} from '#/state/session'
@@ -25,7 +24,7 @@ function renderItem({
     <ProfileCardWithFollowBtn
       key={item.did}
       profile={item}
-      noBorder={index === 0 && !isWeb}
+      noBorder={index === 0}
     />
   )
 }
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index 0e25fe5e6..cd11611a8 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -1,29 +1,24 @@
 import React from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
+import {Pressable, View} from 'react-native'
 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-import {BACK_HITSLOP} from '#/lib/constants'
 import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {makeProfileLink} from '#/lib/routes/links'
 import {NavigationProp} from '#/lib/routes/types'
 import {sanitizeHandle} from '#/lib/strings/handles'
-import {isNative} from '#/platform/detection'
 import {emitSoftReset} from '#/state/events'
 import {useLightboxControls} from '#/state/lightbox'
-import {useSetDrawerOpen} from '#/state/shell'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {TextLink} from '#/view/com/util/Link'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {Text} from '#/view/com/util/text/Text'
+import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
 import {StarterPack} from '#/components/icons/StarterPack'
-import {TextLink} from '../util/Link'
-import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {Text} from '../util/text/Text'
-import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
-import {CenteredView} from '../util/Views'
+import * as Layout from '#/components/Layout'
 
 export function ProfileSubpageHeader({
   isLoading,
@@ -48,7 +43,6 @@ export function ProfileSubpageHeader({
     | undefined
   avatarType: UserAvatarType | 'starter-pack'
 }>) {
-  const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
@@ -57,18 +51,6 @@ export function ProfileSubpageHeader({
   const canGoBack = navigation.canGoBack()
   const aviRef = useHandleRef()
 
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
   const _openLightbox = React.useCallback(
     (uri: string, thumbRect: MeasuredDimensions | null) => {
       openLightbox({
@@ -106,42 +88,17 @@ export function ProfileSubpageHeader({
   }, [_openLightbox, avatar, aviRef])
 
   return (
-    <CenteredView style={pal.view}>
-      {isMobile && (
-        <View
-          style={[
-            {
-              flexDirection: 'row',
-              alignItems: 'center',
-              borderBottomWidth: StyleSheet.hairlineWidth,
-              paddingTop: isNative ? 0 : 8,
-              paddingBottom: 8,
-              paddingHorizontal: isMobile ? 12 : 14,
-            },
-            pal.border,
-          ]}>
-          <Pressable
-            testID="headerDrawerBtn"
-            onPress={canGoBack ? onPressBack : onPressMenu}
-            hitSlop={BACK_HITSLOP}
-            style={canGoBack ? styles.backBtn : styles.backBtnWide}
-            accessibilityRole="button"
-            accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
-            accessibilityHint="">
-            {canGoBack ? (
-              <FontAwesomeIcon
-                size={18}
-                icon="angle-left"
-                style={[styles.backIcon, pal.text]}
-              />
-            ) : (
-              <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
-            )}
-          </Pressable>
-          <View style={{flex: 1}} />
-          {children}
-        </View>
-      )}
+    <>
+      <Layout.Header.Outer>
+        {canGoBack ? (
+          <Layout.Header.BackButton />
+        ) : (
+          <Layout.Header.MenuButton />
+        )}
+        <Layout.Header.Content />
+        {children}
+      </Layout.Header.Outer>
+
       <View
         style={{
           flexDirection: 'row',
@@ -206,31 +163,7 @@ export function ProfileSubpageHeader({
             </Text>
           )}
         </View>
-        {!isMobile && (
-          <View
-            style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-            }}>
-            {children}
-          </View>
-        )}
       </View>
-    </CenteredView>
+    </>
   )
 }
-
-const styles = StyleSheet.create({
-  backBtn: {
-    width: 20,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 20,
-    height: 30,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx
index f112d2d0a..18f7d6fa7 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -4,10 +4,9 @@ import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook
 
 import {batchedUpdates} from '#/lib/batchedUpdates'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {useScrollHandlers} from '#/lib/ScrollContext'
 import {addStyle} from '#/lib/styles'
+import * as Layout from '#/components/Layout'
 
 export type ListMethods = any // TODO: Better types.
 export type ListProps<ItemT> = Omit<
@@ -24,6 +23,9 @@ export type ListProps<ItemT> = Omit<
   desktopFixedHeight?: number | boolean
   // Web only prop to contain the scroll to the container rather than the window
   disableFullWindowScroll?: boolean
+  /**
+   * @deprecated Should be using Layout components
+   */
   sideBorders?: boolean
 }
 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
@@ -56,20 +58,11 @@ function ListImpl<ItemT>(
     renderItem,
     extraData,
     style,
-    sideBorders = true,
     ...props
   }: ListProps<ItemT>,
   ref: React.Ref<ListMethods>,
 ) {
   const contextScrollHandlers = useScrollHandlers()
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  if (!isMobile) {
-    contentContainerStyle = addStyle(
-      contentContainerStyle,
-      styles.containerScroll,
-    )
-  }
 
   const isEmpty = !data || data.length === 0
 
@@ -326,53 +319,53 @@ function ListImpl<ItemT>(
           styles.parentTreeVisibilityDetector
         }
       />
-      <View
-        ref={containerRef}
-        style={[
-          !isMobile && sideBorders && styles.sideBorders,
-          contentContainerStyle,
-          desktopFixedHeight ? styles.minHeightViewport : null,
-          pal.border,
-        ]}>
-        <Visibility
-          root={disableFullWindowScroll ? nativeRef : null}
-          onVisibleChange={handleAboveTheFoldVisibleChange}
-          style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
-        />
-        {onStartReached && !isEmpty && (
-          <EdgeVisibility
-            root={disableFullWindowScroll ? nativeRef : null}
-            onVisibleChange={onHeadVisibilityChange}
-            topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
-            containerRef={containerRef}
-          />
-        )}
-        {headerComponent}
-        {isEmpty
-          ? emptyComponent
-          : (data as Array<ItemT>)?.map((item, index) => {
-              const key = keyExtractor!(item, index)
-              return (
-                <Row<ItemT>
-                  key={key}
-                  item={item}
-                  index={index}
-                  renderItem={renderItem}
-                  extraData={extraData}
-                  onItemSeen={onItemSeen}
-                />
-              )
-            })}
-        {onEndReached && !isEmpty && (
-          <EdgeVisibility
+      <Layout.Center>
+        <View
+          ref={containerRef}
+          style={[
+            contentContainerStyle,
+            desktopFixedHeight ? styles.minHeightViewport : null,
+          ]}>
+          <Visibility
             root={disableFullWindowScroll ? nativeRef : null}
-            onVisibleChange={onTailVisibilityChange}
-            bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
-            containerRef={containerRef}
+            onVisibleChange={handleAboveTheFoldVisibleChange}
+            style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
           />
-        )}
-        {footerComponent}
-      </View>
+          {onStartReached && !isEmpty && (
+            <EdgeVisibility
+              root={disableFullWindowScroll ? nativeRef : null}
+              onVisibleChange={onHeadVisibilityChange}
+              topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
+              containerRef={containerRef}
+            />
+          )}
+          {headerComponent}
+          {isEmpty
+            ? emptyComponent
+            : (data as Array<ItemT>)?.map((item, index) => {
+                const key = keyExtractor!(item, index)
+                return (
+                  <Row<ItemT>
+                    key={key}
+                    item={item}
+                    index={index}
+                    renderItem={renderItem}
+                    extraData={extraData}
+                    onItemSeen={onItemSeen}
+                  />
+                )
+              })}
+          {onEndReached && !isEmpty && (
+            <EdgeVisibility
+              root={disableFullWindowScroll ? nativeRef : null}
+              onVisibleChange={onTailVisibilityChange}
+              bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
+              containerRef={containerRef}
+            />
+          )}
+          {footerComponent}
+        </View>
+      </Layout.Center>
     </View>
   )
 }
@@ -558,16 +551,6 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
 // https://stackoverflow.com/questions/7944460/detect-safari-browser
 
 const styles = StyleSheet.create({
-  sideBorders: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  containerScroll: {
-    width: '100%',
-    maxWidth: 600,
-    marginLeft: 'auto',
-    marginRight: 'auto',
-  },
   minHeightViewport: {
     // @ts-ignore web only
     minHeight: '100vh',
diff --git a/src/view/com/util/LoadingScreen.tsx b/src/view/com/util/LoadingScreen.tsx
index 5d2aeb38f..1086c9d17 100644
--- a/src/view/com/util/LoadingScreen.tsx
+++ b/src/view/com/util/LoadingScreen.tsx
@@ -1,14 +1,17 @@
 import {ActivityIndicator, View} from 'react-native'
 
 import {s} from '#/lib/styles'
-import {CenteredView} from './Views'
+import * as Layout from '#/components/Layout'
 
+/**
+ * @deprecated use Layout compoenents directly
+ */
 export function LoadingScreen() {
   return (
-    <CenteredView>
+    <Layout.Content>
       <View style={s.p20}>
         <ActivityIndicator size="large" />
       </View>
-    </CenteredView>
+    </Layout.Content>
   )
 }
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
deleted file mode 100644
index 78b66a929..000000000
--- a/src/view/com/util/SimpleViewHeader.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react'
-import {
-  StyleProp,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useNavigation} from '@react-navigation/native'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {NavigationProp} from '#/lib/routes/types'
-import {isWeb} from '#/platform/detection'
-import {useSetDrawerOpen} from '#/state/shell'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {CenteredView} from './Views'
-
-const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
-
-export function SimpleViewHeader({
-  showBackButton = true,
-  style,
-  children,
-}: React.PropsWithChildren<{
-  showBackButton?: boolean
-  style?: StyleProp<ViewStyle>
-}>) {
-  const pal = usePalette('default')
-  const setDrawerOpen = useSetDrawerOpen()
-  const navigation = useNavigation<NavigationProp>()
-  const {isMobile} = useWebMediaQueries()
-  const canGoBack = navigation.canGoBack()
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
-  const Container = isMobile ? View : CenteredView
-  return (
-    <Container
-      style={[
-        styles.header,
-        isMobile && styles.headerMobile,
-        isWeb && styles.headerWeb,
-        pal.view,
-        style,
-      ]}>
-      {showBackButton ? (
-        <TouchableOpacity
-          testID="viewHeaderDrawerBtn"
-          onPress={canGoBack ? onPressBack : onPressMenu}
-          hitSlop={BACK_HITSLOP}
-          style={canGoBack ? styles.backBtn : styles.backBtnWide}
-          accessibilityRole="button"
-          accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
-          accessibilityHint="">
-          {canGoBack ? (
-            <FontAwesomeIcon
-              size={18}
-              icon="angle-left"
-              style={[styles.backIcon, pal.text]}
-            />
-          ) : (
-            <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
-          )}
-        </TouchableOpacity>
-      ) : null}
-      {children}
-    </Container>
-  )
-}
-
-const styles = StyleSheet.create({
-  header: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingHorizontal: 18,
-    paddingVertical: 12,
-    width: '100%',
-  },
-  headerMobile: {
-    paddingHorizontal: 12,
-    paddingVertical: 10,
-  },
-  headerWeb: {
-    // @ts-ignore web-only
-    position: 'sticky',
-    top: 0,
-    zIndex: 1,
-  },
-  backBtn: {
-    width: 30,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 30,
-    height: 30,
-    paddingLeft: 4,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 1d4cf8ff0..2d413f782 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -1,271 +1,27 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import Animated from 'react-native-reanimated'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
-
-import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {NavigationProp} from '#/lib/routes/types'
-import {useSetDrawerOpen} from '#/state/shell'
-import {useTheme} from '#/alf'
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {Text} from './text/Text'
-import {CenteredView} from './Views'
-
-const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
+import {Header} from '#/components/Layout'
 
+/**
+ * Legacy ViewHeader component. Use Layout.Header going forward.
+ *
+ * @deprecated
+ */
 export function ViewHeader({
   title,
-  subtitle,
-  canGoBack,
-  showBackButton = true,
-  hideOnScroll,
-  showOnDesktop,
-  showBorder,
   renderButton,
 }: {
   title: string
   subtitle?: string
-  canGoBack?: boolean
-  showBackButton?: boolean
-  hideOnScroll?: boolean
   showOnDesktop?: boolean
   showBorder?: boolean
   renderButton?: () => JSX.Element
 }) {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const setDrawerOpen = useSetDrawerOpen()
-  const navigation = useNavigation<NavigationProp>()
-  const {isDesktop, isTablet} = useWebMediaQueries()
-  const t = useTheme()
-
-  const onPressBack = React.useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  const onPressMenu = React.useCallback(() => {
-    setDrawerOpen(true)
-  }, [setDrawerOpen])
-
-  if (isDesktop) {
-    if (showOnDesktop) {
-      return (
-        <DesktopWebHeader
-          title={title}
-          subtitle={subtitle}
-          renderButton={renderButton}
-          showBorder={showBorder}
-        />
-      )
-    }
-    return null
-  } else {
-    if (typeof canGoBack === 'undefined') {
-      canGoBack = navigation.canGoBack()
-    }
-
-    return (
-      <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
-        <View style={{flex: 1}}>
-          <View style={{flexDirection: 'row', alignItems: 'center'}}>
-            {showBackButton ? (
-              <TouchableOpacity
-                testID="viewHeaderDrawerBtn"
-                onPress={canGoBack ? onPressBack : onPressMenu}
-                hitSlop={BACK_HITSLOP}
-                style={canGoBack ? styles.backBtn : styles.backBtnWide}
-                accessibilityRole="button"
-                accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
-                accessibilityHint={
-                  canGoBack ? '' : _(msg`Access navigation links and settings`)
-                }>
-                {canGoBack ? (
-                  <FontAwesomeIcon
-                    size={18}
-                    icon="angle-left"
-                    style={[styles.backIcon, pal.text]}
-                  />
-                ) : !isTablet ? (
-                  <Menu size="lg" style={[{marginTop: 3}, pal.textLight]} />
-                ) : null}
-              </TouchableOpacity>
-            ) : null}
-            <View style={styles.titleContainer} pointerEvents="none">
-              <Text emoji type="title" style={[pal.text, styles.title]}>
-                {title}
-              </Text>
-            </View>
-            {renderButton ? (
-              renderButton()
-            ) : showBackButton ? (
-              <View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
-            ) : null}
-          </View>
-          {subtitle ? (
-            <View
-              style={[styles.titleContainer, {marginTop: -3}]}
-              pointerEvents="none">
-              <Text
-                style={[
-                  pal.text,
-                  styles.subtitle,
-                  t.atoms.text_contrast_medium,
-                ]}>
-                {subtitle}
-              </Text>
-            </View>
-          ) : undefined}
-        </View>
-      </Container>
-    )
-  }
-}
-
-function DesktopWebHeader({
-  title,
-  subtitle,
-  renderButton,
-  showBorder = true,
-}: {
-  title: string
-  subtitle?: string
-  renderButton?: () => JSX.Element
-  showBorder?: boolean
-}) {
-  const pal = usePalette('default')
-  const t = useTheme()
-  return (
-    <CenteredView
-      style={[
-        styles.header,
-        styles.desktopHeader,
-        pal.border,
-        {
-          borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0,
-        },
-        {display: 'flex', flexDirection: 'column'},
-      ]}>
-      <View>
-        <View style={styles.titleContainer} pointerEvents="none">
-          <Text type="title-lg" style={[pal.text, styles.title]}>
-            {title}
-          </Text>
-        </View>
-        {renderButton?.()}
-      </View>
-      {subtitle ? (
-        <View>
-          <View style={[styles.titleContainer]} pointerEvents="none">
-            <Text
-              style={[
-                pal.text,
-                styles.subtitleDesktop,
-                t.atoms.text_contrast_medium,
-              ]}>
-              {subtitle}
-            </Text>
-          </View>
-        </View>
-      ) : null}
-    </CenteredView>
-  )
-}
-
-function Container({
-  children,
-  hideOnScroll,
-  showBorder,
-}: {
-  children: React.ReactNode
-  hideOnScroll: boolean
-  showBorder?: boolean
-}) {
-  const pal = usePalette('default')
-  const headerMinimalShellTransform = useMinimalShellHeaderTransform()
-
-  if (!hideOnScroll) {
-    return (
-      <View
-        style={[
-          styles.header,
-          pal.view,
-          pal.border,
-          showBorder && styles.border,
-        ]}>
-        {children}
-      </View>
-    )
-  }
   return (
-    <Animated.View
-      style={[
-        styles.header,
-        styles.headerFloating,
-        pal.view,
-        pal.border,
-        headerMinimalShellTransform,
-        showBorder && styles.border,
-      ]}>
-      {children}
-    </Animated.View>
+    <Header.Outer>
+      <Header.BackButton />
+      <Header.Content>
+        <Header.TitleText>{title}</Header.TitleText>
+      </Header.Content>
+      <Header.Slot>{renderButton?.() ?? null}</Header.Slot>
+    </Header.Outer>
   )
 }
-
-const styles = StyleSheet.create({
-  header: {
-    flexDirection: 'row',
-    paddingHorizontal: 12,
-    paddingVertical: 6,
-    width: '100%',
-  },
-  headerFloating: {
-    position: 'absolute',
-    top: 0,
-    width: '100%',
-  },
-  desktopHeader: {
-    paddingVertical: 12,
-    maxWidth: 600,
-    marginLeft: 'auto',
-    marginRight: 'auto',
-  },
-  border: {
-    borderBottomWidth: StyleSheet.hairlineWidth,
-  },
-  titleContainer: {
-    marginLeft: 'auto',
-    marginRight: 'auto',
-    alignItems: 'center',
-  },
-  title: {
-    fontWeight: '600',
-  },
-  subtitle: {
-    fontSize: 13,
-  },
-  subtitleDesktop: {
-    fontSize: 15,
-  },
-  backBtn: {
-    width: 30,
-    height: 30,
-  },
-  backBtnWide: {
-    width: 30,
-    height: 30,
-    paddingLeft: 4,
-    marginRight: 4,
-  },
-  backIcon: {
-    marginTop: 6,
-  },
-})
diff --git a/src/view/com/util/Views.tsx b/src/view/com/util/Views.tsx
index 0d3f63794..c9ba0728c 100644
--- a/src/view/com/util/Views.tsx
+++ b/src/view/com/util/Views.tsx
@@ -15,9 +15,16 @@ export type FlatList_INTERNAL<ItemT = any> = Omit<
   FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>,
   'CellRendererComponent'
 >
+
+/**
+ * @deprecated use `Layout` components
+ */
 export const ScrollView = Animated.ScrollView
 export type ScrollView = typeof Animated.ScrollView
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const CenteredView = forwardRef<
   View,
   React.PropsWithChildren<
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 1f030b408..e64b0ce9a 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -31,10 +31,12 @@ interface AddedProps {
   desktopFixedHeight?: boolean | number
 }
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const CenteredView = React.forwardRef(function CenteredView(
   {
     style,
-    sideBorders,
     topBorder,
     ...props
   }: React.PropsWithChildren<
@@ -47,13 +49,6 @@ export const CenteredView = React.forwardRef(function CenteredView(
   if (!isMobile) {
     style = addStyle(style, styles.container)
   }
-  if (sideBorders && !isMobile) {
-    style = addStyle(style, {
-      borderLeftWidth: StyleSheet.hairlineWidth,
-      borderRightWidth: StyleSheet.hairlineWidth,
-    })
-    style = addStyle(style, pal.border)
-  }
   if (topBorder) {
     style = addStyle(style, {
       borderTopWidth: 1,
@@ -75,7 +70,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   >,
   ref: React.Ref<FlatList<ItemT>>,
 ) {
-  const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
     contentContainerStyle = addStyle(
@@ -123,11 +117,7 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   return (
     <Animated.FlatList
       ref={ref}
-      contentContainerStyle={[
-        styles.contentContainer,
-        contentContainerStyle,
-        pal.border,
-      ]}
+      contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
       style={style}
       contentOffset={contentOffset}
       {...props}
@@ -135,12 +125,13 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   )
 })
 
+/**
+ * @deprecated use `Layout` components
+ */
 export const ScrollView = React.forwardRef(function ScrollViewImpl(
   {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
   ref: React.Ref<Animated.ScrollView>,
 ) {
-  const pal = usePalette('default')
-
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
     contentContainerStyle = addStyle(
@@ -150,11 +141,7 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
   }
   return (
     <Animated.ScrollView
-      contentContainerStyle={[
-        styles.contentContainer,
-        contentContainerStyle,
-        pal.border,
-      ]}
+      contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
       // @ts-ignore something is wrong with the reanimated types -prf
       ref={ref}
       {...props}
@@ -164,8 +151,6 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
 
 const styles = StyleSheet.create({
   contentContainer: {
-    borderLeftWidth: StyleSheet.hairlineWidth,
-    borderRightWidth: StyleSheet.hairlineWidth,
     // @ts-ignore web only
     minHeight: '100vh',
   },
diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index b66f43789..846f4d295 100644
--- a/src/view/com/util/error/ErrorScreen.tsx
+++ b/src/view/com/util/error/ErrorScreen.tsx
@@ -36,7 +36,9 @@ export function ErrorScreen({
 
   return (
     <>
-      {showHeader && isMobile && <ViewHeader title="Error" showBorder />}
+      {showHeader && isMobile && (
+        <ViewHeader title={_(msg`Error`)} showBorder />
+      )}
       <CenteredView testID={testID} style={[styles.outer, pal.view]}>
         <View style={styles.errorIconContainer}>
           <View