about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.web.tsx39
-rw-r--r--src/Navigation.tsx2
-rw-r--r--src/alf/README.md56
-rw-r--r--src/alf/atoms.ts514
-rw-r--r--src/alf/index.tsx92
-rw-r--r--src/alf/themes.ts108
-rw-r--r--src/alf/tokens.ts100
-rw-r--r--src/alf/types.ts16
-rw-r--r--src/alf/util/platform.ts25
-rw-r--r--src/alf/util/useColorModeTheme.ts10
-rw-r--r--src/view/com/Button.tsx204
-rw-r--r--src/view/com/Typography.tsx104
-rw-r--r--src/view/screens/DebugNew.tsx541
13 files changed, 1793 insertions, 18 deletions
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 6c67dc28b..1bdb3c208 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -7,6 +7,7 @@ import {RootSiblingParent} from 'react-native-root-siblings'
 
 import 'view/icons'
 
+import {ThemeProvider as Alf} from '#/alf'
 import {init as initPersistedState} from '#/state/persisted'
 import {useColorMode} from 'state/shell'
 import {Shell} from 'view/shell/index'
@@ -28,11 +29,13 @@ import {
 } from 'state/session'
 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
 import * as persisted from '#/state/persisted'
+import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
 
 function InnerApp() {
   const {isInitialLoad, currentAccount} = useSession()
   const {resumeSession} = useSessionApi()
   const colorMode = useColorMode()
+  const theme = useColorModeTheme(colorMode)
 
   // init
   useEffect(() => {
@@ -44,23 +47,25 @@ function InnerApp() {
   if (isInitialLoad) return null
 
   return (
-    <React.Fragment
-      // Resets the entire tree below when it changes:
-      key={currentAccount?.did}>
-      <LoggedOutViewProvider>
-        <UnreadNotifsProvider>
-          <ThemeProvider theme={colorMode}>
-            {/* All components should be within this provider */}
-            <RootSiblingParent>
-              <SafeAreaProvider>
-                <Shell />
-              </SafeAreaProvider>
-            </RootSiblingParent>
-            <ToastContainer />
-          </ThemeProvider>
-        </UnreadNotifsProvider>
-      </LoggedOutViewProvider>
-    </React.Fragment>
+    <Alf theme={theme}>
+      <React.Fragment
+        // Resets the entire tree below when it changes:
+        key={currentAccount?.did}>
+        <LoggedOutViewProvider>
+          <UnreadNotifsProvider>
+            <ThemeProvider theme={colorMode}>
+              {/* All components should be within this provider */}
+              <RootSiblingParent>
+                <SafeAreaProvider>
+                  <Shell />
+                </SafeAreaProvider>
+              </RootSiblingParent>
+              <ToastContainer />
+            </ThemeProvider>
+          </UnreadNotifsProvider>
+        </LoggedOutViewProvider>
+      </React.Fragment>
+    </Alf>
   )
 }
 
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 7bb1aa0ad..76a893c68 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -61,7 +61,7 @@ import {ProfileListScreen} from './view/screens/ProfileList'
 import {PostThreadScreen} from './view/screens/PostThread'
 import {PostLikedByScreen} from './view/screens/PostLikedBy'
 import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
-import {DebugScreen} from './view/screens/Debug'
+import {DebugScreen} from './view/screens/DebugNew'
 import {LogScreen} from './view/screens/Log'
 import {SupportScreen} from './view/screens/Support'
 import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
diff --git a/src/alf/README.md b/src/alf/README.md
new file mode 100644
index 000000000..aa31bcf98
--- /dev/null
+++ b/src/alf/README.md
@@ -0,0 +1,56 @@
+# Application Layout Framework (ALF)
+
+A set of UI primitives and components.
+
+## Usage
+
+Naming conventions follow Tailwind — delimited with a `_` instead of `-` to
+enable object access — with a couple exceptions:
+
+**Spacing**
+
+Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` instead of
+increments of 4px. We only use a few common spacings, and otherwise typically
+rely on many one-off values.
+
+**Text Size**
+
+Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` to match our
+type scale.
+
+**Line Height**
+
+The text size atoms also apply a line-height with the same value as the size,
+for a 1:1 ratio. `tight` and `normal` are retained for use in the few places
+where we need leading.
+
+### Atoms
+
+An (mostly-complete) set of style definitions that match Tailwind CSS selectors.
+These are static and reused throughout the app.
+
+```tsx
+import { atoms } from '#/alf'
+
+<View style={[atoms.flex_row]} />
+```
+
+### Theme
+
+Any values that rely on the theme, namely colors.
+
+```tsx
+const t = useTheme()
+
+<View style={[atoms.flex_row, t.atoms.bg]} />
+```
+
+### Breakpoints
+
+```tsx
+const b = useBreakpoints()
+
+if (b.gtMobile) {
+  // render tablet or desktop UI
+}
+```
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
new file mode 100644
index 000000000..c142f5f71
--- /dev/null
+++ b/src/alf/atoms.ts
@@ -0,0 +1,514 @@
+import * as tokens from '#/alf/tokens'
+
+export const atoms = {
+  /*
+   * Positioning
+   */
+  absolute: {
+    position: 'absolute',
+  },
+  relative: {
+    position: 'relative',
+  },
+  inset_0: {
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+  },
+  z_10: {
+    zIndex: 10,
+  },
+  z_20: {
+    zIndex: 20,
+  },
+  z_30: {
+    zIndex: 30,
+  },
+  z_40: {
+    zIndex: 40,
+  },
+  z_50: {
+    zIndex: 50,
+  },
+
+  /*
+   * Width
+   */
+  w_full: {
+    width: '100%',
+  },
+  h_full: {
+    height: '100%',
+  },
+
+  /*
+   * Border radius
+   */
+  rounded_sm: {
+    borderRadius: tokens.borderRadius.sm,
+  },
+  rounded_md: {
+    borderRadius: tokens.borderRadius.md,
+  },
+  rounded_full: {
+    borderRadius: tokens.borderRadius.full,
+  },
+
+  /*
+   * Flex
+   */
+  gap_xxs: {
+    gap: tokens.space.xxs,
+  },
+  gap_xs: {
+    gap: tokens.space.xs,
+  },
+  gap_sm: {
+    gap: tokens.space.sm,
+  },
+  gap_md: {
+    gap: tokens.space.md,
+  },
+  gap_lg: {
+    gap: tokens.space.lg,
+  },
+  gap_xl: {
+    gap: tokens.space.xl,
+  },
+  gap_xxl: {
+    gap: tokens.space.xxl,
+  },
+  flex: {
+    display: 'flex',
+  },
+  flex_row: {
+    flexDirection: 'row',
+  },
+  flex_wrap: {
+    flexWrap: 'wrap',
+  },
+  flex_1: {
+    flex: 1,
+  },
+  flex_grow: {
+    flexGrow: 1,
+  },
+  flex_shrink: {
+    flexShrink: 1,
+  },
+  justify_center: {
+    justifyContent: 'center',
+  },
+  justify_between: {
+    justifyContent: 'space-between',
+  },
+  justify_end: {
+    justifyContent: 'flex-end',
+  },
+  align_center: {
+    alignItems: 'center',
+  },
+  align_start: {
+    alignItems: 'flex-start',
+  },
+  align_end: {
+    alignItems: 'flex-end',
+  },
+
+  /*
+   * Text
+   */
+  text_center: {
+    textAlign: 'center',
+  },
+  text_right: {
+    textAlign: 'right',
+  },
+  text_xxs: {
+    fontSize: tokens.fontSize.xxs,
+    lineHeight: tokens.fontSize.xxs,
+  },
+  text_xs: {
+    fontSize: tokens.fontSize.xs,
+    lineHeight: tokens.fontSize.xs,
+  },
+  text_sm: {
+    fontSize: tokens.fontSize.sm,
+    lineHeight: tokens.fontSize.sm,
+  },
+  text_md: {
+    fontSize: tokens.fontSize.md,
+    lineHeight: tokens.fontSize.md,
+  },
+  text_lg: {
+    fontSize: tokens.fontSize.lg,
+    lineHeight: tokens.fontSize.lg,
+  },
+  text_xl: {
+    fontSize: tokens.fontSize.xl,
+    lineHeight: tokens.fontSize.xl,
+  },
+  text_xxl: {
+    fontSize: tokens.fontSize.xxl,
+    lineHeight: tokens.fontSize.xxl,
+  },
+  leading_tight: {
+    lineHeight: 1.25,
+  },
+  leading_normal: {
+    lineHeight: 1.5,
+  },
+  font_normal: {
+    fontWeight: tokens.fontWeight.normal,
+  },
+  font_semibold: {
+    fontWeight: tokens.fontWeight.semibold,
+  },
+  font_bold: {
+    fontWeight: tokens.fontWeight.bold,
+  },
+
+  /*
+   * Border
+   */
+  border: {
+    borderWidth: 1,
+  },
+  border_t: {
+    borderTopWidth: 1,
+  },
+  border_b: {
+    borderBottomWidth: 1,
+  },
+
+  /*
+   * Padding
+   */
+  p_xxs: {
+    padding: tokens.space.xxs,
+  },
+  p_xs: {
+    padding: tokens.space.xs,
+  },
+  p_sm: {
+    padding: tokens.space.sm,
+  },
+  p_md: {
+    padding: tokens.space.md,
+  },
+  p_lg: {
+    padding: tokens.space.lg,
+  },
+  p_xl: {
+    padding: tokens.space.xl,
+  },
+  p_xxl: {
+    padding: tokens.space.xxl,
+  },
+  px_xxs: {
+    paddingLeft: tokens.space.xxs,
+    paddingRight: tokens.space.xxs,
+  },
+  px_xs: {
+    paddingLeft: tokens.space.xs,
+    paddingRight: tokens.space.xs,
+  },
+  px_sm: {
+    paddingLeft: tokens.space.sm,
+    paddingRight: tokens.space.sm,
+  },
+  px_md: {
+    paddingLeft: tokens.space.md,
+    paddingRight: tokens.space.md,
+  },
+  px_lg: {
+    paddingLeft: tokens.space.lg,
+    paddingRight: tokens.space.lg,
+  },
+  px_xl: {
+    paddingLeft: tokens.space.xl,
+    paddingRight: tokens.space.xl,
+  },
+  px_xxl: {
+    paddingLeft: tokens.space.xxl,
+    paddingRight: tokens.space.xxl,
+  },
+  py_xxs: {
+    paddingTop: tokens.space.xxs,
+    paddingBottom: tokens.space.xxs,
+  },
+  py_xs: {
+    paddingTop: tokens.space.xs,
+    paddingBottom: tokens.space.xs,
+  },
+  py_sm: {
+    paddingTop: tokens.space.sm,
+    paddingBottom: tokens.space.sm,
+  },
+  py_md: {
+    paddingTop: tokens.space.md,
+    paddingBottom: tokens.space.md,
+  },
+  py_lg: {
+    paddingTop: tokens.space.lg,
+    paddingBottom: tokens.space.lg,
+  },
+  py_xl: {
+    paddingTop: tokens.space.xl,
+    paddingBottom: tokens.space.xl,
+  },
+  py_xxl: {
+    paddingTop: tokens.space.xxl,
+    paddingBottom: tokens.space.xxl,
+  },
+  pt_xxs: {
+    paddingTop: tokens.space.xxs,
+  },
+  pt_xs: {
+    paddingTop: tokens.space.xs,
+  },
+  pt_sm: {
+    paddingTop: tokens.space.sm,
+  },
+  pt_md: {
+    paddingTop: tokens.space.md,
+  },
+  pt_lg: {
+    paddingTop: tokens.space.lg,
+  },
+  pt_xl: {
+    paddingTop: tokens.space.xl,
+  },
+  pt_xxl: {
+    paddingTop: tokens.space.xxl,
+  },
+  pb_xxs: {
+    paddingBottom: tokens.space.xxs,
+  },
+  pb_xs: {
+    paddingBottom: tokens.space.xs,
+  },
+  pb_sm: {
+    paddingBottom: tokens.space.sm,
+  },
+  pb_md: {
+    paddingBottom: tokens.space.md,
+  },
+  pb_lg: {
+    paddingBottom: tokens.space.lg,
+  },
+  pb_xl: {
+    paddingBottom: tokens.space.xl,
+  },
+  pb_xxl: {
+    paddingBottom: tokens.space.xxl,
+  },
+  pl_xxs: {
+    paddingLeft: tokens.space.xxs,
+  },
+  pl_xs: {
+    paddingLeft: tokens.space.xs,
+  },
+  pl_sm: {
+    paddingLeft: tokens.space.sm,
+  },
+  pl_md: {
+    paddingLeft: tokens.space.md,
+  },
+  pl_lg: {
+    paddingLeft: tokens.space.lg,
+  },
+  pl_xl: {
+    paddingLeft: tokens.space.xl,
+  },
+  pl_xxl: {
+    paddingLeft: tokens.space.xxl,
+  },
+  pr_xxs: {
+    paddingRight: tokens.space.xxs,
+  },
+  pr_xs: {
+    paddingRight: tokens.space.xs,
+  },
+  pr_sm: {
+    paddingRight: tokens.space.sm,
+  },
+  pr_md: {
+    paddingRight: tokens.space.md,
+  },
+  pr_lg: {
+    paddingRight: tokens.space.lg,
+  },
+  pr_xl: {
+    paddingRight: tokens.space.xl,
+  },
+  pr_xxl: {
+    paddingRight: tokens.space.xxl,
+  },
+
+  /*
+   * Margin
+   */
+  m_xxs: {
+    margin: tokens.space.xxs,
+  },
+  m_xs: {
+    margin: tokens.space.xs,
+  },
+  m_sm: {
+    margin: tokens.space.sm,
+  },
+  m_md: {
+    margin: tokens.space.md,
+  },
+  m_lg: {
+    margin: tokens.space.lg,
+  },
+  m_xl: {
+    margin: tokens.space.xl,
+  },
+  m_xxl: {
+    margin: tokens.space.xxl,
+  },
+  mx_xxs: {
+    marginLeft: tokens.space.xxs,
+    marginRight: tokens.space.xxs,
+  },
+  mx_xs: {
+    marginLeft: tokens.space.xs,
+    marginRight: tokens.space.xs,
+  },
+  mx_sm: {
+    marginLeft: tokens.space.sm,
+    marginRight: tokens.space.sm,
+  },
+  mx_md: {
+    marginLeft: tokens.space.md,
+    marginRight: tokens.space.md,
+  },
+  mx_lg: {
+    marginLeft: tokens.space.lg,
+    marginRight: tokens.space.lg,
+  },
+  mx_xl: {
+    marginLeft: tokens.space.xl,
+    marginRight: tokens.space.xl,
+  },
+  mx_xxl: {
+    marginLeft: tokens.space.xxl,
+    marginRight: tokens.space.xxl,
+  },
+  my_xxs: {
+    marginTop: tokens.space.xxs,
+    marginBottom: tokens.space.xxs,
+  },
+  my_xs: {
+    marginTop: tokens.space.xs,
+    marginBottom: tokens.space.xs,
+  },
+  my_sm: {
+    marginTop: tokens.space.sm,
+    marginBottom: tokens.space.sm,
+  },
+  my_md: {
+    marginTop: tokens.space.md,
+    marginBottom: tokens.space.md,
+  },
+  my_lg: {
+    marginTop: tokens.space.lg,
+    marginBottom: tokens.space.lg,
+  },
+  my_xl: {
+    marginTop: tokens.space.xl,
+    marginBottom: tokens.space.xl,
+  },
+  my_xxl: {
+    marginTop: tokens.space.xxl,
+    marginBottom: tokens.space.xxl,
+  },
+  mt_xxs: {
+    marginTop: tokens.space.xxs,
+  },
+  mt_xs: {
+    marginTop: tokens.space.xs,
+  },
+  mt_sm: {
+    marginTop: tokens.space.sm,
+  },
+  mt_md: {
+    marginTop: tokens.space.md,
+  },
+  mt_lg: {
+    marginTop: tokens.space.lg,
+  },
+  mt_xl: {
+    marginTop: tokens.space.xl,
+  },
+  mt_xxl: {
+    marginTop: tokens.space.xxl,
+  },
+  mb_xxs: {
+    marginBottom: tokens.space.xxs,
+  },
+  mb_xs: {
+    marginBottom: tokens.space.xs,
+  },
+  mb_sm: {
+    marginBottom: tokens.space.sm,
+  },
+  mb_md: {
+    marginBottom: tokens.space.md,
+  },
+  mb_lg: {
+    marginBottom: tokens.space.lg,
+  },
+  mb_xl: {
+    marginBottom: tokens.space.xl,
+  },
+  mb_xxl: {
+    marginBottom: tokens.space.xxl,
+  },
+  ml_xxs: {
+    marginLeft: tokens.space.xxs,
+  },
+  ml_xs: {
+    marginLeft: tokens.space.xs,
+  },
+  ml_sm: {
+    marginLeft: tokens.space.sm,
+  },
+  ml_md: {
+    marginLeft: tokens.space.md,
+  },
+  ml_lg: {
+    marginLeft: tokens.space.lg,
+  },
+  ml_xl: {
+    marginLeft: tokens.space.xl,
+  },
+  ml_xxl: {
+    marginLeft: tokens.space.xxl,
+  },
+  mr_xxs: {
+    marginRight: tokens.space.xxs,
+  },
+  mr_xs: {
+    marginRight: tokens.space.xs,
+  },
+  mr_sm: {
+    marginRight: tokens.space.sm,
+  },
+  mr_md: {
+    marginRight: tokens.space.md,
+  },
+  mr_lg: {
+    marginRight: tokens.space.lg,
+  },
+  mr_xl: {
+    marginRight: tokens.space.xl,
+  },
+  mr_xxl: {
+    marginRight: tokens.space.xxl,
+  },
+} as const
diff --git a/src/alf/index.tsx b/src/alf/index.tsx
new file mode 100644
index 000000000..1daa0bfed
--- /dev/null
+++ b/src/alf/index.tsx
@@ -0,0 +1,92 @@
+import React from 'react'
+import {Dimensions} from 'react-native'
+import * as themes from '#/alf/themes'
+
+export * as tokens from '#/alf/tokens'
+export {atoms} from '#/alf/atoms'
+export * from '#/alf/util/platform'
+
+type BreakpointName = keyof typeof breakpoints
+
+/*
+ * Breakpoints
+ */
+const breakpoints: {
+  [key: string]: number
+} = {
+  gtMobile: 800,
+  gtTablet: 1200,
+}
+function getActiveBreakpoints({width}: {width: number}) {
+  const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter(
+    breakpoint => width >= breakpoints[breakpoint],
+  )
+
+  return {
+    active: active[active.length - 1],
+    gtMobile: active.includes('gtMobile'),
+    gtTablet: active.includes('gtTablet'),
+  }
+}
+
+/*
+ * Context
+ */
+export const Context = React.createContext<{
+  themeName: themes.ThemeName
+  theme: themes.Theme
+  breakpoints: {
+    active: BreakpointName | undefined
+    gtMobile: boolean
+    gtTablet: boolean
+  }
+}>({
+  themeName: 'light',
+  theme: themes.light,
+  breakpoints: {
+    active: undefined,
+    gtMobile: false,
+    gtTablet: false,
+  },
+})
+
+export function ThemeProvider({
+  children,
+  theme: themeName,
+}: React.PropsWithChildren<{theme: themes.ThemeName}>) {
+  const theme = themes[themeName]
+  const [breakpoints, setBreakpoints] = React.useState(() =>
+    getActiveBreakpoints({width: Dimensions.get('window').width}),
+  )
+
+  React.useEffect(() => {
+    const listener = Dimensions.addEventListener('change', ({window}) => {
+      const bp = getActiveBreakpoints({width: window.width})
+      if (bp.active !== breakpoints.active) setBreakpoints(bp)
+    })
+
+    return listener.remove
+  }, [breakpoints, setBreakpoints])
+
+  return (
+    <Context.Provider
+      value={React.useMemo(
+        () => ({
+          themeName: themeName,
+          theme: theme,
+          breakpoints,
+        }),
+        [theme, themeName, breakpoints],
+      )}>
+      {children}
+    </Context.Provider>
+  )
+}
+
+export function useTheme() {
+  return React.useContext(Context).theme
+}
+
+export function useBreakpoints() {
+  return React.useContext(Context).breakpoints
+}
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
new file mode 100644
index 000000000..aae5c5893
--- /dev/null
+++ b/src/alf/themes.ts
@@ -0,0 +1,108 @@
+import * as tokens from '#/alf/tokens'
+import type {Mutable} from '#/alf/types'
+
+export type ThemeName = 'light' | 'dark'
+export type ReadonlyTheme = typeof light
+export type Theme = Mutable<ReadonlyTheme>
+
+export type Palette = {
+  primary: string
+  positive: string
+  negative: string
+}
+
+export const lightPalette: Palette = {
+  primary: tokens.color.blue_500,
+  positive: tokens.color.green_500,
+  negative: tokens.color.red_500,
+} as const
+
+export const darkPalette: Palette = {
+  primary: tokens.color.blue_500,
+  positive: tokens.color.green_400,
+  negative: tokens.color.red_400,
+} as const
+
+export const light = {
+  palette: lightPalette,
+  atoms: {
+    text: {
+      color: tokens.color.gray_1000,
+    },
+    text_contrast_700: {
+      color: tokens.color.gray_700,
+    },
+    text_contrast_500: {
+      color: tokens.color.gray_500,
+    },
+    text_inverted: {
+      color: tokens.color.white,
+    },
+    bg: {
+      backgroundColor: tokens.color.white,
+    },
+    bg_contrast_100: {
+      backgroundColor: tokens.color.gray_100,
+    },
+    bg_contrast_200: {
+      backgroundColor: tokens.color.gray_200,
+    },
+    bg_contrast_300: {
+      backgroundColor: tokens.color.gray_300,
+    },
+    bg_positive: {
+      backgroundColor: tokens.color.green_500,
+    },
+    bg_negative: {
+      backgroundColor: tokens.color.red_400,
+    },
+    border: {
+      borderColor: tokens.color.gray_200,
+    },
+    border_contrast_500: {
+      borderColor: tokens.color.gray_500,
+    },
+  },
+}
+
+export const dark: Theme = {
+  palette: darkPalette,
+  atoms: {
+    text: {
+      color: tokens.color.white,
+    },
+    text_contrast_700: {
+      color: tokens.color.gray_300,
+    },
+    text_contrast_500: {
+      color: tokens.color.gray_500,
+    },
+    text_inverted: {
+      color: tokens.color.gray_1000,
+    },
+    bg: {
+      backgroundColor: tokens.color.gray_1000,
+    },
+    bg_contrast_100: {
+      backgroundColor: tokens.color.gray_900,
+    },
+    bg_contrast_200: {
+      backgroundColor: tokens.color.gray_800,
+    },
+    bg_contrast_300: {
+      backgroundColor: tokens.color.gray_700,
+    },
+    bg_positive: {
+      backgroundColor: tokens.color.green_400,
+    },
+    bg_negative: {
+      backgroundColor: tokens.color.red_400,
+    },
+    border: {
+      borderColor: tokens.color.gray_800,
+    },
+    border_contrast_500: {
+      borderColor: tokens.color.gray_500,
+    },
+  },
+}
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
new file mode 100644
index 000000000..4034e0deb
--- /dev/null
+++ b/src/alf/tokens.ts
@@ -0,0 +1,100 @@
+const BLUE_HUE = 211
+const GRAYSCALE_SATURATION = 22
+
+export const color = {
+  white: '#FFFFFF',
+
+  gray_0: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 100%)`,
+  gray_100: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`,
+  gray_200: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`,
+  gray_300: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`,
+  gray_400: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 65%)`,
+  gray_500: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 55%)`,
+  gray_600: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 45%)`,
+  gray_700: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 35%)`,
+  gray_800: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 25%)`,
+  gray_900: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 15%)`,
+  gray_1000: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`,
+
+  blue_0: `hsl(${BLUE_HUE}, 99%, 100%)`,
+  blue_100: `hsl(${BLUE_HUE}, 99%, 93%)`,
+  blue_200: `hsl(${BLUE_HUE}, 99%, 83%)`,
+  blue_300: `hsl(${BLUE_HUE}, 99%, 73%)`,
+  blue_400: `hsl(${BLUE_HUE}, 99%, 63%)`,
+  blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
+  blue_600: `hsl(${BLUE_HUE}, 99%, 43%)`,
+  blue_700: `hsl(${BLUE_HUE}, 99%, 33%)`,
+  blue_800: `hsl(${BLUE_HUE}, 99%, 23%)`,
+  blue_900: `hsl(${BLUE_HUE}, 99%, 13%)`,
+  blue_1000: `hsl(${BLUE_HUE}, 99%, 8%)`,
+
+  green_0: `hsl(130, 60%, 100%)`,
+  green_100: `hsl(130, 60%, 95%)`,
+  green_200: `hsl(130, 60%, 85%)`,
+  green_300: `hsl(130, 60%, 75%)`,
+  green_400: `hsl(130, 60%, 65%)`,
+  green_500: `hsl(130, 60%, 55%)`,
+  green_600: `hsl(130, 60%, 45%)`,
+  green_700: `hsl(130, 60%, 35%)`,
+  green_800: `hsl(130, 60%, 25%)`,
+  green_900: `hsl(130, 60%, 15%)`,
+  green_1000: `hsl(130, 60%, 5%)`,
+
+  red_0: `hsl(349, 96%, 100%)`,
+  red_100: `hsl(349, 96%, 95%)`,
+  red_200: `hsl(349, 96%, 85%)`,
+  red_300: `hsl(349, 96%, 75%)`,
+  red_400: `hsl(349, 96%, 65%)`,
+  red_500: `hsl(349, 96%, 55%)`,
+  red_600: `hsl(349, 96%, 45%)`,
+  red_700: `hsl(349, 96%, 35%)`,
+  red_800: `hsl(349, 96%, 25%)`,
+  red_900: `hsl(349, 96%, 15%)`,
+  red_1000: `hsl(349, 96%, 5%)`,
+} as const
+
+export const space = {
+  xxs: 2,
+  xs: 4,
+  sm: 8,
+  md: 12,
+  lg: 18,
+  xl: 24,
+  xxl: 32,
+} as const
+
+export const fontSize = {
+  xxs: 10,
+  xs: 12,
+  sm: 14,
+  md: 16,
+  lg: 18,
+  xl: 22,
+  xxl: 26,
+} as const
+
+// TODO test
+export const lineHeight = {
+  none: 1,
+  normal: 1.5,
+  relaxed: 1.625,
+} as const
+
+export const borderRadius = {
+  sm: 8,
+  md: 12,
+  full: 999,
+} as const
+
+export const fontWeight = {
+  normal: '400',
+  semibold: '600',
+  bold: '900',
+} as const
+
+export type Color = keyof typeof color
+export type Space = keyof typeof space
+export type FontSize = keyof typeof fontSize
+export type LineHeight = keyof typeof lineHeight
+export type BorderRadius = keyof typeof borderRadius
+export type FontWeight = keyof typeof fontWeight
diff --git a/src/alf/types.ts b/src/alf/types.ts
new file mode 100644
index 000000000..76ac05d40
--- /dev/null
+++ b/src/alf/types.ts
@@ -0,0 +1,16 @@
+type LiteralToCommon<T extends PropertyKey> = T extends number
+  ? number
+  : T extends string
+  ? string
+  : T extends symbol
+  ? symbol
+  : never
+
+/**
+ * @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers
+ */
+export type Mutable<T> = {
+  -readonly [K in keyof T]: T[K] extends PropertyKey
+    ? LiteralToCommon<T[K]>
+    : Mutable<T[K]>
+}
diff --git a/src/alf/util/platform.ts b/src/alf/util/platform.ts
new file mode 100644
index 000000000..544f5480b
--- /dev/null
+++ b/src/alf/util/platform.ts
@@ -0,0 +1,25 @@
+import {Platform} from 'react-native'
+
+export function web(value: any) {
+  return Platform.select({
+    web: value,
+  })
+}
+
+export function ios(value: any) {
+  return Platform.select({
+    ios: value,
+  })
+}
+
+export function android(value: any) {
+  return Platform.select({
+    android: value,
+  })
+}
+
+export function native(value: any) {
+  return Platform.select({
+    native: value,
+  })
+}
diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts
new file mode 100644
index 000000000..79cebc139
--- /dev/null
+++ b/src/alf/util/useColorModeTheme.ts
@@ -0,0 +1,10 @@
+import {useColorScheme} from 'react-native'
+
+import * as persisted from '#/state/persisted'
+
+export function useColorModeTheme(
+  theme: persisted.Schema['colorMode'],
+): 'light' | 'dark' {
+  const colorScheme = useColorScheme()
+  return (theme === 'system' ? colorScheme : theme) || 'light'
+}
diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx
new file mode 100644
index 000000000..d1f70d4ae
--- /dev/null
+++ b/src/view/com/Button.tsx
@@ -0,0 +1,204 @@
+import React from 'react'
+import {Pressable, Text, PressableProps, TextProps} from 'react-native'
+import * as tokens from '#/alf/tokens'
+import {atoms} from '#/alf'
+
+export type ButtonType =
+  | 'primary'
+  | 'secondary'
+  | 'tertiary'
+  | 'positive'
+  | 'negative'
+export type ButtonSize = 'small' | 'large'
+
+export type VariantProps = {
+  type?: ButtonType
+  size?: ButtonSize
+}
+type ButtonState = {
+  pressed: boolean
+  hovered: boolean
+  focused: boolean
+}
+export type ButtonProps = Omit<PressableProps, 'children'> &
+  VariantProps & {
+    children:
+      | ((props: {
+          state: ButtonState
+          type?: ButtonType
+          size?: ButtonSize
+        }) => React.ReactNode)
+      | React.ReactNode
+      | string
+  }
+export type ButtonTextProps = TextProps & VariantProps
+
+export function Button({children, style, type, size, ...rest}: ButtonProps) {
+  const {baseStyles, hoverStyles} = React.useMemo(() => {
+    const baseStyles = []
+    const hoverStyles = []
+
+    switch (type) {
+      case 'primary':
+        baseStyles.push({
+          backgroundColor: tokens.color.blue_500,
+        })
+        break
+      case 'secondary':
+        baseStyles.push({
+          backgroundColor: tokens.color.gray_200,
+        })
+        hoverStyles.push({
+          backgroundColor: tokens.color.gray_100,
+        })
+        break
+      default:
+    }
+
+    switch (size) {
+      case 'large':
+        baseStyles.push(
+          atoms.py_md,
+          atoms.px_xl,
+          atoms.rounded_md,
+          atoms.gap_sm,
+        )
+        break
+      case 'small':
+        baseStyles.push(
+          atoms.py_sm,
+          atoms.px_md,
+          atoms.rounded_sm,
+          atoms.gap_xs,
+        )
+        break
+      default:
+    }
+
+    return {
+      baseStyles,
+      hoverStyles,
+    }
+  }, [type, size])
+
+  const [state, setState] = React.useState({
+    pressed: false,
+    hovered: false,
+    focused: false,
+  })
+
+  const onPressIn = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      pressed: true,
+    }))
+  }, [setState])
+  const onPressOut = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      pressed: false,
+    }))
+  }, [setState])
+  const onHoverIn = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      hovered: true,
+    }))
+  }, [setState])
+  const onHoverOut = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      hovered: false,
+    }))
+  }, [setState])
+  const onFocus = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      focused: true,
+    }))
+  }, [setState])
+  const onBlur = React.useCallback(() => {
+    setState(s => ({
+      ...s,
+      focused: false,
+    }))
+  }, [setState])
+
+  return (
+    <Pressable
+      {...rest}
+      style={state => [
+        atoms.flex_row,
+        atoms.align_center,
+        ...baseStyles,
+        ...(state.hovered ? hoverStyles : []),
+        typeof style === 'function' ? style(state) : style,
+      ]}
+      onPressIn={onPressIn}
+      onPressOut={onPressOut}
+      onHoverIn={onHoverIn}
+      onHoverOut={onHoverOut}
+      onFocus={onFocus}
+      onBlur={onBlur}>
+      {typeof children === 'string' ? (
+        <ButtonText type={type} size={size}>
+          {children}
+        </ButtonText>
+      ) : typeof children === 'function' ? (
+        children({state, type, size})
+      ) : (
+        children
+      )}
+    </Pressable>
+  )
+}
+
+export function ButtonText({
+  children,
+  style,
+  type,
+  size,
+  ...rest
+}: ButtonTextProps) {
+  const textStyles = React.useMemo(() => {
+    const base = []
+
+    switch (type) {
+      case 'primary':
+        base.push({color: tokens.color.white})
+        break
+      case 'secondary':
+        base.push({
+          color: tokens.color.gray_700,
+        })
+        break
+      default:
+    }
+
+    switch (size) {
+      case 'small':
+        base.push(atoms.text_sm, {paddingBottom: 1})
+        break
+      case 'large':
+        base.push(atoms.text_md, {paddingBottom: 1})
+        break
+      default:
+    }
+
+    return base
+  }, [type, size])
+
+  return (
+    <Text
+      {...rest}
+      style={[
+        atoms.flex_1,
+        atoms.font_semibold,
+        atoms.text_center,
+        ...textStyles,
+        style,
+      ]}>
+      {children}
+    </Text>
+  )
+}
diff --git a/src/view/com/Typography.tsx b/src/view/com/Typography.tsx
new file mode 100644
index 000000000..6579c2e51
--- /dev/null
+++ b/src/view/com/Typography.tsx
@@ -0,0 +1,104 @@
+import React from 'react'
+import {Text as RNText, TextProps} from 'react-native'
+import {useTheme, atoms, web} from '#/alf'
+
+export function Text({style, ...rest}: TextProps) {
+  const t = useTheme()
+  return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
+}
+
+export function H1({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 1,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
+
+export function H2({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 2,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
+
+export function H3({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 3,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
+
+export function H4({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 4,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
+
+export function H5({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 5,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
+
+export function H6({style, ...rest}: TextProps) {
+  const t = useTheme()
+  const attr =
+    web({
+      role: 'heading',
+      'aria-level': 6,
+    }) || {}
+  return (
+    <RNText
+      {...attr}
+      {...rest}
+      style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]}
+    />
+  )
+}
diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx
new file mode 100644
index 000000000..0b7c5f03b
--- /dev/null
+++ b/src/view/screens/DebugNew.tsx
@@ -0,0 +1,541 @@
+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>
+  )
+}