dark mode with css variables instead

This commit is contained in:
keyan 2021-11-09 16:43:56 -06:00
parent c6e6ddfa65
commit a3544fb67f
12 changed files with 219 additions and 205 deletions

View File

@ -3,7 +3,6 @@
"next/babel" "next/babel"
], ],
"plugins": [ "plugins": [
"styled-components",
[ [
"inline-react-svg", "inline-react-svg",
{ {

View File

@ -13,7 +13,6 @@ import { useMe } from './me'
import CommentEdit from './comment-edit' import CommentEdit from './comment-edit'
import Countdown from './countdown' import Countdown from './countdown'
import { NOFOLLOW_LIMIT } from '../lib/constants' import { NOFOLLOW_LIMIT } from '../lib/constants'
import styled from 'styled-components'
function Parent ({ item, rootText }) { function Parent ({ item, rootText }) {
const ParentFrag = () => ( const ParentFrag = () => (
@ -40,10 +39,6 @@ function Parent ({ item, rootText }) {
) )
} }
const StyledComment = styled.div`
background-color: ${({ theme, includeParent }) => includeParent ? undefined : theme.commentBg};
`
export default function Comment ({ export default function Comment ({
item, children, replyOpen, includeParent, item, children, replyOpen, includeParent,
rootText, noComments, noReply rootText, noComments, noReply
@ -69,8 +64,7 @@ export default function Comment ({
const op = item.root.user.name === item.user.name const op = item.root.user.name === item.user.name
return ( return (
<StyledComment <div
includeParent={includeParent}
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse ? styles.collapsed : ''}`} ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse ? styles.collapsed : ''}`}
> >
<div className={`${itemStyles.item} ${styles.item}`}> <div className={`${itemStyles.item} ${styles.item}`}>
@ -162,7 +156,7 @@ export default function Comment ({
: null} : null}
</div> </div>
</div> </div>
</StyledComment> </div>
) )
} }

View File

@ -72,6 +72,7 @@
border-radius: .4rem; border-radius: .4rem;
padding-top: .5rem; padding-top: .5rem;
padding-left: .2rem; padding-left: .2rem;
background-color: var(--theme-commentBg);
} }
.comment:not(:last-child) { .comment:not(:last-child) {

View File

@ -8,9 +8,9 @@ import Github from '../svgs/github-fill.svg'
import Twitter from '../svgs/twitter-fill.svg' import Twitter from '../svgs/twitter-fill.svg'
import Link from 'next/link' import Link from 'next/link'
import useDarkMode from 'use-dark-mode' import useDarkMode from 'use-dark-mode'
import styled from 'styled-components'
import Sun from '../svgs/sun-fill.svg' import Sun from '../svgs/sun-fill.svg'
import Moon from '../svgs/moon-fill.svg' import Moon from '../svgs/moon-fill.svg'
import { handleThemeChange } from '../public/darkmode'
const ChatPopover = ( const ChatPopover = (
<Popover> <Popover>
@ -32,16 +32,6 @@ const ChatPopover = (
</Popover> </Popover>
) )
const ContrastLink = styled.a`
color: ${({ theme }) => theme.color};
&:hover {
color: ${({ theme }) => theme.color};
}
& svg {
fill: ${({ theme }) => theme.color};
}
`
export default function Footer ({ noLinks }) { export default function Footer ({ noLinks }) {
const query = gql` const query = gql`
{ {
@ -53,7 +43,7 @@ export default function Footer ({ noLinks }) {
const darkMode = useDarkMode(false, { const darkMode = useDarkMode(false, {
// set this so it doesn't try to use classes // set this so it doesn't try to use classes
onChange: () => { } onChange: handleThemeChange
}) })
return ( return (
@ -108,9 +98,9 @@ export default function Footer ({ noLinks }) {
/> />
</div>} </div>}
<small> <small>
<ContrastLink className='d-inline-block' href='https://github.com/stackernews/stacker.news'> <a className={`d-inline-block ${styles.contrastLink}`} href='https://github.com/stackernews/stacker.news'>
This is free open source software<Github width={20} height={20} className='mx-1' /> This is free open source software<Github width={20} height={20} className='mx-1' />
</ContrastLink> </a>
<span className='d-inline-block text-muted'> <span className='d-inline-block text-muted'>
made with sound love in Austin<Texas className='mx-1' width={20} height={20} /> made with sound love in Austin<Texas className='mx-1' width={20} height={20} />
by<a href='https://twitter.com/k00bideh' className='text-twitter d-inline-block'><Twitter width={20} height={20} className='ml-1' />@k00bideh</a> by<a href='https://twitter.com/k00bideh' className='text-twitter d-inline-block'><Twitter width={20} height={20} className='ml-1' />@k00bideh</a>

View File

@ -9,3 +9,13 @@
.connect:hover { .connect:hover {
opacity: 1; opacity: 1;
} }
.contrastLink {
color: var(--theme-color);
}
.contrastLink:hover {
color: var(--theme-color);
}
.contrastLink svg {
fill: var(--theme-color);
}

View File

@ -11,48 +11,6 @@ import { signOut, signIn, useSession } from 'next-auth/client'
import { useLightning } from './lightning' import { useLightning } from './lightning'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { randInRange } from '../lib/rand' import { randInRange } from '../lib/rand'
import styled from 'styled-components'
const Brand = styled(Navbar.Brand)`
color: ${({ theme }) => theme.brandColor}
`
export const StyledNavbar = styled(Navbar).attrs(({ theme }) => ({
variant: theme.navbarVariant,
className: styles.navbar
}))`
& .dropdown-menu {
background-color: ${({ theme }) => theme.body};
border: 1px solid ${({ theme }) => theme.borderColor};
}
& .dropdown-item {
color: ${({ theme }) => theme.dropdownItemColor};
}
& .dropdown-item:hover {
color: ${({ theme }) => theme.dropdownItemColorHover};
}
& .dropdown-item.active {
color: ${({ theme }) => theme.brandColor};
text-shadow: 0 0 10px var(--primary);
}
& .dropdown-divider {
border-top: 1px solid ${({ theme }) => theme.borderColor};
}
& .theme {
margin-right: 1rem;
cursor: pointer;
fill: ${({ theme }) => theme.dropdownItemColor};
}
& .theme:hover {
fill: ${({ theme }) => theme.dropdownItemColorHover};
}
`
function WalletSummary ({ me }) { function WalletSummary ({ me }) {
return `${me?.sats} \\ ${me?.stacked}` return `${me?.sats} \\ ${me?.stacked}`
@ -172,16 +130,16 @@ export default function Header () {
return ( return (
<> <>
<Container className='px-sm-0'> <Container className='px-sm-0'>
<StyledNavbar> <Navbar>
<Nav <Nav
className={styles.navbarNav} className={styles.navbarNav}
activeKey={path} activeKey={path}
> >
<Link href='/' passHref> <Link href='/' passHref>
<Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Brand> <Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
</Link> </Link>
<Link href='/' passHref> <Link href='/' passHref>
<Brand className={`${styles.brand} d-block d-sm-none`}>SN</Brand> <Navbar.Brand className={`${styles.brand} d-block d-sm-none`}>SN</Navbar.Brand>
</Link> </Link>
<Nav.Item className='d-md-flex d-none nav-dropdown-toggle'> <Nav.Item className='d-md-flex d-none nav-dropdown-toggle'>
<SplitButton <SplitButton
@ -217,7 +175,7 @@ export default function Header () {
</Nav.Item> </Nav.Item>
<Corner /> <Corner />
</Nav> </Nav>
</StyledNavbar> </Navbar>
</Container> </Container>
</> </>
) )
@ -227,6 +185,7 @@ export function HeaderPreview () {
return ( return (
<> <>
<Container className='px-sm-0'> <Container className='px-sm-0'>
{/* still need to set variant */}
<Navbar className={styles.navbar}> <Navbar className={styles.navbar}>
<Nav className='w-100 justify-content-between flex-wrap align-items-center'> <Nav className='w-100 justify-content-between flex-wrap align-items-center'>
<Link href='/' passHref> <Link href='/' passHref>

View File

@ -6,6 +6,7 @@
margin-bottom: -.3rem; margin-bottom: -.3rem;
margin-right: 0; margin-right: 0;
text-shadow: 0 0 10px #FADA5E; text-shadow: 0 0 10px #FADA5E;
color: var(--theme-brandColor) !important;
} }
.navLink { .navLink {

View File

@ -7,92 +7,10 @@ import PlausibleProvider from 'next-plausible'
import { LightningProvider } from '../components/lightning' import { LightningProvider } from '../components/lightning'
import { ItemActModal, ItemActProvider } from '../components/item-act' import { ItemActModal, ItemActProvider } from '../components/item-act'
import getApolloClient from '../lib/apollo' import getApolloClient from '../lib/apollo'
import { createGlobalStyle, ThemeProvider } from 'styled-components'
import useDarkMode from 'use-dark-mode'
const GlobalStyle = createGlobalStyle`
body {
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.color};
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
color: inherit;
background-color: ${({ theme }) => theme.inputBg};
border-color: ${({ theme }) => theme.borderColor};
border-bottom-color: ${({ theme }) => theme.inputBg};
}
.form-control {
background-color: ${({ theme }) => theme.inputBg};
color: ${({ theme }) => theme.color};
border-color: ${({ theme }) => theme.borderColor};
}
.form-control:focus {
background-color: ${({ theme }) => theme.inputBg};
color: ${({ theme }) => theme.color};
}
.form-control:disabled, .form-control[readonly] {
background-color: ${({ theme }) => theme.inputBg};
border-color: ${({ theme }) => theme.borderColor};
opacity: 1;
}
.clickToContext {
border-radius: .4rem;
padding: .2rem 0;
cursor: pointer;
}
.clickToContext:hover {
background-color: ${({ theme }) => theme.clickToContextColor};
}
.fresh {
background-color: ${({ theme }) => theme.clickToContextColor};
border-radius: .4rem;
}
.modal-content {
background-color: ${({ theme }) => theme.body};
border-color: ${({ theme }) => theme.borderColor};
}
`
const lightTheme = {
body: '#f5f5f5',
color: '#212529',
navbarVariant: 'light',
borderColor: '#ced4da',
inputBg: '#ffffff',
dropdownItemColor: 'inherit',
dropdownItemColorHover: 'rgba(0, 0, 0, 0.9)',
commentBg: 'rgba(0, 0, 0, 0.03)',
clickToContextColor: 'rgba(0, 0, 0, 0.05)',
brandColor: 'rgba(0, 0, 0, 0.9)'
}
const darkTheme = {
body: '#000000',
inputBg: '#000000',
navbarVariant: 'dark',
borderColor: 'rgb(255 255 255 / 50%)',
dropdownItemColor: 'rgba(255, 255, 255, 0.7)',
dropdownItemColorHover: 'rgba(255, 255, 255, 0.9)',
commentBg: 'rgba(255, 255, 255, 0.04)',
clickToContextColor: 'rgba(255, 255, 255, 0.08)',
color: '#f8f9fa',
brandColor: 'var(--primary) !important'
}
function MyApp ({ Component, pageProps: { session, ...props } }) { function MyApp ({ Component, pageProps: { session, ...props } }) {
const client = getApolloClient() const client = getApolloClient()
const darkMode = useDarkMode(false, {
// set this so it doesn't try to use clas
onChange: (e) => { console.log(e) }
})
/* /*
If we are on the client, we populate the apollo cache with the If we are on the client, we populate the apollo cache with the
ssr data ssr data
@ -113,18 +31,15 @@ function MyApp ({ Component, pageProps: { session, ...props } }) {
<Provider session={session}> <Provider session={session}>
<ApolloProvider client={client}> <ApolloProvider client={client}>
<MeProvider> <MeProvider>
<ThemeProvider theme={darkMode.value ? darkTheme : lightTheme}> <LightningProvider>
<GlobalStyle /> <FundErrorProvider>
<LightningProvider> <FundErrorModal />
<FundErrorProvider> <ItemActProvider>
<FundErrorModal /> <ItemActModal />
<ItemActProvider> <Component {...props} />
<ItemActModal /> </ItemActProvider>
<Component {...props} /> </FundErrorProvider>
</ItemActProvider> </LightningProvider>
</FundErrorProvider>
</LightningProvider>
</ThemeProvider>
</MeProvider> </MeProvider>
</ApolloProvider> </ApolloProvider>
</Provider> </Provider>

View File

@ -1,30 +1,18 @@
import Document from 'next/document' import Document, { Html, Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps (ctx) { render () {
const sheet = new ServerStyleSheet() return (
const originalRenderPage = ctx.renderPage <Html>
<Head />
try { <body>
ctx.renderPage = () => <script src='darkmode.js' />
originalRenderPage({ <Main />
enhanceApp: (App) => (props) => <NextScript />
sheet.collectStyles(<App {...props} />) </body>
}) </Html>
)
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
}
} finally {
sheet.seal()
}
} }
} }
export default MyDocument

View File

@ -3,10 +3,9 @@ import Items from '../../components/items'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { getGetServerSideProps } from '../../api/ssrApollo' import { getGetServerSideProps } from '../../api/ssrApollo'
import { MORE_ITEMS } from '../../fragments/items' import { MORE_ITEMS } from '../../fragments/items'
import { Nav } from 'react-bootstrap' import { Nav, Navbar } from 'react-bootstrap'
import styles from '../../components/header.module.css' import styles from '../../components/header.module.css'
import Link from 'next/link' import Link from 'next/link'
import { StyledNavbar } from '../../components/header'
export const getServerSideProps = getGetServerSideProps(MORE_ITEMS, { sort: 'top'}) export const getServerSideProps = getGetServerSideProps(MORE_ITEMS, { sort: 'top'})
@ -16,7 +15,7 @@ export default function Index ({ data: { moreItems: { items, cursor } } }) {
return ( return (
<Layout> <Layout>
<StyledNavbar> <Navbar>
<Nav <Nav
className={styles.navbarNav} className={styles.navbarNav}
activeKey={path} activeKey={path}
@ -67,7 +66,7 @@ export default function Index ({ data: { moreItems: { items, cursor } } }) {
</Link> </Link>
</Nav.Item> </Nav.Item>
</Nav> </Nav>
</StyledNavbar> </Navbar>
<Items <Items
items={items} cursor={cursor} items={items} cursor={cursor}
variables={{ sort: 'top', within: router.query?.within }} rank variables={{ sort: 'top', within: router.query?.within }} rank

71
public/darkmode.js Normal file
View File

@ -0,0 +1,71 @@
// Insert this script in your index.html right after the <body> tag.
// This will help to prevent a flash if dark mode is the default.
export const COLORS = {
light: {
body: '#f5f5f5',
color: '#212529',
navbarVariant: 'light',
navLink: 'rgba(0, 0, 0, 0.5)',
navLinkFocus: 'rgba(0, 0, 0, 0.7)',
navLinkActive: 'rgba(0, 0, 0, 0.9)',
borderColor: '#ced4da',
inputBg: '#ffffff',
dropdownItemColor: 'inherit',
dropdownItemColorHover: 'rgba(0, 0, 0, 0.9)',
commentBg: 'rgba(0, 0, 0, 0.03)',
clickToContextColor: 'rgba(0, 0, 0, 0.05)',
brandColor: 'rgba(0, 0, 0, 0.9)'
},
dark: {
body: '#000000',
inputBg: '#000000',
navLink: 'rgba(255, 255, 255, 0.5)',
navLinkFocus: 'rgba(255, 255, 255, 0.75)',
navLinkActive: 'rgba(255, 255, 255, 0.9)',
borderColor: 'rgba(255, 255, 255, 0.5)',
dropdownItemColor: 'rgba(255, 255, 255, 0.7)',
dropdownItemColorHover: 'rgba(255, 255, 255, 0.9)',
commentBg: 'rgba(255, 255, 255, 0.04)',
clickToContextColor: 'rgba(255, 255, 255, 0.08)',
color: '#f8f9fa',
brandColor: 'var(--primary)'
}
}
export const handleThemeChange = (dark) => {
const root = window.document.documentElement
const colors = COLORS[dark ? 'dark' : 'light']
Object.entries(colors).forEach(([varName, value]) => {
const cssVarName = `--theme-${varName}`
root.style.setProperty(cssVarName, value)
})
}
if (typeof window !== 'undefined') {
(function () {
// Change these if you use something different in your hook.
const storageKey = 'darkMode'
const preferDarkQuery = '(prefers-color-scheme: dark)'
const mql = window.matchMedia(preferDarkQuery)
const supportsColorSchemeQuery = mql.media === preferDarkQuery
let localStorageTheme = null
try {
localStorageTheme = localStorage.getItem(storageKey)
} catch (err) {}
const localStorageExists = localStorageTheme !== null
if (localStorageExists) {
localStorageTheme = JSON.parse(localStorageTheme)
}
// Determine the source of truth
if (localStorageExists) {
// source of truth from localStorage
handleThemeChange(localStorageTheme)
} else if (supportsColorSchemeQuery) {
// source of truth from system
handleThemeChange(mql.matches)
localStorage.setItem(storageKey, mql.matches)
}
})()
}

View File

@ -60,12 +60,103 @@ $tooltip-bg: #5c8001;
} }
body { body {
background-image: linear-gradient(180deg, #f5f5f5, #f5f5f5, white); background: var(--theme-body);
background-attachment:fixed; color: var(--theme-color);
min-height: 100vh; min-height: 100vh;
height: 100%; height: 100%;
} }
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
color: inherit;
background-color: var(--theme-inputBg);
border-color: var(--theme-borderColor);
border-bottom-color: var(--theme-inputBg);
}
.form-control {
background-color: var(--theme-inputBg);
color: var(--theme-color);
border-color: var(--theme-borderColor);
}
.form-control:focus {
background-color: var(--theme-inputBg);
color: var(--theme-color);
}
.form-control:disabled, .form-control[readonly] {
background-color: var(--theme-inputBg);
border-color: var(--theme-borderColor);
opacity: 1;
}
.clickToContext {
border-radius: .4rem;
padding: .2rem 0;
cursor: pointer;
}
.clickToContext:hover {
background-color: var(--theme-clickToContextColor);
}
.fresh {
background-color: var(--theme-clickToContextColor);
border-radius: .4rem;
}
.modal-content {
background-color: var(--theme-body);
border-color: var(--theme-borderColor);
}
.navbar-nav .nav-link:not(.text-success) {
color: var(--theme-navLink) !important;
}
.navbar-nav .nav-link:not(.text-success):hover, .navbar-nav .nav-link:not(.text-success):focus {
color: var(--theme-navLinkFocus) !important;
}
.nav-dropdown-toggle .btn-link {
color: var(--theme-navLink) !important;
}
.nav-dropdown-toggle .btn-link[aria-expanded="true"] {
color: var(--theme-navLinkFocus) !important;
}
.navbar-nav .dropdown-menu {
background-color: var(--theme-body);
border: 1px solid var(--theme-borderColor);
}
.navbar-nav .dropdown-item {
color: var(--theme-dropdownItemColor);
}
.navbar-nav .dropdown-item:hover {
color: var(--theme-dropdownItemColorHover);
}
.navbar-nav .dropdown-item.active {
color: var(--theme-brandColor);
text-shadow: 0 0 10px var(--primary);
}
.navbar-nav .dropdown-divider {
border-top: 1px solid var(--theme-borderColor);
}
.theme {
cursor: pointer;
fill: var(--theme-dropdownItemColor);
}
.theme:hover {
fill: var(--theme-dropdownItemColorHover);
}
#__next { #__next {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -143,14 +234,6 @@ footer {
display: inline-block; display: inline-block;
} }
.nav-dropdown-toggle .btn-link {
color: rgba(0, 0, 0, 0.5) !important;
}
.nav-dropdown-toggle .btn-link[aria-expanded="true"] {
color: rgba(0, 0, 0, 0.9) !important;
}
.nav-dropdown-toggle .dropdown .btn { .nav-dropdown-toggle .dropdown .btn {
padding: 0; padding: 0;
} }
@ -200,6 +283,10 @@ footer {
font-weight: bold; font-weight: bold;
} }
.nav-link.active:not(.text-success) {
color: var(--theme-navLinkActive) !important;
}
.fill-grey { .fill-grey {
fill: grey; fill: grey;
} }