Add color presets with light/dark/OLED per preset (#956)

* These changes include adding an Unplayed filter and also fixing shuffle all to shuffle based on the filtered selection

* Added genre filter and selection page

* Forgot to hit save on this file

* Fixed bug affecting genre filtering

* Shuffle all query now includes the genres

* Fixed trigger to triggerHaptic

* Fixed trigger to triggerHaptic

* Added optional color themes

* Slight adjustment to some dark color presets

---------

Co-authored-by: StephenArg <stephen@vody.com>
Co-authored-by: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com>
This commit is contained in:
Stephen Arg
2026-02-01 19:15:49 +01:00
committed by GitHub
parent 1c0069e1df
commit bd63fc51de
13 changed files with 532 additions and 157 deletions
+5 -13
View File
@@ -17,14 +17,14 @@ import TrackPlayer, {
import { CAPABILITIES } from './src/constants/player'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { NavigationContainer } from '@react-navigation/native'
import { JellifyDarkTheme, JellifyLightTheme, JellifyOLEDTheme } from './src/components/theme'
import { getJellifyNavTheme } from './src/components/theme'
import { requestStoragePermission } from './src/utils/permisson-helpers'
import ErrorBoundary from './src/components/ErrorBoundary'
import OTAUpdateScreen from './src/components/OtaUpdates'
import { usePerformanceMonitor } from './src/hooks/use-performance-monitor'
import navigationRef from './navigation'
import { BUFFERS, PROGRESS_UPDATE_EVENT_INTERVAL } from './src/configs/player.config'
import { useThemeSetting } from './src/stores/settings/app'
import { useColorPresetSetting, useThemeSetting } from './src/stores/settings/app'
import { getApi } from './src/stores'
import CarPlayNavigation from './src/components/CarPlay/Navigation'
import { CarPlay } from 'react-native-carplay'
@@ -124,23 +124,15 @@ export default function App(): React.JSX.Element {
function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Element {
const [theme] = useThemeSetting()
const [colorPreset] = useColorPresetSetting()
const isDarkMode = useColorScheme() === 'dark'
const resolvedMode = theme === 'system' ? (isDarkMode ? 'dark' : 'light') : theme
return (
<NavigationContainer
ref={navigationRef}
theme={
theme === 'system'
? isDarkMode
? JellifyDarkTheme
: JellifyLightTheme
: theme === 'dark'
? JellifyDarkTheme
: theme === 'oled'
? JellifyOLEDTheme
: JellifyLightTheme
}
theme={getJellifyNavTheme(colorPreset, resolvedMode)}
>
<GestureHandlerRootView>
<TamaguiProvider config={jellifyConfig}>
+11 -7
View File
@@ -64,6 +64,16 @@ export default function Icon({
const pressStyle = animation ? { opacity: 0.6 } : undefined
// Tamagui theme keys are unprefixed (e.g. "primary" not "$primary"); resolve for token strings
const themeColorKey =
color && typeof color === 'string' && color.startsWith('$') ? color.slice(1) : color
const resolvedColor =
color && !disabled
? (theme[themeColorKey as keyof typeof theme]?.val ?? theme.color.val)
: disabled
? theme.neutral.val
: theme.color.val
return (
<YStack
animation={animation}
@@ -76,13 +86,7 @@ export default function Icon({
flex={flex}
>
<MaterialDesignIcon
color={
color && !disabled
? theme[color]?.val
: disabled
? theme.neutral.val
: theme.color.val
}
color={resolvedColor}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name={name as any}
size={size}
@@ -1,4 +1,4 @@
import { SizeTokens, XStack, Separator, Switch, styled, getToken } from 'tamagui'
import { SizeTokens, XStack, Separator, Switch, styled } from 'tamagui'
import { Label } from './text'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
@@ -10,9 +10,10 @@ interface SwitchWithLabelProps {
width?: number | undefined
}
// Use theme tokens so thumb colors follow the active color preset
const JellifySliderThumb = styled(Switch.Thumb, {
borderColor: getToken('$color.amethyst'),
backgroundColor: getToken('$color.purpleDark'),
borderColor: '$primary',
backgroundColor: '$background',
})
export function SwitchWithLabel(props: SwitchWithLabelProps) {
@@ -1,5 +1,5 @@
import React from 'react'
import { getToken, useTheme, View, YStack, ZStack } from 'tamagui'
import { useTheme, View, YStack, ZStack } from 'tamagui'
import { useWindowDimensions } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import { getBlurhashFromDto } from '../../../utils/parsing/blurhash'
@@ -18,13 +18,13 @@ export default function BlurredBackground(): React.JSX.Element {
// Get blurhash safely
const blurhash = nowPlaying?.item ? getBlurhashFromDto(nowPlaying.item) : null
// Define gradient colors
const darkGradientColors = [getToken('$black'), getToken('$black25')]
// Use theme colors so the gradient follows the active color preset
const darkGradientColors = [theme.background.val, theme.background25.val]
const darkGradientColors2 = [
getToken('$black25'),
getToken('$black75'),
getToken('$black'),
getToken('$black'),
theme.background25.val,
theme.background75.val,
theme.background.val,
theme.background.val,
]
// Define styles
+9 -4
View File
@@ -1,5 +1,5 @@
import React from 'react'
import { Progress, XStack, YStack } from 'tamagui'
import { Progress, useTheme, XStack, YStack } from 'tamagui'
import { useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
import TextTicker from 'react-native-text-ticker'
@@ -30,6 +30,7 @@ export default function Miniplayer(): React.JSX.Element {
const nowPlaying = useCurrentTrack()
const skip = useSkip()
const previous = usePrevious()
const theme = useTheme()
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
@@ -90,7 +91,7 @@ export default function Miniplayer(): React.JSX.Element {
pressStyle={pressStyle}
animation={'quick'}
onPress={openPlayer}
backgroundColor='$background'
backgroundColor={theme.background.val}
>
<MiniPlayerProgress />
<XStack alignItems='center' padding={'$2'}>
@@ -144,15 +145,19 @@ export default function Miniplayer(): React.JSX.Element {
function MiniPlayerProgress(): React.JSX.Element {
const progress = useProgress(UPDATE_INTERVAL)
const theme = useTheme()
return (
<Progress
height={'$0.25'}
value={calculateProgressPercentage(progress)}
backgroundColor={'$borderColor'}
backgroundColor={theme.borderColor.val}
borderBottomEndRadius={'$2'}
>
<Progress.Indicator borderColor={'$primary'} backgroundColor={'$primary'} />
<Progress.Indicator
borderColor={theme.primary.val}
backgroundColor={theme.primary.val}
/>
</Progress>
)
}
+10 -1
View File
@@ -1,6 +1,8 @@
import React, { Suspense, lazy } from 'react'
import { useColorScheme } from 'react-native'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import { getToken, getTokenValue, useTheme, Spinner, YStack } from 'tamagui'
import { getTokenValue, useTheme, Spinner, YStack } from 'tamagui'
import { useColorPresetSetting, useThemeSetting } from '../../stores/settings/app'
import SettingsTabBar from './tab-bar'
// Lazy load tab components to improve initial render
@@ -63,9 +65,16 @@ function LazyInfoTab() {
export default function Settings(): React.JSX.Element {
const theme = useTheme()
const [themeSetting] = useThemeSetting()
const [colorPreset] = useColorPresetSetting()
const isDarkMode = useColorScheme() === 'dark'
const resolvedMode = themeSetting === 'system' ? (isDarkMode ? 'dark' : 'light') : themeSetting
// Key forces navigator to remount when preset/mode changes so tab bar colors update
const themeKey = `${colorPreset}_${resolvedMode}`
return (
<SettingsTabsNavigator.Navigator
key={themeKey}
screenOptions={{
tabBarIndicatorStyle: {
borderColor: theme.background.val,
@@ -2,7 +2,9 @@ import { YStack, XStack, Paragraph, SizableText } from 'tamagui'
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
import SettingsListGroup from './settings-list-group'
import {
ColorPreset,
ThemeSetting,
useColorPresetSetting,
useHideRunTimesSetting,
useReducedHapticsSetting,
useSendMetricsSetting,
@@ -18,6 +20,12 @@ type ThemeOptionConfig = {
icon: string
}
type ColorPresetOptionConfig = {
value: ColorPreset
label: string
icon: string
}
const THEME_OPTIONS: ThemeOptionConfig[] = [
{
value: 'system',
@@ -41,6 +49,34 @@ const THEME_OPTIONS: ThemeOptionConfig[] = [
},
]
const COLOR_PRESET_OPTIONS: ColorPresetOptionConfig[] = [
{
value: 'purple',
label: 'Purple',
icon: 'crown',
},
{
value: 'ocean',
label: 'Ocean',
icon: 'waves',
},
{
value: 'forest',
label: 'Forest',
icon: 'forest',
},
{
value: 'sunset',
label: 'Sunset',
icon: 'weather-sunset',
},
{
value: 'peanut',
label: 'Peanut',
icon: 'peanut',
},
]
function ActionChip({
active,
label,
@@ -113,6 +149,42 @@ function ThemeOptionCard({
)
}
function ColorPresetOptionCard({
option,
isSelected,
onPress,
}: {
option: ColorPresetOptionConfig
isSelected: boolean
onPress: () => void
}) {
return (
<YStack
onPress={onPress}
pressStyle={{ scale: 0.97 }}
animation='quick'
borderWidth={'$1'}
borderColor={isSelected ? '$primary' : '$borderColor'}
backgroundColor={isSelected ? '$background25' : '$background'}
borderRadius={'$9'}
padding='$3'
gap='$2'
hitSlop={8}
role='button'
aria-label={`${option.label} color preset option`}
aria-selected={isSelected}
>
<XStack alignItems='center' gap='$2'>
<Icon small name={option.icon} color={isSelected ? '$primary' : '$borderColor'} />
<SizableText size={'$4'} flex={1} fontWeight='600'>
{option.label}
</SizableText>
{isSelected && <Icon small name='check-circle-outline' color={'$primary'} />}
</XStack>
</YStack>
)
}
function getThemeSubtitle(themeSetting: ThemeSetting): string {
switch (themeSetting) {
case 'light':
@@ -126,10 +198,28 @@ function getThemeSubtitle(themeSetting: ThemeSetting): string {
}
}
function getColorPresetSubtitle(colorPreset: ColorPreset): string {
switch (colorPreset) {
case 'purple':
return 'Purple vibes'
case 'ocean':
return 'Oceanic vibes'
case 'forest':
return 'Foresty vibes'
case 'sunset':
return 'Sunset vibes'
case 'peanut':
return 'Sandbox vibes'
default:
return 'Default vibes'
}
}
export default function PreferencesTab(): React.JSX.Element {
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
const [reducedHaptics, setReducedHaptics] = useReducedHapticsSetting()
const [themeSetting, setThemeSetting] = useThemeSetting()
const [colorPreset, setColorPreset] = useColorPresetSetting()
const [hideRunTimes, setHideRunTimes] = useHideRunTimesSetting()
const left = useSwipeSettingsStore((s) => s.left)
@@ -138,7 +228,7 @@ export default function PreferencesTab(): React.JSX.Element {
const toggleRight = useSwipeSettingsStore((s) => s.toggleRight)
const themeSubtitle = getThemeSubtitle(themeSetting)
const colorPresetSubtitle = getColorPresetSubtitle(colorPreset)
return (
<SettingsListGroup
settingsList={[
@@ -160,6 +250,24 @@ export default function PreferencesTab(): React.JSX.Element {
</YStack>
),
},
{
title: 'Color Preset',
subTitle: colorPresetSubtitle && `${colorPresetSubtitle}`,
iconName: 'palette',
iconColor: '$primary',
children: (
<YStack gap='$2' paddingVertical='$2'>
{COLOR_PRESET_OPTIONS.map((option) => (
<ColorPresetOptionCard
key={option.value}
option={option}
isSelected={colorPreset === option.value}
onPress={() => setColorPreset(option.value)}
/>
))}
</YStack>
),
},
{
title: 'Track Swipe Actions',
subTitle: 'Choose actions for left/right swipes',
+12 -3
View File
@@ -10,13 +10,17 @@ import {
} from '@typedigital/telemetrydeck-react'
import telemetryDeckConfig from '../../telemetrydeck.json'
import * as Sentry from '@sentry/react-native'
import { getToken, Theme, useTheme } from 'tamagui'
import { getToken, Theme, ThemeName, useTheme } from 'tamagui'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../configs/toast.config'
import { useColorScheme } from 'react-native'
import { StorageProvider } from '../providers/Storage'
import { useSelectPlayerEngine } from '../stores/player/engine'
import { useSendMetricsSetting, useThemeSetting } from '../stores/settings/app'
import {
useColorPresetSetting,
useSendMetricsSetting,
useThemeSetting,
} from '../stores/settings/app'
import { GLITCHTIP_DSN } from '../configs/config'
import useDownloadProcessor from '../hooks/use-download-processor'
/**
@@ -25,12 +29,17 @@ import useDownloadProcessor from '../hooks/use-download-processor'
*/
export default function Jellify(): React.JSX.Element {
const [theme] = useThemeSetting()
const [colorPreset] = useColorPresetSetting()
const isDarkMode = useColorScheme() === 'dark'
const resolvedMode = theme === 'system' ? (isDarkMode ? 'dark' : 'light') : theme
const themeName = `${colorPreset}_${resolvedMode}` // e.g. 'purple_dark'
useSelectPlayerEngine()
return (
<Theme name={theme === 'system' ? (isDarkMode ? 'dark' : 'light') : theme}>
<Theme name={themeName as ThemeName | null}>
<JellifyLoggingWrapper>
<DisplayProvider>
<App />
+32 -32
View File
@@ -1,5 +1,7 @@
import { DarkTheme, DefaultTheme } from '@react-navigation/native'
import { getToken, getTokens } from 'tamagui'
import type { Theme } from '@react-navigation/native'
import { PRESET_PALETTES } from '../../tamagui.config'
import type { ColorPreset } from '../stores/settings/app'
interface Fonts {
regular: FontStyle
@@ -43,38 +45,36 @@ const JellifyFonts: Fonts = {
},
}
export const JellifyDarkTheme: ReactNavigation.Theme = {
dark: true,
colors: {
...DarkTheme.colors,
card: getTokens().color.$darkBackground.val,
border: getTokens().color.$neutral.val,
background: getTokens().color.$darkBackground.val,
primary: getTokens().color.$primaryDark.val,
},
fonts: JellifyFonts,
function paletteToNavTheme(
palette: (typeof PRESET_PALETTES)['purple']['dark'],
dark: boolean,
): Theme {
const base = dark ? DarkTheme : DefaultTheme
return {
...base,
dark,
colors: {
...base.colors,
background: palette.background,
card: palette.background,
border: palette.borderColor,
primary: palette.primary,
},
fonts: JellifyFonts,
}
}
export const JellifyLightTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: getTokens().color.$primaryLight.val,
border: getTokens().color.$neutral.val,
background: getTokens().color.$white.val,
card: getTokens().color.$white.val,
},
fonts: JellifyFonts,
/** React Navigation theme for a given color preset and mode (purple_dark, ocean_light, etc.) */
export function getJellifyNavTheme(preset: ColorPreset, mode: 'light' | 'dark' | 'oled'): Theme {
const palette = PRESET_PALETTES[preset][mode]
return paletteToNavTheme(palette, mode !== 'light')
}
export const JellifyOLEDTheme: ReactNavigation.Theme = {
dark: true,
colors: {
...DarkTheme.colors,
card: getTokens().color.$black.val,
border: getTokens().color.$neutral.val,
background: getTokens().color.$black.val,
primary: getTokens().color.$primaryDark.val,
},
fonts: JellifyFonts,
}
/** Purple dark — matches Tamagui purple_dark (current JellifyDarkTheme) */
export const JellifyDarkTheme: Theme = paletteToNavTheme(PRESET_PALETTES.purple.dark, true)
/** Purple light — matches Tamagui purple_light (current JellifyLightTheme) */
export const JellifyLightTheme: Theme = paletteToNavTheme(PRESET_PALETTES.purple.light, false)
/** Purple oled — matches Tamagui purple_oled (current JellifyOLEDTheme) */
export const JellifyOLEDTheme: Theme = paletteToNavTheme(PRESET_PALETTES.purple.oled, true)
+1
View File
@@ -28,6 +28,7 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
animation: 'shift',
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.borderColor.val,
tabBarStyle: { backgroundColor: theme.background.val },
lazy: true,
}}
tabBar={(props) => <TabBar {...props} />}
+43 -3
View File
@@ -1,17 +1,57 @@
import React, { useMemo } from 'react'
import Miniplayer from '../../components/Player/mini-player'
import InternetConnectionWatcher from '../../components/Network/internetConnectionWatcher'
import { BottomTabBar, BottomTabBarProps } from '@react-navigation/bottom-tabs'
import useIsMiniPlayerActive from '../../hooks/use-mini-player'
import { useTheme } from 'tamagui'
export default function TabBar({ ...props }: BottomTabBarProps): React.JSX.Element {
/**
* Merge theme-driven tab bar options into the focused route's descriptor
* so the bar updates immediately when color preset changes (the navigator
* often does not re-pass updated screenOptions to the tab bar).
*/
export default function TabBar(props: BottomTabBarProps): React.JSX.Element {
const isMiniPlayerActive = useIsMiniPlayerActive()
const theme = useTheme()
const descriptorsWithTheme = useMemo(() => {
const focusedRoute = props.state.routes[props.state.index]
const focusedDescriptor = props.descriptors[focusedRoute.key]
if (!focusedDescriptor) return props.descriptors
return {
...props.descriptors,
[focusedRoute.key]: {
...focusedDescriptor,
options: {
...focusedDescriptor.options,
tabBarStyle: {
...focusedDescriptor.options.tabBarStyle,
backgroundColor: theme.background.val,
},
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.borderColor.val,
},
},
}
}, [
props.descriptors,
props.state.routes,
props.state.index,
theme.background.val,
theme.primary.val,
theme.borderColor.val,
])
// Key forces mini-player to remount when theme changes so colors update
// (avoids stale styles from Reanimated/Progress when preset changes without interaction)
const themeKey = `${theme.background.val}-${theme.primary.val}`
return (
<>
{isMiniPlayerActive && <Miniplayer />}
{isMiniPlayerActive && <Miniplayer key={themeKey} />}
<InternetConnectionWatcher />
<BottomTabBar {...props} />
<BottomTabBar {...props} descriptors={descriptorsWithTheme} />
</>
)
}
+19
View File
@@ -4,6 +4,7 @@ import { createJSONStorage, devtools, persist } from 'zustand/middleware'
import { useShallow } from 'zustand/react/shallow'
export type ThemeSetting = 'system' | 'light' | 'dark' | 'oled'
export type ColorPreset = 'purple' | 'ocean' | 'forest' | 'sunset' | 'peanut'
type AppSettingsStore = {
sendMetrics: boolean
@@ -17,6 +18,9 @@ type AppSettingsStore = {
theme: ThemeSetting
setTheme: (theme: ThemeSetting) => void
colorPreset: ColorPreset
setColorPreset: (colorPreset: ColorPreset) => void
}
export const useAppSettingsStore = create<AppSettingsStore>()(
@@ -34,6 +38,9 @@ export const useAppSettingsStore = create<AppSettingsStore>()(
theme: 'system',
setTheme: (theme: ThemeSetting) => set({ theme }),
colorPreset: 'purple',
setColorPreset: (colorPreset: ColorPreset) => set({ colorPreset }),
}),
{
name: 'app-settings-storage',
@@ -50,6 +57,18 @@ export const useThemeSetting: () => [ThemeSetting, (theme: ThemeSetting) => void
return [theme, setTheme]
}
export const useColorPresetSetting: () => [
ColorPreset,
(colorPreset: ColorPreset) => void,
] = () => {
const colorPreset = useAppSettingsStore((state) => state.colorPreset)
const setColorPreset = useAppSettingsStore((state) => state.setColorPreset)
return [colorPreset, setColorPreset]
}
export const useReducedHapticsSetting: () => [boolean, (reducedHaptics: boolean) => void] = () => {
const reducedHaptics = useAppSettingsStore((state) => state.reducedHaptics)
+270 -83
View File
@@ -51,6 +51,275 @@ const tokens = createTokens({
},
})
/** Theme mode palette: semantic keys used by Tamagui and React Navigation */
type PresetModePalette = {
background: string
background75: string
background50: string
background25: string
borderColor: string
color: string
success: string
secondary: string
primary: string
danger: string
warning: string
neutral: string
translucent: string
}
/** Palettes per preset (purple = current Jellify themes), for Tamagui + nav */
export const PRESET_PALETTES: Record<
'purple' | 'ocean' | 'forest' | 'sunset' | 'peanut',
{ light: PresetModePalette; dark: PresetModePalette; oled: PresetModePalette }
> = {
purple: {
// Matches current JellifyDarkTheme / JellifyLightTheme / JellifyOLEDTheme
dark: {
background: 'rgba(25, 24, 28, 1)',
background75: 'rgba(25, 24, 28, 0.75)',
background50: 'rgba(25, 24, 28, 0.5)',
background25: 'rgba(25, 24, 28, 0.25)',
borderColor: '#77748E',
color: '#ffffff',
success: 'rgba(87, 233, 201, 1)',
secondary: 'rgba(75, 125, 215, 1)',
primary: '#887BFF',
danger: '#FF066F',
warning: '#FF6625',
neutral: '#77748E',
translucent: 'rgba(0, 0, 0, 0.5)',
},
light: {
background: '#ffffff',
background75: 'rgba(235, 221, 255, 0.75)',
background50: 'rgba(235, 221, 255, 0.5)',
background25: 'rgba(235, 221, 255, 0.25)',
borderColor: '#77748E',
color: '#0C0622',
success: 'rgba(16, 175, 141, 1)',
secondary: 'rgba(0, 58, 159, 1)',
primary: '#4b0fd6ff',
danger: '#B30077',
warning: '#a93300ff',
neutral: '#77748E',
translucent: 'rgba(255, 255, 255, 0.75)',
},
oled: {
background: '#000000',
background75: 'rgba(0, 0, 0, 0.75)',
background50: 'rgba(0, 0, 0, 0.5)',
background25: 'rgba(0, 0, 0, 0.25)',
borderColor: '#77748E',
color: '#ffffff',
success: 'rgba(87, 233, 201, 1)',
secondary: 'rgba(75, 125, 215, 1)',
primary: '#887BFF',
danger: '#FF066F',
warning: '#FF6625',
neutral: '#77748E',
translucent: 'rgba(0, 0, 0, 0.5)',
},
},
ocean: {
dark: {
background: 'rgba(25, 24, 28, 1)',
background75: 'rgba(25, 24, 28, 0.75)',
background50: 'rgba(25, 24, 28, 0.5)',
background25: 'rgba(25, 24, 28, 0.25)',
borderColor: '#78909C',
color: '#ffffff',
success: '#4DD0E1',
secondary: '#81D4FA',
primary: '#4FC3F7',
danger: '#FF7043',
warning: '#FFB74D',
neutral: '#78909C',
translucent: 'rgba(0, 0, 0, 0.5)',
},
light: {
background: '#E1F5FE',
background75: 'rgba(225, 245, 254, 0.75)',
background50: 'rgba(225, 245, 254, 0.5)',
background25: 'rgba(225, 245, 254, 0.25)',
borderColor: '#546E7A',
color: '#01579B',
success: '#00838F',
secondary: '#0277BD',
primary: '#0288D1',
danger: '#D84315',
warning: '#EF6C00',
neutral: '#546E7A',
translucent: 'rgba(255, 255, 255, 0.75)',
},
oled: {
background: '#000000',
background75: 'rgba(0, 0, 0, 0.75)',
background50: 'rgba(0, 0, 0, 0.5)',
background25: 'rgba(0, 0, 0, 0.25)',
borderColor: '#78909C',
color: '#ffffff',
success: '#4DD0E1',
secondary: '#81D4FA',
primary: '#4FC3F7',
danger: '#FF7043',
warning: '#FFB74D',
neutral: '#78909C',
translucent: 'rgba(0, 0, 0, 0.5)',
},
},
forest: {
dark: {
background: 'rgb(35, 47, 35)',
background75: 'rgba(35, 47, 35, 0.75)',
background50: 'rgba(35, 47, 35, 0.5)',
background25: 'rgba(35, 47, 35, 0.25)',
borderColor: '#8D9E8C',
color: '#ffffff',
success: '#66BB6A',
secondary: '#9CCC65',
primary: 'rgb(56, 105, 56)',
danger: '#E57373',
warning: '#FFB74D',
neutral: '#8D9E8C',
translucent: 'rgba(0, 0, 0, 0.5)',
},
light: {
background: '#E8F5E9',
background75: 'rgba(232, 245, 233, 0.75)',
background50: 'rgba(232, 245, 233, 0.5)',
background25: 'rgba(232, 245, 233, 0.25)',
borderColor: '#558B2F',
color: '#1B5E20',
success: '#2E7D32',
secondary: '#43A047',
primary: 'rgb(14, 143, 21)',
danger: '#C62828',
warning: '#E65100',
neutral: '#558B2F',
translucent: 'rgba(255, 255, 255, 0.75)',
},
oled: {
background: '#000000',
background75: 'rgba(0, 0, 0, 0.75)',
background50: 'rgba(0, 0, 0, 0.5)',
background25: 'rgba(0, 0, 0, 0.25)',
borderColor: '#8D9E8C',
color: '#ffffff',
success: '#66BB6A',
secondary: '#9CCC65',
primary: 'rgb(11, 128, 17)',
danger: '#E57373',
warning: '#FFB74D',
neutral: '#8D9E8C',
translucent: 'rgba(0, 0, 0, 0.5)',
},
},
sunset: {
dark: {
background: 'rgb(52, 34, 28)',
background75: 'rgba(52, 34, 28, 0.75)',
background50: 'rgba(52, 34, 28, 0.5)',
background25: 'rgba(52, 34, 28, 0.25)',
borderColor: '#A1887F',
color: '#ffffff',
success: '#FFAB91',
secondary: '#FF8A65',
primary: '#FF7043',
danger: '#EF5350',
warning: '#FFCA28',
neutral: '#A1887F',
translucent: 'rgba(0, 0, 0, 0.5)',
},
light: {
background: '#FFF3E0',
background75: 'rgba(255, 243, 224, 0.75)',
background50: 'rgba(255, 243, 224, 0.5)',
background25: 'rgba(255, 243, 224, 0.25)',
borderColor: '#BF360C',
color: '#3E2723',
success: '#E64A19',
secondary: '#FF5722',
primary: '#FF5722',
danger: '#B71C1C',
warning: '#F57C00',
neutral: '#BF360C',
translucent: 'rgba(255, 255, 255, 0.75)',
},
oled: {
background: '#000000',
background75: 'rgba(0, 0, 0, 0.75)',
background50: 'rgba(0, 0, 0, 0.5)',
background25: 'rgba(0, 0, 0, 0.25)',
borderColor: '#A1887F',
color: '#ffffff',
success: '#FFAB91',
secondary: '#FF8A65',
primary: '#FF7043',
danger: '#EF5350',
warning: '#FFCA28',
neutral: '#A1887F',
translucent: 'rgba(0, 0, 0, 0.5)',
},
},
peanut: {
dark: {
background: 'rgba(62, 39, 22, 1)',
background75: 'rgba(62, 39, 22, 0.75)',
background50: 'rgba(62, 39, 22, 0.5)',
background25: 'rgba(62, 39, 22, 0.25)',
borderColor: '#BCAAA4',
color: '#ffffff',
success: '#D7CCC8',
secondary: '#A1887F',
primary: '#D7CCC8',
danger: '#8D6E63',
warning: '#FFAB91',
neutral: '#BCAAA4',
translucent: 'rgba(0, 0, 0, 0.5)',
},
light: {
background: '#EFEBE9',
background75: 'rgba(239, 235, 233, 0.75)',
background50: 'rgba(239, 235, 233, 0.5)',
background25: 'rgba(239, 235, 233, 0.25)',
borderColor: '#6D4C41',
color: '#3E2723',
success: '#5D4037',
secondary: '#795548',
primary: '#8D6E63',
danger: '#4E342E',
warning: '#BF360C',
neutral: '#6D4C41',
translucent: 'rgba(255, 255, 255, 0.75)',
},
oled: {
background: '#000000',
background75: 'rgba(0, 0, 0, 0.75)',
background50: 'rgba(0, 0, 0, 0.5)',
background25: 'rgba(0, 0, 0, 0.25)',
borderColor: '#BCAAA4',
color: '#ffffff',
success: '#D7CCC8',
secondary: '#A1887F',
primary: '#D7CCC8',
danger: '#8D6E63',
warning: '#FFAB91',
neutral: '#BCAAA4',
translucent: 'rgba(0, 0, 0, 0.5)',
},
},
}
const presetNames = ['purple', 'ocean', 'forest', 'sunset', 'peanut'] as const
const themes: Record<string, PresetModePalette> = {}
for (const preset of presetNames) {
for (const mode of ['light', 'dark', 'oled'] as const) {
themes[`${preset}_${mode}`] = PRESET_PALETTES[preset][mode]
}
}
const jellifyConfig = createTamagui({
animations,
fonts: {
@@ -60,89 +329,7 @@ const jellifyConfig = createTamagui({
media,
shorthands,
tokens,
themes: {
dark: {
background: tokens.color.darkBackground,
background75: tokens.color.darkBackground75,
background50: tokens.color.darkBackground50,
background25: tokens.color.darkBackground25,
borderColor: tokens.color.neutral,
color: tokens.color.white,
success: tokens.color.tealDark,
secondary: tokens.color.secondaryDark,
primary: tokens.color.primaryDark,
danger: tokens.color.dangerDark,
warning: tokens.color.warningDark,
neutral: tokens.color.neutral,
translucent: tokens.color.darkTranslucent,
},
oled: {
// True black OLED theme
background: tokens.color.black,
background75: tokens.color.black75,
background50: tokens.color.black50,
background25: tokens.color.black25,
borderColor: tokens.color.neutral,
color: tokens.color.white,
success: tokens.color.tealDark,
secondary: tokens.color.secondaryDark,
primary: tokens.color.primaryDark,
danger: tokens.color.dangerDark,
warning: tokens.color.warningDark,
neutral: tokens.color.neutral,
translucent: tokens.color.darkTranslucent,
},
dark_inverted_purple: {
color: tokens.color.purpleDark,
borderColor: tokens.color.amethyst,
background: tokens.color.amethyst,
background25: tokens.color.amethyst25,
background50: tokens.color.amethyst50,
background75: tokens.color.amethyst75,
success: tokens.color.tealDark,
secondary: tokens.color.secondaryDark,
primary: tokens.color.primaryDark,
danger: tokens.color.dangerDark,
warning: tokens.color.warningDark,
neutral: tokens.color.neutral,
translucent: tokens.color.darkTranslucent,
},
light: {
background: tokens.color.white,
background75: tokens.color.lightBackground75,
background50: tokens.color.lightBackground50,
background25: tokens.color.lightBackground25,
borderColor: tokens.color.neutral,
color: tokens.color.purpleDark,
success: tokens.color.tealLight,
secondary: tokens.color.secondaryLight,
primary: tokens.color.primaryLight,
danger: tokens.color.dangerLight,
warning: tokens.color.warningLight,
neutral: tokens.color.neutral,
translucent: tokens.color.lightTranslucent,
},
light_inverted_purple: {
color: tokens.color.purpleDark,
borderColor: tokens.color.neutral,
background: tokens.color.amethyst,
background25: tokens.color.amethyst25,
background50: tokens.color.amethyst50,
background75: tokens.color.amethyst75,
success: tokens.color.tealLight,
secondary: tokens.color.secondaryLight,
primary: tokens.color.primaryLight,
danger: tokens.color.dangerLight,
warning: tokens.color.warningLight,
neutral: tokens.color.neutral,
translucent: tokens.color.lightTranslucent,
},
},
themes,
})
export type JellifyConfig = typeof jellifyConfig