Options for selecting dark theme, fix some white flashes when in dark mode (#2722)
* add dark theme selection to settings/schema
* use `useThemePrefs` where needed
* adjust theme providers to support various themes
* update storybook
* handle web themes
* better themeing for web
* dont show dark theme prefs when color mode is light
* drop the inverted text change on oled theme
* get the color mode inside of `useColorModeTheme`
* use `ThemeName` type everywhere
* typo
* use dim/dark instead of dark/oled
* prevent any fickers on web
* fix styles
* use `dim` for dark default
* more cleanup
* 🤔
* set system background color
* ts
zio/stable
parent
856f80fc6d
commit
ec86282403
|
@ -1,13 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
|
||||||
<meta name="referrer" content="origin-when-cross-origin">
|
<meta name="referrer" content="origin-when-cross-origin">
|
||||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||||
|
|
||||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/**
|
/**
|
||||||
* Extend the react-native-web reset:
|
* Extend the react-native-web reset:
|
||||||
|
@ -40,31 +40,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color theming */
|
/* Color theming */
|
||||||
|
/* Default will always be white */
|
||||||
:root {
|
:root {
|
||||||
--text: black;
|
|
||||||
--background: white;
|
|
||||||
--backgroundLight: hsl(211, 20%, 95%);
|
|
||||||
}
|
|
||||||
html.colorMode--dark {
|
|
||||||
--text: white;
|
|
||||||
--background: hsl(211, 20%, 4%);
|
|
||||||
--backgroundLight: hsl(211, 20%, 20%);
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
html.colorMode--system {
|
|
||||||
--text: black;
|
--text: black;
|
||||||
--background: white;
|
--background: white;
|
||||||
--backgroundLight: hsl(211, 20%, 95%);
|
--backgroundLight: hsl(211, 20%, 95%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/* This gives us a black background when system is dark and we have not loaded the theme/color scheme values in JS */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html.colorMode--system {
|
:root {
|
||||||
|
--text: white;
|
||||||
|
--background: black;
|
||||||
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overwrite those preferences with the selected theme */
|
||||||
|
html.theme--light {
|
||||||
|
--text: black;
|
||||||
|
--background: white;
|
||||||
|
--backgroundLight: hsl(211, 20%, 95%);
|
||||||
|
}
|
||||||
|
html.theme--dark {
|
||||||
--text: white;
|
--text: white;
|
||||||
--background: hsl(211, 20%, 4%);
|
--background: black;
|
||||||
--backgroundLight: hsl(211, 20%, 20%);
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
html.theme--dim {
|
||||||
|
--text: white;
|
||||||
|
--background: hsl(211, 20%, 4%);
|
||||||
|
--backgroundLight: hsl(211, 20%, 10%);
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
/* Remove autofill styles on Webkit */
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import {listenSessionDropped} from './state/events'
|
import {listenSessionDropped} from './state/events'
|
||||||
import {useColorMode} from 'state/shell'
|
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
import {ThemeProvider} from 'lib/ThemeContext'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {Shell} from 'view/shell'
|
import {Shell} from 'view/shell'
|
||||||
|
@ -49,10 +48,9 @@ import {useLingui} from '@lingui/react'
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const colorMode = useColorMode()
|
|
||||||
const {isInitialLoad, currentAccount} = useSession()
|
const {isInitialLoad, currentAccount} = useSession()
|
||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
const theme = useColorModeTheme(colorMode)
|
const theme = useColorModeTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
// init
|
// init
|
||||||
|
@ -75,7 +73,7 @@ function InnerApp() {
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<LoggedOutViewProvider>
|
<LoggedOutViewProvider>
|
||||||
<UnreadNotifsProvider>
|
<UnreadNotifsProvider>
|
||||||
<ThemeProvider theme={colorMode}>
|
<ThemeProvider theme={theme}>
|
||||||
{/* All components should be within this provider */}
|
{/* All components should be within this provider */}
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import 'view/icons'
|
||||||
import {ThemeProvider as Alf} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import {useColorMode} from 'state/shell'
|
|
||||||
import {Shell} from 'view/shell/index'
|
import {Shell} from 'view/shell/index'
|
||||||
import {ToastContainer} from 'view/com/util/Toast.web'
|
import {ToastContainer} from 'view/com/util/Toast.web'
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
import {ThemeProvider} from 'lib/ThemeContext'
|
||||||
|
@ -36,8 +35,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const {isInitialLoad, currentAccount} = useSession()
|
const {isInitialLoad, currentAccount} = useSession()
|
||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
const colorMode = useColorMode()
|
const theme = useColorModeTheme()
|
||||||
const theme = useColorModeTheme(colorMode)
|
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -55,7 +53,7 @@ function InnerApp() {
|
||||||
key={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<LoggedOutViewProvider>
|
<LoggedOutViewProvider>
|
||||||
<UnreadNotifsProvider>
|
<UnreadNotifsProvider>
|
||||||
<ThemeProvider theme={colorMode}>
|
<ThemeProvider theme={theme}>
|
||||||
{/* All components should be within this provider */}
|
{/* All components should be within this provider */}
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import Svg, {Path, SvgProps} from 'react-native-svg'
|
import Svg, {Path, SvgProps} from 'react-native-svg'
|
||||||
|
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useColorMode} from '#/state/shell'
|
import {useThemePrefs} from 'state/shell'
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
import {Logotype} from '#/view/icons/Logotype'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -75,7 +75,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||||
isLayoutReady &&
|
isLayoutReady &&
|
||||||
reduceMotion !== undefined
|
reduceMotion !== undefined
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const {colorMode} = useThemePrefs()
|
||||||
const colorScheme = useColorScheme()
|
const colorScheme = useColorScheme()
|
||||||
const themeName = colorMode === 'system' ? colorScheme : colorMode
|
const themeName = colorMode === 'system' ? colorScheme : colorMode
|
||||||
const isDarkMode = themeName === 'dark'
|
const isDarkMode = themeName === 'dark'
|
||||||
|
|
|
@ -71,7 +71,7 @@ export const lightPalette = {
|
||||||
|
|
||||||
export const darkPalette: Palette = {
|
export const darkPalette: Palette = {
|
||||||
white: tokens.color.gray_0,
|
white: tokens.color.gray_0,
|
||||||
black: tokens.color.gray_1000,
|
black: tokens.color.trueBlack,
|
||||||
|
|
||||||
contrast_25: tokens.color.gray_975,
|
contrast_25: tokens.color.gray_975,
|
||||||
contrast_50: tokens.color.gray_950,
|
contrast_50: tokens.color.gray_950,
|
||||||
|
@ -130,6 +130,11 @@ export const darkPalette: Palette = {
|
||||||
negative_975: tokens.color.red_975,
|
negative_975: tokens.color.red_975,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const dimPalette: Palette = {
|
||||||
|
...darkPalette,
|
||||||
|
black: tokens.color.gray_1000,
|
||||||
|
} as const
|
||||||
|
|
||||||
export const light = {
|
export const light = {
|
||||||
name: 'light',
|
name: 'light',
|
||||||
palette: lightPalette,
|
palette: lightPalette,
|
||||||
|
@ -191,70 +196,6 @@ export const light = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dim: Theme = {
|
|
||||||
name: 'dim',
|
|
||||||
palette: darkPalette,
|
|
||||||
atoms: {
|
|
||||||
text: {
|
|
||||||
color: darkPalette.white,
|
|
||||||
},
|
|
||||||
text_contrast_700: {
|
|
||||||
color: darkPalette.contrast_800,
|
|
||||||
},
|
|
||||||
text_contrast_600: {
|
|
||||||
color: darkPalette.contrast_700,
|
|
||||||
},
|
|
||||||
text_contrast_500: {
|
|
||||||
color: darkPalette.contrast_600,
|
|
||||||
},
|
|
||||||
text_contrast_400: {
|
|
||||||
color: darkPalette.contrast_500,
|
|
||||||
},
|
|
||||||
text_inverted: {
|
|
||||||
color: darkPalette.black,
|
|
||||||
},
|
|
||||||
bg: {
|
|
||||||
backgroundColor: darkPalette.contrast_50,
|
|
||||||
},
|
|
||||||
bg_contrast_25: {
|
|
||||||
backgroundColor: darkPalette.contrast_100,
|
|
||||||
},
|
|
||||||
bg_contrast_50: {
|
|
||||||
backgroundColor: darkPalette.contrast_200,
|
|
||||||
},
|
|
||||||
bg_contrast_100: {
|
|
||||||
backgroundColor: darkPalette.contrast_300,
|
|
||||||
},
|
|
||||||
bg_contrast_200: {
|
|
||||||
backgroundColor: darkPalette.contrast_400,
|
|
||||||
},
|
|
||||||
bg_contrast_300: {
|
|
||||||
backgroundColor: darkPalette.contrast_500,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
borderColor: darkPalette.contrast_200,
|
|
||||||
},
|
|
||||||
border_contrast: {
|
|
||||||
borderColor: darkPalette.contrast_400,
|
|
||||||
},
|
|
||||||
shadow_sm: {
|
|
||||||
...atoms.shadow_sm,
|
|
||||||
shadowOpacity: 0.7,
|
|
||||||
shadowColor: tokens.color.trueBlack,
|
|
||||||
},
|
|
||||||
shadow_md: {
|
|
||||||
...atoms.shadow_md,
|
|
||||||
shadowOpacity: 0.7,
|
|
||||||
shadowColor: tokens.color.trueBlack,
|
|
||||||
},
|
|
||||||
shadow_lg: {
|
|
||||||
...atoms.shadow_lg,
|
|
||||||
shadowOpacity: 0.7,
|
|
||||||
shadowColor: tokens.color.trueBlack,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dark: Theme = {
|
export const dark: Theme = {
|
||||||
name: 'dark',
|
name: 'dark',
|
||||||
palette: darkPalette,
|
palette: darkPalette,
|
||||||
|
@ -318,3 +259,17 @@ export const dark: Theme = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dim: Theme = {
|
||||||
|
...dark,
|
||||||
|
name: 'dim',
|
||||||
|
atoms: {
|
||||||
|
...dark.atoms,
|
||||||
|
text_inverted: {
|
||||||
|
color: dimPalette.black,
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
backgroundColor: dimPalette.black,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,54 @@
|
||||||
|
import React from 'react'
|
||||||
import {useColorScheme} from 'react-native'
|
import {useColorScheme} from 'react-native'
|
||||||
|
|
||||||
import * as persisted from '#/state/persisted'
|
import {useThemePrefs} from 'state/shell'
|
||||||
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {ThemeName, light, dark, dim} from '#/alf/themes'
|
||||||
|
import * as SystemUI from 'expo-system-ui'
|
||||||
|
|
||||||
export function useColorModeTheme(
|
export function useColorModeTheme(): ThemeName {
|
||||||
theme: persisted.Schema['colorMode'],
|
|
||||||
): 'light' | 'dark' {
|
|
||||||
const colorScheme = useColorScheme()
|
const colorScheme = useColorScheme()
|
||||||
return (theme === 'system' ? colorScheme : theme) || 'light'
|
const {colorMode, darkTheme} = useThemePrefs()
|
||||||
|
|
||||||
|
return React.useMemo(() => {
|
||||||
|
if (
|
||||||
|
(colorMode === 'system' && colorScheme === 'light') ||
|
||||||
|
colorMode === 'light'
|
||||||
|
) {
|
||||||
|
updateDocument('light')
|
||||||
|
updateSystemBackground('light')
|
||||||
|
return 'light'
|
||||||
|
} else {
|
||||||
|
const themeName = darkTheme ?? 'dim'
|
||||||
|
updateDocument(themeName)
|
||||||
|
updateSystemBackground(themeName)
|
||||||
|
return themeName
|
||||||
|
}
|
||||||
|
}, [colorMode, darkTheme, colorScheme])
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDocument(theme: ThemeName) {
|
||||||
|
// @ts-ignore web only
|
||||||
|
if (isWeb && typeof window !== 'undefined') {
|
||||||
|
// @ts-ignore web only
|
||||||
|
const html = window.document.documentElement
|
||||||
|
// remove any other color mode classes
|
||||||
|
html.className = html.className.replace(/(theme)--\w+/g, '')
|
||||||
|
|
||||||
|
html.classList.add(`theme--${theme}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSystemBackground(theme: ThemeName) {
|
||||||
|
switch (theme) {
|
||||||
|
case 'light':
|
||||||
|
SystemUI.setBackgroundColorAsync(light.atoms.bg.backgroundColor)
|
||||||
|
break
|
||||||
|
case 'dark':
|
||||||
|
SystemUI.setBackgroundColorAsync(dark.atoms.bg.backgroundColor)
|
||||||
|
break
|
||||||
|
case 'dim':
|
||||||
|
SystemUI.setBackgroundColorAsync(dim.atoms.bg.backgroundColor)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import React, {ReactNode, createContext, useContext} from 'react'
|
import React, {ReactNode, createContext, useContext} from 'react'
|
||||||
import {
|
import {TextStyle, ViewStyle} from 'react-native'
|
||||||
TextStyle,
|
import {darkTheme, defaultTheme, dimTheme} from './themes'
|
||||||
useColorScheme,
|
import {ThemeName} from '#/alf/themes'
|
||||||
ViewStyle,
|
|
||||||
ColorSchemeName,
|
|
||||||
} from 'react-native'
|
|
||||||
import {darkTheme, defaultTheme} from './themes'
|
|
||||||
|
|
||||||
export type ColorScheme = 'light' | 'dark'
|
export type ColorScheme = 'light' | 'dark'
|
||||||
|
|
||||||
|
@ -84,23 +80,31 @@ export interface Theme {
|
||||||
|
|
||||||
export interface ThemeProviderProps {
|
export interface ThemeProviderProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
theme?: 'light' | 'dark' | 'system'
|
theme: ThemeName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeContext = createContext<Theme>(defaultTheme)
|
export const ThemeContext = createContext<Theme>(defaultTheme)
|
||||||
|
|
||||||
export const useTheme = () => useContext(ThemeContext)
|
export const useTheme = () => useContext(ThemeContext)
|
||||||
|
|
||||||
function getTheme(theme: ColorSchemeName) {
|
function getTheme(theme: ThemeName) {
|
||||||
return theme === 'dark' ? darkTheme : defaultTheme
|
switch (theme) {
|
||||||
|
case 'light':
|
||||||
|
return defaultTheme
|
||||||
|
case 'dim':
|
||||||
|
return dimTheme
|
||||||
|
case 'dark':
|
||||||
|
return darkTheme
|
||||||
|
default:
|
||||||
|
return defaultTheme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
||||||
theme,
|
theme,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const colorScheme = useColorScheme()
|
const themeValue = getTheme(theme)
|
||||||
const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
|
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Platform} from 'react-native'
|
||||||
import type {Theme} from './ThemeContext'
|
import type {Theme} from './ThemeContext'
|
||||||
import {colors} from './styles'
|
import {colors} from './styles'
|
||||||
|
|
||||||
import {darkPalette, lightPalette} from '#/alf/themes'
|
import {darkPalette, lightPalette, dimPalette} from '#/alf/themes'
|
||||||
|
|
||||||
export const defaultTheme: Theme = {
|
export const defaultTheme: Theme = {
|
||||||
colorScheme: 'light',
|
colorScheme: 'light',
|
||||||
|
@ -336,3 +336,14 @@ export const darkTheme: Theme = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dimTheme: Theme = {
|
||||||
|
...darkTheme,
|
||||||
|
palette: {
|
||||||
|
...darkTheme.palette,
|
||||||
|
default: {
|
||||||
|
...darkTheme.palette.default,
|
||||||
|
background: dimPalette.black,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
|
||||||
export function transform(legacy: Partial<LegacySchema>): Schema {
|
export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||||
return {
|
return {
|
||||||
colorMode: legacy.shell?.colorMode || defaults.colorMode,
|
colorMode: legacy.shell?.colorMode || defaults.colorMode,
|
||||||
|
darkTheme: defaults.darkTheme,
|
||||||
session: {
|
session: {
|
||||||
accounts: legacy.session?.accounts || defaults.session.accounts,
|
accounts: legacy.session?.accounts || defaults.session.accounts,
|
||||||
currentAccount:
|
currentAccount:
|
||||||
|
|
|
@ -18,6 +18,7 @@ export type PersistedAccount = z.infer<typeof accountSchema>
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
colorMode: z.enum(['system', 'light', 'dark']),
|
colorMode: z.enum(['system', 'light', 'dark']),
|
||||||
|
darkTheme: z.enum(['dim', 'dark']).optional(),
|
||||||
session: z.object({
|
session: z.object({
|
||||||
accounts: z.array(accountSchema),
|
accounts: z.array(accountSchema),
|
||||||
currentAccount: accountSchema.optional(),
|
currentAccount: accountSchema.optional(),
|
||||||
|
@ -60,6 +61,7 @@ export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
export const defaults: Schema = {
|
export const defaults: Schema = {
|
||||||
colorMode: 'system',
|
colorMode: 'system',
|
||||||
|
darkTheme: 'dim',
|
||||||
session: {
|
session: {
|
||||||
accounts: [],
|
accounts: [],
|
||||||
currentAccount: undefined,
|
currentAccount: undefined,
|
||||||
|
|
|
@ -1,57 +1,65 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
type StateContext = persisted.Schema['colorMode']
|
type StateContext = {
|
||||||
type SetContext = (v: persisted.Schema['colorMode']) => void
|
colorMode: persisted.Schema['colorMode']
|
||||||
|
darkTheme: persisted.Schema['darkTheme']
|
||||||
|
}
|
||||||
|
type SetContext = {
|
||||||
|
setColorMode: (v: persisted.Schema['colorMode']) => void
|
||||||
|
setDarkTheme: (v: persisted.Schema['darkTheme']) => void
|
||||||
|
}
|
||||||
|
|
||||||
const stateContext = React.createContext<StateContext>('system')
|
const stateContext = React.createContext<StateContext>({
|
||||||
const setContext = React.createContext<SetContext>(
|
colorMode: 'system',
|
||||||
(_: persisted.Schema['colorMode']) => {},
|
darkTheme: 'dark',
|
||||||
)
|
})
|
||||||
|
const setContext = React.createContext<SetContext>({} as SetContext)
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const [state, setState] = React.useState(persisted.get('colorMode'))
|
const [colorMode, setColorMode] = React.useState(persisted.get('colorMode'))
|
||||||
|
const [darkTheme, setDarkTheme] = React.useState(persisted.get('darkTheme'))
|
||||||
|
|
||||||
const setStateWrapped = React.useCallback(
|
const setColorModeWrapped = React.useCallback(
|
||||||
(colorMode: persisted.Schema['colorMode']) => {
|
(_colorMode: persisted.Schema['colorMode']) => {
|
||||||
setState(colorMode)
|
setColorMode(_colorMode)
|
||||||
persisted.write('colorMode', colorMode)
|
persisted.write('colorMode', _colorMode)
|
||||||
updateDocument(colorMode)
|
|
||||||
},
|
},
|
||||||
[setState],
|
[setColorMode],
|
||||||
|
)
|
||||||
|
|
||||||
|
const setDarkThemeWrapped = React.useCallback(
|
||||||
|
(_darkTheme: persisted.Schema['darkTheme']) => {
|
||||||
|
setDarkTheme(_darkTheme)
|
||||||
|
persisted.write('darkTheme', _darkTheme)
|
||||||
|
},
|
||||||
|
[setDarkTheme],
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
updateDocument(persisted.get('colorMode')) // set on load
|
|
||||||
return persisted.onUpdate(() => {
|
return persisted.onUpdate(() => {
|
||||||
setState(persisted.get('colorMode'))
|
setColorModeWrapped(persisted.get('colorMode'))
|
||||||
updateDocument(persisted.get('colorMode'))
|
setDarkThemeWrapped(persisted.get('darkTheme'))
|
||||||
})
|
})
|
||||||
}, [setState])
|
}, [setColorModeWrapped, setDarkThemeWrapped])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<stateContext.Provider value={state}>
|
<stateContext.Provider value={{colorMode, darkTheme}}>
|
||||||
<setContext.Provider value={setStateWrapped}>
|
<setContext.Provider
|
||||||
|
value={{
|
||||||
|
setDarkTheme: setDarkThemeWrapped,
|
||||||
|
setColorMode: setColorModeWrapped,
|
||||||
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</setContext.Provider>
|
</setContext.Provider>
|
||||||
</stateContext.Provider>
|
</stateContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useColorMode() {
|
export function useThemePrefs() {
|
||||||
return React.useContext(stateContext)
|
return React.useContext(stateContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSetColorMode() {
|
export function useSetThemePrefs() {
|
||||||
return React.useContext(setContext)
|
return React.useContext(setContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDocument(colorMode: string) {
|
|
||||||
if (isWeb && typeof window !== 'undefined') {
|
|
||||||
const html = window.document.documentElement
|
|
||||||
// remove any other color mode classes
|
|
||||||
html.className = html.className.replace(/colorMode--\w+/g, '')
|
|
||||||
html.classList.add(`colorMode--${colorMode}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export {
|
||||||
useSetDrawerSwipeDisabled,
|
useSetDrawerSwipeDisabled,
|
||||||
} from './drawer-swipe-disabled'
|
} from './drawer-swipe-disabled'
|
||||||
export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
|
export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
|
||||||
export {useColorMode, useSetColorMode} from './color-mode'
|
export {useThemePrefs, useSetThemePrefs} from './color-mode'
|
||||||
export {useOnboardingState, useOnboardingDispatch} from './onboarding'
|
export {useOnboardingState, useOnboardingDispatch} from './onboarding'
|
||||||
export {useComposerState, useComposerControls} from './composer'
|
export {useComposerState, useComposerControls} from './composer'
|
||||||
export {useTickEveryMinute} from './tick-every-minute'
|
export {useTickEveryMinute} from './tick-every-minute'
|
||||||
|
|
|
@ -40,8 +40,8 @@ import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {
|
||||||
useSetMinimalShellMode,
|
useSetMinimalShellMode,
|
||||||
useColorMode,
|
useThemePrefs,
|
||||||
useSetColorMode,
|
useSetThemePrefs,
|
||||||
useOnboardingDispatch,
|
useOnboardingDispatch,
|
||||||
} from '#/state/shell'
|
} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
|
@ -144,8 +144,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||||
export function SettingsScreen({}: Props) {
|
export function SettingsScreen({}: Props) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const colorMode = useColorMode()
|
const {colorMode, darkTheme} = useThemePrefs()
|
||||||
const setColorMode = useSetColorMode()
|
const {setColorMode, setDarkTheme} = useSetThemePrefs()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
@ -483,8 +483,36 @@ export function SettingsScreen({}: Props) {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.spacer20} />
|
<View style={styles.spacer20} />
|
||||||
|
|
||||||
|
{colorMode !== 'light' && (
|
||||||
|
<>
|
||||||
|
<Text type="xl-bold" style={[pal.text, styles.heading]}>
|
||||||
|
<Trans>Dark Theme</Trans>
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
<View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
|
||||||
|
<SelectableBtn
|
||||||
|
selected={!darkTheme || darkTheme === 'dim'}
|
||||||
|
label={_(msg`Dim`)}
|
||||||
|
left
|
||||||
|
onSelect={() => setDarkTheme('dim')}
|
||||||
|
accessibilityHint={_(msg`Set dark theme to the dim theme`)}
|
||||||
|
/>
|
||||||
|
<SelectableBtn
|
||||||
|
selected={darkTheme === 'dark'}
|
||||||
|
label={_(msg`Dark`)}
|
||||||
|
right
|
||||||
|
onSelect={() => setDarkTheme('dark')}
|
||||||
|
accessibilityHint={_(msg`Set dark theme to the dark theme`)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.spacer20} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text type="xl-bold" style={[pal.text, styles.heading]}>
|
<Text type="xl-bold" style={[pal.text, styles.heading]}>
|
||||||
<Trans>Basics</Trans>
|
<Trans>Basics</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
|
|
||||||
import {atoms as a, useTheme, ThemeProvider} from '#/alf'
|
import {atoms as a, useTheme, ThemeProvider} from '#/alf'
|
||||||
import {useSetColorMode} from '#/state/shell'
|
import {useSetThemePrefs} from '#/state/shell'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
|
|
||||||
import {Theming} from './Theming'
|
import {Theming} from './Theming'
|
||||||
|
@ -19,7 +19,7 @@ import {Icons} from './Icons'
|
||||||
|
|
||||||
export function Storybook() {
|
export function Storybook() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const setColorMode = useSetColorMode()
|
const {setColorMode, setDarkTheme} = useSetThemePrefs()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
@ -38,7 +38,7 @@ export function Storybook() {
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
label='Set theme to "system"'
|
label='Set theme to "light"'
|
||||||
onPress={() => setColorMode('light')}>
|
onPress={() => setColorMode('light')}>
|
||||||
Light
|
Light
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -46,8 +46,22 @@ export function Storybook() {
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
label='Set theme to "system"'
|
label='Set theme to "dim"'
|
||||||
onPress={() => setColorMode('dark')}>
|
onPress={() => {
|
||||||
|
setColorMode('dark')
|
||||||
|
setDarkTheme('dim')
|
||||||
|
}}>
|
||||||
|
Dim
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
label='Set theme to "dark"'
|
||||||
|
onPress={() => {
|
||||||
|
setColorMode('dark')
|
||||||
|
setDarkTheme('dark')
|
||||||
|
}}>
|
||||||
Dark
|
Dark
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<!--
|
<!--
|
||||||
This viewport works for phones with notches.
|
This viewport works for phones with notches.
|
||||||
It's optimized for gestures by disabling global zoom.
|
It's optimized for gestures by disabling global zoom.
|
||||||
-->
|
-->
|
||||||
|
@ -44,31 +44,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color theming */
|
/* Color theming */
|
||||||
|
/* Default will always be white */
|
||||||
:root {
|
:root {
|
||||||
--text: black;
|
|
||||||
--background: white;
|
|
||||||
--backgroundLight: hsl(211, 20%, 95%);
|
|
||||||
}
|
|
||||||
html.colorMode--dark {
|
|
||||||
--text: white;
|
|
||||||
--background: hsl(211, 20%, 4%);
|
|
||||||
--backgroundLight: hsl(211, 20%, 20%);
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
html.colorMode--system {
|
|
||||||
--text: black;
|
--text: black;
|
||||||
--background: white;
|
--background: white;
|
||||||
--backgroundLight: hsl(211, 20%, 95%);
|
--backgroundLight: hsl(211, 20%, 95%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/* This gives us a black background when system is dark and we have not loaded the theme/color scheme values in JS */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html.colorMode--system {
|
:root {
|
||||||
|
--text: white;
|
||||||
|
--background: black;
|
||||||
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overwrite those preferences with the selected theme */
|
||||||
|
html.theme--light {
|
||||||
|
--text: black;
|
||||||
|
--background: white;
|
||||||
|
--backgroundLight: hsl(211, 20%, 95%);
|
||||||
|
}
|
||||||
|
html.theme--dark {
|
||||||
--text: white;
|
--text: white;
|
||||||
--background: hsl(211, 20%, 4%);
|
--background: black;
|
||||||
--backgroundLight: hsl(211, 20%, 20%);
|
--backgroundLight: hsl(211, 20%, 20%);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
html.theme--dim {
|
||||||
|
--text: white;
|
||||||
|
--background: hsl(211, 20%, 4%);
|
||||||
|
--backgroundLight: hsl(211, 20%, 10%);
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove autofill styles on Webkit */
|
/* Remove autofill styles on Webkit */
|
||||||
|
@ -142,7 +150,7 @@
|
||||||
.ProseMirror .mention {
|
.ProseMirror .mention {
|
||||||
color: #0085ff;
|
color: #0085ff;
|
||||||
}
|
}
|
||||||
.ProseMirror a,
|
.ProseMirror a,
|
||||||
.ProseMirror .autolink {
|
.ProseMirror .autolink {
|
||||||
color: #0085ff;
|
color: #0085ff;
|
||||||
}
|
}
|
||||||
|
@ -200,7 +208,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!--
|
<!--
|
||||||
A generic no script element with a reload button and a message.
|
A generic no script element with a reload button and a message.
|
||||||
Feel free to customize this however you'd like.
|
Feel free to customize this however you'd like.
|
||||||
-->
|
-->
|
||||||
|
|
Loading…
Reference in New Issue