[Neue] Base (#5395)

* Add fontScale, gate it, fix some computes

* Add inter, integrate

* Clean up

* Apply to old Text component

* Use numeric weight

* Cleanup

* Clean up appearance settings

* Global tracking

* Fix regular italic variant

* Refactor settings and fontScale values

* Remove flags

* Get rid of lower weight font usage

* Remove gate from settings

* Refactor appearance settings for reuse

* Add neue type nux

* Update defaults

* Load fonts, add fallback families

* Load fonts via plugin in production

* Fixes

* Fix for web

* Nits

---------

Co-authored-by: Hailey <me@haileyok.com>
zio/dev^2
Eric Bailey 2024-09-18 19:35:34 -05:00 committed by GitHub
parent fb3be79820
commit cbc7cd0808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 835 additions and 256 deletions

View File

@ -230,6 +230,31 @@ module.exports = function (config) {
'./plugins/shareExtension/withShareExtensions.js', './plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js', './plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js', './plugins/withAppDelegateReferrer.js',
[
'expo-font',
{
fonts: [
// './assets/fonts/inter/Inter-Thin.otf',
// './assets/fonts/inter/Inter-ThinItalic.otf',
// './assets/fonts/inter/Inter-ExtraLight.otf',
// './assets/fonts/inter/Inter-ExtraLightItalic.otf',
// './assets/fonts/inter/Inter-Light.otf',
// './assets/fonts/inter/Inter-LightItalic.otf',
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
'./assets/fonts/inter/Inter-Medium.otf',
'./assets/fonts/inter/Inter-MediumItalic.otf',
'./assets/fonts/inter/Inter-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
'./assets/fonts/inter/Inter-Black.otf',
'./assets/fonts/inter/Inter-BlackItalic.otf',
],
},
],
].filter(Boolean), ].filter(Boolean),
extra: { extra: {
eas: { eas: {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -124,6 +124,7 @@
"expo-dev-client": "^4.0.14", "expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2", "expo-device": "~6.0.2",
"expo-file-system": "^17.0.1", "expo-file-system": "^17.0.1",
"expo-font": "~12.0.10",
"expo-haptics": "^13.0.1", "expo-haptics": "^13.0.1",
"expo-image": "~1.12.9", "expo-image": "~1.12.9",
"expo-image-manipulator": "^12.0.5", "expo-image-manipulator": "^12.0.5",

View File

@ -55,7 +55,7 @@ import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell' import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs' import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@ -106,62 +106,60 @@ function InnerApp() {
}, [_]) }, [_])
return ( return (
<Alf theme={theme}> <StatsigProvider
<ThemeProvider theme={theme}> // Resets the entire tree below when it changes:
<Splash isReady={isReady && hasCheckedReferrer}> key={currentAccount?.did}>
<RootSiblingParent> <Alf theme={theme}>
<VideoVolumeProvider> <ThemeProvider theme={theme}>
<React.Fragment <Splash isReady={isReady && hasCheckedReferrer}>
// Resets the entire tree below when it changes: <RootSiblingParent>
key={currentAccount?.did}> <VideoVolumeProvider>
<QueryProvider currentDid={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider> <MessagesProvider>
<MessagesProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <LabelDefsProvider>
<LabelDefsProvider> <ModerationOptsProvider>
<ModerationOptsProvider> <LoggedOutViewProvider>
<LoggedOutViewProvider> <SelectedFeedProvider>
<SelectedFeedProvider> <HiddenRepliesProvider>
<HiddenRepliesProvider> <UnreadNotifsProvider>
<UnreadNotifsProvider> <BackgroundNotificationPreferencesProvider>
<BackgroundNotificationPreferencesProvider> <MutedThreadsProvider>
<MutedThreadsProvider> <ProgressGuideProvider>
<ProgressGuideProvider> <GestureHandlerRootView style={s.h100pct}>
<GestureHandlerRootView <TestCtrls />
style={s.h100pct}> <Shell />
<TestCtrls /> <NuxDialogs />
<Shell /> </GestureHandlerRootView>
<NuxDialogs /> </ProgressGuideProvider>
</GestureHandlerRootView> </MutedThreadsProvider>
</ProgressGuideProvider> </BackgroundNotificationPreferencesProvider>
</MutedThreadsProvider> </UnreadNotifsProvider>
</BackgroundNotificationPreferencesProvider> </HiddenRepliesProvider>
</UnreadNotifsProvider> </SelectedFeedProvider>
</HiddenRepliesProvider> </LoggedOutViewProvider>
</SelectedFeedProvider> </ModerationOptsProvider>
</LoggedOutViewProvider> </LabelDefsProvider>
</ModerationOptsProvider> </MessagesProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider> </QueryProvider>
</React.Fragment> </VideoVolumeProvider>
</VideoVolumeProvider> </RootSiblingParent>
</RootSiblingParent> </Splash>
</Splash> </ThemeProvider>
</ThemeProvider> </Alf>
</Alf> </StatsigProvider>
) )
} }
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
const [loaded] = useFonts()
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) initPersistedState().then(() => setReady(true))
}, []) }, [])
if (!isReady) { if (!isReady || !loaded) {
return null return null
} }

