diff --git a/src/components/Globals/index.tsx b/src/components/Globals/index.tsx index cca69eff..c2eb3f1f 100644 --- a/src/components/Globals/index.tsx +++ b/src/components/Globals/index.tsx @@ -5,13 +5,25 @@ import { PULSAR_GLOBAL_FONT_HREF, } from '@swingby-protocol/pulsar'; import Head from 'next/head'; -import React from 'react'; +import React, { useEffect } from 'react'; -import { useThemeSettings } from '../../modules/store/settings'; +import { useThemeSettings, useSystemTheme } from '../../modules/store/settings'; import { Favicon } from '../Favicon'; export const Globals = ({ children }: { children: React.ReactNode }) => { const [theme] = useThemeSettings(); + const systemTheme = useSystemTheme(); + + useEffect(() => { + if (['light', 'dark'].includes(theme)) { + document.body.setAttribute('data-theme', theme); + } else if (['auto'].includes(theme)) { + document.body.setAttribute('data-theme', systemTheme); + } else { + document.body.removeAttribute('data-theme'); + } + }, [theme, systemTheme]); + return ( diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 28609a98..af13dff0 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,33 +1,54 @@ -import { SwingbyHeader, LocaleSwitcher, ThemeSwitcher } from '@swingby-protocol/header'; -import { useRouter } from 'next/router'; -import React, { useCallback } from 'react'; -import { useIntl } from 'react-intl'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Icon } from '@swingby-protocol/pulsar'; -import { useThemeSettings } from '../../modules/store/settings'; +import { Sidebar } from '../Sidebar'; +import { NavHandlerProps } from '../Layout'; +import { useOnboard } from '../../modules/onboard'; +import { AccountId } from '../../components/AccountId'; -import { HeaderContainer } from './styled'; +import { + HeaderContainer, + SidebarToggleMobile, + HeaderAction, + HeaderLogo, + ButtonConnect, +} from './styled'; -export const Header = () => { - const { push, asPath, locales } = useRouter(); - const { locale } = useIntl(); - const [theme, setTheme] = useThemeSettings(); +type Props = NavHandlerProps; - const changeLocale = useCallback((locale: string) => push(asPath, null, { locale }), [ - push, - asPath, - ]); +const ConnectWallet = () => { + const { address, onboard } = useOnboard(); + + if (address) { + return ; + } return ( - - - - - - } - /> + await onboard?.walletSelect()} + > + + + ); +}; + +export const Header = ({ navOpen, toggleNav }: Props) => { + return ( + + + + + + + + + + + + ); }; diff --git a/src/components/Header/styled.tsx b/src/components/Header/styled.tsx index ae260991..37c98fc7 100644 --- a/src/components/Header/styled.tsx +++ b/src/components/Header/styled.tsx @@ -1,9 +1,64 @@ import styled from 'styled-components'; +import { rem } from 'polished'; +import { Button } from '@swingby-protocol/pulsar'; -export const HeaderContainer = styled.div` - width: 100%; - left: 0; - top: 0; +import { StylingConstants } from '../../modules/styles'; + +const { media } = StylingConstants; + +export const HeaderContainer = styled.div<{ open: boolean }>` position: fixed; + top: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + align-items: center; + height: 70px; + padding: ${({ theme }) => rem(theme.pulsar.size.street)}; + background-color: var(--theme-card-color); + box-shadow: var(--theme-card-shadow); + border-bottom: 1px solid ${({ theme }) => theme.pulsar.color.border.normal}; + transition: all 0.2s linear; z-index: 20; + + @media (min-width: ${rem(media.md)}) { + left: ${({ open }) => (open ? '216px' : '72px')}; + } +`; + +export const HeaderLogo = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => rem(theme.pulsar.size.closet)}; +`; + +export const HeaderAction = styled.div``; + +export const SidebarToggleMobile = styled.label` + cursor: pointer; + + @media (min-width: ${rem(media.md)}) { + display: none; + } +`; + +export const AppLogoLink = styled.a` + color: inherit; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + + > svg { + height: 1.5em; + } +`; +export const ButtonConnect = styled(Button)` + width: ${rem(176)}; + z-index: 10; + background-color: var(--theme-blue-color); + :hover { + background-color: var(--theme-blue400-color); + } `; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index f6709840..6062c17e 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { mode } from '../../modules/env'; @@ -10,12 +10,24 @@ import { Header } from '../Header'; import { Swap } from '../Swap'; import { CookieConsentHandler } from './CookieConsentHandler'; -import { SwapContainer } from './styled'; +import { SwapContainer, LayoutBody } from './styled'; type Props = { children: React.ReactNode }; +export type NavHandlerProps = { + navOpen: boolean; + toggleNav: (e: React.MouseEvent) => void; +}; + export const Layout = ({ children }: Props) => { const dispatch = useDispatch(); + const [navOpen, setNavOpen] = useState(false); + + const toggleNav = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setNavOpen(!navOpen); + }; useEffect(() => { (async () => { @@ -28,13 +40,15 @@ export const Layout = ({ children }: Props) => { <> -
+
- - - + + + + - {children} + {children} + diff --git a/src/components/Layout/styled.tsx b/src/components/Layout/styled.tsx index 73f1c784..ae20f9ff 100644 --- a/src/components/Layout/styled.tsx +++ b/src/components/Layout/styled.tsx @@ -1,6 +1,10 @@ import { rem } from 'polished'; import styled from 'styled-components'; +import { StylingConstants } from '../../modules/styles'; + +const { media } = StylingConstants; + export const SwapContainer = styled.div` max-width: ${rem(847)}; display: flex; @@ -9,3 +13,23 @@ export const SwapContainer = styled.div` margin: 0 auto; padding-top: ${rem(70)}; `; + +export const LayoutBody = styled.div<{ open: boolean }>` + transition: all 0.2s linear; + + @media (min-width: ${rem(media.md)}) { + margin-left: ${({ open }) => (open ? '216px' : '72px')}; + } +`; + +export const AppLogoLink = styled.a` + color: inherit; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + + > svg { + height: 1.5em; + } +`; diff --git a/src/components/Search/styled.tsx b/src/components/Search/styled.tsx index ef109138..e390bf7f 100644 --- a/src/components/Search/styled.tsx +++ b/src/components/Search/styled.tsx @@ -13,6 +13,7 @@ export const SearchInput = styled(TextInput)` padding-left: ${({ theme }) => rem(theme.pulsar.size.closet)}; padding-right: ${({ theme }) => rem(theme.pulsar.size.closet)}; font-size: ${({ theme }) => rem(theme.pulsar.size.house)}; + @media (min-width: ${rem(media.sm)}) { width: ${rem(250)}; font-size: ${({ theme }) => rem(theme.pulsar.size.room)}; @@ -34,6 +35,6 @@ export const SearchInput = styled(TextInput)` `; export const SearchIcon = styled(Icon.Search)` - color: ${({ theme }) => theme.pulsar.color.primary.normal}; + color: var(--theme-blue-color); font-size: ${({ theme }) => rem(theme.pulsar.size.street)}; `; diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx new file mode 100644 index 00000000..4c931b68 --- /dev/null +++ b/src/components/Sidebar/index.tsx @@ -0,0 +1,227 @@ +import { Icon, Testable } from '@swingby-protocol/pulsar'; +import { useRouter } from 'next/router'; +import { LocaleSwitcher, ThemeSwitcher } from '@swingby-protocol/header'; +import { useIntl } from 'react-intl'; +import { useCallback, useEffect, useState, useMemo } from 'react'; + +import { useThemeSettings } from '../../modules/store/settings'; +import { NavHandlerProps } from '../Layout'; + +import { + SidebarInput, + SidebarToggle, + SidebarToggleMobile, + SidebarContainer, + SidebarActionContainer, + AppLogoLink, + MenuContainer, + MenuItemContainer, + MenuItemAnchor, +} from './styled'; + +type Props = { + productName?: string; + logoHref?: string; + barItems?: React.ReactNode; + items?: Array< + { render: React.ReactNode; icon: React.ReactNode; key: string } & Pick< + React.ComponentPropsWithoutRef, + 'onClick' | 'target' | 'href' + > + > | null; +} & NavHandlerProps; + +type MenuItemProps = { children?: React.ReactNode; isActive?: boolean } & Testable & + Pick, 'onClick' | 'target' | 'href'>; + +const TOGGLE_ID = 'sidebar-menu-toggle'; +export const DEFAULT_ITEMS: Props['items'] = [ + { + render: 'Explorer', + icon: , + key: 'explorer', + href: 'https://skybridge.info', + }, + { + render: 'Liquidity', + icon: , + key: 'liquidity', + href: 'https://skybridge.info/pool', + }, + { + render: 'Farm', + icon: , + key: 'farm', + href: 'https://farm.swingby.network', + }, + { + render: 'Metanodes', + icon: , + key: 'metanodes', + href: 'https://skybridge.info/metanodes', + }, + { + render: 'ERC20 Bridge', + icon: , + key: 'erc20-bridge', + href: 'https://bridge.swingby.network', + }, + { + render: 'DAO', + icon: , + key: 'dao', + href: 'https://dao.swingby.network/', + }, +]; + +const MenuItem = ({ + children, + href, + onClick, + target, + isActive = false, + 'data-testid': testId, +}: MenuItemProps) => { + return ( + + {href ? ( + + {children} + + ) : ( + children + )} + + ); +}; + +export const Sidebar = ({ navOpen, toggleNav, items: itemsParam = DEFAULT_ITEMS }: Props) => { + const [href, setHref] = useState( + (() => { + try { + return window.location.href; + } catch (e) { + return null; + } + })(), + ); + + const { locale } = useIntl(); + + const items = useMemo((): typeof itemsParam => { + if (!itemsParam) return null; + if (!locale) return itemsParam; + + return itemsParam.map((it) => { + if (it === DEFAULT_ITEMS[0]) { + return { ...it, href: `https://skybridge.info/${locale}` }; + } + + if (it === DEFAULT_ITEMS[1]) { + return { ...it, href: `https://skybridge.info/${locale}/pool` }; + } + + if (it === DEFAULT_ITEMS[2]) { + return { ...it, href: `https://farm.swingby.network/${locale}` }; + } + + if (it === DEFAULT_ITEMS[3]) { + return { ...it, href: `https://skybridge.info/${locale}/metanodes` }; + } + + if (it === DEFAULT_ITEMS[4]) { + return { ...it, href: `https://bridge.swingby.network/${locale}` }; + } + + if (it === DEFAULT_ITEMS[5]) { + return { ...it, href: `https://dao.swingby.network/${locale}` }; + } + + return it; + }); + }, [itemsParam, locale]); + + const currentItem = useMemo(() => { + if (!href) return null; + if (!items) return null; + const results = items + .filter((it) => { + if (!it.href) return false; + return href.toLowerCase().startsWith(it.href.toLowerCase()); + }) + .sort((a, b) => b.href!.length - a.href!.length); + + return results[0] ?? null; + }, [href, items]); + + useEffect(() => { + if (typeof document === 'undefined') return; + const body = document.querySelector('body'); + if (!body) return; + + const observer = new MutationObserver((mutations) => { + mutations.forEach(() => { + setHref(document.location.href); + }); + }); + + observer.observe(body, { childList: true, subtree: true }); + return () => { + observer.disconnect(); + }; + }, []); + + const { push, asPath, locales } = useRouter(); + const [theme, setTheme] = useThemeSettings(); + + const changeLocale = useCallback((locale: string) => push(asPath, null, { locale }), [ + push, + asPath, + ]); + + return ( + <> + + + + + {navOpen ? : } + + + + + + + {navOpen ? ( + + ) : ( + + )} + + + {!!items && items.length > 0 && ( + + {items.map((it) => ( + + {it.icon} + {navOpen && {it.render}} + + ))} + + )} + + + + {navOpen && } + + + + ); +}; diff --git a/src/components/Sidebar/styled.tsx b/src/components/Sidebar/styled.tsx new file mode 100644 index 00000000..8ca0ee4f --- /dev/null +++ b/src/components/Sidebar/styled.tsx @@ -0,0 +1,180 @@ +import styled, { css } from 'styled-components'; +import { rem, transitions } from 'polished'; + +import { StylingConstants } from '../../modules/styles'; + +const { media } = StylingConstants; + +export const SidebarInput = styled.input<{ open: boolean }>` + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + visibility: hidden; + opacity: 0; + z-index: 10; + + @media (max-width: ${rem(media.md - 1)}) { + visibility: ${({ open }) => (open ? 'visible' : 'hidden')}; + } +`; + +export const SidebarToggle = styled.label` + position: absolute; + top: 24px; + right: -12px; + width: 24px; + height: 24px; + line-height: 24px; + text-align: center; + border-radius: 50%; + border: 1px solid ${({ theme }) => theme.pulsar.color.border.normal}; + cursor: pointer; + + svg { + width: 0.7em; + height: 0.7em; + } + + @media (max-width: ${rem(media.md - 1)}) { + display: none; + } +`; + +export const SidebarToggleMobile = styled.label` + position: absolute; + top: 24px; + left: 24px; + width: 24px; + height: 24px; + line-height: 24px; + text-align: center; + cursor: pointer; + + @media (min-width: ${rem(media.md)}) { + display: none; + } +`; + +export const SidebarContainer = styled.aside<{ open: boolean }>` + background-color: var(--theme-card-color); + box-shadow: var(--theme-card-shadow); + width: ${({ open }) => (open ? '216px' : '72px')}; + border-right: 1px solid ${({ theme }) => theme.pulsar.color.border.normal}; + padding: ${({ theme }) => `${rem(theme.pulsar.size.street)} ${rem(theme.pulsar.size.closet)}`}; + position: fixed; + top: 0; + left: 0; + height: 100vh; + transition: all 0.2s linear; + display: flex; + flex-direction: column; + align-items: center; + z-index: 20; + + @media (max-width: ${rem(media.md - 1)}) { + width: 275px; + left: ${({ open }) => (open ? '0' : '-100%')}; + + &:before { + ${({ open }) => + open && + ` + content: ''; + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + background-color: rgba(0,0,0,0.3); + z-index: -1; + `} + } + } +`; + +export const SidebarActionContainer = styled.div` + display: flex; + flex-direction: row; + + > * + * { + margin-left: ${({ theme }) => rem(theme.pulsar.size.closet)}; + } +`; + +export const AppLogoLink = styled.a` + color: inherit; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + align-self: flex-start; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: ${({ theme }) => rem(theme.pulsar.size.closet)}; + padding-right: ${({ theme }) => rem(theme.pulsar.size.closet)}; + + > svg { + height: 1.5em; + } + + @media (max-width: ${rem(media.md - 1)}) { + margin-left: 40px; + } +`; + +export const MenuContainer = styled.ol` + flex: 1; + width: 100%; + padding: 0; + list-style: none; +`; + +export const MenuItemContainer = styled.li` + display: flex; + flex-direction: column; + align-items: stretch; + padding: ${({ theme }) => rem(theme.pulsar.size.house)} 0; +`; + +const activeAnchor = css` + color: ${({ theme }) => theme.pulsar.color.primary.normal}; +`; + +export const MenuItemAnchor = styled.a<{ isActive: boolean }>` + display: flex; + flex-direction: row; + align-items: center; + margin: ${({ theme }) => rem(-theme.pulsar.size.house)} 0; + padding-top: ${({ theme }) => rem(theme.pulsar.size.house)}; + padding-bottom: ${({ theme }) => rem(theme.pulsar.size.house)}; + padding-left: ${({ theme }) => rem(theme.pulsar.size.closet)}; + padding-right: ${({ theme }) => rem(theme.pulsar.size.closet)}; + font-size: ${({ theme }) => rem(theme.pulsar.size.closet)}; + font-weight: 500; + text-decoration: none; + color: inherit; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + ${({ theme }) => transitions(['background'], theme.pulsar.duration.normal)}; + ${({ isActive }) => isActive && activeAnchor}; + + :hover { + background: ${({ theme }) => theme.pulsar.color.bg.hover}; + } + + :has(span) { + > svg { + margin-right: ${({ theme }) => rem(theme.pulsar.size.house)}; + } + } + + > svg { + font-size: 20px; + flex-shrink: 0; + } +`; diff --git a/src/modules/scenes/Common/ExplorerMain/styled.tsx b/src/modules/scenes/Common/ExplorerMain/styled.tsx index 7654c924..232c8592 100644 --- a/src/modules/scenes/Common/ExplorerMain/styled.tsx +++ b/src/modules/scenes/Common/ExplorerMain/styled.tsx @@ -11,13 +11,9 @@ interface ThemeProps { const { media } = StylingConstants; export const ExplorerMainContainer = styled.div` position: relative; - height: ${rem(250)}; - background: ${({ theme }) => theme.pulsar.color.bg.normal}; - background-image: url(${logos.StarsBgAnimated}); z-index: 0; background-size: ${(props) => (props.isLightTheme ? '80%' : '55%')}; @media (min-width: ${rem(media.xs)}) { - height: ${rem(240)}; display: grid; width: 100%; } @@ -31,7 +27,6 @@ export const ExplorerMainContainer = styled.div` padding-right: ${({ theme }) => rem(theme.pulsar.size.street)}; } @media (min-width: ${rem(media.lg)}) { - height: ${rem(274)}; padding-left: ${({ theme }) => rem(theme.pulsar.size.closet)}; padding-right: ${({ theme }) => rem(theme.pulsar.size.closet)}; padding-top: ${({ theme }) => rem(theme.pulsar.size.country)}; diff --git a/src/modules/scenes/Common/index.tsx b/src/modules/scenes/Common/index.tsx index edb1b4fb..8ed12edb 100644 --- a/src/modules/scenes/Common/index.tsx +++ b/src/modules/scenes/Common/index.tsx @@ -1,4 +1,29 @@ +import { + IconInfo, + Atag, + IconArrowLeft, + LineBox, + TextDanger, + TextSecondary, + TextPrimary, + TextBlock, + TextEllipsis, + SizeS, + SizeM, + SizeL, + ButtonScale, + IconBack, + TextEstimated, + TextChosenFilter, + AddressLinkP, + TextRoom, + IconExternalLink, + ColumnInlineBlock, + ButtonScaleNarrow, +} from './Styled'; + export { ExplorerMain } from './ExplorerMain'; + export { IconInfo, Atag, @@ -21,4 +46,4 @@ export { IconExternalLink, ColumnInlineBlock, ButtonScaleNarrow, -} from './Styled'; +}; diff --git a/src/modules/scenes/Main/Explorer/Browser/styled.tsx b/src/modules/scenes/Main/Explorer/Browser/styled.tsx index cb8b2f13..5c99ae0a 100644 --- a/src/modules/scenes/Main/Explorer/Browser/styled.tsx +++ b/src/modules/scenes/Main/Explorer/Browser/styled.tsx @@ -26,6 +26,8 @@ export const BrowserContainer = styled.div` `; export const BrowserDiv = styled(Card)` + background-color: var(--theme-card-color); + box-shadow: var(--theme-card-shadow); padding-bottom: ${({ theme }) => rem(theme.pulsar.size.drawer)}; @media (min-width: ${rem(media.lg)}) { padding-top: ${({ theme }) => rem(theme.pulsar.size.town)}; @@ -34,7 +36,6 @@ export const BrowserDiv = styled(Card)` padding-bottom: ${({ theme }) => rem(theme.pulsar.size.closet)}; } @media (min-width: ${rem(media.xl)}) { - width: ${rem(1188)}; padding-left: ${({ theme }) => rem(theme.pulsar.size.town)}; padding-right: ${({ theme }) => rem(theme.pulsar.size.town)}; padding-bottom: ${({ theme }) => rem(theme.pulsar.size.house)}; @@ -44,8 +45,7 @@ export const BrowserDiv = styled(Card)` export const Top = styled.div` @media (min-width: ${rem(media.lg)}) { display: grid; - grid-template-columns: auto auto; - grid-template-rows: 1fr; + grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: start; } padding-top: 0; diff --git a/src/modules/scenes/Main/Explorer/FloatVolume/styled.tsx b/src/modules/scenes/Main/Explorer/FloatVolume/styled.tsx index f6d5bfe9..5a60993b 100644 --- a/src/modules/scenes/Main/Explorer/FloatVolume/styled.tsx +++ b/src/modules/scenes/Main/Explorer/FloatVolume/styled.tsx @@ -23,6 +23,7 @@ export const FloatVolumeContainer = styled.div` } @media (min-width: ${rem(media.lg)}) { grid-area: auto; + grid-column: span 4 / span 4; padding-top: 0; padding-bottom: 0; padding-right: 0; @@ -53,7 +54,6 @@ export const CoinContainer = styled.div` grid-column-gap: ${({ theme }) => rem(theme.pulsar.size.room)}; } @media (min-width: ${rem(media.xl)}) { - padding-right: ${({ theme }) => rem(theme.pulsar.size.city)}; grid-column-gap: ${({ theme }) => rem(theme.pulsar.size.street)}; } `; diff --git a/src/modules/scenes/Main/Explorer/StatsInfo/styled.tsx b/src/modules/scenes/Main/Explorer/StatsInfo/styled.tsx index 0a36a23c..5833929a 100644 --- a/src/modules/scenes/Main/Explorer/StatsInfo/styled.tsx +++ b/src/modules/scenes/Main/Explorer/StatsInfo/styled.tsx @@ -15,6 +15,7 @@ export const StatsInfoContainer = styled.div` } @media (min-width: ${rem(media.lg)}) { grid-area: auto; + grid-column: span 8 / span 8; border-left: 1px solid #cecddc; padding-right: 0; padding-left: ${({ theme }) => rem(theme.pulsar.size.house)}; @@ -147,36 +148,32 @@ export const Left = styled.div` margin-right: ${({ theme }) => rem(theme.pulsar.size.box)}; } `; -export const Right = styled.div``; +export const Right = styled.div` + flex: 1; +`; export const ChartBox = styled.div` - width: ${rem(176)}; height: ${rem(60)}; @media (min-width: ${rem(media.xs)}) { - width: ${rem(186)}; height: ${rem(64)}; } @media (min-width: ${rem(media.md)}) { - width: ${rem(200)}; height: ${rem(70)}; } @media (min-width: ${rem((media.md + media.lg) / 2)}) { - width: ${rem(258)}; height: ${rem(90)}; } @media (min-width: ${rem(media.lg)}) { - width: ${rem(170)}; height: ${rem(76)}; } @media (min-width: ${rem(media.xl)}) { - width: ${rem(220)}; height: ${rem(76)}; } `; export const StatsWithoutChart = styled.div` display: flex; - justify-content: space-around; + justify-content: space-between; `; export const DataRow = styled.div` diff --git a/src/modules/store/settings/index.tsx b/src/modules/store/settings/index.tsx index a10b3f5d..43671a50 100644 --- a/src/modules/store/settings/index.tsx +++ b/src/modules/store/settings/index.tsx @@ -1,3 +1,5 @@ export { settings } from './reducer'; export { useThemeSettings } from './useThemeSettings'; +export { useMatchMedia } from './useMatchMedia'; +export { useSystemTheme } from './useSystemTheme'; export { settingsSelector, themeSelector } from './selectors'; diff --git a/src/modules/store/settings/useMatchMedia.tsx b/src/modules/store/settings/useMatchMedia.tsx new file mode 100644 index 00000000..7ff2eda6 --- /dev/null +++ b/src/modules/store/settings/useMatchMedia.tsx @@ -0,0 +1,46 @@ +import { useState, useEffect, useLayoutEffect as _useLayoutEffect } from 'react'; + +const useLayoutEffect = typeof window !== 'undefined' ? _useLayoutEffect : useEffect; + +export const useMatchMedia = ({ + defaultValue = false, + query, +}: { + defaultValue?: boolean; + query: string; +}) => { + const [value, setValue] = useState(defaultValue); + + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) { + return; + } + + const media = window.matchMedia(query); + const listener = ({ matches }: MediaQueryListEventMap['change']) => { + setValue(matches); + }; + + if (media.addEventListener) { + media.addEventListener('change', listener); + return () => { + media.removeEventListener('change', listener); + }; + } + + media.addListener(listener); + return () => { + media.removeListener(listener); + }; + }, [query]); + + useLayoutEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) { + return; + } + + setValue(window.matchMedia(query).matches); + }, [query]); + + return value; +}; diff --git a/src/modules/store/settings/useSystemTheme.tsx b/src/modules/store/settings/useSystemTheme.tsx new file mode 100644 index 00000000..d43bc8d5 --- /dev/null +++ b/src/modules/store/settings/useSystemTheme.tsx @@ -0,0 +1,12 @@ +import { useMatchMedia } from './useMatchMedia'; + +export const useSystemTheme = ({ + defaultTheme = 'light', +}: { defaultTheme?: 'light' | 'dark' } = {}) => { + const perfersDark = useMatchMedia({ + query: '(prefers-color-scheme: dark)', + defaultValue: defaultTheme === 'dark', + }); + + return perfersDark ? 'dark' : 'light'; +}; diff --git a/src/pages/style.css b/src/pages/style.css index 138363ac..f920dd2d 100644 --- a/src/pages/style.css +++ b/src/pages/style.css @@ -5,5 +5,129 @@ /* Memo: To fix stars background bug */ body { + background: var(--theme-body-color); + color: var(--theme-primary-color); + font-family: var(--font-family); overflow-x: hidden; } + +:root { + /* --font-family: Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; */ + --font-family: sans-serif; + --font-size-h1: 32px; + --font-size-h2: 24px; + --font-size-h3: 18px; + --font-size-p1: 16px; + --font-size-p2: 14px; + --font-size-lb1: 14px; + --font-size-lb2: 12px; + --font-size-sm: 12px; + + --font-h1: var(--font-size-h1) / 1.25 var(--font-family); + --font-h1-bold: bold var(--font-h1); + --font-h1-semibold: 600 var(--font-h1); + --font-h2: var(--font-size-h2) / 1.3333333333 var(--font-family); + --font-h2-bold: bold var(--font-h2); + --font-h2-semibold: 600 var(--font-h2); + --font-h3: var(--font-size-h3) / 1.3333333333 var(--font-family); + --font-h3-bold: bold var(--font-h3); + --font-h3-semibold: 600 var(--font-h3); + --font-p1: var(--font-size-p1) / 1.5 var(--font-family); + --font-p1-bold: bold var(--font-p1); + --font-p1-semibold: 600 var(--font-p1); + --font-p2: var(--font-size-p2) / 1.7142857143 var(--font-family); + --font-p2-bold: bold var(--font-p2); + --font-p2-semibold: 600 var(--font-p2); + --font-lb1: var(--font-size-lb1) / 1.7142857143 var(--font-family); + --font-lb1-bold: bold var(--font-lb1); + --font-lb1-semibold: 600 var(--font-lb1); + --font-lb2: var(--font-size-lb2) / 1.3333333333 var(--font-family); + --font-lb2-bold: bold var(--font-lb2); + --font-lb2-semibold: 600 var(--font-lb2); + --font-sm: var(--font-size-sm) / 1.3333333333 var(--font-family); + --font-sm-bold: bold var(--font-sm); + --font-sm-semibold: 600 var(--font-sm); + + --theme-body-color: #f8f8f9; /* grey50 */ + --theme-card-color: #fff; + --theme-overlay-color: #fff; + --theme-mask-color: rgba(19, 32, 43, 0.8); + --theme-border-color: #e3e4e6; /* grey100 */ + --theme-border-color-rgb: 227, 228, 230; + --theme-icon-color: #aaafb3; /* grey300 */ + --theme-icon-hover-color: #060a0d; /* grey900 */ + + --theme-primary-color: #060a0d; /* grey900 */ + --theme-secondary-color: #717980; /* grey500 */ + --theme-secondary-color-rgb: 113, 121, 128; /* grey500 */ + --theme-default-color: #aaafb3; /* grey300 */ + + --theme-card-shadow: 0px 0px 0px 1px rgba(6, 10, 13, 0.03), 0px 2px 4px rgba(6, 10, 13, 0.04); + --theme-modal-shadow: 0 0 0.5px rgba(6, 10, 13, 0.4), 0 16px 32px rgba(19, 32, 43, 0.4); + --theme-overlay-shadow: 0 0 0.5px rgba(0, 0, 0, 0.4), 0 32px 64px rgba(113, 121, 128, 0.32); + + --theme-input-background: #fff; + --theme-input-placeholder: #aaafb3; + + --theme-red-color: #ff4339; /* red500 */ + --theme-red-color-rgb: 255, 67, 57; /* red500 */ + --theme-red400-color: #ff6961; + --theme-red400-color-rgb: 255, 105, 97; + --theme-red600-color: #cc362e; + --theme-red600-color-rgb: 204, 54, 46; + + --theme-green-color: #00d395; /* green500 */ + --theme-green-color-rgb: 0, 211, 149; /* green500 */ + + --theme-green700-color: #007f59; + + --theme-blue-color: #4f6ae5; /* blue500 */ + --theme-blue-color-rgb: 79, 106, 230; /* blue500 */ + --theme-blue400-color: #7288ea; + + --theme-yellow-color: #cca74d; /* yellow600 */ + --theme-yellow-color-rgb: 204, 167, 77; /* yellow600 */ + + --theme-purple-color: #a26ee3; /* purple500 */ + --theme-purple-color-rgb: 162, 110, 227; /* purple500 */ + --theme-purple700-color: #614288; + + --theme-grey-color: #c6c9cc; + --theme-grey-color-rgb: 198, 201, 204; + --theme-grey200-color: #c6c9cc; + --theme-grey300-color: #aaafb3; + --theme-grey900-color: #060a0d; + + --theme-white-color: #fff; + + --horizontal-padding: 64px; +} + +@media (max-width: 768px) { + :root { + --horizontal-padding: 24px; + } +} + +[data-theme='dark'] { + --theme-body-color: #202529; + --theme-card-color: #282c30; + --theme-overlay-color: #36393d; + --theme-mask-color: rgba(32, 37, 41, 0.8); + --theme-border-color: #43484d; + --theme-icon-color: #606268; + --theme-icon-hover-color: #fff; + + --theme-primary-color: #fff; + --theme-secondary-color: #939496; + --theme-default-color: #939496; + + --theme-card-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.08), 0px 2px 4px rgba(0, 0, 0, 0.16); + --theme-modal-shadow: 0 0 0.5px #fff, 0 32px 64px rgba(0, 0, 0, 0.32); + --theme-overlay-shadow: 0 0 0.5px #fff, 0 32px 64px rgba(0, 0, 0, 0.32); + + --theme-input-background: #202529; + --theme-input-placeholder: #606268; + + --theme-grey-color: #606268; +}