[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>
This commit is contained in:
Eric Bailey
2024-09-18 19:35:34 -05:00
committed by GitHub
parent fb3be79820
commit cbc7cd0808
45 changed files with 835 additions and 256 deletions
+43 -45
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 * as Toast from '#/view/com/util/Toast'
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 {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -106,62 +106,60 @@ function InnerApp() {
}, [_])
return (
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<StatsigProvider
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
<VideoVolumeProvider>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</RootSiblingParent>
</Splash>
</ThemeProvider>
</Alf>
</VideoVolumeProvider>
</RootSiblingParent>
</Splash>
</ThemeProvider>
</Alf>
</StatsigProvider>
)
}
function App() {
const [isReady, setReady] = useState(false)
const [loaded] = useFonts()
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
}, [])
if (!isReady) {
if (!isReady || !loaded) {
return null
}
+43 -44
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 {ToastContainer} from '#/view/com/util/Toast.web'
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 {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -96,62 +96,61 @@ function InnerApp() {
return (
<KeyboardProvider enabled={false}>
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<StatsigProvider
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</QueryProvider>
</React.Fragment>
<ToastContainer />
</ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent>
</ThemeProvider>
</Alf>
<ToastContainer />
</ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent>
</ThemeProvider>
</Alf>
</StatsigProvider>
</KeyboardProvider>
)
}
function App() {
const [isReady, setReady] = useState(false)
const [loaded] = useFonts()
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
}, [])
if (!isReady) {
if (!isReady || !loaded) {
return null
}
+11 -14
View File
@@ -225,43 +225,43 @@ export const atoms = {
},
text_2xs: {
fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_xs: {
fontSize: tokens.fontSize.xs,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_sm: {
fontSize: tokens.fontSize.sm,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_md: {
fontSize: tokens.fontSize.md,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_lg: {
fontSize: tokens.fontSize.lg,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_xl: {
fontSize: tokens.fontSize.xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_2xl: {
fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_3xl: {
fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_4xl: {
fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
text_5xl: {
fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
leading_tight: {
lineHeight: 1.15,
@@ -273,10 +273,7 @@ export const atoms = {
lineHeight: 1.5,
},
tracking_normal: {
letterSpacing: 0,
},
tracking_wide: {
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
},
font_normal: {
fontWeight: tokens.fontWeight.normal,
+111
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
}
}
}
+89 -12
View File
@@ -1,25 +1,47 @@
import React from 'react'
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 {Theme, ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
/*
* Context
*/
export const Context = React.createContext<{
export type Alf = {
themeName: ThemeName
theme: Theme
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',
theme: defaultTheme,
themes: createThemes({
@@ -29,12 +51,48 @@ export const Context = React.createContext<{
positive: GREEN_HUE,
},
}),
fonts: {
scale: getFontScale(),
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
family: getFontFamily(),
setFontScale: () => {},
setFontFamily: () => {},
},
flags: {},
})
export function ThemeProvider({
children,
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(() => {
return createThemes({
hues: {
@@ -44,28 +102,47 @@ export function ThemeProvider({
},
})
}, [])
const theme = themes[themeName]
return (
<Context.Provider
value={React.useMemo(
value={React.useMemo<Alf>(
() => ({
themes,
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}
</Context.Provider>
)
}
export function useAlf() {
return React.useContext(Context)
}
export function useTheme(theme?: ThemeName) {
const ctx = React.useContext(Context)
const alf = useAlf()
return React.useMemo(() => {
return theme ? ctx.themes[theme] : ctx.theme
}, [theme, ctx])
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
}
export function useBreakpoints() {
+4
View File
@@ -1,3 +1,7 @@
import {Platform} from 'react-native'
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
export const color = {
temp_purple: 'rgb(105 0 255)',
temp_purple_dark: 'rgb(83 0 202)',
+2 -10
View File
@@ -7,7 +7,6 @@ import {
PressableProps,
StyleProp,
StyleSheet,
Text,
TextProps,
TextStyle,
View,
@@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
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 ButtonColor =
@@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text
{...rest}
style={normalizeTextStyles([
a.font_bold,
a.text_center,
textStyles,
style,
])}>
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{children}
</Text>
)
+1
View File
@@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
+1
View File
@@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation()
+18
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])
}
+21 -5
View File
@@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
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 & {
/**
@@ -34,19 +34,30 @@ export function leading<
* If the `lineHeight` value is > 2, we assume it's an absolute value and
* 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)
// 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 !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(fontSize * s.lineHeight)
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
}
} else if (!isNative) {
s.lineHeight = s.fontSize
}
applyFonts(s, fontFamily)
return s
}
@@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
* Our main text component. Use this most of the time.
*/
export function Text({style, selectable, ...rest}: TextProps) {
const {fonts, flags} = useAlf()
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} />
}
@@ -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>
)
}
+71 -9
View File
@@ -1,4 +1,5 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
@@ -8,9 +9,16 @@ import {
useRemoveNuxsMutation,
useUpsertNuxMutation,
} 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 {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
// NUXs
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env'
@@ -21,11 +29,27 @@ type Context = {
const queuedNuxs: {
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.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>({
@@ -38,12 +62,31 @@ export function useNuxDialogContext() {
}
export function NuxDialogs() {
const {hasSession} = useSession()
const onboardingState = useOnboardingState()
return hasSession && !onboardingState.isActive ? <Inner /> : null
const {currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
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 {nuxs} = useNuxs()
const [snoozed, setSnoozed] = React.useState(() => {
@@ -80,10 +123,19 @@ function Inner() {
const nux = nuxs.find(nux => nux.id === id)
// check if completed first
if (nux && nux.completed) continue
if (nux && nux.completed) {
continue
}
// 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
setActiveNux(id)
@@ -104,7 +156,16 @@ function Inner() {
break
}
}, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate])
}, [
nuxs,
snoozed,
snoozeNuxDialog,
upsertNux,
gate,
currentAccount,
currentProfile,
preferences,
])
const ctx = React.useMemo(() => {
return {
@@ -116,6 +177,7 @@ function Inner() {
return (
<Context.Provider value={ctx}>
{activeNux === Nux.TenMillionDialog && <TenMillion />}
{activeNux === Nux.NeueTypography && <NeueTypography />}
</Context.Provider>
)
}
+5
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',
})
+5
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',
})
+3 -3
View File
@@ -79,13 +79,13 @@ export const s = StyleSheet.create({
// font weights
fw600: {fontWeight: '600'},
bold: {fontWeight: 'bold'},
bold: {fontWeight: '700'},
fw500: {fontWeight: '500'},
semiBold: {fontWeight: '500'},
fw400: {fontWeight: '400'},
normal: {fontWeight: '400'},
fw300: {fontWeight: '300'},
light: {fontWeight: '300'},
fw300: {fontWeight: '400'},
light: {fontWeight: '400'},
fw200: {fontWeight: '200'},
// text decoration
+45 -44
View File
@@ -1,5 +1,6 @@
import {Platform} from 'react-native'
import {tokens} from '#/alf'
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
import {colors} from './styles'
import type {Theme} from './ThemeContext'
@@ -88,163 +89,163 @@ export const defaultTheme: Theme = {
typography: {
'2xl-thin': {
fontSize: 18,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'2xl': {
fontSize: 18,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'2xl-medium': {
fontSize: 18,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'2xl-bold': {
fontSize: 18,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'2xl-heavy': {
fontSize: 18,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'xl-thin': {
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
xl: {
fontSize: 17,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'xl-medium': {
fontSize: 17,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'xl-bold': {
fontSize: 17,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'xl-heavy': {
fontSize: 17,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'lg-thin': {
fontSize: 16,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
lg: {
fontSize: 16,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'lg-medium': {
fontSize: 16,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'lg-bold': {
fontSize: 16,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'lg-heavy': {
fontSize: 16,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'md-thin': {
fontSize: 15,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
md: {
fontSize: 15,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'md-medium': {
fontSize: 15,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'md-bold': {
fontSize: 15,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'md-heavy': {
fontSize: 15,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'sm-thin': {
fontSize: 14,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
sm: {
fontSize: 14,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'sm-medium': {
fontSize: 14,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'sm-bold': {
fontSize: 14,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'sm-heavy': {
fontSize: 14,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'xs-thin': {
fontSize: 13,
letterSpacing: 0.25,
fontWeight: '300',
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
xs: {
fontSize: 13,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'xs-medium': {
fontSize: 13,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'xs-bold': {
fontSize: 13,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '700',
},
'xs-heavy': {
fontSize: 13,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '800',
},
'title-2xl': {
fontSize: 34,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'title-xl': {
fontSize: 28,
letterSpacing: 0.25,
letterSpacing: tokens.TRACKING,
fontWeight: '500',
},
'title-lg': {
@@ -254,32 +255,32 @@ export const defaultTheme: Theme = {
title: {
fontWeight: '500',
fontSize: 20,
letterSpacing: 0.15,
letterSpacing: tokens.TRACKING,
},
'title-sm': {
fontWeight: 'bold',
fontSize: 17,
letterSpacing: 0.15,
letterSpacing: tokens.TRACKING,
},
'post-text': {
fontSize: 16,
letterSpacing: 0.2,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'post-text-lg': {
fontSize: 20,
letterSpacing: 0.2,
letterSpacing: tokens.TRACKING,
fontWeight: '400',
},
'button-lg': {
fontWeight: '500',
fontSize: 18,
letterSpacing: 0.5,
letterSpacing: tokens.TRACKING,
},
button: {
fontWeight: '500',
fontSize: 14,
letterSpacing: 0.5,
letterSpacing: tokens.TRACKING,
},
mono: {
fontSize: 14,
+155 -57
View File
@@ -14,17 +14,21 @@ import {s} from '#/lib/styles'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
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 {Props as SVGIconProps} from '#/components/icons/common'
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
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'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
export function AppearanceSettingsScreen({}: Props) {
const {_} = useLingui()
const t = useTheme()
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
const {fonts} = useAlf()
const {colorMode, darkTheme} = useThemePrefs()
const {setColorMode, setDarkTheme} = useSetThemePrefs()
@@ -54,6 +58,22 @@ export function AppearanceSettingsScreen({}: Props) {
[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 (
<LayoutAnimationConfig skipExiting skipEntering>
<View testID="preferencesThreadsScreen" style={s.hContentRegion}>
@@ -71,65 +91,143 @@ export function AppearanceSettingsScreen({}: Props) {
</View>
</SimpleViewHeader>
<View style={[a.p_xl, a.gap_lg]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<PhoneIcon style={t.atoms.text} />
<Text style={a.text_md}>
<Trans>Mode</Trans>
</Text>
</View>
<ToggleButton.Group
label={_(msg`Dark mode`)}
values={[colorMode]}
onChange={onChangeAppearance}>
<ToggleButton.Button label={_(msg`System`)} name="system">
<ToggleButton.ButtonText>
<Trans>System</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button label={_(msg`Light`)} name="light">
<ToggleButton.ButtonText>
<Trans>Light</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<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>
<View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
<View style={[a.gap_lg]}>
<AppearanceToggleButtonGroup
title={_(msg`Color mode`)}
icon={PhoneIcon}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Light`),
name: 'light',
},
{
label: _(msg`Dark`),
name: 'dark',
},
]}
values={[colorMode]}
onChange={onChangeAppearance}
/>
<ToggleButton.Group
label={_(msg`Dark theme`)}
values={[darkTheme ?? 'dim']}
onChange={onChangeDarkTheme}>
<ToggleButton.Button label={_(msg`Dim`)} name="dim">
<ToggleButton.ButtonText>
<Trans>Dim</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button label={_(msg`Dark`)} name="dark">
<ToggleButton.ButtonText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</Animated.View>
)}
{colorMode !== 'light' && (
<Animated.View
entering={native(FadeInDown)}
exiting={native(FadeOutDown)}>
<AppearanceToggleButtonGroup
title={_(msg`Dark theme`)}
icon={MoonIcon}
items={[
{
label: _(msg`Dim`),
name: 'dim',
},
{
label: _(msg`Dark`),
name: 'dark',
},
]}
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>
</ScrollView>
</View>
</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>
)
}
+11 -4
View File
@@ -4,15 +4,22 @@ import {BaseNux} from '#/state/queries/nuxs/types'
export enum Nux {
TenMillionDialog = 'TenMillionDialog',
NeueTypography = 'NeueTypography',
}
export const nuxNames = new Set(Object.values(Nux))
export type AppNux = BaseNux<{
id: Nux.TenMillionDialog
data: undefined
}>
export type AppNux =
| BaseNux<{
id: Nux.TenMillionDialog
data: undefined
}>
| BaseNux<{
id: Nux.NeueTypography
data: undefined
}>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.TenMillionDialog]: undefined,
[Nux.NeueTypography]: undefined,
}
+2
View File
@@ -2,6 +2,8 @@ import {MMKV} from 'react-native-mmkv'
import {Device} from '#/storage/schema'
export * from '#/storage/schema'
/**
* Generic storage class. DO NOT use this directly. Instead, use the exported
* storage instances below.
+2
View File
@@ -2,5 +2,7 @@
* Device data that's specific to the device and does not vary based account
*/
export type Device = {
fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined
}
+38 -9
View File
@@ -1,10 +1,11 @@
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 {lh, s} from 'lib/styles'
import {TypographyVariant, useTheme} from 'lib/ThemeContext'
import {isIOS, isWeb} from 'platform/detection'
import {applyFonts, useAlf} from '#/alf'
export type CustomTextProps = TextProps & {
type?: TypographyVariant
@@ -32,11 +33,28 @@ export function Text({
const theme = useTheme()
const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
const {fonts} = useAlf()
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 (
<UITextView
style={[s.black, typography, lineHeightStyle, style]}
style={flattened}
selectable={selectable}
uiTextView
{...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 (
<RNText
style={[
s.black,
typography,
isWeb && fontFamilyStyle,
lineHeightStyle,
style,
]}
style={flattened}
// @ts-ignore web only -esb
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}