Compare commits

...
Author SHA1 Message Date
Matthew Russell 917c452116 refactor: use zustand for theme store 2022-09-28 15:54:14 -07:00
Matthew Russell 166c9b6fb9 fix: hydration error due to theme switcher 2022-09-28 15:53:22 -07:00
25 changed files with 181 additions and 227 deletions
+22 -27
View File
@@ -1,8 +1,6 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { createClient } from './lib/apollo-client';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import {
VegaConnectDialog,
@@ -18,8 +16,7 @@ import LocalContext from './context/local-context';
import useLocalValues from './hooks/use-local-values';
function App() {
const [theme, toggleTheme] = useThemeSwitcher();
const localValues = useLocalValues(theme, toggleTheme);
const localValues = useLocalValues();
const {
vegaWalletDialog,
menu: { setMenuOpen },
@@ -32,29 +29,27 @@ function App() {
return (
<EnvironmentProvider>
<ThemeContext.Provider value={theme}>
<NetworkLoader createClient={createClient}>
<VegaWalletProvider>
<LocalContext.Provider value={localValues}>
<AppLoader>
<div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]">
<Header />
<Main />
<VegaConnectDialog
connectors={Connectors}
dialogOpen={vegaWalletDialog.connect}
setDialogOpen={vegaWalletDialog.setConnect}
/>
<VegaManageDialog
dialogOpen={vegaWalletDialog.manage}
setDialogOpen={vegaWalletDialog.setManage}
/>
</div>
</AppLoader>
</LocalContext.Provider>
</VegaWalletProvider>
</NetworkLoader>
</ThemeContext.Provider>
<NetworkLoader createClient={createClient}>
<VegaWalletProvider>
<LocalContext.Provider value={localValues}>
<AppLoader>
<div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]">
<Header />
<Main />
<VegaConnectDialog
connectors={Connectors}
dialogOpen={vegaWalletDialog.connect}
setDialogOpen={vegaWalletDialog.setConnect}
/>
<VegaManageDialog
dialogOpen={vegaWalletDialog.manage}
setDialogOpen={vegaWalletDialog.setManage}
/>
</div>
</AppLoader>
</LocalContext.Provider>
</VegaWalletProvider>
</NetworkLoader>
</EnvironmentProvider>
);
}
@@ -1,14 +1,8 @@
import React, {
forwardRef,
useCallback,
useContext,
useMemo,
useRef,
} from 'react';
import React, { forwardRef, useCallback, useMemo, useRef } from 'react';
import classNames from 'classnames';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { ThemeContext, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useScreenDimensions, useTheme } from '@vegaprotocol/react-helpers';
import type {
GridOptions,
GetRowIdParams,
@@ -43,7 +37,7 @@ const ConsoleLiteGrid = <T extends { id?: string }>(
) => {
const { isMobile, screenSize } = useScreenDimensions();
const gridRef = useRef<AgGridReact | null>(null);
const theme = useContext(ThemeContext);
const { theme } = useTheme();
const handleOnGridReady = useCallback(() => {
(
(ref as React.RefObject<AgGridReact>) || gridRef
@@ -16,8 +16,6 @@ interface MenuState {
export interface LocalValues {
vegaWalletDialog: VegaWalletDialogState;
menu: MenuState;
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const LocalContext = createContext<LocalValues>({} as LocalValues);
@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import type { LocalValues } from '../context/local-context';
const useLocalValues = (theme: 'light' | 'dark', toggleTheme: () => void) => {
const useLocalValues = () => {
const [connect, setConnect] = useState<boolean>(false);
const [manage, setManage] = useState<boolean>(false);
const [menuOpen, setMenuOpen] = useState(false);
@@ -9,10 +9,8 @@ const useLocalValues = (theme: 'light' | 'dark', toggleTheme: () => void) => {
() => ({
vegaWalletDialog: { connect, manage, setConnect, setManage },
menu: { menuOpen, setMenuOpen, onToggle: () => setMenuOpen(!menuOpen) },
theme,
toggleTheme,
}),
[connect, manage, theme, toggleTheme, menuOpen]
[connect, manage, menuOpen]
);
};
+17 -26
View File
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import { useLocation } from 'react-router-dom';
import { ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
EnvironmentProvider,
NetworkLoader,
@@ -18,7 +17,6 @@ import { ENV } from './config/env';
function App() {
const { VEGA_ENV } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
@@ -37,31 +35,24 @@ function App() {
}, [VEGA_ENV]);
return (
<ThemeContext.Provider value={theme}>
<TendermintWebsocketProvider>
<NetworkLoader createClient={createClient}>
<div
className={`${
menuOpen && 'h-[100vh] overflow-hidden'
} antialiased m-0 bg-white dark:bg-black text-black dark:text-white`}
>
<div className="grid grid-rows-[repeat(2,_auto)_1fr] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)] md:grid-cols-[300px_1fr] border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r mx-auto">
<Header
theme={theme}
toggleTheme={toggleTheme}
menuOpen={menuOpen}
setMenuOpen={setMenuOpen}
/>
<Nav menuOpen={menuOpen} />
<Main />
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-sm md:text-md md:flex md:col-span-2 p-4 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<NetworkInfo />
</footer>
</div>
<TendermintWebsocketProvider>
<NetworkLoader createClient={createClient}>
<div
className={`${
menuOpen && 'h-[100vh] overflow-hidden'
} antialiased m-0 bg-white dark:bg-black text-black dark:text-white`}
>
<div className="grid grid-rows-[repeat(2,_auto)_1fr] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)] md:grid-cols-[300px_1fr] border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r mx-auto">
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
<Nav menuOpen={menuOpen} />
<Main />
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-sm md:text-md md:flex md:col-span-2 p-4 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<NetworkInfo />
</footer>
</div>
</NetworkLoader>
</TendermintWebsocketProvider>
</ThemeContext.Provider>
</div>
</NetworkLoader>
</TendermintWebsocketProvider>
);
}
@@ -8,18 +8,11 @@ import type { Dispatch, SetStateAction } from 'react';
import { NetworkSwitcher } from '@vegaprotocol/environment';
interface ThemeToggleProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
menuOpen: boolean;
setMenuOpen: Dispatch<SetStateAction<boolean>>;
}
export const Header = ({
theme,
toggleTheme,
menuOpen,
setMenuOpen,
}: ThemeToggleProps) => {
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
const headerClasses = classnames(
'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
@@ -48,7 +41,7 @@ export const Header = ({
<Icon name={menuOpen ? 'cross' : 'menu'} />
</button>
<Search />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} className="-my-4" />
<ThemeSwitcher className="-my-4" />
</header>
);
};
+7 -13
View File
@@ -1,24 +1,18 @@
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import { Header } from './components/header';
import { StatsManager } from '@vegaprotocol/network-stats';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { createClient } from './lib/apollo-client';
function App() {
const [theme, toggleTheme] = useThemeSwitcher();
return (
<ThemeContext.Provider value={theme}>
<NetworkLoader createClient={createClient}>
<div className="w-screen min-h-screen grid pb-24 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100">
<div className="layout-grid w-screen justify-self-center">
<Header theme={theme} toggleTheme={toggleTheme} />
<StatsManager className="max-w-3xl px-24" />
</div>
<NetworkLoader createClient={createClient}>
<div className="w-screen min-h-screen grid pb-24 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100">
<div className="layout-grid w-screen justify-self-center">
<Header />
<StatsManager className="max-w-3xl px-24" />
</div>
</NetworkLoader>
</ThemeContext.Provider>
</div>
</NetworkLoader>
);
}
+2 -8
View File
@@ -1,20 +1,14 @@
import { VegaLogo, ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { VegaBackgroundVideo } from '../videos';
interface ThemeToggleProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => {
export const Header = () => {
return (
<header className="relative overflow-hidden py-8 mb-40 md:mb-64">
<VegaBackgroundVideo />
<div className="relative flex justify-center px-8 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-20 flex items-center justify-between">
<VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
<ThemeSwitcher />
</div>
</div>
</header>
+2 -7
View File
@@ -8,12 +8,7 @@ import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { Vega } from '../icons/vega';
interface NavbarProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Navbar = ({ theme, toggleTheme }: NavbarProps) => {
export const Navbar = () => {
const { marketId, update } = useGlobalStore((store) => ({
marketId: store.marketId,
update: store.update,
@@ -43,7 +38,7 @@ export const Navbar = ({ theme, toggleTheme }: NavbarProps) => {
))}
</nav>
<div className="flex items-center gap-2 ml-auto">
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
<ThemeSwitcher />
<VegaWalletConnectButton
setConnectDialog={(open) => update({ connectDialog: open })}
/>
+4 -5
View File
@@ -1,7 +1,7 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
import {
EnvironmentProvider,
@@ -29,7 +29,6 @@ function AppBody({ Component, pageProps }: AppProps) {
update: store.update,
}));
const { isOpen, symbol, trigger, setOpen } = useAssetDetailsDialogStore();
const [theme, toggleTheme] = useThemeSwitcher();
const { VEGA_ENV } = useEnvironment();
const networkName = envTriggerMapping[VEGA_ENV];
@@ -41,13 +40,13 @@ function AppBody({ Component, pageProps }: AppProps) {
}, [pageTitle, networkName]);
return (
<ThemeContext.Provider value={theme}>
<>
<Head>
<title>{title}</title>
</Head>
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
<AppLoader>
<Navbar theme={theme} toggleTheme={toggleTheme} />
<Navbar />
<main data-testid={pageProps.page}>
{/* @ts-ignore conflict between @types/react and nextjs internal types */}
<Component {...pageProps} />
@@ -67,7 +66,7 @@ function AppBody({ Component, pageProps }: AppProps) {
<RiskNoticeDialog />
</AppLoader>
</div>
</ThemeContext.Provider>
</>
);
}
+3 -3
View File
@@ -13,9 +13,9 @@ import {
} from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useContext, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useTheme } from '@vegaprotocol/react-helpers';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -46,7 +46,7 @@ export const CandlesChartContainer = ({
}: CandlesChartContainerProps) => {
const client = useApolloClient();
const { keypair } = useVegaWallet();
const theme = useContext(ThemeContext);
const { theme } = useTheme();
const [interval, setInterval] = useState<Interval>(Interval.I15M);
const [chartType, setChartType] = useState<ChartType>(ChartType.CANDLE);
+4 -29
View File
@@ -1,6 +1,5 @@
import './styles.css';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
import { useThemeWatcher } from '@vegaprotocol/react-helpers';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
@@ -15,36 +14,12 @@ export const parameters = {
};
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesnt seem to provide the current selected
// theme in context, we need to provid it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, []);
(Story) => {
useThemeWatcher();
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
<Story />
</div>
);
},
+3 -10
View File
@@ -4,18 +4,11 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import {
useDataProvider,
addDecimal,
ThemeContext,
getNumberFormat,
useTheme,
} from '@vegaprotocol/react-helpers';
import dataProvider from './market-depth-provider';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
useContext,
} from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import type { MarketData } from '@vegaprotocol/market-list';
import type {
@@ -36,7 +29,7 @@ const formatMidPrice = (midPrice: string, decimalPlaces: number) =>
type DepthData = Pick<DepthChartProps, 'data' | 'midPrice'>;
export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
const theme = useContext(ThemeContext);
const { theme } = useTheme();
const variables = useMemo(() => ({ marketId }), [marketId]);
const [depthData, setDepthData] = useState<DepthData | null>(null);
const dataRef = useRef<DepthData | null>(null);
+2 -3
View File
@@ -8,11 +8,10 @@ import {
useState,
useMemo,
useCallback,
useContext,
} from 'react';
import classNames from 'classnames';
import { formatNumber, t, ThemeContext } from '@vegaprotocol/react-helpers';
import { formatNumber, t, useTheme } from '@vegaprotocol/react-helpers';
import { MarketTradingMode } from '@vegaprotocol/types';
import { OrderbookRow } from './orderbook-row';
import { createRow, getPriceLevel } from './orderbook-data';
@@ -107,7 +106,7 @@ export const Orderbook = ({
resolution,
onResolutionChange,
}: OrderbookProps) => {
const theme = useContext(ThemeContext);
const { theme } = useTheme();
const scrollElement = useRef<HTMLDivElement>(null);
// scroll offset for which rendered rows are selected, will change after user will scroll to margin of rendered data
const [scrollOffset, setScrollOffset] = useState(0);
+7 -9
View File
@@ -1,6 +1,6 @@
import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
import { useTheme } from '@vegaprotocol/react-helpers';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true },
@@ -16,18 +16,18 @@ export const parameters = {
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesnt seem to provide the current selected
// theme in context, we need to provid it in JS as some components
// theme in context, we need to provide it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
const { toggle } = useTheme();
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
toggle('dark');
} else {
setTheme('light');
toggle('light');
}
}
});
@@ -37,13 +37,11 @@ export const decorators = [
return () => {
observer.disconnect();
};
}, []);
}, [toggle]);
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
<Story />
</div>
);
},
+2 -1
View File
@@ -5,4 +5,5 @@ export * from './use-network-params';
export * from './use-outside-click';
export * from './use-resize';
export * from './use-screen-dimensions';
export * from './use-theme-switcher';
export * from './use-theme';
export * from './use-theme-watcher';
@@ -1,42 +0,0 @@
import { useEffect, useState } from 'react';
import { LocalStorage } from '../lib/storage';
const darkTheme = 'dark';
const lightTheme = 'light';
type themeVariant = typeof darkTheme | typeof lightTheme;
const darkThemeCssClass = darkTheme;
const getCurrentTheme = () => {
const theme = LocalStorage.getItem('theme');
if (
theme === darkTheme ||
(!theme &&
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
return darkTheme;
}
return lightTheme;
};
const toggleTheme = () => {
const theme = document.documentElement.classList.contains(darkThemeCssClass)
? lightTheme
: darkTheme;
LocalStorage.setItem('theme', theme);
return theme;
};
export function useThemeSwitcher(): [themeVariant, () => void] {
const [theme, setTheme] = useState<themeVariant>(lightTheme);
useEffect(() => setTheme(getCurrentTheme()), []);
useEffect(() => {
if (theme === darkTheme) {
document.documentElement.classList.add(darkThemeCssClass);
} else {
document.documentElement.classList.remove(darkThemeCssClass);
}
}, [theme]);
return [theme, () => setTheme(toggleTheme)];
}
@@ -0,0 +1,28 @@
import { useEffect } from 'react';
import { useTheme } from './use-theme';
export const useThemeWatcher = () => {
// storybook-addon-themes doesnt seem to provide the current selected
// theme in context, we need to provide it in JS as some components
// rely on it for rendering
const { toggle } = useTheme();
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target as HTMLElement;
if (body.classList.contains('dark')) {
toggle('dark');
} else {
toggle('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, [toggle]);
};
+63
View File
@@ -0,0 +1,63 @@
import { useEffect } from 'react';
import create from 'zustand';
import { LocalStorage } from '../lib/storage';
type ThemeVariant = typeof darkTheme | typeof lightTheme;
const darkTheme = 'dark';
const lightTheme = 'light';
const storageKey = 'theme';
const getCurrentTheme = () => {
const theme = LocalStorage.getItem(storageKey);
if (
theme === darkTheme ||
(!theme &&
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
return darkTheme;
}
return lightTheme;
};
const applyClass = (theme: ThemeVariant) => {
if (theme === darkTheme) {
document.documentElement.classList.add(darkTheme);
} else {
document.documentElement.classList.remove(darkTheme);
}
};
interface ThemeStore {
theme: ThemeVariant;
toggle: (theme?: ThemeVariant) => void;
}
export const useThemeStore = create<ThemeStore>((set) => ({
theme: lightTheme,
toggle: (theme) => {
set((curr) => {
let newTheme: ThemeVariant;
if (theme) {
newTheme = theme;
} else {
newTheme = curr.theme === lightTheme ? darkTheme : lightTheme;
}
LocalStorage.setItem(storageKey, newTheme);
applyClass(newTheme);
return { theme: newTheme };
});
},
}));
export function useTheme() {
const { theme, toggle } = useThemeStore();
useEffect(() => {
const storedTheme = getCurrentTheme();
toggle(storedTheme);
}, [toggle]);
return { theme, toggle };
}
-1
View File
@@ -1,6 +1,5 @@
export * from './hooks';
export * from './lib/assets';
export * from './lib/context';
export * from './lib/determine-id';
export * from './lib/format';
export * from './lib/generic-data-provider';
@@ -1,3 +0,0 @@
import * as React from 'react';
export const ThemeContext = React.createContext<'light' | 'dark'>('dark');
@@ -1 +0,0 @@
export * from './context';
@@ -1,9 +1,8 @@
import type { ReactNode, FunctionComponent } from 'react';
import { useContext } from 'react';
import dynamic from 'next/dynamic';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import { AgGridReact } from 'ag-grid-react';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useTheme } from '@vegaprotocol/react-helpers';
import 'ag-grid-community/dist/styles/ag-grid.css';
interface GridProps {
@@ -33,7 +32,7 @@ export const AgGridThemed = ({
gridRef?: React.ForwardedRef<AgGridReact>;
customThemeParams?: string;
}) => {
const theme = useContext(ThemeContext);
const { theme } = useTheme();
const defaultProps = { rowHeight: 22, headerHeight: 22 };
return (
<div
@@ -1,8 +1,8 @@
import * as React from 'react';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import { AgGridReact } from 'ag-grid-react';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import 'ag-grid-community/dist/styles/ag-grid.css';
import { useTheme } from '@vegaprotocol/react-helpers';
const AgGridLightTheme = React.lazy(() =>
import('./ag-grid-light').then((module) => ({
@@ -23,7 +23,7 @@ export const AgGridThemed = React.forwardRef<
className?: string;
}
>(({ style, className, ...props }, ref) => {
const theme = React.useContext(ThemeContext);
const { theme } = useTheme();
return (
<div
className={`${className ?? ''} ${
@@ -1,20 +1,14 @@
import { useTheme } from '@vegaprotocol/react-helpers';
import React from 'react';
import { SunIcon, MoonIcon } from './icons';
export const ThemeSwitcher = ({
theme,
onToggle,
className,
}: {
theme: 'light' | 'dark';
onToggle: () => void;
className?: string;
}) => {
export const ThemeSwitcher = ({ className }: { className?: string }) => {
const { theme, toggle } = useTheme();
const classes = 'text-neutral-800 dark:text-neutral-300';
return (
<button
type="button"
onClick={() => onToggle()}
onClick={() => toggle()}
className={className}
data-testid="theme-switcher"
>