1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-28 00:27:58 +02:00

Merge pull request #173 from kremalicious/feature/dark-mode

add dark mode
This commit is contained in:
Matthias Kretschmann 2019-10-03 19:55:24 +02:00 committed by GitHub
commit df1438e6b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 346 additions and 91 deletions

View File

@ -22,6 +22,7 @@
- [🔍 Search](#-search)
- [🕸 Related Posts](#-related-posts)
- [📝 GitHub changelog rendering](#-github-changelog-rendering)
- [🌗 Theme Switcher](#-theme-switcher)
- [🏆 SEO component](#-seo-component)
- [📈 Matomo (formerly Piwik) analytics tracking](#-matomo-formerly-piwik-analytics-tracking)
- [gatsby-redirect-from](#gatsby-redirect-from)
@ -117,6 +118,14 @@ If you want to know how this works, have a look at the respective component unde
- [`src/components/atoms/Changelog.jsx`](src/components/atoms/Changelog.jsx)
### 🌗 Theme Switcher
Includes a theme switcher which allows user to toggle between a light and a dark theme. Switching between them also happens automatically based on user's system preferences utilizing [use-dark-mode](https://github.com/donavon/use-dark-mode).
If you want to know how, have a look at the respective components:
- [`src/components/molecules/ThemeSwitch.jsx`](src/components/molecules/ThemeSwitch.jsx)
### 🏆 SEO component
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.

View File

@ -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',

View File

@ -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",

View File

@ -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

View File

@ -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%;

View File

@ -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;

View File

@ -9,6 +9,10 @@
color: $color-headings;
margin-top: 0;
margin-bottom: $spacer;
:global(.dark) & {
color: $color-headings--dark;
}
}
.hentry__title__link {

View File

@ -15,6 +15,10 @@
-webkit-overflow-scrolling: touch;
height: 91vh;
:global(.dark) & {
background: rgba($body-background-color--dark, 0.95);
}
ul {
@include breakoutviewport;

View File

@ -38,7 +38,7 @@ export default function Search({ lng }: { lng: string }) {
{searchOpen && (
<>
<Helmet>
<body className="hasSearchOpen" />
<html className="hasSearchOpen" lang="en" />
</Helmet>
<CSSTransition

View File

@ -11,6 +11,10 @@
margin-left: $spacer / 2;
border-left: 1px solid $brand-grey-dimmed;
:global(.dark) & {
border-left-color: darken($body-background-color--dark, 5%);
}
h2 {
position: relative;

View File

@ -22,18 +22,22 @@
flex: 1 1 20%;
white-space: nowrap;
padding: $spacer / 1.5;
border-bottom: 1px solid $brand-grey-dimmed;
border-bottom: 1px dashed $brand-grey-dimmed;
&:first-child {
flex-basis: 100%;
}
:global(.dark) & {
border-bottom-color: $brand-grey;
}
}
@media (min-width: $screen-sm) {
margin-bottom: 0;
span {
border-left: 1px solid $brand-grey-dimmed;
border-left: 1px dashed $brand-grey-dimmed;
border-bottom: 0;
padding: $spacer;
@ -45,6 +49,10 @@
&:first-child {
border-left: 0;
}
:global(.dark) & {
border-left-color: $brand-grey;
}
}
}
}

View File

@ -8,6 +8,9 @@ const MAPBOX_ACCESS_TOKEN =
const retina =
typeof window !== 'undefined' && window.devicePixelRatio >= 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'}
>

View File

@ -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;

View File

@ -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;

View File

@ -43,5 +43,9 @@
padding: $padding-base-horizontal;
display: block;
text-align: center;
:global(.dark) & {
text-shadow: 0 -1px 0 rgba(#000, 0.5);
}
}
}

View File

@ -24,7 +24,7 @@ export default function Menu() {
return (
<>
<Helmet>
<body className={menuOpen ? 'has-menu-open' : null} />
<html className={menuOpen ? 'has-menu-open' : null} lang="en" />
</Helmet>
<Hamburger onClick={toggleMenu} />
<ul className={styles.menu}>{MenuItems}</ul>

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}
}
}

View File

@ -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(<ThemeSwitch />)
const switchContainer = container.querySelector('aside')
expect(switchContainer).toBeInTheDocument()
})
it('checkbox can be changed', () => {
const { container } = render(<ThemeSwitch />)
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()
})
})

View File

@ -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 = () => (
<span id="toggle" className={styles.checkboxContainer} aria-live="assertive">
<Day />
<span className={styles.checkboxFake} />
<Night />
</span>
)
const ThemeToggleInput = ({
isDark,
toggleDark
}: {
isDark: boolean
toggleDark(): void
}) => (
<input
onChange={toggleDark}
type="checkbox"
name="toggle"
value="toggle"
aria-describedby="toggle"
checked={isDark}
/>
)
export default function ThemeSwitch() {
const darkMode = useDarkMode(false, {
classNameDark: 'dark',
classNameLight: 'light'
})
return (
<aside className={styles.themeSwitch}>
<label
htmlFor="toggle"
className={styles.checkbox}
onClick={darkMode.toggle}
>
<span className={styles.label}>Toggle Dark Mode</span>
<ThemeToggleInput
isDark={darkMode.value}
toggleDark={darkMode.toggle}
/>
<ThemeToggle />
</label>
</aside>
)
}

View File

@ -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;
}
}
}
}

