diff --git a/.ladle/components.tsx b/.ladle/components.tsx index 04ee536..c6e7beb 100644 --- a/.ladle/components.tsx +++ b/.ladle/components.tsx @@ -9,9 +9,9 @@ import { GlobalStyle } from '@/styles/globalStyle'; import { SelectMenu, SelectItem } from '@/components/SelectMenu'; -import { AppThemeProvider } from '@/hooks/useAppTheme'; +import { AppThemeAndColorModeProvider } from '@/hooks/useAppThemeAndColorMode'; -import { AppTheme, setAppTheme } from '@/state/configs'; +import { AppTheme, AppColorMode, setAppTheme, setAppColorMode } from '@/state/configs'; import { setLocaleLoaded } from '@/state/localization'; import '@/index.css'; @@ -19,9 +19,12 @@ import './ladle.css'; export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [theme, setTheme] = useState(AppTheme.Classic); + const [colorMode, setColorMode] = useState(AppColorMode.GreenUp); useEffect(() => { store.dispatch(setAppTheme(theme)); + store.dispatch(setAppColorMode(colorMode)); + switch (theme) { case AppTheme.Dark: { document?.documentElement?.classList.remove('theme-light'); @@ -38,7 +41,7 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children break; } } - }, [theme]); + }, [theme, colorMode]); useEffect(() => { store.dispatch(setLocaleLoaded(true)); @@ -48,10 +51,7 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children

Active Theme:

- + {[ { value: AppTheme.Classic, @@ -66,20 +66,31 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children label: 'Light theme', }, ].map(({ value, label }) => ( - + + ))} + +

Active Color Mode:

+ + {[ + { + value: AppColorMode.GreenUp, + label: 'Green up', + }, + { + value: AppColorMode.RedUp, + label: 'Red up', + }, + ].map(({ value, label }) => ( + ))}

- + {children} - -
+ + ); }; diff --git a/src/App.tsx b/src/App.tsx index 23d1a39..d80de5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,7 @@ import { } from '@/hooks'; import { DydxProvider } from '@/hooks/useDydxClient'; import { AccountsProvider } from '@/hooks/useAccounts'; -import { AppThemeProvider } from '@/hooks/useAppTheme'; +import { AppThemeAndColorModeProvider } from '@/hooks/useAppThemeAndColorMode'; import { DialogAreaProvider, useDialogArea } from '@/hooks/useDialogArea'; import { LocaleProvider } from '@/hooks/useLocaleSeparators'; import { NotificationsProvider } from '@/hooks/useNotifications'; @@ -141,8 +141,7 @@ const providers = [ wrapProvider(LocalNotificationsProvider), wrapProvider(NotificationsProvider), wrapProvider(DialogAreaProvider), - wrapProvider(PotentialMarketsProvider), - wrapProvider(AppThemeProvider), + wrapProvider(AppThemeAndColorModeProvider), ]; const App = () => { diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 863f24e..5ac32cb 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -20,6 +20,7 @@ export enum LocalStorageKey { SelectedLocale = 'dydx.SelectedLocale', SelectedNetwork = 'dydx.SelectedNetwork', SelectedTheme = 'dydx.SelectedTheme', + SelectedColorMode = 'dydx.SelectedColorMode', SelectedTradeLayout = 'dydx.SelectedTradeLayout', TradingViewChartConfig = 'dydx.TradingViewChartConfig', HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives', diff --git a/src/constants/styles/colors.ts b/src/constants/styles/colors.ts index a7c9eb1..b05a71d 100644 --- a/src/constants/styles/colors.ts +++ b/src/constants/styles/colors.ts @@ -1,4 +1,11 @@ -export type ThemeColors = LayerColors & +import { AppColorMode } from '@/state/configs'; + +export type Theme = { + [AppColorMode.GreenUp]: ThemeColorBase; + [AppColorMode.RedUp]: ThemeColorBase; +}; + +export type ThemeColorBase = LayerColors & BorderColors & TextColors & GradientColors & @@ -49,6 +56,8 @@ type StatusColors = { error: string; }; +/** ##InvertDirectionalColors + * When adding colors here, make sure to update linked function to invert colors for AppColorMode. */ type DirectionalColors = { positive: string; negative: string; diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index 887d3f3..7577796 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -12,8 +12,9 @@ import { ORDERBOOK_WIDTH, } from '@/constants/orderbook'; +import { useAppThemeAndColorModeContext } from '@/hooks/useAppThemeAndColorMode'; + import { getCurrentMarketConfig, getCurrentMarketOrderbookMap } from '@/state/perpetualsSelectors'; -import { getAppTheme } from '@/state/configsSelectors'; import { MustBigNumber } from '@/lib/numbers'; @@ -23,7 +24,6 @@ import { getXByColumn, getYForElements, } from '@/lib/orderbookHelpers'; -import { useAppThemeContext } from '../useAppTheme'; type ElementProps = { data: Array; @@ -53,7 +53,7 @@ export const useDrawOrderbook = ({ const { stepSizeDecimals = TOKEN_DECIMALS, tickSizeDecimals = SMALL_USD_DECIMALS } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; const prevData = useRef(data); - const theme = useAppThemeContext(); + const theme = useAppThemeAndColorModeContext(); /** * Scale canvas using device pixel ratio to unblur drawn text diff --git a/src/hooks/tradingView/useTradingView.ts b/src/hooks/tradingView/useTradingView.ts index 756ecc2..68065d8 100644 --- a/src/hooks/tradingView/useTradingView.ts +++ b/src/hooks/tradingView/useTradingView.ts @@ -11,7 +11,7 @@ import { useDydxClient, useLocalStorage } from '@/hooks'; import { store } from '@/state/_store'; import { getSelectedNetwork } from '@/state/appSelectors'; -import { getAppTheme } from '@/state/configsSelectors'; +import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; import { getSelectedLocale } from '@/state/localizationSelectors'; import { getCurrentMarketId, getMarketIds } from '@/state/perpetualsSelectors'; @@ -30,6 +30,7 @@ export const useTradingView = ({ }) => { const marketId = useSelector(getCurrentMarketId); const appTheme = useSelector(getAppTheme); + const appColorMode = useSelector(getAppColorMode); const marketIds = useSelector(getMarketIds, shallowEqual); const selectedLocale = useSelector(getSelectedLocale); const selectedNetwork = useSelector(getSelectedNetwork); @@ -46,7 +47,7 @@ export const useTradingView = ({ useEffect(() => { if (hasMarkets && isClientConnected && marketId) { const widgetOptions = getWidgetOptions(); - const widgetOverrides = getWidgetOverrides(appTheme); + const widgetOverrides = getWidgetOverrides({ appTheme, appColorMode }); const options = { // debug: true, ...widgetOptions, @@ -75,7 +76,14 @@ export const useTradingView = ({ tvWidgetRef.current = null; setIsChartReady(false); }; - }, [getCandlesForDatafeed, isClientConnected, hasMarkets, selectedLocale, selectedNetwork, !!marketId]); + }, [ + getCandlesForDatafeed, + isClientConnected, + hasMarkets, + selectedLocale, + selectedNetwork, + !!marketId, + ]); return { savedResolution }; }; diff --git a/src/hooks/tradingView/useTradingViewTheme.ts b/src/hooks/tradingView/useTradingViewTheme.ts index 6436063..b0ed677 100644 --- a/src/hooks/tradingView/useTradingViewTheme.ts +++ b/src/hooks/tradingView/useTradingViewTheme.ts @@ -3,8 +3,8 @@ import { useSelector } from 'react-redux'; import type { IChartingLibraryWidget, ThemeName } from 'public/tradingview/charting_library'; -import { AppTheme } from '@/state/configs'; -import { getAppTheme } from '@/state/configsSelectors'; +import { AppColorMode, AppTheme } from '@/state/configs'; +import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; import { getWidgetOverrides } from '@/lib/tradingView/utils'; @@ -29,6 +29,7 @@ export const useTradingViewTheme = ({ isWidgetReady?: boolean; }) => { const appTheme: AppTheme = useSelector(getAppTheme); + const appColorMode: AppColorMode = useSelector(getAppColorMode); useEffect(() => { if (tvWidget && isWidgetReady) { @@ -55,10 +56,24 @@ export const useTradingViewTheme = ({ } } - const { overrides, studies_overrides } = getWidgetOverrides(appTheme); + const { overrides, studies_overrides } = getWidgetOverrides({ appTheme, appColorMode }); tvWidget?.applyOverrides(overrides); tvWidget?.applyStudiesOverrides(studies_overrides); + + // Necessary to update existing indicators + const volumeStudyId = tvWidget + ?.activeChart() + ?.getAllStudies() + ?.find((x) => x.name === 'Volume')?.id; + + if (volumeStudyId) { + const volume = tvWidget?.activeChart()?.getStudyById(volumeStudyId); + volume.applyOverrides({ + 'volume.color.0': studies_overrides['volume.volume.color.0'], + 'volume.color.1': studies_overrides['volume.volume.color.1'], + }); + } }); } - }, [appTheme]); + }, [appTheme, appColorMode]); }; diff --git a/src/hooks/useAppTheme.tsx b/src/hooks/useAppTheme.tsx deleted file mode 100644 index 790cc89..0000000 --- a/src/hooks/useAppTheme.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSelector } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; - -import { AppTheme } from '@/state/configs'; -import { getAppTheme } from '@/state/configsSelectors'; - -import { Themes } from '@/styles/themes'; - -export const AppThemeProvider = ({ ...props }) => { - return -}; - -export const useAppThemeContext = () => { - const theme: AppTheme = useSelector(getAppTheme); - return Themes[theme]; -} diff --git a/src/hooks/useAppThemeAndColorMode.tsx b/src/hooks/useAppThemeAndColorMode.tsx new file mode 100644 index 0000000..08572fa --- /dev/null +++ b/src/hooks/useAppThemeAndColorMode.tsx @@ -0,0 +1,18 @@ +import { useSelector } from 'react-redux'; +import { ThemeProvider } from 'styled-components'; + +import { AppTheme, AppColorMode } from '@/state/configs'; +import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; + +import { Themes } from '@/styles/themes'; + +export const AppThemeAndColorModeProvider = ({ ...props }) => { + return ; +}; + +export const useAppThemeAndColorModeContext = () => { + const theme: AppTheme = useSelector(getAppTheme); + const colorMode: AppColorMode = useSelector(getAppColorMode); + + return Themes[theme][colorMode]; +}; diff --git a/src/icons/logo-short.tsx b/src/icons/logo-short.tsx index 81ffe2f..a681fdc 100644 --- a/src/icons/logo-short.tsx +++ b/src/icons/logo-short.tsx @@ -1,7 +1,7 @@ -import { useAppThemeContext } from '@/hooks/useAppTheme'; +import { useAppThemeAndColorModeContext } from '@/hooks/useAppThemeAndColorMode'; const LogoShortIcon: React.FC<{ id?: string }> = ({ id }: { id?: string }) => { - const theme = useAppThemeContext(); + const theme = useAppThemeAndColorModeContext(); const fill = theme.logoFill; return ( diff --git a/src/lib/tradingView/utils.ts b/src/lib/tradingView/utils.ts index 5a50209..27c26b6 100644 --- a/src/lib/tradingView/utils.ts +++ b/src/lib/tradingView/utils.ts @@ -1,6 +1,6 @@ import { Candle, TradingViewBar, TradingViewSymbol } from '@/constants/candles'; -import { AppTheme } from '@/state/configs'; +import { AppTheme, AppColorMode } from '@/state/configs'; import { Themes } from '@/styles/themes'; @@ -47,8 +47,14 @@ export const getHistorySlice = ({ return bars.filter(({ time }) => time >= fromMs); }; -export const getWidgetOverrides = (appTheme: AppTheme) => { - const theme = Themes[appTheme]; +export const getWidgetOverrides = ({ + appTheme, + appColorMode, +}: { + appTheme: AppTheme; + appColorMode: AppColorMode; +}) => { + const theme = Themes[appTheme][appColorMode]; return { overrides: { diff --git a/src/state/configs.ts b/src/state/configs.ts index 9da0355..db1b3e7 100644 --- a/src/state/configs.ts +++ b/src/state/configs.ts @@ -12,8 +12,14 @@ export enum AppTheme { Light = 'Light', } +export enum AppColorMode { + GreenUp = 'GreenUp', + RedUp = 'RedUp', +} + export interface ConfigsState { appTheme: AppTheme; + appColorMode: AppColorMode; feeTiers?: kollections.List; feeDiscounts?: FeeDiscount[]; network?: NetworkConfigs; @@ -41,6 +47,10 @@ const initialState: ConfigsState = { key: LocalStorageKey.SelectedTheme, defaultValue: AppTheme.Classic, }), + appColorMode: getLocalStorage({ + key: LocalStorageKey.SelectedColorMode, + defaultValue: AppColorMode.GreenUp, + }), feeDiscounts: undefined, feeTiers: undefined, network: undefined, @@ -61,6 +71,10 @@ export const configsSlice = createSlice({ changeTheme(payload); state.appTheme = payload; }, + setAppColorMode: (state: ConfigsState, { payload }: PayloadAction) => { + setLocalStorage({ key: LocalStorageKey.SelectedColorMode, value: payload }); + state.appColorMode = payload; + }, setConfigs: (state: ConfigsState, action: PayloadAction>) => ({ ...state, ...action.payload, @@ -72,4 +86,5 @@ export const configsSlice = createSlice({ }, }); -export const { setAppTheme, setConfigs, markLaunchIncentivesSeen } = configsSlice.actions; +export const { setAppTheme, setAppColorMode, setConfigs, markLaunchIncentivesSeen } = + configsSlice.actions; diff --git a/src/state/configsSelectors.ts b/src/state/configsSelectors.ts index d83bd84..51dc198 100644 --- a/src/state/configsSelectors.ts +++ b/src/state/configsSelectors.ts @@ -2,6 +2,8 @@ import type { RootState } from './_store'; export const getAppTheme = (state: RootState) => state.configs.appTheme; +export const getAppColorMode = (state: RootState) => state.configs.appColorMode; + export const getFeeTiers = (state: RootState) => state.configs.feeTiers?.toArray(); export const getFeeDiscounts = (state: RootState) => state.configs.feeDiscounts; diff --git a/src/styles/themes.ts b/src/styles/themes.ts index 4b910c5..3db52b8 100644 --- a/src/styles/themes.ts +++ b/src/styles/themes.ts @@ -1,9 +1,9 @@ -import { AppTheme } from '@/state/configs'; -import type { ThemeColors } from '@/constants/styles/colors'; +import { AppTheme, AppColorMode } from '@/state/configs'; +import type { Theme, ThemeColorBase } from '@/constants/styles/colors'; import { ColorToken, OpacityToken } from '@/constants/styles/base'; import { generateFadedColorVariant } from '@/lib/styles'; -const ClassicTheme: ThemeColors = { +const ClassicThemeBase: ThemeColorBase = { layer0: ColorToken.GrayBlue7, layer1: ColorToken.GrayBlue6, layer2: ColorToken.GrayBlue5, @@ -52,7 +52,7 @@ const ClassicTheme: ThemeColors = { tooltipBackground: generateFadedColorVariant(ColorToken.GrayBlue3, OpacityToken.Opacity66), }; -const DarkTheme: ThemeColors = { +const DarkThemeBase: ThemeColorBase = { layer0: ColorToken.Black, layer1: ColorToken.DarkGray11, layer2: ColorToken.DarkGray13, @@ -101,7 +101,7 @@ const DarkTheme: ThemeColors = { tooltipBackground: generateFadedColorVariant(ColorToken.DarkGray6, OpacityToken.Opacity66), }; -const LightTheme: ThemeColors = { +const LightThemeBase: ThemeColorBase = { layer0: ColorToken.White, layer1: ColorToken.LightGray6, layer2: ColorToken.White, @@ -150,8 +150,22 @@ const LightTheme: ThemeColors = { tooltipBackground: generateFadedColorVariant(ColorToken.LightGray7, OpacityToken.Opacity66), }; -export const Themes = { - [AppTheme.Classic]: ClassicTheme, - [AppTheme.Dark]: DarkTheme, - [AppTheme.Light]: LightTheme, +const generateTheme = (themeBase: ThemeColorBase): Theme => { + return { + [AppColorMode.GreenUp]: themeBase, + [AppColorMode.RedUp]: { + ...themeBase, + // #InvertDirectionalColors + positive: themeBase.negative, + negative: themeBase.positive, + positiveFaded: themeBase.negativeFaded, + negativeFaded: themeBase.positiveFaded, + }, + }; +}; + +export const Themes = { + [AppTheme.Classic]: generateTheme(ClassicThemeBase), + [AppTheme.Dark]: generateTheme(DarkThemeBase), + [AppTheme.Light]: generateTheme(LightThemeBase), }; diff --git a/src/views/dialogs/DisplaySettingsDialog.tsx b/src/views/dialogs/DisplaySettingsDialog.tsx index 1cdd030..1e84c48 100644 --- a/src/views/dialogs/DisplaySettingsDialog.tsx +++ b/src/views/dialogs/DisplaySettingsDialog.tsx @@ -5,8 +5,8 @@ import { Root, Item, Indicator } from '@radix-ui/react-radio-group'; import { useStringGetter } from '@/hooks'; -import { AppTheme, setAppTheme } from '@/state/configs'; -import { getAppTheme } from '@/state/configsSelectors'; +import { AppTheme, AppColorMode, setAppTheme } from '@/state/configs'; +import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; import { layoutMixins } from '@/styles/layoutMixins'; import { Themes } from '@/styles/themes'; @@ -26,6 +26,7 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const currentTheme: AppTheme = useSelector(getAppTheme); + const currentColorMode: AppColorMode = useSelector(getAppColorMode); const sectionHeader = (heading: string) => { return ( @@ -56,13 +57,13 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { { dispatch(setAppTheme(theme)); }} > - + {stringGetter({ key: label })}