diff --git a/gatsby-config.js b/gatsby-config.js index abeeb8d7..43ca80ef 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -38,7 +38,8 @@ module.exports = { withWebp: true, linkImagesToOriginal: true, showCaptions: true, - backgroundColor: '#e7eef4' + backgroundColor: 'transparent', + disableBgImageOnAlpha: true } }, { @@ -208,6 +209,14 @@ module.exports = { exclude: ['/page/*', '/tags/*'] } }, + { + resolve: 'gatsby-plugin-use-dark-mode', + options: { + classNameDark: 'dark', + classNameLight: 'light', + minify: true + } + }, 'gatsby-plugin-webpack-size', 'gatsby-plugin-react-helmet', 'gatsby-plugin-catch-links', diff --git a/package.json b/package.json index bb1e0ed9..be0f346c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "not op_mini all" ], "dependencies": { + "classnames": "^2.2.6", "dms2dec": "^1.1.0", "fast-exif": "^1.0.1", "fraction.js": "^4.0.12", @@ -46,6 +47,7 @@ "gatsby-plugin-sitemap": "^2.2.16", "gatsby-plugin-svgr": "^2.0.2", "gatsby-plugin-typescript": "^2.1.11", + "gatsby-plugin-use-dark-mode": "^1.1.2", "gatsby-plugin-webpack-size": "^1.0.0", "gatsby-redirect-from": "^0.2.1", "gatsby-remark-autolink-headers": "^2.1.13", @@ -76,6 +78,7 @@ "remark": "^11.0.1", "remark-react": "^6.0.0", "slugify": "^1.3.4", + "use-dark-mode": "^2.3.1", "web3": "^1.2.1" }, "devDependencies": { @@ -85,6 +88,7 @@ "@svgr/webpack": "^4.3.3", "@testing-library/jest-dom": "^4.1.0", "@testing-library/react": "^9.2.0", + "@types/classnames": "^2.2.9", "@types/jest": "^24.0.18", "@types/node": "^12.7.8", "@types/react": "^16.9.4", diff --git a/src/components/Layout.module.scss b/src/components/Layout.module.scss index 7d1a9aa2..ada64bc4 100644 --- a/src/components/Layout.module.scss +++ b/src/components/Layout.module.scss @@ -22,22 +22,31 @@ ///////////////////////////////////// .document { - @include transition; - width: 100%; padding-top: ($spacer * 2); - background-color: $page-background-color; + background-color: $body-background-color; border-top: 1px solid rgba(255, 255, 255, 0.7); border-bottom: 1px solid rgba(255, 255, 255, 0.7); padding-bottom: $spacer * 2; box-shadow: 0 1px 4px rgba($brand-main, 0.1), 0 -1px 4px rgba($brand-main, 0.2); transform: translate3d(0, 0, 0); + transition: 0.4s $easing; + transition-property: transform, background; :global(.has-menu-open) & { transform: translate3d(0, ($spacer * 3), 0); } + :global(.dark) & { + background-color: $body-background-color--dark; + color: $text-color--dark; + border-top-color: darken($brand-grey, 15%); + border-bottom-color: darken($brand-grey, 15%); + box-shadow: 0 1px 4px darken($brand-main, 10%), + 0 -1px 4px darken($brand-main, 15%); + } + @media (min-width: $screen-sm) and (min-height: 500px) { margin-top: $spacer * 2.65; margin-bottom: $spacer * 19; // height of footer diff --git a/src/components/Post/PostActions.module.scss b/src/components/Post/PostActions.module.scss index 007c1210..0ba10de8 100644 --- a/src/components/Post/PostActions.module.scss +++ b/src/components/Post/PostActions.module.scss @@ -13,6 +13,10 @@ flex-wrap: wrap; justify-content: space-between; + :global(.dark) & { + background: darken($body-background-color--dark, 2%); + } + @media (min-width: $screen-md) { margin-left: -100%; margin-right: -18%; diff --git a/src/components/Post/PostImage.module.scss b/src/components/Post/PostImage.module.scss index 061c624c..da0a36e3 100644 --- a/src/components/Post/PostImage.module.scss +++ b/src/components/Post/PostImage.module.scss @@ -15,7 +15,7 @@ top: 10%; padding: $spacer / 3 $spacer; background: rgba($link-color, 0.85); - color: #fff; + color: #fff !important; text-shadow: 0 1px 0 #000; left: 0; opacity: 0; diff --git a/src/components/Post/PostTitle.module.scss b/src/components/Post/PostTitle.module.scss index bc970961..e8eb1642 100644 --- a/src/components/Post/PostTitle.module.scss +++ b/src/components/Post/PostTitle.module.scss @@ -9,6 +9,10 @@ color: $color-headings; margin-top: 0; margin-bottom: $spacer; + + :global(.dark) & { + color: $color-headings--dark; + } } .hentry__title__link { diff --git a/src/components/Search/SearchResults.module.scss b/src/components/Search/SearchResults.module.scss index fca406b1..7c0c1160 100644 --- a/src/components/Search/SearchResults.module.scss +++ b/src/components/Search/SearchResults.module.scss @@ -15,6 +15,10 @@ -webkit-overflow-scrolling: touch; height: 91vh; + :global(.dark) & { + background: rgba($body-background-color--dark, 0.95); + } + ul { @include breakoutviewport; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f3a4be52..a4f90171 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -38,7 +38,7 @@ export default function Search({ lng }: { lng: string }) { {searchOpen && ( <> - + = 2 ? '@2x' : '' +const isDarkMode = + typeof window !== 'undefined' && document.body.classList.contains('dark') + const mapbox = (mapboxId: string, accessToken: string) => ( x: string, y: string, @@ -50,7 +53,7 @@ export default function ExifMap({ zoom={zoom} height={160} attribution={false} - provider={providers['light']} + provider={isDarkMode ? providers['dark'] : providers['light']} metaWheelZoom={true} metaWheelZoomWarning={'META+wheel to zoom'} > diff --git a/src/components/atoms/Modal.module.scss b/src/components/atoms/Modal.module.scss index 3bf22e3b..49bd3945 100644 --- a/src/components/atoms/Modal.module.scss +++ b/src/components/atoms/Modal.module.scss @@ -14,6 +14,10 @@ animation: fadein 0.3s; padding: $spacer; + :global(.dark) & { + background: rgba($body-background-color--dark, 0.95); + } + @media (min-width: $screen-sm) { display: flex; align-items: flex-start; @@ -24,7 +28,7 @@ .modal__content { outline: 0; - background: transparent; + background: $body-background-color; position: relative; border-radius: $border-radius; border: 1px solid rgba($brand-grey-light, 0.4); @@ -32,6 +36,11 @@ padding: 0 $spacer / 2 $spacer / 2; max-width: 100%; + :global(.dark) & { + background: $body-background-color--dark; + box-shadow: 0 5px 30px rgba(darken($brand-main, 20%), 0.5); + } + @media (min-width: $screen-md) { max-width: $screen-sm; padding: 0 $spacer $spacer; diff --git a/src/components/molecules/Featured.module.scss b/src/components/molecules/Featured.module.scss index 5b601ee2..342fc162 100644 --- a/src/components/molecules/Featured.module.scss +++ b/src/components/molecules/Featured.module.scss @@ -35,7 +35,7 @@ text-align: right; padding: $spacer / 3; background: rgba($link-color, 0.85); - color: #fff; + color: #fff !important; text-shadow: 0 1px 0 #000; left: 0; opacity: 0; diff --git a/src/components/molecules/Menu.module.scss b/src/components/molecules/Menu.module.scss index 44bd9f56..ecbc11f1 100644 --- a/src/components/molecules/Menu.module.scss +++ b/src/components/molecules/Menu.module.scss @@ -43,5 +43,9 @@ padding: $padding-base-horizontal; display: block; text-align: center; + + :global(.dark) & { + text-shadow: 0 -1px 0 rgba(#000, 0.5); + } } } diff --git a/src/components/molecules/Menu.tsx b/src/components/molecules/Menu.tsx index 201a7a85..77c7b154 100644 --- a/src/components/molecules/Menu.tsx +++ b/src/components/molecules/Menu.tsx @@ -24,7 +24,7 @@ export default function Menu() { return ( <> - + diff --git a/src/components/molecules/ModalThanks.module.scss b/src/components/molecules/ModalThanks.module.scss index 67570bb6..5deb94d0 100644 --- a/src/components/molecules/ModalThanks.module.scss +++ b/src/components/molecules/ModalThanks.module.scss @@ -13,6 +13,10 @@ margin-bottom: $spacer / 2; color: $brand-grey; text-transform: capitalize; + + :global(.dark) & { + color: $brand-grey-light; + } } header { @@ -20,6 +24,7 @@ text-align: center; margin-bottom: $spacer; + // stylelint-disable-next-line no-descending-specificity h4 { font-size: $font-size-large; margin-top: 0; @@ -28,6 +33,10 @@ p { color: $brand-grey-light; + + :global(.dark) & { + color: $brand-grey; + } } } } diff --git a/src/components/molecules/ThemeSwitch.module.scss b/src/components/molecules/ThemeSwitch.module.scss new file mode 100644 index 00000000..4735aaa9 --- /dev/null +++ b/src/components/molecules/ThemeSwitch.module.scss @@ -0,0 +1,84 @@ +@import 'variables'; +@import 'mixins'; + +.themeSwitch { + position: relative; + display: inline-block; + margin-right: $spacer; + opacity: 0.75; + bottom: -0.1rem; + + svg { + width: 0.8rem; + height: 0.8rem; + margin-top: -0.05rem; + fill: $brand-grey-light; + + &:last-child { + margin-top: -0.1rem; + width: 0.7rem; + height: 0.7rem; + } + } +} + +.checkboxContainer { + display: flex; + align-items: center; +} + +$knob-size: 8px; +$knob-space: 1px; + +.checkboxFake { + display: block; + position: relative; + width: ($knob-size + ($knob-space * 2)) * 2; + height: $knob-size + ($knob-space * 4); + border: 1px solid $brand-grey-light; + border-radius: 15rem; + margin-left: $spacer / 3; + margin-right: $spacer / 3; + + &::after { + content: ''; + position: absolute; + top: $knob-space; + left: $knob-space; + width: $knob-size; + height: $knob-size; + background-color: $brand-grey-light; + border-radius: 15rem; + transition: transform 0.2s $easing; + transform: translate3d(0, 0, 0); + } +} + +.checkbox { + position: relative; + cursor: pointer; + + [type='checkbox'], + .label { + width: 1px; + height: 1px; + border: 0; + clip: rect(0 0 0 0); + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + } + + [type='checkbox'] { + &:checked { + + .checkboxContainer { + .checkboxFake { + &::after { + transform: translate3d(100%, 0, 0); + } + } + } + } + } +} diff --git a/src/components/molecules/ThemeSwitch.test.tsx b/src/components/molecules/ThemeSwitch.test.tsx new file mode 100644 index 00000000..9f27cdbb --- /dev/null +++ b/src/components/molecules/ThemeSwitch.test.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { render, fireEvent, cleanup } from '@testing-library/react' +import ThemeSwitch from './ThemeSwitch' + +describe('ThemeSwitch', () => { + afterEach(cleanup) + + it('renders correctly', () => { + const { container } = render() + const switchContainer = container.querySelector('aside') + expect(switchContainer).toBeInTheDocument() + }) + + it('checkbox can be changed', () => { + const { container } = render() + + const toggle = container.querySelector('input') + const label = container.querySelector('label') + expect(toggle.checked).toBeFalsy() + fireEvent.click(label) + fireEvent.change(toggle, { target: { checked: true } }) + expect(toggle.checked).toBeTruthy() + }) +}) diff --git a/src/components/molecules/ThemeSwitch.tsx b/src/components/molecules/ThemeSwitch.tsx new file mode 100644 index 00000000..cbe769b5 --- /dev/null +++ b/src/components/molecules/ThemeSwitch.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import useDarkMode from 'use-dark-mode' + +import { ReactComponent as Day } from '../../images/day.svg' +import { ReactComponent as Night } from '../../images/night.svg' +import styles from './ThemeSwitch.module.scss' + +const ThemeToggle = () => ( + + + + + +) + +const ThemeToggleInput = ({ + isDark, + toggleDark +}: { + isDark: boolean + toggleDark(): void +}) => ( + +) + +export default function ThemeSwitch() { + const darkMode = useDarkMode(false, { + classNameDark: 'dark', + classNameLight: 'light' + }) + + return ( + + ) +} diff --git a/src/components/organisms/Header.module.scss b/src/components/organisms/Header.module.scss index cbd69b9d..7bb79f01 100644 --- a/src/components/organisms/Header.module.scss +++ b/src/components/organisms/Header.module.scss @@ -15,7 +15,7 @@ } } -.header__content { +.headerContent { @include breakoutviewport; position: relative; @@ -31,56 +31,28 @@ // Logo ///////////////////////////////////// -.logo { - display: block; - background-image: url('../../images/logo.png'); - background-repeat: no-repeat; - background-position: left top; - width: 47px; - height: 31px; - - @media (min-width: $screen-sm) { - width: 183px; - } - - @media (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { - background-image: url('../../images/logo@2x.png'); - background-size: 183px 62px; - } - - @media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 344dpi) { - background-image: url('../../images/logo@3x.png'); - background-size: 183px 62px; - } -} - .title { margin-top: $spacer / 5; margin-bottom: 0; - // display toned down logo - // by default - @extend .logo; -} -.header__logo { - @include hide-text; - // repeat logo - // but display hover version - @extend .logo; - - background-position: left bottom; - - // hide by default - opacity: 0; - // show smooooothly on hover - &:hover { - opacity: 1; - transform: none; + svg { + fill: $brand-grey-light; + width: 160px; + height: 30px; + margin: 0; + transition: 0.2s $easing; } - &:active { - top: 0; - box-shadow: none; + a { + display: block; + @include hide-text; + + &:hover, + &:focus { + svg { + fill: $brand-cyan; + } + } } } diff --git a/src/components/organisms/Header.tsx b/src/components/organisms/Header.tsx index d04a61f7..375870da 100644 --- a/src/components/organisms/Header.tsx +++ b/src/components/organisms/Header.tsx @@ -3,6 +3,8 @@ import { Link } from 'gatsby' import Container from '../atoms/Container' import Search from '../Search' import Menu from '../molecules/Menu' +import ThemeSwitch from '../molecules/ThemeSwitch' +import { ReactComponent as Logo } from '../../images/logo.svg' import styles from './Header.module.scss' @@ -10,14 +12,15 @@ export default function Header() { return (
-
+

- - kremalicious + + kremalicious

diff --git a/src/images/day.svg b/src/images/day.svg new file mode 100644 index 00000000..b48ffbb3 --- /dev/null +++ b/src/images/day.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/logo.png b/src/images/logo.png deleted file mode 100644 index ab0ef47f..00000000 Binary files a/src/images/logo.png and /dev/null differ diff --git a/src/images/logo.svg b/src/images/logo.svg new file mode 100644 index 00000000..4c82100e --- /dev/null +++ b/src/images/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/logo@2x.png b/src/images/logo@2x.png deleted file mode 100644 index 1459db5d..00000000 Binary files a/src/images/logo@2x.png and /dev/null differ diff --git a/src/images/logo@3x.png b/src/images/logo@3x.png deleted file mode 100644 index 6f6fb4ef..00000000 Binary files a/src/images/logo@3x.png and /dev/null differ diff --git a/src/images/night.svg b/src/images/night.svg new file mode 100644 index 00000000..c2ee7c9d --- /dev/null +++ b/src/images/night.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 31f58a5b..2583af2d 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -97,6 +97,14 @@ width: 100%; border-bottom: 1px dashed #fff; } + + :global(.dark) & { + border-bottom-color: darken($brand-main, 25%); + + &::before { + border-bottom-color: darken($brand-grey, 12%); + } + } } @mixin divider-top() { @@ -114,6 +122,14 @@ width: 100%; border-bottom: 1px dashed #fff; } + + :global(.dark) & { + border-top-color: darken($brand-main, 25%); + + &::after { + border-bottom-color: darken($brand-grey, 12%); + } + } } // Heading band @@ -124,6 +140,10 @@ background: rgba(255, 255, 255, 0.5); padding: ($spacer/2) $spacer ($spacer/2) 100%; margin-left: -100%; + + :global(.dark) & { + background: darken($body-background-color--dark, 2%); + } } // Layout breakout @@ -224,6 +244,11 @@ border-radius: $border-radius; box-shadow: 0 1px 3px rgba($brand-grey, 0.2); + :global(.dark) & { + box-shadow: 0 3px 5px rgba(darken($brand-main, 20%), 0.15), + 0 5px 16px rgba(darken($brand-main, 20%), 0.15); + } + @media (min-width: $screen-sm) { border: 2px solid transparent; } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 6b3e6dd1..fdb55f64 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -23,15 +23,13 @@ $alert-error: #f2dede; $body-background-color: $brand-light; $body-background-color--dark: darken($brand-grey, 22%); -$page-background-color: $brand-light; - // Text Colors ///////////////////////////////////// $text-color: $brand-grey; $text-color-light: $brand-grey-light; -$text-color--dark: lighten($brand-grey-light, 5%); +$text-color--dark: lighten($brand-grey-light, 10%); $text-color-light--dark: lighten($brand-grey-light, 5%); $link-color: $brand-cyan; diff --git a/src/styles/global.scss b/src/styles/global.scss index 1586524c..4f1787ae 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -13,7 +13,6 @@ html, body { margin: 0; padding: 0; - background: $body-background-color; } html { @@ -32,7 +31,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; min-height: 100vh; - transition: background 0.6s $easing; + transition: background 0.4s $easing; + background: $body-background-color; // handling long text, like URLs overflow-wrap: break-word; @@ -82,6 +82,26 @@ button { } } +// Links +///////////////////////////////////// + +a { + color: $link-color; + text-decoration: none; + transition: 0.2s ease-out; + + &:hover, + &:focus { + outline: 0; + color: $link-color-hover; + } + + &:active { + transition: none; + color: $link-color-active; + } +} + // Headings ///////////////////////////////////// @@ -153,38 +173,19 @@ h5, h6 { font-family: $font-family-headings; line-height: $line-height-headings; - color: $color-headings; font-weight: $font-weight-headings; letter-spacing: -0.02em; - .dark & { - color: $color-headings--dark; - } -} - -// Links -///////////////////////////////////// - -a { - color: $link-color; - text-decoration: none; - transition: 0.2s ease-out; - - h1 &, - h2 &, - h3 & { + // stylelint-disable no-descending-specificity + &, + a { color: $color-headings; } + // stylelint-enable no-descending-specificity - &:hover, - &:focus { - outline: 0; - color: $link-color-hover; - } - - &:active { - transition: none; - color: $link-color-active; + .dark &, + .dark & a { + color: $color-headings--dark; } } @@ -215,6 +216,10 @@ figcaption { font-style: italic; text-align: center; margin-top: -($spacer / $line-height); + + .dark & { + color: $brand-grey-light; + } } // Lists