View File

@ -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 (
<header role="banner" className={styles.header}>
<Container>
<div className={styles.header__content}>
<div className={styles.headerContent}>
<h1 className={styles.title}>
<Link className={styles.header__logo} to="/">
kremalicious
<Link to="/">
<Logo /> kremalicious
</Link>
</h1>
<nav role="navigation" className={styles.nav}>
<ThemeSwitch />
<Search lng="en" />
<Menu />
</nav>

3
src/images/day.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21">
<path d="M1001,1344.5 C1001,1347.53669 998.535313,1350 995.5,1350 C992.459875,1350 990,1347.53669 990,1344.5 C990,1341.46331 992.459875,1339 995.5,1339 C998.535313,1339 1001,1341.46331 1001,1344.5 Z M995.5,1336.484 C994.633125,1336.484 993.81625,1336.69175 993.035,1337 L993,1337 L995.5,1334 L998,1337 L997.96125,1337 C997.181875,1336.691 996.365,1336.484 995.5,1336.484 Z M995.5,1352.516 C996.365,1352.516 997.181875,1352.30825 997.96125,1352 L998,1352 L995.5,1355 L993,1352 L993.035,1352 C993.81625,1352.309 994.633125,1352.516 995.5,1352.516 Z M1003.516,1344.5 C1003.516,1343.63562 1003.3045,1342.81562 1003,1342.03625 L1003,1342 L1006,1344.5 L1003,1347 L1003,1346.96375 C1003.30525,1346.18438 1003.516,1345.36438 1003.516,1344.5 Z M987.484,1344.5 C987.484,1345.36438 987.69025,1346.18438 988,1346.96375 L988,1347 L985,1344.5 L988,1342 L988,1342.03625 C987.6895,1342.81562 987.484,1343.63562 987.484,1344.5 Z M1001.34229,1350.34229 C1002.03819,1349.65134 1002.55304,1348.85785 1002.96959,1348.0297 L1003,1348 L1003,1352 L999,1352 L999.028289,1351.97242 C999.856436,1351.55233 1000.65205,1351.03819 1001.34229,1350.34229 Z M989.657001,1338.65771 C988.961103,1339.34866 988.447666,1340.14215 988.028289,1340.9703 L988,1341 L988,1337 L992,1337 L991.966761,1337.02758 C991.137907,1337.44767 990.348656,1337.96181 989.657001,1338.65771 Z M989.657709,1350.343 C990.349364,1351.0389 991.138614,1351.55304 991.966761,1351.97242 L992,1352 L988,1352 L988,1348 L988.028289,1348.0297 C988.448373,1348.85856 988.96181,1349.65205 989.657709,1350.343 Z M1001.34229,1338.657 C1000.65205,1337.9611 999.856436,1337.44696 999.028289,1337.02758 L999,1337 L1003,1337 L1003,1341 L1002.96959,1340.9703 C1002.55304,1340.14144 1002.03819,1339.34795 1001.34229,1338.657 Z" transform="translate(-985 -1334)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

3
src/images/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

3
src/images/night.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" viewBox="0 0 18 17">
<path d="M1057.35991,1345.6935 C1052.91908,1345.6935 1049.32216,1342.19489 1049.32216,1337.88132 C1049.32216,1336.46068 1049.74141,1335.14652 1050.42369,1334 C1046.72047,1335.03741 1044,1338.31568 1044,1342.24655 C1044,1347.00713 1047.97006,1350.86789 1052.86864,1350.86789 C1056.91247,1350.86789 1060.28811,1348.22007 1061.35547,1344.62446 C1060.17313,1345.28549 1058.82157,1345.6935 1057.35991,1345.6935 Z" transform="translate(-1044 -1334)"/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@ -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;
}

View File

@ -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;

View File

@ -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