View File

@ -46,7 +46,7 @@ import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/Video
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web' import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs' import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@ -96,62 +96,61 @@ function InnerApp() {
return ( return (
<KeyboardProvider enabled={false}> <KeyboardProvider enabled={false}>
<Alf theme={theme}> <StatsigProvider
<ThemeProvider theme={theme}> // Resets the entire tree below when it changes:
<RootSiblingParent> key={currentAccount?.did}>
<VideoVolumeProvider> <Alf theme={theme}>
<ActiveVideoProvider> <ThemeProvider theme={theme}>
<React.Fragment <RootSiblingParent>
// Resets the entire tree below when it changes: <VideoVolumeProvider>
key={currentAccount?.did}> <ActiveVideoProvider>
<QueryProvider currentDid={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider> <MessagesProvider>
<MessagesProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <LabelDefsProvider>
<LabelDefsProvider> <ModerationOptsProvider>
<ModerationOptsProvider> <LoggedOutViewProvider>
<LoggedOutViewProvider> <SelectedFeedProvider>
<SelectedFeedProvider> <HiddenRepliesProvider>
<HiddenRepliesProvider> <UnreadNotifsProvider>
<UnreadNotifsProvider> <BackgroundNotificationPreferencesProvider>
<BackgroundNotificationPreferencesProvider> <MutedThreadsProvider>
<MutedThreadsProvider> <SafeAreaProvider>
<SafeAreaProvider> <ProgressGuideProvider>
<ProgressGuideProvider> <Shell />
<Shell /> <NuxDialogs />
<NuxDialogs /> </ProgressGuideProvider>
</ProgressGuideProvider> </SafeAreaProvider>
</SafeAreaProvider> </MutedThreadsProvider>
</MutedThreadsProvider> </BackgroundNotificationPreferencesProvider>
</BackgroundNotificationPreferencesProvider> </UnreadNotifsProvider>
</UnreadNotifsProvider> </HiddenRepliesProvider>
</HiddenRepliesProvider> </SelectedFeedProvider>
</SelectedFeedProvider> </LoggedOutViewProvider>
</LoggedOutViewProvider> </ModerationOptsProvider>
</ModerationOptsProvider> </LabelDefsProvider>
</LabelDefsProvider> </MessagesProvider>
</MessagesProvider>
</StatsigProvider>
</QueryProvider> </QueryProvider>
</React.Fragment> <ToastContainer />
<ToastContainer /> </ActiveVideoProvider>
</ActiveVideoProvider> </VideoVolumeProvider>
</VideoVolumeProvider> </RootSiblingParent>
</RootSiblingParent> </ThemeProvider>
</ThemeProvider> </Alf>
</Alf> </StatsigProvider>
</KeyboardProvider> </KeyboardProvider>
) )
} }
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
const [loaded] = useFonts()
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) initPersistedState().then(() => setReady(true))
}, []) }, [])
if (!isReady) { if (!isReady || !loaded) {
return null return null
} }

View File

