diff --git a/gatsby-ssr.js b/gatsby-ssr.js
index b17b8fc1..41dfcfd4 100644
--- a/gatsby-ssr.js
+++ b/gatsby-ssr.js
@@ -3,5 +3,9 @@
*
* See: https://www.gatsbyjs.org/docs/ssr-apis/
*/
+import React from 'react';
+import HydrateTheme from './src/components/hydrate-theme';
-// You can delete this file if you're not using it
+export const onRenderBody = ({ setPreBodyComponents }) => {
+ setPreBodyComponents([]);
+};
diff --git a/package.json b/package.json
index 5c51dabb..26b306be 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,6 @@
"@wkovacs64/normalize.css": "8.0.1",
"axios": "0.18.0",
"dotenv": "6.2.0",
- "emotion-theming": "10.0.7",
"gatsby": "2.0.118",
"gatsby-plugin-emotion": "4.0.3",
"gatsby-plugin-manifest": "2.0.17",
@@ -56,9 +55,9 @@
"react-helmet": "6.0.0-beta",
"react-icons": "3.3.0",
"react-key-handler": "1.2.0-beta.3",
- "react-use": "5.3.0",
"typeface-nunito": "0.0.54",
- "typeface-source-sans-pro": "0.0.54"
+ "typeface-source-sans-pro": "0.0.54",
+ "use-dark-mode": "2.2.2"
},
"devDependencies": {
"@babel/core": "7.2.2",
diff --git a/src/@types/react-wait.d.ts b/src/@types/react-wait.d.ts
deleted file mode 100644
index 71a54ba9..00000000
--- a/src/@types/react-wait.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'react-wait';
diff --git a/src/@types/use-callbag.d.ts b/src/@types/use-callbag.d.ts
deleted file mode 100644
index 9908446e..00000000
--- a/src/@types/use-callbag.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'use-callbag';
diff --git a/src/@types/use-onclickoutside.d.ts b/src/@types/use-onclickoutside.d.ts
deleted file mode 100644
index f0727c5e..00000000
--- a/src/@types/use-onclickoutside.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module 'use-onclickoutside';
diff --git a/src/components/alert-on-update.tsx b/src/components/alert-on-update.tsx
index 1da7d4b8..63919b48 100644
--- a/src/components/alert-on-update.tsx
+++ b/src/components/alert-on-update.tsx
@@ -2,8 +2,9 @@ import React, { useState } from 'react';
import { StaticQuery, graphql } from 'gatsby';
import axios from 'axios';
import ms from 'ms';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import isMobile from '../utils/is-mobile';
+import { light, dark } from '../theme';
import UpdatePoller from './update-poller';
import UpdateAlert from './update-alert';
@@ -12,15 +13,28 @@ const UpdateAlertContainer = styled.div`
justify-content: center;
border-style: solid;
border-width: 0 0 1px;
- border-color: ${({ theme }) => theme.colors.alertBorder};
- box-shadow: 4px 4px 8px 0px ${({ theme }) => theme.colors.alertShadow};
- color: ${({ theme }) => theme.colors.alertText};
- background-color: ${({ theme }) => theme.colors.alertBackground};
transition: color 0.3s ease, background-color 0.3s ease;
- &:hover,
- &:focus-within {
- color: ${({ theme }) => theme.colors.alertBackground};
- background-color: ${({ theme }) => theme.colors.alertText};
+ body.light-mode & {
+ border-color: ${light.colors.alertBorder};
+ box-shadow: 4px 4px 8px 0px ${light.colors.alertShadow};
+ color: ${light.colors.alertText};
+ background-color: ${light.colors.alertBackground};
+ &:hover,
+ &:focus-within {
+ color: ${light.colors.alertBackground};
+ background-color: ${light.colors.alertText};
+ }
+ }
+ body.dark-mode & {
+ border-color: ${dark.colors.alertBorder};
+ box-shadow: 4px 4px 8px 0px ${dark.colors.alertShadow};
+ color: ${dark.colors.alertText};
+ background-color: ${dark.colors.alertBackground};
+ &:hover,
+ &:focus-within {
+ color: ${dark.colors.alertBackground};
+ background-color: ${dark.colors.alertText};
+ }
}
`;
diff --git a/src/components/footer.tsx b/src/components/footer.tsx
index 7a48bb50..282c9514 100644
--- a/src/components/footer.tsx
+++ b/src/components/footer.tsx
@@ -1,12 +1,18 @@
import React from 'react';
import { css } from '@emotion/core';
+import styled from '@emotion/styled';
import { FaGithub } from 'react-icons/fa';
-import styled from '../utils/styled';
+import { light, dark } from '../theme';
const SourceLink = styled.a`
- color: ${({ theme }) => theme.colors.pageText};
text-decoration: none;
padding: 0.5rem;
+ body.light-mode & {
+ color: ${light.colors.pageText};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.pageText};
+ }
`;
const SourceLinkIcon = styled(FaGithub)`
diff --git a/src/components/header.tsx b/src/components/header.tsx
index 96421bae..7e75cf8f 100644
--- a/src/components/header.tsx
+++ b/src/components/header.tsx
@@ -1,8 +1,9 @@
import React from 'react';
import { StaticQuery, graphql } from 'gatsby';
import { FiSun } from 'react-icons/fi';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import mq from '../utils/mq';
+import { light, dark } from '../theme';
import AlertOnUpdate from './alert-on-update';
const HeaderContent = styled.section`
@@ -20,10 +21,15 @@ const ThemeToggleButton = styled.button`
top: 1rem;
right: 1rem;
cursor: pointer;
- color: ${({ theme }) => theme.colors.pageText};
background-color: transparent;
border: none;
padding: 0.5rem;
+ body.light-mode & {
+ color: ${light.colors.pageText};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.pageText};
+ }
`;
const ThemeToggleButtonIcon = styled(FiSun)`
@@ -37,8 +43,6 @@ const H1 = styled.h1`
font-family: 'Nunito', sans-serif;
font-size: 2.25rem;
font-variant: small-caps;
- color: ${({ theme }) => theme.colors.headline};
- text-shadow: 1px 1px 1px ${({ theme }) => theme.colors.headlineShadow};
margin: 0;
${mq.md} {
font-size: 3rem;
@@ -47,6 +51,14 @@ const H1 = styled.h1`
${mq.lg} {
font-size: 5rem;
}
+ body.light-mode & {
+ color: ${light.colors.headline};
+ text-shadow: 1px 1px 1px ${light.colors.headlineShadow};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.headline};
+ text-shadow: 1px 1px 1px ${dark.colors.headlineShadow};
+ }
`;
interface HeaderProps {
diff --git a/src/components/hydrate-theme.tsx b/src/components/hydrate-theme.tsx
new file mode 100644
index 00000000..1176fc43
--- /dev/null
+++ b/src/components/hydrate-theme.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+
+// https://raw.githubusercontent.com/donavon/use-dark-mode/develop/noflash.js.txt
+const hydrateThemeScript = `
+ (function() {
+ // Change these if you use something different in your hook.
+ var storageKey = 'darkMode';
+ var classNameDark = 'dark-mode';
+ var classNameLight = 'light-mode';
+
+ function setClassOnDocumentBody(darkMode) {
+ document.body.classList.add(darkMode ? classNameDark : classNameLight);
+ document.body.classList.remove(darkMode ? classNameLight : classNameDark);
+ }
+
+ var preferDarkQuery = '(prefers-color-scheme: dark)';
+ var mql = window.matchMedia(preferDarkQuery);
+ var supportsColorSchemeQuery = mql.media === preferDarkQuery;
+ var localStorageTheme = null;
+ try {
+ localStorageTheme = localStorage.getItem(storageKey);
+ } catch (err) {}
+ var localStorageExists = localStorageTheme !== null;
+ if (localStorageExists) {
+ localStorageTheme = JSON.parse(localStorageTheme);
+ }
+
+ // Determine the source of truth
+ if (localStorageExists) {
+ // source of truth from localStorage
+ setClassOnDocumentBody(localStorageTheme);
+ } else if (supportsColorSchemeQuery) {
+ // source of truth from system
+ setClassOnDocumentBody(mql.matches);
+ localStorage.setItem(storageKey, mql.matches);
+ } else {
+ // source of truth from document.body
+ var isDarkMode = document.body.classList.contains(classNameDark);
+ localStorage.setItem(storageKey, JSON.stringify(isDarkMode));
+ }
+ })();
+`;
+
+const HydrateTheme: React.FunctionComponent = () => (
+ // eslint-disable-next-line react/no-danger
+
+);
+
+export default HydrateTheme;
diff --git a/src/components/layout.tsx b/src/components/layout.tsx
index 00d6ee52..802640af 100644
--- a/src/components/layout.tsx
+++ b/src/components/layout.tsx
@@ -4,25 +4,30 @@ import 'typeface-source-sans-pro';
import React from 'react';
import { Helmet } from 'react-helmet';
import { css, Global, ClassNames } from '@emotion/core';
-import { ThemeProvider } from 'emotion-theming';
+import styled from '@emotion/styled';
import { IconContext } from 'react-icons';
import { StaticQuery, graphql } from 'gatsby';
-import useLocalStorageState from '../utils/use-local-storage-state';
-import styled from '../utils/styled';
+import useDarkMode from 'use-dark-mode';
import { light, dark } from '../theme';
import Header from './header';
import Main from './main';
import Footer from './footer';
const FullHeightThemedContainer = styled.div`
- color: ${({ theme }) => theme.colors.pageText};
- background-color: ${({ theme }) => theme.colors.pageBackground};
min-height: 100vh;
padding-bottom: 2rem;
+ body.light-mode & {
+ color: ${light.colors.pageText};
+ background-color: ${light.colors.pageBackground};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.pageText};
+ background-color: ${dark.colors.pageBackground};
+ }
`;
const Layout: React.FunctionComponent = ({ children }) => {
- const [darkMode, setDarkMode] = useLocalStorageState('pwl:darkMode', false);
+ const darkMode = useDarkMode(false);
return (
{
data-version={siteMetadata.buildInfo.version}
/>
-
-
- setDarkMode(!darkMode)} />
- {children}
-
-
-
+
+
+ {children}
+
+
)}
diff --git a/src/components/legend-item.tsx b/src/components/legend-item.tsx
index 760f5cd3..2febde58 100644
--- a/src/components/legend-item.tsx
+++ b/src/components/legend-item.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
const LegendRow = styled.div`
display: flex;
diff --git a/src/components/password-input.tsx b/src/components/password-input.tsx
index 3e6452d5..253c4846 100644
--- a/src/components/password-input.tsx
+++ b/src/components/password-input.tsx
@@ -1,6 +1,7 @@
import React from 'react';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import mq from '../utils/mq';
+import { light, dark } from '../theme';
const Input = styled.input`
font-family: 'Courier New', Courier, monospace;
@@ -8,9 +9,6 @@ const Input = styled.input`
text-align: center;
letter-spacing: 0.25rem;
white-space: pre;
- color: ${({ theme }) => theme.colors.pageText};
- background-color: ${({ theme }) => theme.colors.inputBackground};
- border: 2px solid ${({ theme }) => theme.colors.inputBorder};
width: 100%;
font-size: 1.25rem;
${mq.md} {
@@ -22,8 +20,21 @@ const Input = styled.input`
&::-ms-clear {
display: none;
}
- &::placeholder {
- color: ${({ theme }) => theme.colors.dullText};
+ body.light-mode & {
+ color: ${light.colors.pageText};
+ background-color: ${light.colors.inputBackground};
+ border: 2px solid ${light.colors.inputBorder};
+ &::placeholder {
+ color: ${light.colors.dullText};
+ }
+ }
+ body.dark-mode & {
+ color: ${dark.colors.pageText};
+ background-color: ${dark.colors.inputBackground};
+ border: 2px solid ${dark.colors.inputBorder};
+ &::placeholder {
+ color: ${dark.colors.dullText};
+ }
}
`;
diff --git a/src/components/password-through-lense.tsx b/src/components/password-through-lense.tsx
index 235c6ade..65f3f649 100644
--- a/src/components/password-through-lense.tsx
+++ b/src/components/password-through-lense.tsx
@@ -1,28 +1,17 @@
import React from 'react';
import { css } from '@emotion/core';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import { ColorMap } from '../legend/colors';
import { LabelMap } from '../legend/labels';
import classifyCharacters, {
ClassifiedCharacter,
} from '../utils/classify-characters';
import mq from '../utils/mq';
+import { light, dark } from '../theme';
const Lense = styled.div`
- background-color: ${({ theme }) => theme.colors.lenseBackground};
- border: 2px solid ${({ theme }) => theme.colors.lenseBorder};
- scrollbar-color: ${({ theme }) =>
- `${theme.colors.lenseScrollThumb} ${theme.colors.lenseScrollTrack}}`};
- /* TODO: remove -webkit-scrollbar once Chrome supports scrollbar-color */
- &::-webkit-scrollbar {
- width: 1rem;
- }
- &::-webkit-scrollbar-thumb {
- background-color: ${({ theme }) => theme.colors.lenseScrollThumb};
- }
- &::-webkit-scrollbar-track {
- background-color: ${({ theme }) => theme.colors.lenseScrollTrack};
- }
+ border-width: 2px;
+ border-style: solid;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
@@ -35,11 +24,48 @@ const Lense = styled.div`
${mq.lg} {
font-size: 2.25rem;
}
+ /* TODO: remove -webkit-scrollbar once Chrome supports scrollbar-color */
+ &::-webkit-scrollbar {
+ width: 1rem;
+ }
+ body.light-mode & {
+ background-color: ${light.colors.lenseBackground};
+ border-color: ${light.colors.lenseBorder};
+ scrollbar-color: ${`${light.colors.lenseScrollThumb} ${
+ light.colors.lenseScrollTrack
+ }}`};
+ &::-webkit-scrollbar-thumb {
+ background-color: ${light.colors.lenseScrollThumb};
+ }
+ &::-webkit-scrollbar-track {
+ background-color: ${light.colors.lenseScrollTrack};
+ }
+ }
+ body.dark-mode & {
+ background-color: ${dark.colors.lenseBackground};
+ border-color: ${dark.colors.lenseBorder};
+ scrollbar-color: ${`${dark.colors.lenseScrollThumb} ${
+ dark.colors.lenseScrollTrack
+ }}`};
+ &::-webkit-scrollbar-thumb {
+ background-color: ${dark.colors.lenseScrollThumb};
+ }
+ &::-webkit-scrollbar-track {
+ background-color: ${dark.colors.lenseScrollTrack};
+ }
+ }
`;
const Character = styled.span`
- border-bottom: 1px dotted ${({ theme }) => theme.colors.lenseUnderline};
+ border-bottom-width: 1px;
+ border-bottom-style: dotted;
white-space: pre;
+ body.light-mode & {
+ border-bottom-color: ${light.colors.lenseUnderline};
+ }
+ body.dark-mode & {
+ border-bottom-color: ${dark.colors.lenseUnderline};
+ }
`;
const PasswordThroughLense: React.FunctionComponent<
diff --git a/src/components/pwned-info.tsx b/src/components/pwned-info.tsx
index d0d8f8d9..90ed5608 100644
--- a/src/components/pwned-info.tsx
+++ b/src/components/pwned-info.tsx
@@ -1,13 +1,24 @@
import React, { useReducer, useEffect } from 'react';
+import styled from '@emotion/styled';
import { pwnedPassword } from 'hibp';
-import styled from '../utils/styled';
+import { light, dark } from '../theme';
const CleanExclamation = styled.span`
- color: ${({ theme }) => theme.colors.cleanExclamation};
+ body.light-mode & {
+ color: ${light.colors.cleanExclamation};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.cleanExclamation};
+ }
`;
const PwnedExclamation = styled.span`
- color: ${({ theme }) => theme.colors.pwnedExclamation};
+ body.light-mode & {
+ color: ${light.colors.pwnedExclamation};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.pwnedExclamation};
+ }
`;
enum ActionType {
diff --git a/src/components/results.tsx b/src/components/results.tsx
index 8bebaf88..b067d787 100644
--- a/src/components/results.tsx
+++ b/src/components/results.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { css } from '@emotion/core';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import { ColorMap } from '../legend/colors';
import { LabelMap } from '../legend/labels';
import PasswordThroughLense from './password-through-lense';
diff --git a/src/components/update-alert.tsx b/src/components/update-alert.tsx
index 0a2d0fd6..6563152a 100644
--- a/src/components/update-alert.tsx
+++ b/src/components/update-alert.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import mq from '../utils/mq';
const Alert = styled.div`
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 902632ce..d2ab1b85 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { FaChevronLeft } from 'react-icons/fa';
-import styled from '../utils/styled';
+import styled from '@emotion/styled';
import mq from '../utils/mq';
import Layout from '../components/layout';
+import { light, dark } from '../theme';
const Content = styled.article`
display: flex;
@@ -15,13 +16,17 @@ const Content = styled.article`
const Section = styled.section`
border-left-style: solid;
border-width: 0.5rem;
- border-color: ${({ theme }) => theme.colors.headline};
padding-left: 2rem;
width: 48rem;
+ body.light-mode & {
+ border-color: ${light.colors.headline};
+ }
+ body.dark-mode & {
+ border-color: ${dark.colors.headline};
+ }
`;
const H2 = styled.h2`
- color: ${({ theme }) => theme.colors.dullText};
font-style: italic;
margin-top: 0;
margin-bottom: 4rem;
@@ -29,16 +34,27 @@ const H2 = styled.h2`
${mq.lg} {
font-size: 3rem;
}
+ body.light-mode & {
+ color: ${light.colors.dullText};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.dullText};
+ }
`;
const P = styled.p`
- color: ${({ theme }) => theme.colors.brandedText};
margin: 4rem 0;
font-weight: 300;
font-size: 1.25rem;
${mq.lg} {
font-size: 1.5rem;
}
+ body.light-mode & {
+ color: ${light.colors.brandedText};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.brandedText};
+ }
`;
const Nav = styled.nav`
@@ -53,8 +69,13 @@ const Button = styled.button`
text-decoration: none;
cursor: pointer;
border: none;
- color: ${({ theme }) => theme.colors.headline};
background-color: transparent;
+ body.light-mode & {
+ color: ${light.colors.headline};
+ }
+ body.dark-mode & {
+ color: ${dark.colors.headline};
+ }
`;
const ButtonIcon = styled(FaChevronLeft)`
@@ -74,11 +95,20 @@ const ButtonText = styled.span`
}
border-width: 0 0 1px;
border-style: solid;
- border-color: ${({ theme }) => theme.colors.headline};
transition: box-shadow 0.3s ease;
- button:hover &, /* inside a focused