deer social fork for personal usage. but you might see a use idk. github mirror

[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>

authored by Eric Bailey Hailey and committed by GitHub cbc7cd08 fb3be798

+25
app.config.js
··· 230 230 './plugins/shareExtension/withShareExtensions.js', 231 231 './plugins/notificationsExtension/withNotificationsExtension.js', 232 232 './plugins/withAppDelegateReferrer.js', 233 + [ 234 + 'expo-font', 235 + { 236 + fonts: [ 237 + // './assets/fonts/inter/Inter-Thin.otf', 238 + // './assets/fonts/inter/Inter-ThinItalic.otf', 239 + // './assets/fonts/inter/Inter-ExtraLight.otf', 240 + // './assets/fonts/inter/Inter-ExtraLightItalic.otf', 241 + // './assets/fonts/inter/Inter-Light.otf', 242 + // './assets/fonts/inter/Inter-LightItalic.otf', 243 + './assets/fonts/inter/Inter-Regular.otf', 244 + './assets/fonts/inter/Inter-Italic.otf', 245 + './assets/fonts/inter/Inter-Medium.otf', 246 + './assets/fonts/inter/Inter-MediumItalic.otf', 247 + './assets/fonts/inter/Inter-SemiBold.otf', 248 + './assets/fonts/inter/Inter-SemiBoldItalic.otf', 249 + './assets/fonts/inter/Inter-Bold.otf', 250 + './assets/fonts/inter/Inter-BoldItalic.otf', 251 + './assets/fonts/inter/Inter-ExtraBold.otf', 252 + './assets/fonts/inter/Inter-ExtraBoldItalic.otf', 253 + './assets/fonts/inter/Inter-Black.otf', 254 + './assets/fonts/inter/Inter-BlackItalic.otf', 255 + ], 256 + }, 257 + ], 233 258 ].filter(Boolean), 234 259 extra: { 235 260 eas: {
assets/fonts/inter/Inter-Black.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-BlackItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Bold.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-BoldItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-ExtraBold.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-ExtraBoldItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-ExtraLight.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-ExtraLightItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Italic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Light.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-LightItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Medium.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-MediumItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Regular.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-SemiBold.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-SemiBoldItalic.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-Thin.otf

This is a binary file and will not be displayed.

assets/fonts/inter/Inter-ThinItalic.otf

This is a binary file and will not be displayed.

+1
assets/icons/textSize_stroke2_corner0_rounded.svg
··· 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>
+1
assets/icons/titleCase_stroke2_corner0_rounded.svg
··· 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>
+1
package.json
··· 124 124 "expo-dev-client": "^4.0.14", 125 125 "expo-device": "~6.0.2", 126 126 "expo-file-system": "^17.0.1", 127 + "expo-font": "~12.0.10", 127 128 "expo-haptics": "^13.0.1", 128 129 "expo-image": "~1.12.9", 129 130 "expo-image-manipulator": "^12.0.5",
+43 -45
src/App.native.tsx
··· 55 55 import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 56 56 import * as Toast from '#/view/com/util/Toast' 57 57 import {Shell} from '#/view/shell' 58 - import {ThemeProvider as Alf} from '#/alf' 58 + import {ThemeProvider as Alf, useFonts} from '#/alf' 59 59 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 60 60 import {NuxDialogs} from '#/components/dialogs/nuxs' 61 61 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' ··· 106 106 }, [_]) 107 107 108 108 return ( 109 - <Alf theme={theme}> 110 - <ThemeProvider theme={theme}> 111 - <Splash isReady={isReady && hasCheckedReferrer}> 112 - <RootSiblingParent> 113 - <VideoVolumeProvider> 114 - <React.Fragment 115 - // Resets the entire tree below when it changes: 116 - key={currentAccount?.did}> 109 + <StatsigProvider 110 + // Resets the entire tree below when it changes: 111 + key={currentAccount?.did}> 112 + <Alf theme={theme}> 113 + <ThemeProvider theme={theme}> 114 + <Splash isReady={isReady && hasCheckedReferrer}> 115 + <RootSiblingParent> 116 + <VideoVolumeProvider> 117 117 <QueryProvider currentDid={currentAccount?.did}> 118 - <StatsigProvider> 119 - <MessagesProvider> 120 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 121 - <LabelDefsProvider> 122 - <ModerationOptsProvider> 123 - <LoggedOutViewProvider> 124 - <SelectedFeedProvider> 125 - <HiddenRepliesProvider> 126 - <UnreadNotifsProvider> 127 - <BackgroundNotificationPreferencesProvider> 128 - <MutedThreadsProvider> 129 - <ProgressGuideProvider> 130 - <GestureHandlerRootView 131 - style={s.h100pct}> 132 - <TestCtrls /> 133 - <Shell /> 134 - <NuxDialogs /> 135 - </GestureHandlerRootView> 136 - </ProgressGuideProvider> 137 - </MutedThreadsProvider> 138 - </BackgroundNotificationPreferencesProvider> 139 - </UnreadNotifsProvider> 140 - </HiddenRepliesProvider> 141 - </SelectedFeedProvider> 142 - </LoggedOutViewProvider> 143 - </ModerationOptsProvider> 144 - </LabelDefsProvider> 145 - </MessagesProvider> 146 - </StatsigProvider> 118 + <MessagesProvider> 119 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 120 + <LabelDefsProvider> 121 + <ModerationOptsProvider> 122 + <LoggedOutViewProvider> 123 + <SelectedFeedProvider> 124 + <HiddenRepliesProvider> 125 + <UnreadNotifsProvider> 126 + <BackgroundNotificationPreferencesProvider> 127 + <MutedThreadsProvider> 128 + <ProgressGuideProvider> 129 + <GestureHandlerRootView style={s.h100pct}> 130 + <TestCtrls /> 131 + <Shell /> 132 + <NuxDialogs /> 133 + </GestureHandlerRootView> 134 + </ProgressGuideProvider> 135 + </MutedThreadsProvider> 136 + </BackgroundNotificationPreferencesProvider> 137 + </UnreadNotifsProvider> 138 + </HiddenRepliesProvider> 139 + </SelectedFeedProvider> 140 + </LoggedOutViewProvider> 141 + </ModerationOptsProvider> 142 + </LabelDefsProvider> 143 + </MessagesProvider> 147 144 </QueryProvider> 148 - </React.Fragment> 149 - </VideoVolumeProvider> 150 - </RootSiblingParent> 151 - </Splash> 152 - </ThemeProvider> 153 - </Alf> 145 + </VideoVolumeProvider> 146 + </RootSiblingParent> 147 + </Splash> 148 + </ThemeProvider> 149 + </Alf> 150 + </StatsigProvider> 154 151 ) 155 152 } 156 153 157 154 function App() { 158 155 const [isReady, setReady] = useState(false) 156 + const [loaded] = useFonts() 159 157 160 158 React.useEffect(() => { 161 159 initPersistedState().then(() => setReady(true)) 162 160 }, []) 163 161 164 - if (!isReady) { 162 + if (!isReady || !loaded) { 165 163 return null 166 164 } 167 165
+43 -44
src/App.web.tsx
··· 46 46 import * as Toast from '#/view/com/util/Toast' 47 47 import {ToastContainer} from '#/view/com/util/Toast.web' 48 48 import {Shell} from '#/view/shell/index' 49 - import {ThemeProvider as Alf} from '#/alf' 49 + import {ThemeProvider as Alf, useFonts} from '#/alf' 50 50 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 51 51 import {NuxDialogs} from '#/components/dialogs/nuxs' 52 52 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' ··· 96 96 97 97 return ( 98 98 <KeyboardProvider enabled={false}> 99 - <Alf theme={theme}> 100 - <ThemeProvider theme={theme}> 101 - <RootSiblingParent> 102 - <VideoVolumeProvider> 103 - <ActiveVideoProvider> 104 - <React.Fragment 105 - // Resets the entire tree below when it changes: 106 - key={currentAccount?.did}> 99 + <StatsigProvider 100 + // Resets the entire tree below when it changes: 101 + key={currentAccount?.did}> 102 + <Alf theme={theme}> 103 + <ThemeProvider theme={theme}> 104 + <RootSiblingParent> 105 + <VideoVolumeProvider> 106 + <ActiveVideoProvider> 107 107 <QueryProvider currentDid={currentAccount?.did}> 108 - <StatsigProvider> 109 - <MessagesProvider> 110 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 111 - <LabelDefsProvider> 112 - <ModerationOptsProvider> 113 - <LoggedOutViewProvider> 114 - <SelectedFeedProvider> 115 - <HiddenRepliesProvider> 116 - <UnreadNotifsProvider> 117 - <BackgroundNotificationPreferencesProvider> 118 - <MutedThreadsProvider> 119 - <SafeAreaProvider> 120 - <ProgressGuideProvider> 121 - <Shell /> 122 - <NuxDialogs /> 123 - </ProgressGuideProvider> 124 - </SafeAreaProvider> 125 - </MutedThreadsProvider> 126 - </BackgroundNotificationPreferencesProvider> 127 - </UnreadNotifsProvider> 128 - </HiddenRepliesProvider> 129 - </SelectedFeedProvider> 130 - </LoggedOutViewProvider> 131 - </ModerationOptsProvider> 132 - </LabelDefsProvider> 133 - </MessagesProvider> 134 - </StatsigProvider> 108 + <MessagesProvider> 109 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 110 + <LabelDefsProvider> 111 + <ModerationOptsProvider> 112 + <LoggedOutViewProvider> 113 + <SelectedFeedProvider> 114 + <HiddenRepliesProvider> 115 + <UnreadNotifsProvider> 116 + <BackgroundNotificationPreferencesProvider> 117 + <MutedThreadsProvider> 118 + <SafeAreaProvider> 119 + <ProgressGuideProvider> 120 + <Shell /> 121 + <NuxDialogs /> 122 + </ProgressGuideProvider> 123 + </SafeAreaProvider> 124 + </MutedThreadsProvider> 125 + </BackgroundNotificationPreferencesProvider> 126 + </UnreadNotifsProvider> 127 + </HiddenRepliesProvider> 128 + </SelectedFeedProvider> 129 + </LoggedOutViewProvider> 130 + </ModerationOptsProvider> 131 + </LabelDefsProvider> 132 + </MessagesProvider> 135 133 </QueryProvider> 136 - </React.Fragment> 137 - <ToastContainer /> 138 - </ActiveVideoProvider> 139 - </VideoVolumeProvider> 140 - </RootSiblingParent> 141 - </ThemeProvider> 142 - </Alf> 134 + <ToastContainer /> 135 + </ActiveVideoProvider> 136 + </VideoVolumeProvider> 137 + </RootSiblingParent> 138 + </ThemeProvider> 139 + </Alf> 140 + </StatsigProvider> 143 141 </KeyboardProvider> 144 142 ) 145 143 } 146 144 147 145 function App() { 148 146 const [isReady, setReady] = useState(false) 147 + const [loaded] = useFonts() 149 148 150 149 React.useEffect(() => { 151 150 initPersistedState().then(() => setReady(true)) 152 151 }, []) 153 152 154 - if (!isReady) { 153 + if (!isReady || !loaded) { 155 154 return null 156 155 } 157 156
+11 -14
src/alf/atoms.ts
··· 225 225 }, 226 226 text_2xs: { 227 227 fontSize: tokens.fontSize._2xs, 228 - letterSpacing: 0.25, 228 + letterSpacing: tokens.TRACKING, 229 229 }, 230 230 text_xs: { 231 231 fontSize: tokens.fontSize.xs, 232 - letterSpacing: 0.25, 232 + letterSpacing: tokens.TRACKING, 233 233 }, 234 234 text_sm: { 235 235 fontSize: tokens.fontSize.sm, 236 - letterSpacing: 0.25, 236 + letterSpacing: tokens.TRACKING, 237 237 }, 238 238 text_md: { 239 239 fontSize: tokens.fontSize.md, 240 - letterSpacing: 0.25, 240 + letterSpacing: tokens.TRACKING, 241 241 }, 242 242 text_lg: { 243 243 fontSize: tokens.fontSize.lg, 244 - letterSpacing: 0.25, 244 + letterSpacing: tokens.TRACKING, 245 245 }, 246 246 text_xl: { 247 247 fontSize: tokens.fontSize.xl, 248 - letterSpacing: 0.25, 248 + letterSpacing: tokens.TRACKING, 249 249 }, 250 250 text_2xl: { 251 251 fontSize: tokens.fontSize._2xl, 252 - letterSpacing: 0.25, 252 + letterSpacing: tokens.TRACKING, 253 253 }, 254 254 text_3xl: { 255 255 fontSize: tokens.fontSize._3xl, 256 - letterSpacing: 0.25, 256 + letterSpacing: tokens.TRACKING, 257 257 }, 258 258 text_4xl: { 259 259 fontSize: tokens.fontSize._4xl, 260 - letterSpacing: 0.25, 260 + letterSpacing: tokens.TRACKING, 261 261 }, 262 262 text_5xl: { 263 263 fontSize: tokens.fontSize._5xl, 264 - letterSpacing: 0.25, 264 + letterSpacing: tokens.TRACKING, 265 265 }, 266 266 leading_tight: { 267 267 lineHeight: 1.15, ··· 273 273 lineHeight: 1.5, 274 274 }, 275 275 tracking_normal: { 276 - letterSpacing: 0, 277 - }, 278 - tracking_wide: { 279 - letterSpacing: 0.25, 276 + letterSpacing: tokens.TRACKING, 280 277 }, 281 278 font_normal: { 282 279 fontWeight: tokens.fontWeight.normal,
+111
src/alf/fonts.ts
··· 1 + import {useFonts as defaultUseFonts} from 'expo-font' 2 + 3 + import {isNative, isWeb} from '#/platform/detection' 4 + import {Device, device} from '#/storage' 5 + 6 + const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif` 7 + 8 + const factor = 0.0625 // 1 - (15/16) 9 + const fontScaleMultipliers: Record<Device['fontScale'], number> = { 10 + '-2': 1 - factor * 3, 11 + '-1': 1 - factor * 2, 12 + '0': 1 - factor * 1, // default 13 + '1': 1, 14 + '2': 1 + factor * 1, 15 + } 16 + 17 + export function computeFontScaleMultiplier(scale: Device['fontScale']) { 18 + return fontScaleMultipliers[scale] 19 + } 20 + 21 + export function getFontScale() { 22 + return device.get(['fontScale']) ?? '0' 23 + } 24 + 25 + export function setFontScale(fontScale: Device['fontScale']) { 26 + device.set(['fontScale'], fontScale) 27 + } 28 + 29 + export function getFontFamily() { 30 + return device.get(['fontFamily']) || 'theme' 31 + } 32 + 33 + export function setFontFamily(fontFamily: Device['fontFamily']) { 34 + device.set(['fontFamily'], fontFamily) 35 + } 36 + 37 + /* 38 + * Unused fonts are commented out, but the files are there if we need them. 39 + */ 40 + export function useFonts() { 41 + /** 42 + * For native, the `expo-font` config plugin embeds the fonts in the 43 + * application binary. But `expo-font` isn't supported on web, so we fall 44 + * back to async loading here. 45 + */ 46 + if (isNative) return [true, null] 47 + return defaultUseFonts({ 48 + // 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'), 49 + // 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'), 50 + // 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'), 51 + // 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'), 52 + // 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'), 53 + // 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'), 54 + 'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'), 55 + 'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'), 56 + 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'), 57 + 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'), 58 + 'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'), 59 + 'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'), 60 + 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'), 61 + 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'), 62 + 'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'), 63 + 'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'), 64 + 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'), 65 + 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'), 66 + }) 67 + } 68 + 69 + /* 70 + * Unused fonts are commented out, but the files are there if we need them. 71 + */ 72 + export function applyFonts( 73 + style: Record<string, any>, 74 + fontFamily: 'system' | 'theme', 75 + ) { 76 + if (fontFamily === 'theme') { 77 + style.fontFamily = 78 + { 79 + // '100': 'Inter-Thin', 80 + // '200': 'Inter-ExtraLight', 81 + // '300': 'Inter-Light', 82 + '100': 'Inter-Regular', 83 + '200': 'Inter-Regular', 84 + '300': 'Inter-Regular', 85 + '400': 'Inter-Regular', 86 + '500': 'Inter-Medium', 87 + '600': 'Inter-SemiBold', 88 + '700': 'Inter-Bold', 89 + '800': 'Inter-ExtraBold', 90 + '900': 'Inter-Black', 91 + }[style.fontWeight as string] || 'Inter-Regular' 92 + 93 + if (style.fontStyle === 'italic') { 94 + if (style.fontFamily === 'Inter-Regular') { 95 + style.fontFamily = 'Inter-Italic' 96 + } else { 97 + style.fontFamily += 'Italic' 98 + } 99 + } 100 + 101 + // fallback families only supported on web 102 + if (isWeb) { 103 + style.fontFamily += `, ${FAMILIES}` 104 + } 105 + } else { 106 + // fallback families only supported on web 107 + if (isWeb) { 108 + style.fontFamily = style.fontFamily || FAMILIES 109 + } 110 + } 111 + }
+89 -12
src/alf/index.tsx
··· 1 1 import React from 'react' 2 2 import {useMediaQuery} from 'react-responsive' 3 3 4 + import { 5 + computeFontScaleMultiplier, 6 + getFontFamily, 7 + getFontScale, 8 + setFontFamily as persistFontFamily, 9 + setFontScale as persistFontScale, 10 + } from '#/alf/fonts' 4 11 import {createThemes, defaultTheme} from '#/alf/themes' 5 12 import {Theme, ThemeName} from '#/alf/types' 6 13 import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' 14 + import {Device} from '#/storage' 7 15 8 16 export {atoms} from '#/alf/atoms' 17 + export * from '#/alf/fonts' 9 18 export * as tokens from '#/alf/tokens' 10 19 export * from '#/alf/types' 11 20 export * from '#/alf/util/flatten' 12 21 export * from '#/alf/util/platform' 13 22 export * from '#/alf/util/themeSelector' 23 + 24 + export type Alf = { 25 + themeName: ThemeName 26 + theme: Theme 27 + themes: ReturnType<typeof createThemes> 28 + fonts: { 29 + scale: Exclude<Device['fontScale'], undefined> 30 + scaleMultiplier: number 31 + family: Device['fontFamily'] 32 + setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void 33 + setFontFamily: (fontFamily: Device['fontFamily']) => void 34 + } 35 + /** 36 + * Feature flags or other gated options 37 + */ 38 + flags: {} 39 + } 14 40 15 41 /* 16 42 * Context 17 43 */ 18 - export const Context = React.createContext<{ 19 - themeName: ThemeName 20 - theme: Theme 21 - themes: ReturnType<typeof createThemes> 22 - }>({ 44 + export const Context = React.createContext<Alf>({ 23 45 themeName: 'light', 24 46 theme: defaultTheme, 25 47 themes: createThemes({ ··· 29 51 positive: GREEN_HUE, 30 52 }, 31 53 }), 54 + fonts: { 55 + scale: getFontScale(), 56 + scaleMultiplier: computeFontScaleMultiplier(getFontScale()), 57 + family: getFontFamily(), 58 + setFontScale: () => {}, 59 + setFontFamily: () => {}, 60 + }, 61 + flags: {}, 32 62 }) 33 63 34 64 export function ThemeProvider({ 35 65 children, 36 66 theme: themeName, 37 67 }: React.PropsWithChildren<{theme: ThemeName}>) { 68 + const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() => 69 + getFontScale(), 70 + ) 71 + const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() => 72 + computeFontScaleMultiplier(fontScale), 73 + ) 74 + const setFontScaleAndPersist = React.useCallback< 75 + Alf['fonts']['setFontScale'] 76 + >( 77 + fontScale => { 78 + setFontScale(fontScale) 79 + persistFontScale(fontScale) 80 + setFontScaleMultiplier(computeFontScaleMultiplier(fontScale)) 81 + }, 82 + [setFontScale], 83 + ) 84 + const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>( 85 + () => getFontFamily(), 86 + ) 87 + const setFontFamilyAndPersist = React.useCallback< 88 + Alf['fonts']['setFontFamily'] 89 + >( 90 + fontFamily => { 91 + setFontFamily(fontFamily) 92 + persistFontFamily(fontFamily) 93 + }, 94 + [setFontFamily], 95 + ) 38 96 const themes = React.useMemo(() => { 39 97 return createThemes({ 40 98 hues: { ··· 44 102 }, 45 103 }) 46 104 }, []) 47 - const theme = themes[themeName] 48 105 49 106 return ( 50 107 <Context.Provider 51 - value={React.useMemo( 108 + value={React.useMemo<Alf>( 52 109 () => ({ 53 110 themes, 54 111 themeName: themeName, 55 - theme: theme, 112 + theme: themes[themeName], 113 + fonts: { 114 + scale: fontScale, 115 + scaleMultiplier: fontScaleMultiplier, 116 + family: fontFamily, 117 + setFontScale: setFontScaleAndPersist, 118 + setFontFamily: setFontFamilyAndPersist, 119 + }, 120 + flags: {}, 56 121 }), 57 - [theme, themeName, themes], 122 + [ 123 + themeName, 124 + themes, 125 + fontScale, 126 + setFontScaleAndPersist, 127 + fontFamily, 128 + setFontFamilyAndPersist, 129 + fontScaleMultiplier, 130 + ], 58 131 )}> 59 132 {children} 60 133 </Context.Provider> 61 134 ) 135 + } 136 + 137 + export function useAlf() { 138 + return React.useContext(Context) 62 139 } 63 140 64 141 export function useTheme(theme?: ThemeName) { 65 - const ctx = React.useContext(Context) 142 + const alf = useAlf() 66 143 return React.useMemo(() => { 67 - return theme ? ctx.themes[theme] : ctx.theme 68 - }, [theme, ctx]) 144 + return theme ? alf.themes[theme] : alf.theme 145 + }, [theme, alf]) 69 146 } 70 147 71 148 export function useBreakpoints() {
+4
src/alf/tokens.ts
··· 1 + import {Platform} from 'react-native' 2 + 3 + export const TRACKING = Platform.OS === 'android' ? 0.1 : 0 4 + 1 5 export const color = { 2 6 temp_purple: 'rgb(105 0 255)', 3 7 temp_purple_dark: 'rgb(83 0 202)',
+2 -10
src/components/Button.tsx
··· 7 7 PressableProps, 8 8 StyleProp, 9 9 StyleSheet, 10 - Text, 11 10 TextProps, 12 11 TextStyle, 13 12 View, ··· 17 16 18 17 import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf' 19 18 import {Props as SVGIconProps} from '#/components/icons/common' 20 - import {normalizeTextStyles} from '#/components/Typography' 19 + import {Text} from '#/components/Typography' 21 20 22 21 export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' 23 22 export type ButtonColor = ··· 635 634 const textStyles = useSharedButtonTextStyles() 636 635 637 636 return ( 638 - <Text 639 - {...rest} 640 - style={normalizeTextStyles([ 641 - a.font_bold, 642 - a.text_center, 643 - textStyles, 644 - style, 645 - ])}> 637 + <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}> 646 638 {children} 647 639 </Text> 648 640 )
+1
src/components/Dialog/index.tsx
··· 37 37 38 38 export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 39 39 export * from '#/components/Dialog/types' 40 + export * from '#/components/Dialog/utils' 40 41 // @ts-ignore 41 42 export const Input = createInput(BottomSheetTextInput) 42 43
+1
src/components/Dialog/index.web.tsx
··· 27 27 28 28 export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 29 29 export * from '#/components/Dialog/types' 30 + export * from '#/components/Dialog/utils' 30 31 export {Input} from '#/components/forms/TextField' 31 32 32 33 const stopPropagation = (e: any) => e.stopPropagation()
+18
src/components/Dialog/utils.ts
··· 1 + import React from 'react' 2 + 3 + import {DialogControlProps} from '#/components/Dialog/types' 4 + 5 + export function useAutoOpen(control: DialogControlProps, showTimeout?: number) { 6 + React.useEffect(() => { 7 + if (showTimeout) { 8 + const timeout = setTimeout(() => { 9 + control.open() 10 + }, showTimeout) 11 + return () => { 12 + clearTimeout(timeout) 13 + } 14 + } else { 15 + control.open() 16 + } 17 + }, [control, showTimeout]) 18 + }
+21 -5
src/components/Typography.tsx
··· 3 3 import {UITextView} from 'react-native-uitextview' 4 4 5 5 import {isNative} from '#/platform/detection' 6 - import {atoms, flatten, useTheme, web} from '#/alf' 6 + import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' 7 7 8 8 export type TextProps = RNTextProps & { 9 9 /** ··· 34 34 * If the `lineHeight` value is > 2, we assume it's an absolute value and 35 35 * returns it as-is. 36 36 */ 37 - export function normalizeTextStyles(styles: StyleProp<TextStyle>) { 37 + export function normalizeTextStyles( 38 + styles: StyleProp<TextStyle>, 39 + { 40 + fontScale, 41 + fontFamily, 42 + }: { 43 + fontScale: number 44 + fontFamily: Alf['fonts']['family'] 45 + } & Pick<Alf, 'flags'>, 46 + ) { 38 47 const s = flatten(styles) 39 48 // should always be defined on these components 40 - const fontSize = s.fontSize || atoms.text_md.fontSize 49 + s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale 41 50 42 51 if (s?.lineHeight) { 43 52 if (s.lineHeight !== 0 && s.lineHeight <= 2) { 44 - s.lineHeight = Math.round(fontSize * s.lineHeight) 53 + s.lineHeight = Math.round(s.fontSize * s.lineHeight) 45 54 } 46 55 } else if (!isNative) { 47 56 s.lineHeight = s.fontSize 48 57 } 49 58 59 + applyFonts(s, fontFamily) 60 + 50 61 return s 51 62 } 52 63 ··· 54 65 * Our main text component. Use this most of the time. 55 66 */ 56 67 export function Text({style, selectable, ...rest}: TextProps) { 68 + const {fonts, flags} = useAlf() 57 69 const t = useTheme() 58 - const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) 70 + const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], { 71 + fontScale: fonts.scaleMultiplier, 72 + fontFamily: fonts.family, 73 + flags, 74 + }) 59 75 60 76 return <UITextView selectable={selectable} uiTextView style={s} {...rest} /> 61 77 }
+119
src/components/dialogs/nuxs/NeueTypography.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings' 7 + import {atoms as a, useAlf, useTheme} from '#/alf' 8 + import * as Dialog from '#/components/Dialog' 9 + import {useNuxDialogContext} from '#/components/dialogs/nuxs' 10 + import {Divider} from '#/components/Divider' 11 + import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' 12 + import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 13 + import {Text} from '#/components/Typography' 14 + 15 + export function NeueTypography() { 16 + const t = useTheme() 17 + const {_} = useLingui() 18 + const nuxDialogs = useNuxDialogContext() 19 + const control = Dialog.useDialogControl() 20 + const {fonts} = useAlf() 21 + 22 + Dialog.useAutoOpen(control, 3e3) 23 + 24 + const onClose = React.useCallback(() => { 25 + nuxDialogs.dismissActiveNux() 26 + }, [nuxDialogs]) 27 + 28 + const onChangeFontFamily = React.useCallback( 29 + (values: string[]) => { 30 + const next = values[0] === 'system' ? 'system' : 'theme' 31 + fonts.setFontFamily(next) 32 + }, 33 + [fonts], 34 + ) 35 + 36 + const onChangeFontScale = React.useCallback( 37 + (values: string[]) => { 38 + const next = values[0] || ('0' as any) 39 + fonts.setFontScale(next) 40 + }, 41 + [fonts], 42 + ) 43 + 44 + return ( 45 + <Dialog.Outer control={control} onClose={onClose}> 46 + <Dialog.Handle /> 47 + 48 + <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> 49 + <View style={[a.gap_xl]}> 50 + <View style={[a.gap_md]}> 51 + <Text style={[a.text_3xl, {fontWeight: '900'}]}> 52 + <Trans>Introducing new font settings ✨</Trans> 53 + </Text> 54 + <Text style={[a.text_lg, a.leading_snug]}> 55 + <Trans> 56 + To the ensure the best possible experience, we're introducing a 57 + new theme font, along with adjustable font sizing settings. 58 + </Trans> 59 + </Text> 60 + <Text 61 + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 62 + <Trans> 63 + Defaults are shown below. You can edit these in your Appearance 64 + Settings later. 65 + </Trans> 66 + </Text> 67 + </View> 68 + 69 + <Divider /> 70 + 71 + <View style={[a.gap_lg]}> 72 + <AppearanceToggleButtonGroup 73 + title={_(msg`Font`)} 74 + description={_( 75 + msg`For the best experience, we recommend using the theme font.`, 76 + )} 77 + icon={Aa} 78 + items={[ 79 + { 80 + label: _(msg`System`), 81 + name: 'system', 82 + }, 83 + { 84 + label: _(msg`Theme`), 85 + name: 'theme', 86 + }, 87 + ]} 88 + values={[fonts.family]} 89 + onChange={onChangeFontFamily} 90 + /> 91 + 92 + <AppearanceToggleButtonGroup 93 + title={_(msg`Font size`)} 94 + icon={TextSize} 95 + items={[ 96 + { 97 + label: _(msg`Smaller`), 98 + name: '-1', 99 + }, 100 + { 101 + label: _(msg`Default`), 102 + name: '0', 103 + }, 104 + { 105 + label: _(msg`Larger`), 106 + name: '1', 107 + }, 108 + ]} 109 + values={[fonts.scale]} 110 + onChange={onChangeFontScale} 111 + /> 112 + </View> 113 + </View> 114 + 115 + <Dialog.Close /> 116 + </Dialog.ScrollableInner> 117 + </Dialog.Outer> 118 + ) 119 + }
+71 -9
src/components/dialogs/nuxs/index.tsx
··· 1 1 import React from 'react' 2 + import {AppBskyActorDefs} from '@atproto/api' 2 3 3 4 import {useGate} from '#/lib/statsig/statsig' 4 5 import {logger} from '#/logger' ··· 8 9 useRemoveNuxsMutation, 9 10 useUpsertNuxMutation, 10 11 } from '#/state/queries/nuxs' 11 - import {useSession} from '#/state/session' 12 + import { 13 + usePreferencesQuery, 14 + UsePreferencesQueryResponse, 15 + } from '#/state/queries/preferences' 16 + import {useProfileQuery} from '#/state/queries/profile' 17 + import {SessionAccount, useSession} from '#/state/session' 12 18 import {useOnboardingState} from '#/state/shell' 19 + import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography' 13 20 import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' 21 + // NUXs 14 22 import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' 15 23 import {IS_DEV} from '#/env' 16 24 ··· 21 29 22 30 const queuedNuxs: { 23 31 id: Nux 24 - enabled?: (props: {gate: ReturnType<typeof useGate>}) => boolean 32 + enabled?: (props: { 33 + gate: ReturnType<typeof useGate> 34 + currentAccount: SessionAccount 35 + currentProfile: AppBskyActorDefs.ProfileViewDetailed 36 + preferences: UsePreferencesQueryResponse 37 + }) => boolean 25 38 }[] = [ 26 39 { 27 40 id: Nux.TenMillionDialog, 28 41 }, 42 + { 43 + id: Nux.NeueTypography, 44 + enabled(props) { 45 + if (props.currentProfile.createdAt) { 46 + if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) { 47 + return true 48 + } 49 + } 50 + return false 51 + }, 52 + }, 29 53 ] 30 54 31 55 const Context = React.createContext<Context>({ ··· 38 62 } 39 63 40 64 export function NuxDialogs() { 41 - const {hasSession} = useSession() 42 - const onboardingState = useOnboardingState() 43 - return hasSession && !onboardingState.isActive ? <Inner /> : null 65 + const {currentAccount} = useSession() 66 + const {data: preferences} = usePreferencesQuery() 67 + const {data: profile} = useProfileQuery({did: currentAccount?.did}) 68 + const onboardingActive = useOnboardingState().isActive 69 + 70 + const isLoading = 71 + !currentAccount || !preferences || !profile || onboardingActive 72 + return !isLoading ? ( 73 + <Inner 74 + currentAccount={currentAccount} 75 + currentProfile={profile} 76 + preferences={preferences} 77 + /> 78 + ) : null 44 79 } 45 80 46 - function Inner() { 81 + function Inner({ 82 + currentAccount, 83 + currentProfile, 84 + preferences, 85 + }: { 86 + currentAccount: SessionAccount 87 + currentProfile: AppBskyActorDefs.ProfileViewDetailed 88 + preferences: UsePreferencesQueryResponse 89 + }) { 47 90 const gate = useGate() 48 91 const {nuxs} = useNuxs() 49 92 const [snoozed, setSnoozed] = React.useState(() => { ··· 80 123 const nux = nuxs.find(nux => nux.id === id) 81 124 82 125 // check if completed first 83 - if (nux && nux.completed) continue 126 + if (nux && nux.completed) { 127 + continue 128 + } 84 129 85 130 // then check gate (track exposure) 86 - if (enabled && !enabled({gate})) continue 131 + if ( 132 + enabled && 133 + !enabled({gate, currentAccount, currentProfile, preferences}) 134 + ) { 135 + continue 136 + } 137 + 138 + logger.debug(`NUX dialogs: activating '${id}' NUX`) 87 139 88 140 // we have a winner 89 141 setActiveNux(id) ··· 104 156 105 157 break 106 158 } 107 - }, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate]) 159 + }, [ 160 + nuxs, 161 + snoozed, 162 + snoozeNuxDialog, 163 + upsertNux, 164 + gate, 165 + currentAccount, 166 + currentProfile, 167 + preferences, 168 + ]) 108 169 109 170 const ctx = React.useMemo(() => { 110 171 return { ··· 116 177 return ( 117 178 <Context.Provider value={ctx}> 118 179 {activeNux === Nux.TenMillionDialog && <TenMillion />} 180 + {activeNux === Nux.NeueTypography && <NeueTypography />} 119 181 </Context.Provider> 120 182 ) 121 183 }
+5
src/components/icons/TextSize.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const TextSize_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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 + })
+5
src/components/icons/TitleCase.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const TitleCase_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+3 -3
src/lib/styles.ts
··· 79 79 80 80 // font weights 81 81 fw600: {fontWeight: '600'}, 82 - bold: {fontWeight: 'bold'}, 82 + bold: {fontWeight: '700'}, 83 83 fw500: {fontWeight: '500'}, 84 84 semiBold: {fontWeight: '500'}, 85 85 fw400: {fontWeight: '400'}, 86 86 normal: {fontWeight: '400'}, 87 - fw300: {fontWeight: '300'}, 88 - light: {fontWeight: '300'}, 87 + fw300: {fontWeight: '400'}, 88 + light: {fontWeight: '400'}, 89 89 fw200: {fontWeight: '200'}, 90 90 91 91 // text decoration
+45 -44
src/lib/themes.ts
··· 1 1 import {Platform} from 'react-native' 2 2 3 + import {tokens} from '#/alf' 3 4 import {darkPalette, dimPalette, lightPalette} from '#/alf/themes' 4 5 import {colors} from './styles' 5 6 import type {Theme} from './ThemeContext' ··· 88 89 typography: { 89 90 '2xl-thin': { 90 91 fontSize: 18, 91 - letterSpacing: 0.25, 92 - fontWeight: '300', 92 + letterSpacing: tokens.TRACKING, 93 + fontWeight: '400', 93 94 }, 94 95 '2xl': { 95 96 fontSize: 18, 96 - letterSpacing: 0.25, 97 + letterSpacing: tokens.TRACKING, 97 98 fontWeight: '400', 98 99 }, 99 100 '2xl-medium': { 100 101 fontSize: 18, 101 - letterSpacing: 0.25, 102 + letterSpacing: tokens.TRACKING, 102 103 fontWeight: '500', 103 104 }, 104 105 '2xl-bold': { 105 106 fontSize: 18, 106 - letterSpacing: 0.25, 107 + letterSpacing: tokens.TRACKING, 107 108 fontWeight: '700', 108 109 }, 109 110 '2xl-heavy': { 110 111 fontSize: 18, 111 - letterSpacing: 0.25, 112 + letterSpacing: tokens.TRACKING, 112 113 fontWeight: '800', 113 114 }, 114 115 'xl-thin': { 115 116 fontSize: 17, 116 - letterSpacing: 0.25, 117 - fontWeight: '300', 117 + letterSpacing: tokens.TRACKING, 118 + fontWeight: '400', 118 119 }, 119 120 xl: { 120 121 fontSize: 17, 121 - letterSpacing: 0.25, 122 + letterSpacing: tokens.TRACKING, 122 123 fontWeight: '400', 123 124 }, 124 125 'xl-medium': { 125 126 fontSize: 17, 126 - letterSpacing: 0.25, 127 + letterSpacing: tokens.TRACKING, 127 128 fontWeight: '500', 128 129 }, 129 130 'xl-bold': { 130 131 fontSize: 17, 131 - letterSpacing: 0.25, 132 + letterSpacing: tokens.TRACKING, 132 133 fontWeight: '700', 133 134 }, 134 135 'xl-heavy': { 135 136 fontSize: 17, 136 - letterSpacing: 0.25, 137 + letterSpacing: tokens.TRACKING, 137 138 fontWeight: '800', 138 139 }, 139 140 'lg-thin': { 140 141 fontSize: 16, 141 - letterSpacing: 0.25, 142 - fontWeight: '300', 142 + letterSpacing: tokens.TRACKING, 143 + fontWeight: '400', 143 144 }, 144 145 lg: { 145 146 fontSize: 16, 146 - letterSpacing: 0.25, 147 + letterSpacing: tokens.TRACKING, 147 148 fontWeight: '400', 148 149 }, 149 150 'lg-medium': { 150 151 fontSize: 16, 151 - letterSpacing: 0.25, 152 + letterSpacing: tokens.TRACKING, 152 153 fontWeight: '500', 153 154 }, 154 155 'lg-bold': { 155 156 fontSize: 16, 156 - letterSpacing: 0.25, 157 + letterSpacing: tokens.TRACKING, 157 158 fontWeight: '700', 158 159 }, 159 160 'lg-heavy': { 160 161 fontSize: 16, 161 - letterSpacing: 0.25, 162 + letterSpacing: tokens.TRACKING, 162 163 fontWeight: '800', 163 164 }, 164 165 'md-thin': { 165 166 fontSize: 15, 166 - letterSpacing: 0.25, 167 - fontWeight: '300', 167 + letterSpacing: tokens.TRACKING, 168 + fontWeight: '400', 168 169 }, 169 170 md: { 170 171 fontSize: 15, 171 - letterSpacing: 0.25, 172 + letterSpacing: tokens.TRACKING, 172 173 fontWeight: '400', 173 174 }, 174 175 'md-medium': { 175 176 fontSize: 15, 176 - letterSpacing: 0.25, 177 + letterSpacing: tokens.TRACKING, 177 178 fontWeight: '500', 178 179 }, 179 180 'md-bold': { 180 181 fontSize: 15, 181 - letterSpacing: 0.25, 182 + letterSpacing: tokens.TRACKING, 182 183 fontWeight: '700', 183 184 }, 184 185 'md-heavy': { 185 186 fontSize: 15, 186 - letterSpacing: 0.25, 187 + letterSpacing: tokens.TRACKING, 187 188 fontWeight: '800', 188 189 }, 189 190 'sm-thin': { 190 191 fontSize: 14, 191 - letterSpacing: 0.25, 192 - fontWeight: '300', 192 + letterSpacing: tokens.TRACKING, 193 + fontWeight: '400', 193 194 }, 194 195 sm: { 195 196 fontSize: 14, 196 - letterSpacing: 0.25, 197 + letterSpacing: tokens.TRACKING, 197 198 fontWeight: '400', 198 199 }, 199 200 'sm-medium': { 200 201 fontSize: 14, 201 - letterSpacing: 0.25, 202 + letterSpacing: tokens.TRACKING, 202 203 fontWeight: '500', 203 204 }, 204 205 'sm-bold': { 205 206 fontSize: 14, 206 - letterSpacing: 0.25, 207 + letterSpacing: tokens.TRACKING, 207 208 fontWeight: '700', 208 209 }, 209 210 'sm-heavy': { 210 211 fontSize: 14, 211 - letterSpacing: 0.25, 212 + letterSpacing: tokens.TRACKING, 212 213 fontWeight: '800', 213 214 }, 214 215 'xs-thin': { 215 216 fontSize: 13, 216 - letterSpacing: 0.25, 217 - fontWeight: '300', 217 + letterSpacing: tokens.TRACKING, 218 + fontWeight: '400', 218 219 }, 219 220 xs: { 220 221 fontSize: 13, 221 - letterSpacing: 0.25, 222 + letterSpacing: tokens.TRACKING, 222 223 fontWeight: '400', 223 224 }, 224 225 'xs-medium': { 225 226 fontSize: 13, 226 - letterSpacing: 0.25, 227 + letterSpacing: tokens.TRACKING, 227 228 fontWeight: '500', 228 229 }, 229 230 'xs-bold': { 230 231 fontSize: 13, 231 - letterSpacing: 0.25, 232 + letterSpacing: tokens.TRACKING, 232 233 fontWeight: '700', 233 234 }, 234 235 'xs-heavy': { 235 236 fontSize: 13, 236 - letterSpacing: 0.25, 237 + letterSpacing: tokens.TRACKING, 237 238 fontWeight: '800', 238 239 }, 239 240 240 241 'title-2xl': { 241 242 fontSize: 34, 242 - letterSpacing: 0.25, 243 + letterSpacing: tokens.TRACKING, 243 244 fontWeight: '500', 244 245 }, 245 246 'title-xl': { 246 247 fontSize: 28, 247 - letterSpacing: 0.25, 248 + letterSpacing: tokens.TRACKING, 248 249 fontWeight: '500', 249 250 }, 250 251 'title-lg': { ··· 254 255 title: { 255 256 fontWeight: '500', 256 257 fontSize: 20, 257 - letterSpacing: 0.15, 258 + letterSpacing: tokens.TRACKING, 258 259 }, 259 260 'title-sm': { 260 261 fontWeight: 'bold', 261 262 fontSize: 17, 262 - letterSpacing: 0.15, 263 + letterSpacing: tokens.TRACKING, 263 264 }, 264 265 'post-text': { 265 266 fontSize: 16, 266 - letterSpacing: 0.2, 267 + letterSpacing: tokens.TRACKING, 267 268 fontWeight: '400', 268 269 }, 269 270 'post-text-lg': { 270 271 fontSize: 20, 271 - letterSpacing: 0.2, 272 + letterSpacing: tokens.TRACKING, 272 273 fontWeight: '400', 273 274 }, 274 275 'button-lg': { 275 276 fontWeight: '500', 276 277 fontSize: 18, 277 - letterSpacing: 0.5, 278 + letterSpacing: tokens.TRACKING, 278 279 }, 279 280 button: { 280 281 fontWeight: '500', 281 282 fontSize: 14, 282 - letterSpacing: 0.5, 283 + letterSpacing: tokens.TRACKING, 283 284 }, 284 285 mono: { 285 286 fontSize: 14,
+155 -57
src/screens/Settings/AppearanceSettings.tsx
··· 14 14 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 15 15 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 16 16 import {ScrollView} from '#/view/com/util/Views' 17 - import {atoms as a, native, useTheme} from '#/alf' 17 + import {atoms as a, native, useAlf, useTheme} from '#/alf' 18 18 import * as ToggleButton from '#/components/forms/ToggleButton' 19 + import {Props as SVGIconProps} from '#/components/icons/common' 19 20 import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' 20 21 import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' 22 + import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' 23 + import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 21 24 import {Text} from '#/components/Typography' 22 25 23 26 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> 24 27 export function AppearanceSettingsScreen({}: Props) { 25 - const {_} = useLingui() 26 28 const t = useTheme() 29 + const {_} = useLingui() 27 30 const {isTabletOrMobile} = useWebMediaQueries() 31 + const {fonts} = useAlf() 28 32 29 33 const {colorMode, darkTheme} = useThemePrefs() 30 34 const {setColorMode, setDarkTheme} = useSetThemePrefs() ··· 54 58 [setDarkTheme, darkTheme], 55 59 ) 56 60 61 + const onChangeFontFamily = useCallback( 62 + (values: string[]) => { 63 + const next = values[0] === 'system' ? 'system' : 'theme' 64 + fonts.setFontFamily(next) 65 + }, 66 + [fonts], 67 + ) 68 + 69 + const onChangeFontScale = useCallback( 70 + (values: string[]) => { 71 + const next = values[0] || ('0' as any) 72 + fonts.setFontScale(next) 73 + }, 74 + [fonts], 75 + ) 76 + 57 77 return ( 58 78 <LayoutAnimationConfig skipExiting skipEntering> 59 79 <View testID="preferencesThreadsScreen" style={s.hContentRegion}> ··· 71 91 </View> 72 92 </SimpleViewHeader> 73 93 74 - <View style={[a.p_xl, a.gap_lg]}> 75 - <View style={[a.flex_row, a.align_center, a.gap_md]}> 76 - <PhoneIcon style={t.atoms.text} /> 77 - <Text style={a.text_md}> 78 - <Trans>Mode</Trans> 79 - </Text> 80 - </View> 81 - <ToggleButton.Group 82 - label={_(msg`Dark mode`)} 83 - values={[colorMode]} 84 - onChange={onChangeAppearance}> 85 - <ToggleButton.Button label={_(msg`System`)} name="system"> 86 - <ToggleButton.ButtonText> 87 - <Trans>System</Trans> 88 - </ToggleButton.ButtonText> 89 - </ToggleButton.Button> 90 - <ToggleButton.Button label={_(msg`Light`)} name="light"> 91 - <ToggleButton.ButtonText> 92 - <Trans>Light</Trans> 93 - </ToggleButton.ButtonText> 94 - </ToggleButton.Button> 95 - <ToggleButton.Button label={_(msg`Dark`)} name="dark"> 96 - <ToggleButton.ButtonText> 97 - <Trans>Dark</Trans> 98 - </ToggleButton.ButtonText> 99 - </ToggleButton.Button> 100 - </ToggleButton.Group> 101 - {colorMode !== 'light' && ( 102 - <Animated.View 103 - entering={native(FadeInDown)} 104 - exiting={native(FadeOutDown)} 105 - style={[a.mt_md, a.gap_lg]}> 106 - <View style={[a.flex_row, a.align_center, a.gap_md]}> 107 - <MoonIcon style={t.atoms.text} /> 108 - <Text style={a.text_md}> 109 - <Trans>Dark theme</Trans> 110 - </Text> 111 - </View> 94 + <View style={[a.gap_3xl, a.pt_xl, a.px_xl]}> 95 + <View style={[a.gap_lg]}> 96 + <AppearanceToggleButtonGroup 97 + title={_(msg`Color mode`)} 98 + icon={PhoneIcon} 99 + items={[ 100 + { 101 + label: _(msg`System`), 102 + name: 'system', 103 + }, 104 + { 105 + label: _(msg`Light`), 106 + name: 'light', 107 + }, 108 + { 109 + label: _(msg`Dark`), 110 + name: 'dark', 111 + }, 112 + ]} 113 + values={[colorMode]} 114 + onChange={onChangeAppearance} 115 + /> 112 116 113 - <ToggleButton.Group 114 - label={_(msg`Dark theme`)} 115 - values={[darkTheme ?? 'dim']} 116 - onChange={onChangeDarkTheme}> 117 - <ToggleButton.Button label={_(msg`Dim`)} name="dim"> 118 - <ToggleButton.ButtonText> 119 - <Trans>Dim</Trans> 120 - </ToggleButton.ButtonText> 121 - </ToggleButton.Button> 122 - <ToggleButton.Button label={_(msg`Dark`)} name="dark"> 123 - <ToggleButton.ButtonText> 124 - <Trans>Dark</Trans> 125 - </ToggleButton.ButtonText> 126 - </ToggleButton.Button> 127 - </ToggleButton.Group> 128 - </Animated.View> 129 - )} 117 + {colorMode !== 'light' && ( 118 + <Animated.View 119 + entering={native(FadeInDown)} 120 + exiting={native(FadeOutDown)}> 121 + <AppearanceToggleButtonGroup 122 + title={_(msg`Dark theme`)} 123 + icon={MoonIcon} 124 + items={[ 125 + { 126 + label: _(msg`Dim`), 127 + name: 'dim', 128 + }, 129 + { 130 + label: _(msg`Dark`), 131 + name: 'dark', 132 + }, 133 + ]} 134 + values={[darkTheme ?? 'dim']} 135 + onChange={onChangeDarkTheme} 136 + /> 137 + </Animated.View> 138 + )} 139 + 140 + <AppearanceToggleButtonGroup 141 + title={_(msg`Font`)} 142 + description={_( 143 + msg`For the best experience, we recommend using the theme font.`, 144 + )} 145 + icon={Aa} 146 + items={[ 147 + { 148 + label: _(msg`System`), 149 + name: 'system', 150 + }, 151 + { 152 + label: _(msg`Theme`), 153 + name: 'theme', 154 + }, 155 + ]} 156 + values={[fonts.family]} 157 + onChange={onChangeFontFamily} 158 + /> 159 + 160 + <AppearanceToggleButtonGroup 161 + title={_(msg`Font size`)} 162 + icon={TextSize} 163 + items={[ 164 + { 165 + label: _(msg`Smaller`), 166 + name: '-1', 167 + }, 168 + { 169 + label: _(msg`Default`), 170 + name: '0', 171 + }, 172 + { 173 + label: _(msg`Larger`), 174 + name: '1', 175 + }, 176 + ]} 177 + values={[fonts.scale]} 178 + onChange={onChangeFontScale} 179 + /> 180 + </View> 130 181 </View> 131 182 </ScrollView> 132 183 </View> 133 184 </LayoutAnimationConfig> 134 185 ) 135 186 } 187 + 188 + export function AppearanceToggleButtonGroup({ 189 + title, 190 + description, 191 + icon: Icon, 192 + items, 193 + values, 194 + onChange, 195 + }: { 196 + title: string 197 + description?: string 198 + icon: React.ComponentType<SVGIconProps> 199 + items: { 200 + label: string 201 + name: string 202 + }[] 203 + values: string[] 204 + onChange: (values: string[]) => void 205 + }) { 206 + const t = useTheme() 207 + return ( 208 + <View style={[a.gap_md]}> 209 + <View style={[a.gap_xs]}> 210 + <View style={[a.flex_row, a.align_center, a.gap_md]}> 211 + <Icon style={t.atoms.text} /> 212 + <Text style={[a.text_md, a.font_bold]}>{title}</Text> 213 + </View> 214 + {description && ( 215 + <Text 216 + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 217 + {description} 218 + </Text> 219 + )} 220 + </View> 221 + <ToggleButton.Group label={title} values={values} onChange={onChange}> 222 + {items.map(item => ( 223 + <ToggleButton.Button 224 + key={item.name} 225 + label={item.label} 226 + name={item.name}> 227 + <ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText> 228 + </ToggleButton.Button> 229 + ))} 230 + </ToggleButton.Group> 231 + </View> 232 + ) 233 + }
+11 -4
src/state/queries/nuxs/definitions.ts
··· 4 4 5 5 export enum Nux { 6 6 TenMillionDialog = 'TenMillionDialog', 7 + NeueTypography = 'NeueTypography', 7 8 } 8 9 9 10 export const nuxNames = new Set(Object.values(Nux)) 10 11 11 - export type AppNux = BaseNux<{ 12 - id: Nux.TenMillionDialog 13 - data: undefined 14 - }> 12 + export type AppNux = 13 + | BaseNux<{ 14 + id: Nux.TenMillionDialog 15 + data: undefined 16 + }> 17 + | BaseNux<{ 18 + id: Nux.NeueTypography 19 + data: undefined 20 + }> 15 21 16 22 export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { 17 23 [Nux.TenMillionDialog]: undefined, 24 + [Nux.NeueTypography]: undefined, 18 25 }
+2
src/storage/index.ts
··· 2 2 3 3 import {Device} from '#/storage/schema' 4 4 5 + export * from '#/storage/schema' 6 + 5 7 /** 6 8 * Generic storage class. DO NOT use this directly. Instead, use the exported 7 9 * storage instances below.
+2
src/storage/schema.ts
··· 2 2 * Device data that's specific to the device and does not vary based account 3 3 */ 4 4 export type Device = { 5 + fontScale: '-2' | '-1' | '0' | '1' | '2' 6 + fontFamily: 'system' | 'theme' 5 7 lastNuxDialog: string | undefined 6 8 }
+38 -9
src/view/com/util/text/Text.tsx
··· 1 1 import React from 'react' 2 - import {Text as RNText, TextProps} from 'react-native' 2 + import {StyleSheet, Text as RNText, TextProps} from 'react-native' 3 3 import {UITextView} from 'react-native-uitextview' 4 4 5 5 import {lh, s} from 'lib/styles' 6 6 import {TypographyVariant, useTheme} from 'lib/ThemeContext' 7 7 import {isIOS, isWeb} from 'platform/detection' 8 + import {applyFonts, useAlf} from '#/alf' 8 9 9 10 export type CustomTextProps = TextProps & { 10 11 type?: TypographyVariant ··· 32 33 const theme = useTheme() 33 34 const typography = theme.typography[type] 34 35 const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined 36 + const {fonts} = useAlf() 35 37 36 38 if (selectable && isIOS) { 39 + const flattened = StyleSheet.flatten([ 40 + s.black, 41 + typography, 42 + lineHeightStyle, 43 + style, 44 + ]) 45 + 46 + applyFonts(flattened, fonts.family) 47 + 48 + // should always be defined on `typography` 49 + // @ts-ignore 50 + if (flattened.fontSize) { 51 + // @ts-ignore 52 + flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier 53 + } 54 + 37 55 return ( 38 56 <UITextView 39 - style={[s.black, typography, lineHeightStyle, style]} 57 + style={flattened} 40 58 selectable={selectable} 41 59 uiTextView 42 60 {...props}> ··· 45 63 ) 46 64 } 47 65 66 + const flattened = StyleSheet.flatten([ 67 + s.black, 68 + typography, 69 + isWeb && fontFamilyStyle, 70 + lineHeightStyle, 71 + style, 72 + ]) 73 + 74 + applyFonts(flattened, fonts.family) 75 + 76 + // should always be defined on `typography` 77 + // @ts-ignore 78 + if (flattened.fontSize) { 79 + // @ts-ignore 80 + flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier 81 + } 82 + 48 83 return ( 49 84 <RNText 50 - style={[ 51 - s.black, 52 - typography, 53 - isWeb && fontFamilyStyle, 54 - lineHeightStyle, 55 - style, 56 - ]} 85 + style={flattened} 57 86 // @ts-ignore web only -esb 58 87 dataSet={Object.assign({tooltip: title}, dataSet || {})} 59 88 selectable={selectable}
+7
yarn.lock
··· 12219 12219 resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-17.0.1.tgz#b9f8af8c1c06ec71d96fd7a0d2567fa9e1c88f15" 12220 12220 integrity sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw== 12221 12221 12222 + expo-font@~12.0.10: 12223 + version "12.0.10" 12224 + resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.10.tgz#62deaf1f46159d7839f01305f44079268781b1db" 12225 + integrity sha512-Q1i2NuYri3jy32zdnBaHHCya1wH1yMAsI+3CCmj9zlQzlhsS9Bdwcj2W3c5eU5FvH2hsNQy4O+O1NnM6o/pDaQ== 12226 + dependencies: 12227 + fontfaceobserver "^2.1.0" 12228 + 12222 12229 expo-font@~12.0.5: 12223 12230 version "12.0.5" 12224 12231 resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.5.tgz#3451c2bd3f98859b127a6484d3474a292889b93f"