@ -225,43 +225,43 @@ export const atoms = {
}, },
text_2xs: { text_2xs: {
fontSize: tokens.fontSize._2xs, fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_xs: { text_xs: {
fontSize: tokens.fontSize.xs, fontSize: tokens.fontSize.xs,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_sm: { text_sm: {
fontSize: tokens.fontSize.sm, fontSize: tokens.fontSize.sm,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_md: { text_md: {
fontSize: tokens.fontSize.md, fontSize: tokens.fontSize.md,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_lg: { text_lg: {
fontSize: tokens.fontSize.lg, fontSize: tokens.fontSize.lg,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_xl: { text_xl: {
fontSize: tokens.fontSize.xl, fontSize: tokens.fontSize.xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_2xl: { text_2xl: {
fontSize: tokens.fontSize._2xl, fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_3xl: { text_3xl: {
fontSize: tokens.fontSize._3xl, fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_4xl: { text_4xl: {
fontSize: tokens.fontSize._4xl, fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_5xl: { text_5xl: {
fontSize: tokens.fontSize._5xl, fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
leading_tight: { leading_tight: {
lineHeight: 1.15, lineHeight: 1.15,
@ -273,10 +273,7 @@ export const atoms = {
lineHeight: 1.5, lineHeight: 1.5,
}, },
tracking_normal: { tracking_normal: {
letterSpacing: 0, letterSpacing: tokens.TRACKING,
},
tracking_wide: {
letterSpacing: 0.25,
}, },
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.normal,

111
src/alf/fonts.ts 100644
View File

@ -0,0 +1,111 @@
import {useFonts as defaultUseFonts} from 'expo-font'
import {isNative, isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
const factor = 0.0625 // 1 - (15/16)
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
'-2': 1 - factor * 3,
'-1': 1 - factor * 2,
'0': 1 - factor * 1, // default
'1': 1,
'2': 1 + factor * 1,
}
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
return fontScaleMultipliers[scale]
}
export function getFontScale() {
return device.get(['fontScale']) ?? '0'
}
export function setFontScale(fontScale: Device['fontScale']) {
device.set(['fontScale'], fontScale)
}
export function getFontFamily() {
return device.get(['fontFamily']) || 'theme'
}
export function setFontFamily(fontFamily: Device['fontFamily']) {
device.set(['fontFamily'], fontFamily)
}
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
export function useFonts() {
/**
* For native, the `expo-font` config plugin embeds the fonts in the
* application binary. But `expo-font` isn't supported on web, so we fall
* back to async loading here.
*/
if (isNative) return [true, null]
return defaultUseFonts({
// 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'),
// 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'),
// 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'),
// 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'),
// 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'),
// 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
})
}
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
export function applyFonts(
style: Record<string, any>,
fontFamily: 'system' | 'theme',
) {
if (fontFamily === 'theme') {
style.fontFamily =
{
// '100': 'Inter-Thin',
// '200': 'Inter-ExtraLight',
// '300': 'Inter-Light',
'100': 'Inter-Regular',
'200': 'Inter-Regular',
'300': 'Inter-Regular',
'400': 'Inter-Regular',
'500': 'Inter-Medium',
'600': 'Inter-SemiBold',
'700': 'Inter-Bold',
'800': 'Inter-ExtraBold',
'900': 'Inter-Black',
}[style.fontWeight as string] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
if (style.fontFamily === 'Inter-Regular') {
style.fontFamily = 'Inter-Italic'
} else {
style.fontFamily += 'Italic'
}
}
// fallback families only supported on web
if (isWeb) {
style.fontFamily += `, ${FAMILIES}`
}
} else {
// fallback families only supported on web
if (isWeb) {
style.fontFamily = style.fontFamily || FAMILIES
}
}
}

View File

@ -1,25 +1,47 @@
import React from 'react' import React from 'react'
import {useMediaQuery} from 'react-responsive' import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
getFontFamily,
getFontScale,
setFontFamily as persistFontFamily,
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {createThemes, defaultTheme} from '#/alf/themes' import {createThemes, defaultTheme} from '#/alf/themes'
import {Theme, ThemeName} from '#/alf/types' import {Theme, ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms' export {atoms} from '#/alf/atoms'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens' export * as tokens from '#/alf/tokens'
export * from '#/alf/types' export * from '#/alf/types'
export * from '#/alf/util/flatten' export * from '#/alf/util/flatten'
export * from '#/alf/util/platform' export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector' export * from '#/alf/util/themeSelector'
/* export type Alf = {
* Context
*/
export const Context = React.createContext<{
themeName: ThemeName themeName: ThemeName
theme: Theme theme: Theme
themes: ReturnType<typeof createThemes> themes: ReturnType<typeof createThemes>
}>({ fonts: {
scale: Exclude<Device['fontScale'], undefined>
scaleMultiplier: number
family: Device['fontFamily']
setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
setFontFamily: (fontFamily: Device['fontFamily']) => void
}
/**
* Feature flags or other gated options
*/
flags: {}
}
/*
* Context
*/
export const Context = React.createContext<Alf>({
themeName: 'light', themeName: 'light',
theme: defaultTheme, theme: defaultTheme,
themes: createThemes({ themes: createThemes({
@ -29,12 +51,48 @@ export const Context = React.createContext<{
positive: GREEN_HUE, positive: GREEN_HUE,
}, },
}), }),
fonts: {
scale: getFontScale(),
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
family: getFontFamily(),
setFontScale: () => {},
setFontFamily: () => {},
},
flags: {},
}) })
export function ThemeProvider({ export function ThemeProvider({
children, children,
theme: themeName, theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) { }: React.PropsWithChildren<{theme: ThemeName}>) {
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
getFontScale(),
)
const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() =>
computeFontScaleMultiplier(fontScale),
)
const setFontScaleAndPersist = React.useCallback<
Alf['fonts']['setFontScale']
>(
fontScale => {
setFontScale(fontScale)
persistFontScale(fontScale)
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
},
[setFontScale],
)
const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>(
() => getFontFamily(),
)
const setFontFamilyAndPersist = React.useCallback<
Alf['fonts']['setFontFamily']
>(
fontFamily => {
setFontFamily(fontFamily)
persistFontFamily(fontFamily)
},
[setFontFamily],
)
const themes = React.useMemo(() => { const themes = React.useMemo(() => {
return createThemes({ return createThemes({
hues: { hues: {
@ -44,28 +102,47 @@ export function ThemeProvider({
}, },
}) })
}, []) }, [])
const theme = themes[themeName]
return ( return (
<Context.Provider <Context.Provider
value={React.useMemo( value={React.useMemo<Alf>(
() => ({ () => ({
themes, themes,
themeName: themeName, themeName: themeName,
theme: theme, theme: themes[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
family: fontFamily,
setFontScale: setFontScaleAndPersist,
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}), }),
[theme, themeName, themes], [
themeName,
themes,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
)}> )}>
{children} {children}
</Context.Provider> </Context.Provider>
) )
} }
export function useAlf() {
return React.useContext(Context)
}
export function useTheme(theme?: ThemeName) { export function useTheme(theme?: ThemeName) {
const ctx = React.useContext(Context) const alf = useAlf()
return React.useMemo(() => { return React.useMemo(() => {
return theme ? ctx.themes[theme] : ctx.theme return theme ? alf.themes[theme] : alf.theme
}, [theme, ctx]) }, [theme, alf])
} }
export function useBreakpoints() { export function useBreakpoints() {

View File

@ -1,3 +1,7 @@
import {Platform} from 'react-native'
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
export const color = { export const color = {
temp_purple: 'rgb(105 0 255)', temp_purple: 'rgb(105 0 255)',
temp_purple_dark: 'rgb(83 0 202)', temp_purple_dark: 'rgb(83 0 202)',

View File

@ -7,7 +7,6 @@ import {
PressableProps, PressableProps,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
Text,
TextProps, TextProps,
TextStyle, TextStyle,
View, View,
@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common' import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography' import {Text} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
export type ButtonColor = export type ButtonColor =
@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles() const textStyles = useSharedButtonTextStyles()
return ( return (
<Text <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{...rest}
style={normalizeTextStyles([
a.font_bold,
a.text_center,
textStyles,
style,
])}>
{children} {children}
</Text> </Text>
) )

View File

@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore // @ts-ignore
export const Input = createInput(BottomSheetTextInput) export const Input = createInput(BottomSheetTextInput)

View File

@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
export {Input} from '#/components/forms/TextField' export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation() const stopPropagation = (e: any) => e.stopPropagation()

View File

@ -0,0 +1,18 @@
import React from 'react'
import {DialogControlProps} from '#/components/Dialog/types'
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
React.useEffect(() => {
if (showTimeout) {
const timeout = setTimeout(() => {
control.open()
}, showTimeout)
return () => {
clearTimeout(timeout)
}
} else {
control.open()
}
}, [control, showTimeout])
}

View File

@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf' import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
export type TextProps = RNTextProps & { export type TextProps = RNTextProps & {
/** /**
@ -34,19 +34,30 @@ export function leading<
* If the `lineHeight` value is > 2, we assume it's an absolute value and * If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is. * returns it as-is.
*/ */
export function normalizeTextStyles(styles: StyleProp<TextStyle>) { export function normalizeTextStyles(
styles: StyleProp<TextStyle>,
{
fontScale,
fontFamily,
}: {
fontScale: number
fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>,
) {
const s = flatten(styles) const s = flatten(styles)
// should always be defined on these components // should always be defined on these components
const fontSize = s.fontSize || atoms.text_md.fontSize s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
if (s?.lineHeight) { if (s?.lineHeight) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) { if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(fontSize * s.lineHeight) s.lineHeight = Math.round(s.fontSize * s.lineHeight)
} }
} else if (!isNative) { } else if (!isNative) {
s.lineHeight = s.fontSize s.lineHeight = s.fontSize
} }
applyFonts(s, fontFamily)
return s return s
} }
@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
* Our main text component. Use this most of the time. * Our main text component. Use this most of the time.
*/ */
export function Text({style, selectable, ...rest}: TextProps) { export function Text({style, selectable, ...rest}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme() const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
fontScale: fonts.scaleMultiplier,
fontFamily: fonts.family,
flags,
})
return <UITextView selectable={selectable} uiTextView style={s} {...rest} /> return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
} }

View File

@ -0,0 +1,119 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
import {atoms as a, useAlf, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Divider} from '#/components/Divider'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import {Text} from '#/components/Typography'
export function NeueTypography() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
const {fonts} = useAlf()
Dialog.useAutoOpen(control, 3e3)
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
const onChangeFontFamily = React.useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = React.useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}>
<View style={[a.gap_md]}>
<Text style={[a.text_3xl, {fontWeight: '900'}]}>
<Trans>Introducing new font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug]}>
<Trans>
To the ensure the best possible experience, we're introducing a
new theme font, along with adjustable font sizing settings.
</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Defaults are shown below. You can edit these in your Appearance
Settings later.
</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_lg]}>
<AppearanceToggleButtonGroup
title={_(msg`Font`)}
description={_(
msg`For the best experience, we recommend using the theme font.`,
)}
icon={Aa}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Theme`),
name: 'theme',
},
]}
values={[fonts.family]}
onChange={onChangeFontFamily}
/>
<AppearanceToggleButtonGroup
title={_(msg`Font size`)}
icon={TextSize}
items={[
{
label: _(msg`Smaller`),
name: '-1',
},
{
label: _(msg`Default`),
name: '0',
},
{
label: _(msg`Larger`),
name: '1',
},
]}
values={[fonts.scale]}
onChange={onChangeFontScale}
/>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
@ -8,9 +9,16 @@ import {
useRemoveNuxsMutation, useRemoveNuxsMutation,
useUpsertNuxMutation, useUpsertNuxMutation,
} from '#/state/queries/nuxs' } from '#/state/queries/nuxs'
import {useSession} from '#/state/session' import {
usePreferencesQuery,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell' import {useOnboardingState} from '#/state/shell'
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
// NUXs
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env' import {IS_DEV} from '#/env'
@ -21,11 +29,27 @@ type Context = {
const queuedNuxs: { const queuedNuxs: {
id: Nux id: Nux
enabled?: (props: {gate: ReturnType<typeof useGate>}) => boolean enabled?: (props: {
gate: ReturnType<typeof useGate>
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = [ }[] = [
{ {
id: Nux.TenMillionDialog, id: Nux.TenMillionDialog,
}, },
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
return true
}
}
return false
},
},
] ]
const Context = React.createContext<Context>({ const Context = React.createContext<Context>({
@ -38,12 +62,31 @@ export function useNuxDialogContext() {
} }
export function NuxDialogs() { export function NuxDialogs() {
const {hasSession} = useSession() const {currentAccount} = useSession()
const onboardingState = useOnboardingState() const {data: preferences} = usePreferencesQuery()
return hasSession && !onboardingState.isActive ? <Inner /> : null const {data: profile} = useProfileQuery({did: currentAccount?.did})
const onboardingActive = useOnboardingState().isActive
const isLoading =
!currentAccount || !preferences || !profile || onboardingActive
return !isLoading ? (
<Inner
currentAccount={currentAccount}
currentProfile={profile}
preferences={preferences}
/>
) : null
} }
function Inner() { function Inner({
currentAccount,
currentProfile,
preferences,
}: {
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) {
const gate = useGate() const gate = useGate()
const {nuxs} = useNuxs() const {nuxs} = useNuxs()
const [snoozed, setSnoozed] = React.useState(() => { const [snoozed, setSnoozed] = React.useState(() => {
@ -80,10 +123,19 @@ function Inner() {
const nux = nuxs.find(nux => nux.id === id) const nux = nuxs.find(nux => nux.id === id)
// check if completed first // check if completed first
if (nux && nux.completed) continue if (nux && nux.completed) {
continue
}
// then check gate (track exposure) // then check gate (track exposure)
if (enabled && !enabled({gate})) continue if (
enabled &&
!enabled({gate, currentAccount, currentProfile, preferences})
) {
continue
}
logger.debug(`NUX dialogs: activating '${id}' NUX`)
// we have a winner // we have a winner
setActiveNux(id) setActiveNux(id)
@ -104,7 +156,16 @@ function Inner() {
break break
} }
}, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate]) }, [
nuxs,
snoozed,
snoozeNuxDialog,
upsertNux,
gate,
currentAccount,
currentProfile,
preferences,
])
const ctx = React.useMemo(() => { const ctx = React.useMemo(() => {
return { return {
@ -116,6 +177,7 @@ function Inner() {
return ( return (
<Context.Provider value={ctx}> <Context.Provider value={ctx}>
{activeNux === Nux.TenMillionDialog && <TenMillion />} {activeNux === Nux.TenMillionDialog && <TenMillion />}
{activeNux === Nux.NeueTypography && <NeueTypography />}
</Context.Provider> </Context.Provider>
) )
} }

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TextSize_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z',
})

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TitleCase_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z',
})

View File

@ -79,13 +79,13 @@ export const s = StyleSheet.create({
// font weights // font weights
fw600: {fontWeight: '600'}, fw600: {fontWeight: '600'},
bold: {fontWeight: 'bold'}, bold: {fontWeight: '700'},
fw500: {fontWeight: '500'}, fw500: {fontWeight: '500'},
semiBold: {fontWeight: '500'}, semiBold: {fontWeight: '500'},
fw400: {fontWeight: '400'}, fw400: {fontWeight: '400'},
normal: {fontWeight: '400'}, normal: {fontWeight: '400'},
fw300: {fontWeight: '300'}, fw300: {fontWeight: '400'},
light: {fontWeight: '300'}, light: {fontWeight: '400'},
fw200: {fontWeight: '200'}, fw200: {fontWeight: '200'},
// text decoration // text decoration

View File

@ -1,5 +1,6 @@
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {tokens} from '#/alf'
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes' import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
import {colors} from './styles' import {colors} from './styles'
import type {Theme} from './ThemeContext' import type {Theme} from './ThemeContext'
@ -88,163 +89,163 @@ export const defaultTheme: Theme = {
typography: { typography: {
'2xl-thin': { '2xl-thin': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
'2xl': { '2xl': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'2xl-medium': { '2xl-medium': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'2xl-bold': { '2xl-bold': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'2xl-heavy': { '2xl-heavy': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'xl-thin': { 'xl-thin': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
xl: { xl: {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'xl-medium': { 'xl-medium': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'xl-bold': { 'xl-bold': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'xl-heavy': { 'xl-heavy': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'lg-thin': { 'lg-thin': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
lg: { lg: {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'lg-medium': { 'lg-medium': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'lg-bold': { 'lg-bold': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'lg-heavy': { 'lg-heavy': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'md-thin': { 'md-thin': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
md: { md: {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'md-medium': { 'md-medium': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'md-bold': { 'md-bold': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'md-heavy': { 'md-heavy': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'sm-thin': { 'sm-thin': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
sm: { sm: {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'sm-medium': { 'sm-medium': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'sm-bold': { 'sm-bold': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'sm-heavy': { 'sm-heavy': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'xs-thin': { 'xs-thin': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
xs: { xs: {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'xs-medium': { 'xs-medium': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'xs-bold': { 'xs-bold': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'xs-heavy': { 'xs-heavy': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'title-2xl': { 'title-2xl': {
fontSize: 34, fontSize: 34,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'title-xl': { 'title-xl': {
fontSize: 28, fontSize: 28,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'title-lg': { 'title-lg': {
@ -254,32 +255,32 @@ export const defaultTheme: Theme = {
title: { title: {
fontWeight: '500', fontWeight: '500',
fontSize: 20, fontSize: 20,
letterSpacing: 0.15, letterSpacing: tokens.TRACKING,
}, },
'title-sm': { 'title-sm': {
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 17, fontSize: 17,
letterSpacing: 0.15, letterSpacing: tokens.TRACKING,
}, },
'post-text': { 'post-text': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.2, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'post-text-lg': { 'post-text-lg': {
fontSize: 20, fontSize: 20,
letterSpacing: 0.2, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'button-lg': { 'button-lg': {
fontWeight: '500', fontWeight: '500',
fontSize: 18, fontSize: 18,
letterSpacing: 0.5, letterSpacing: tokens.TRACKING,
}, },
button: { button: {
fontWeight: '500', fontWeight: '500',
fontSize: 14, fontSize: 14,
letterSpacing: 0.5, letterSpacing: tokens.TRACKING,
}, },
mono: { mono: {
fontSize: 14, fontSize: 14,

View File

@ -14,17 +14,21 @@ import {s} from '#/lib/styles'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell' import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, native, useTheme} from '#/alf' import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
export function AppearanceSettingsScreen({}: Props) { export function AppearanceSettingsScreen({}: Props) {
const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
const {fonts} = useAlf()
const {colorMode, darkTheme} = useThemePrefs() const {colorMode, darkTheme} = useThemePrefs()
const {setColorMode, setDarkTheme} = useSetThemePrefs() const {setColorMode, setDarkTheme} = useSetThemePrefs()
@ -54,6 +58,22 @@ export function AppearanceSettingsScreen({}: Props) {
[setDarkTheme, darkTheme], [setDarkTheme, darkTheme],
) )
const onChangeFontFamily = useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
return ( return (
<LayoutAnimationConfig skipExiting skipEntering> <LayoutAnimationConfig skipExiting skipEntering>
<View testID="preferencesThreadsScreen" style={s.hContentRegion}> <View testID="preferencesThreadsScreen" style={s.hContentRegion}>
@ -71,65 +91,143 @@ export function AppearanceSettingsScreen({}: Props) {
</View> </View>
</SimpleViewHeader> </SimpleViewHeader>
<View style={[a.p_xl, a.gap_lg]}> <View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.gap_lg]}>
<PhoneIcon style={t.atoms.text} /> <AppearanceToggleButtonGroup
<Text style={a.text_md}> title={_(msg`Color mode`)}
<Trans>Mode</Trans> icon={PhoneIcon}
</Text> items={[
</View> {
<ToggleButton.Group label: _(msg`System`),
label={_(msg`Dark mode`)} name: 'system',
values={[colorMode]} },
onChange={onChangeAppearance}> {
<ToggleButton.Button label={_(msg`System`)} name="system"> label: _(msg`Light`),
<ToggleButton.ButtonText> name: 'light',
<Trans>System</Trans> },
</ToggleButton.ButtonText> {
</ToggleButton.Button> label: _(msg`Dark`),
<ToggleButton.Button label={_(msg`Light`)} name="light"> name: 'dark',
<ToggleButton.ButtonText> },
<Trans>Light</Trans> ]}
</ToggleButton.ButtonText> values={[colorMode]}
</ToggleButton.Button> onChange={onChangeAppearance}
<ToggleButton.Button label={_(msg`Dark`)} name="dark"> />
<ToggleButton.ButtonText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
{colorMode !== 'light' && (
<Animated.View
entering={native(FadeInDown)}
exiting={native(FadeOutDown)}
style={[a.mt_md, a.gap_lg]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<MoonIcon style={t.atoms.text} />
<Text style={a.text_md}>
<Trans>Dark theme</Trans>
</Text>
</View>
<ToggleButton.Group {colorMode !== 'light' && (
label={_(msg`Dark theme`)} <Animated.View
values={[darkTheme ?? 'dim']} entering={native(FadeInDown)}
onChange={onChangeDarkTheme}> exiting={native(FadeOutDown)}>
<ToggleButton.Button label={_(msg`Dim`)} name="dim"> <AppearanceToggleButtonGroup
<ToggleButton.ButtonText> title={_(msg`Dark theme`)}
<Trans>Dim</Trans> icon={MoonIcon}
</ToggleButton.ButtonText> items={[
</ToggleButton.Button> {
<ToggleButton.Button label={_(msg`Dark`)} name="dark"> label: _(msg`Dim`),
<ToggleButton.ButtonText> name: 'dim',
<Trans>Dark</Trans> },
</ToggleButton.ButtonText> {
</ToggleButton.Button> label: _(msg`Dark`),
</ToggleButton.Group> name: 'dark',
</Animated.View> },
)} ]}
values={[darkTheme ?? 'dim']}
onChange={onChangeDarkTheme}
/>
</Animated.View>
)}
<AppearanceToggleButtonGroup
title={_(msg`Font`)}
description={_(
msg`For the best experience, we recommend using the theme font.`,
)}
icon={Aa}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Theme`),
name: 'theme',
},
]}
values={[fonts.family]}
onChange={onChangeFontFamily}
/>
<AppearanceToggleButtonGroup
title={_(msg`Font size`)}
icon={TextSize}
items={[
{
label: _(msg`Smaller`),
name: '-1',
},
{
label: _(msg`Default`),
name: '0',
},
{
label: _(msg`Larger`),
name: '1',
},
]}
values={[fonts.scale]}
onChange={onChangeFontScale}
/>
</View>
</View> </View>
</ScrollView> </ScrollView>
</View> </View>
</LayoutAnimationConfig> </LayoutAnimationConfig>
) )
} }
export function AppearanceToggleButtonGroup({
title,
description,
icon: Icon,
items,
values,
onChange,
}: {
title: string
description?: string
icon: React.ComponentType<SVGIconProps>
items: {
label: string
name: string
}[]
values: string[]
onChange: (values: string[]) => void
}) {
const t = useTheme()
return (
<View style={[a.gap_md]}>
<View style={[a.gap_xs]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<Icon style={t.atoms.text} />
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
</View>
{description && (
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
{description}
</Text>
)}
</View>
<ToggleButton.Group label={title} values={values} onChange={onChange}>
{items.map(item => (
<ToggleButton.Button
key={item.name}
label={item.label}
name={item.name}>
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
</ToggleButton.Button>
))}
</ToggleButton.Group>
</View>
)
}

View File

@ -4,15 +4,22 @@ import {BaseNux} from '#/state/queries/nuxs/types'
export enum Nux { export enum Nux {
TenMillionDialog = 'TenMillionDialog', TenMillionDialog = 'TenMillionDialog',
NeueTypography = 'NeueTypography',
} }
export const nuxNames = new Set(Object.values(Nux)) export const nuxNames = new Set(Object.values(Nux))
export type AppNux = BaseNux<{ export type AppNux =
id: Nux.TenMillionDialog | BaseNux<{
data: undefined id: Nux.TenMillionDialog
}> data: undefined
}>
| BaseNux<{
id: Nux.NeueTypography
data: undefined
}>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.TenMillionDialog]: undefined, [Nux.TenMillionDialog]: undefined,
[Nux.NeueTypography]: undefined,
} }

View File

@ -2,6 +2,8 @@ import {MMKV} from 'react-native-mmkv'
import {Device} from '#/storage/schema' import {Device} from '#/storage/schema'
export * from '#/storage/schema'
/** /**
* Generic storage class. DO NOT use this directly. Instead, use the exported * Generic storage class. DO NOT use this directly. Instead, use the exported
* storage instances below. * storage instances below.

View File

@ -2,5 +2,7 @@
* Device data that's specific to the device and does not vary based account * Device data that's specific to the device and does not vary based account
*/ */
export type Device = { export type Device = {
fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined lastNuxDialog: string | undefined
} }

View File

@ -1,10 +1,11 @@
import React from 'react' import React from 'react'
import {Text as RNText, TextProps} from 'react-native' import {StyleSheet, Text as RNText, TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {lh, s} from 'lib/styles' import {lh, s} from 'lib/styles'
import {TypographyVariant, useTheme} from 'lib/ThemeContext' import {TypographyVariant, useTheme} from 'lib/ThemeContext'
import {isIOS, isWeb} from 'platform/detection' import {isIOS, isWeb} from 'platform/detection'
import {applyFonts, useAlf} from '#/alf'
export type CustomTextProps = TextProps & { export type CustomTextProps = TextProps & {
type?: TypographyVariant type?: TypographyVariant
@ -32,11 +33,28 @@ export function Text({
const theme = useTheme() const theme = useTheme()
const typography = theme.typography[type] const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
const {fonts} = useAlf()
if (selectable && isIOS) { if (selectable && isIOS) {
const flattened = StyleSheet.flatten([
s.black,
typography,
lineHeightStyle,
style,
])
applyFonts(flattened, fonts.family)
// should always be defined on `typography`
// @ts-ignore
if (flattened.fontSize) {
// @ts-ignore
flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier
}
return ( return (
<UITextView <UITextView
style={[s.black, typography, lineHeightStyle, style]} style={flattened}
selectable={selectable} selectable={selectable}
uiTextView uiTextView
{...props}> {...props}>
@ -45,15 +63,26 @@ export function Text({
) )
} }
const flattened = StyleSheet.flatten([
s.black,
typography,
isWeb && fontFamilyStyle,
lineHeightStyle,
style,
])
applyFonts(flattened, fonts.family)
// should always be defined on `typography`
// @ts-ignore
if (flattened.fontSize) {
// @ts-ignore
flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier
}
return ( return (
<RNText <RNText
style={[ style={flattened}
s.black,
typography,
isWeb && fontFamilyStyle,
lineHeightStyle,
style,
]}
// @ts-ignore web only -esb // @ts-ignore web only -esb
dataSet={Object.assign({tooltip: title}, dataSet || {})} dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable} selectable={selectable}

View File

@ -12219,6 +12219,13 @@ expo-file-system@^17.0.1, expo-file-system@~17.0.1:
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-17.0.1.tgz#b9f8af8c1c06ec71d96fd7a0d2567fa9e1c88f15" resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-17.0.1.tgz#b9f8af8c1c06ec71d96fd7a0d2567fa9e1c88f15"
integrity sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw== integrity sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==
expo-font@~12.0.10:
version "12.0.10"
resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.10.tgz#62deaf1f46159d7839f01305f44079268781b1db"
integrity sha512-Q1i2NuYri3jy32zdnBaHHCya1wH1yMAsI+3CCmj9zlQzlhsS9Bdwcj2W3c5eU5FvH2hsNQy4O+O1NnM6o/pDaQ==
dependencies:
fontfaceobserver "^2.1.0"
expo-font@~12.0.5: expo-font@~12.0.5:
version "12.0.5" version "12.0.5"
resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.5.tgz#3451c2bd3f98859b127a6484d3474a292889b93f" resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.5.tgz#3451c2bd3f98859b127a6484d3474a292889b93f"