[Neue] Base (#5395)
* Add fontScale, gate it, fix some computes * Add inter, integrate * Clean up * Apply to old Text component * Use numeric weight * Cleanup * Clean up appearance settings * Global tracking * Fix regular italic variant * Refactor settings and fontScale values * Remove flags * Get rid of lower weight font usage * Remove gate from settings * Refactor appearance settings for reuse * Add neue type nux * Update defaults * Load fonts, add fallback families * Load fonts via plugin in production * Fixes * Fix for web * Nits --------- Co-authored-by: Hailey <me@haileyok.com>zio/dev^2
parent
fb3be79820
commit
cbc7cd0808
|
@ -230,6 +230,31 @@ module.exports = function (config) {
|
|||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
[
|
||||
'expo-font',
|
||||
{
|
||||
fonts: [
|
||||
// './assets/fonts/inter/Inter-Thin.otf',
|
||||
// './assets/fonts/inter/Inter-ThinItalic.otf',
|
||||
// './assets/fonts/inter/Inter-ExtraLight.otf',
|
||||
// './assets/fonts/inter/Inter-ExtraLightItalic.otf',
|
||||
// './assets/fonts/inter/Inter-Light.otf',
|
||||
// './assets/fonts/inter/Inter-LightItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Regular.otf',
|
||||
'./assets/fonts/inter/Inter-Italic.otf',
|
||||
'./assets/fonts/inter/Inter-Medium.otf',
|
||||
'./assets/fonts/inter/Inter-MediumItalic.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBold.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Bold.otf',
|
||||
'./assets/fonts/inter/Inter-BoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBold.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Black.otf',
|
||||
'./assets/fonts/inter/Inter-BlackItalic.otf',
|
||||
],
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 317 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 986 B |
|
@ -124,6 +124,7 @@
|
|||
"expo-dev-client": "^4.0.14",
|
||||
"expo-device": "~6.0.2",
|
||||
"expo-file-system": "^17.0.1",
|
||||
"expo-font": "~12.0.10",
|
||||
"expo-haptics": "^13.0.1",
|
||||
"expo-image": "~1.12.9",
|
||||
"expo-image-manipulator": "^12.0.5",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,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,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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -12219,6 +12219,13 @@ expo-file-system@^17.0.1, expo-file-system@~17.0.1:
|
|||
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-17.0.1.tgz#b9f8af8c1c06ec71d96fd7a0d2567fa9e1c88f15"
|
||||
integrity sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==
|
||||
|
||||
expo-font@~12.0.10:
|
||||
version "12.0.10"
|
||||
resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.10.tgz#62deaf1f46159d7839f01305f44079268781b1db"
|
||||
integrity sha512-Q1i2NuYri3jy32zdnBaHHCya1wH1yMAsI+3CCmj9zlQzlhsS9Bdwcj2W3c5eU5FvH2hsNQy4O+O1NnM6o/pDaQ==
|
||||
dependencies:
|
||||
fontfaceobserver "^2.1.0"
|
||||
|
||||
expo-font@~12.0.5:
|
||||
version "12.0.5"
|
||||
resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.5.tgz#3451c2bd3f98859b127a6484d3474a292889b93f"
|
||||
|
|
Loading…
Reference in New Issue