diff --git a/client/assets/moon.svg b/client/assets/moon.svg
new file mode 100644
index 00000000..68640757
--- /dev/null
+++ b/client/assets/moon.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/client/assets/sun.svg b/client/assets/sun.svg
new file mode 100644
index 00000000..c85378f3
--- /dev/null
+++ b/client/assets/sun.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/client/components/AutocompleteInput/AutocompleteInput.scss b/client/components/AutocompleteInput/AutocompleteInput.scss
index 19101cae..adf251a7 100644
--- a/client/components/AutocompleteInput/AutocompleteInput.scss
+++ b/client/components/AutocompleteInput/AutocompleteInput.scss
@@ -179,3 +179,34 @@
}
}
}
+
+.dark-theme {
+ .dummy-input__package-name {
+ color: $ghost-white;
+ }
+
+ .dummy-input__package-version {
+ color: $spun-pearl;
+ }
+
+ .autocomplete-input__suggestions-menu {
+ background: $black-russian;
+ border-color: $autocomplete-border-color-dark;
+ }
+
+ .autocomplete-input__suggestion {
+ color: $spun-pearl;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid transparentize($spun-pearl, 0.6);
+ }
+
+ em {
+ color: $ghost-white;
+ }
+ }
+
+ .autocomplete-input__suggestion--highlight {
+ background-color: lighten($black-russian, 5%);
+ }
+}
diff --git a/client/components/AutocompleteInputBox/AutocompleteInputBox.scss b/client/components/AutocompleteInputBox/AutocompleteInputBox.scss
index 1da86418..283569f1 100644
--- a/client/components/AutocompleteInputBox/AutocompleteInputBox.scss
+++ b/client/components/AutocompleteInputBox/AutocompleteInputBox.scss
@@ -32,3 +32,9 @@
height: 1px;
}
}
+
+.dark-theme {
+ .autocomplete-input-box {
+ border-color: $autocomplete-border-color-dark;
+ }
+}
diff --git a/client/components/ProgressHex/ProgressHex.scss b/client/components/ProgressHex/ProgressHex.scss
index 2b3eb048..75b95fa4 100644
--- a/client/components/ProgressHex/ProgressHex.scss
+++ b/client/components/ProgressHex/ProgressHex.scss
@@ -1,3 +1,5 @@
+@import '../../../stylesheets/colors';
+
.progress-hex {
width: 8rem;
height: 8rem;
@@ -14,3 +16,11 @@
.progress-hex__trail {
stroke-width: 1px;
}
+
+.dark-theme {
+ .progress-hex {
+ circle {
+ fill: $ghost-white;
+ }
+ }
+}
diff --git a/client/components/QuickStatsBar/QuickStatsBar.scss b/client/components/QuickStatsBar/QuickStatsBar.scss
index 0f06af4b..27444819 100644
--- a/client/components/QuickStatsBar/QuickStatsBar.scss
+++ b/client/components/QuickStatsBar/QuickStatsBar.scss
@@ -36,11 +36,6 @@
margin: auto;
left: 0;
}
-
- &:first-of-type,
- &:last-of-type {
- //padding-left: $global-spacing;
- }
}
.quick-stats-bar__stat--optional {
@@ -106,3 +101,26 @@
}
}
}
+
+.dark-theme {
+ .quick-stats-bar {
+ background: lighten($black-russian, 10%);
+ color: $spun-pearl;
+ }
+
+ .quick-stats-bar__stat {
+ &:not(:first-of-type)::before {
+ background: darken($spun-pearl, 30%);
+ }
+ }
+
+ .quick-stats-bar__link {
+ &:hover {
+ .quick-stats-bar__logo-icon--github {
+ path {
+ fill: $ghost-white;
+ }
+ }
+ }
+ }
+}
diff --git a/client/components/ResultLayout/ResultLayout.js b/client/components/ResultLayout/ResultLayout.js
index 128d2304..b8c5c2dc 100644
--- a/client/components/ResultLayout/ResultLayout.js
+++ b/client/components/ResultLayout/ResultLayout.js
@@ -3,6 +3,7 @@ import cx from 'classnames'
import Link from 'next/link'
import Layout from 'client/components/Layout'
+import ThemeToggle from 'client/components/ThemeToggle'
import GithubLogo from '../../assets/github-logo.svg'
import './ResultLayout.scss'
@@ -58,6 +59,7 @@ export default class ResultLayout extends Component {
>
+
{children}
diff --git a/client/components/ResultLayout/ResultLayout.scss b/client/components/ResultLayout/ResultLayout.scss
index 1be5808d..1b4aab1d 100644
--- a/client/components/ResultLayout/ResultLayout.scss
+++ b/client/components/ResultLayout/ResultLayout.scss
@@ -58,7 +58,7 @@
flex-direction: column;
min-height: 100vh;
min-height: calc(100vh - 6px);
- flex-gorw: 1;
+ flex-grow: 1;
}
.page-content {
@@ -105,3 +105,23 @@
overflow: scroll;
}
}
+
+.dark-theme {
+ .github-logo {
+ &:hover {
+ path {
+ fill: $ghost-white;
+ }
+ }
+ }
+
+ .logo-small {
+ color: $ghost-white;
+ }
+
+ .page-header__quicklinks {
+ a {
+ color: $spun-pearl;
+ }
+ }
+}
diff --git a/client/components/Separator.js b/client/components/Separator/Separator.js
similarity index 87%
rename from client/components/Separator.js
rename to client/components/Separator/Separator.js
index 3f00d98f..d763a96c 100644
--- a/client/components/Separator.js
+++ b/client/components/Separator/Separator.js
@@ -1,4 +1,5 @@
import React from 'react'
+import './Separator.scss'
export default function Separator({ text = 'or' }) {
const commonStyles = {
@@ -24,7 +25,7 @@ export default function Separator({ text = 'or' }) {
>
-
- {text}
-
+ {text}
)
}
diff --git a/client/components/Separator/Separator.scss b/client/components/Separator/Separator.scss
new file mode 100644
index 00000000..d2a40426
--- /dev/null
+++ b/client/components/Separator/Separator.scss
@@ -0,0 +1,32 @@
+@import '../../../stylesheets/colors.scss';
+
+.separator-flourish,
+.separator-line {
+ fill: #211915;
+}
+
+.separator-text {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ margin: auto;
+ width: 42px;
+ height: 20px;
+ background: white;
+ padding: 0 15px;
+ border-radius: 50%;
+}
+
+.dark-theme {
+ .separator-flourish,
+ .separator-line {
+ fill: $spun-pearl;
+ }
+
+ .separator-text {
+ background: $black-russian;
+ color: $spun-pearl;
+ }
+}
diff --git a/client/components/Separator/index.js b/client/components/Separator/index.js
new file mode 100644
index 00000000..8384c36b
--- /dev/null
+++ b/client/components/Separator/index.js
@@ -0,0 +1,3 @@
+import Separator from './Separator.js'
+
+export default Separator
diff --git a/client/components/SimilarPackageCard/SimilarPackageCard.scss b/client/components/SimilarPackageCard/SimilarPackageCard.scss
index 897f4f8a..d1dfe664 100644
--- a/client/components/SimilarPackageCard/SimilarPackageCard.scss
+++ b/client/components/SimilarPackageCard/SimilarPackageCard.scss
@@ -155,3 +155,27 @@
fill: transparentize($raven, 0.7);
}
}
+
+.dark-theme {
+ .similar-package-card {
+ background: lighten($black-russian, 10%);
+ }
+
+ .similar-package-card__name {
+ color: lighten($spun-pearl, 10%);
+ }
+
+ .similar-package-card__description {
+ color: darken($spun-pearl, 15%);
+ }
+
+ .similar-package-card__footer {
+ background: $black-russian;
+ }
+
+ .similar-package-card__github-icon {
+ path {
+ fill: $ghost-white;
+ }
+ }
+}
diff --git a/client/components/Stat/Stat.scss b/client/components/Stat/Stat.scss
index 08727375..297bed9f 100644
--- a/client/components/Stat/Stat.scss
+++ b/client/components/Stat/Stat.scss
@@ -30,6 +30,7 @@
color: #212121;
background: inherit;
position: relative;
+ transition: none;
.stat-container--compact & {
@include font-size-lg;
@@ -118,3 +119,9 @@
display: none;
}
}
+
+.dark-theme {
+ .stat-container__value {
+ color: $spun-pearl;
+ }
+}
diff --git a/client/components/ThemeContext/ThemeContext.js b/client/components/ThemeContext/ThemeContext.js
new file mode 100644
index 00000000..78ce4b54
--- /dev/null
+++ b/client/components/ThemeContext/ThemeContext.js
@@ -0,0 +1,25 @@
+import React, { createContext, useState } from 'react'
+
+export const Themes = {
+ light: 'light',
+ dark: 'dark',
+}
+
+export const ThemeContext = createContext({
+ theme: Themes.light,
+ toggleTheme: () => {},
+})
+
+const ThemeContextProvider = props => {
+ const [theme, setTheme] = useState(Themes.light)
+ const toggleTheme = () => {
+ setTheme(theme === Themes.light ? Themes.dark : Themes.light)
+ }
+
+ return (
+
+ {props.children}
+
+ )
+}
+export default ThemeContextProvider
diff --git a/client/components/ThemeContext/index.js b/client/components/ThemeContext/index.js
new file mode 100644
index 00000000..d1a2563c
--- /dev/null
+++ b/client/components/ThemeContext/index.js
@@ -0,0 +1,4 @@
+import ThemeContextProvider from './ThemeContext'
+export { Themes, ThemeContext } from './ThemeContext'
+
+export default ThemeContextProvider
diff --git a/client/components/ThemeToggle/ThemeToggle.js b/client/components/ThemeToggle/ThemeToggle.js
new file mode 100644
index 00000000..19212385
--- /dev/null
+++ b/client/components/ThemeToggle/ThemeToggle.js
@@ -0,0 +1,28 @@
+import React, { useContext, useCallback } from 'react'
+import cx from 'classnames'
+import { Themes, ThemeContext } from 'client/components/ThemeContext'
+import SunIcon from '../../assets/sun.svg'
+import MoonIcon from '../../assets/moon.svg'
+
+import './ThemeToggle.scss'
+
+const ThemeToggle = ({ className }) => {
+ const { theme, toggleTheme } = useContext(ThemeContext)
+ const onToggleTheme = useCallback(() => {
+ if (theme === Themes.dark) {
+ document.body.classList.remove('dark-theme')
+ } else {
+ document.body.classList.add('dark-theme')
+ }
+
+ toggleTheme()
+ })
+
+ return (
+
+ )
+}
+
+export default ThemeToggle
diff --git a/client/components/ThemeToggle/ThemeToggle.scss b/client/components/ThemeToggle/ThemeToggle.scss
new file mode 100644
index 00000000..555ea8e4
--- /dev/null
+++ b/client/components/ThemeToggle/ThemeToggle.scss
@@ -0,0 +1,25 @@
+@import '../../../stylesheets/variables';
+@import '../../../stylesheets/colors';
+
+.theme-toggle {
+ background: none;
+ border: none;
+ margin-left: $global-spacing * 2;
+ outline: none;
+ cursor: pointer;
+
+ .sun-icon,
+ .moon-icon {
+ height: 30px;
+ width: 30px;
+
+ @media screen and (max-width: 40em) {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ .sun-icon {
+ fill: $dandelion;
+ }
+}
diff --git a/client/components/ThemeToggle/index.js b/client/components/ThemeToggle/index.js
new file mode 100644
index 00000000..21f405f3
--- /dev/null
+++ b/client/components/ThemeToggle/index.js
@@ -0,0 +1,3 @@
+import ThemeToggle from './ThemeToggle.js'
+
+export default ThemeToggle
diff --git a/client/components/Warning/Warning.scss b/client/components/Warning/Warning.scss
index 57ce0d64..0cd1b87f 100644
--- a/client/components/Warning/Warning.scss
+++ b/client/components/Warning/Warning.scss
@@ -18,3 +18,10 @@
text-transform: uppercase;
}
}
+
+.dark-theme {
+ .warning-bar {
+ background: $black-russian;
+ border: 1px solid $dandelion;
+ }
+}
diff --git a/pages/_app.js b/pages/_app.js
new file mode 100644
index 00000000..0f0ceb0d
--- /dev/null
+++ b/pages/_app.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import App from 'next/app'
+
+import ThemeContextProvider from 'client/components/ThemeContext'
+
+class MyApp extends App {
+ render() {
+ const { Component, pageProps } = this.props
+ return (
+
+
+
+ )
+ }
+}
+
+export default MyApp
diff --git a/pages/index.js b/pages/index.js
index bff583d3..dc0af0b6 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -4,8 +4,10 @@ import Layout from 'client/components/Layout'
import Router from 'next/router'
import Link from 'next/link'
import Analytics from 'react-ga'
-import './index.scss'
import AutocompleteInputBox from 'client/components/AutocompleteInputBox/AutocompleteInputBox'
+import ThemeToggle from 'client/components/ThemeToggle'
+
+import './index.scss'
export default class Home extends PureComponent {
componentDidMount() {
@@ -33,10 +35,15 @@ export default class Home extends PureComponent {
viewBox="0 0 137 157"
xmlns="http://www.w3.org/2000/svg"
>
-
+
+
)
}
diff --git a/pages/index.scss b/pages/index.scss
index e104c72c..22247a8a 100644
--- a/pages/index.scss
+++ b/pages/index.scss
@@ -42,6 +42,14 @@
color: #888;
}
+.logo__skeleton-base {
+ stroke: black;
+}
+
+.logo__skeleton--filled-circle-mouth {
+ fill: #c0c0c0;
+}
+
.logo__skeleton {
animation: move 2s alternate infinite;
@@ -141,3 +149,31 @@
transform: translate(0.55px, -1px);
}
}
+
+.homepage__theme-toggle {
+ position: absolute;
+ top: $global-spacing * 3;
+ right: $global-spacing * 3;
+}
+
+.dark-theme {
+ .logo {
+ color: $ghost-white;
+ }
+
+ .logo__skeleton-base {
+ stroke: $spun-pearl;
+ }
+
+ .logo__skeleton--filled-circle-mouth {
+ fill: darken($spun-pearl, 25%);
+ }
+
+ .logo__skeleton-group {
+ stroke: $spun-pearl;
+ }
+
+ .logo__skeleton {
+ stroke: $ghost-white;
+ }
+}
diff --git a/pages/result/ResultPage.js b/pages/result/ResultPage.js
index e41ecef4..227eee4b 100644
--- a/pages/result/ResultPage.js
+++ b/pages/result/ResultPage.js
@@ -53,7 +53,7 @@ class ResultPage extends PureComponent {
router: { query },
} = this.props
const {
- url: { query: nextQuery },
+ router: { query: nextQuery },
} = nextProps
if (!nextQuery || !nextQuery.p.trim()) {
@@ -242,7 +242,7 @@ class ResultPage extends PureComponent {
}
getMetaTags = () => {
- const { url } = this.props
+ const { query } = this.props.router
const { resultsPromiseState, results } = this.state
let name, version
@@ -250,8 +250,8 @@ class ResultPage extends PureComponent {
name = results.name
version = results.version
} else {
- name = parsePackageString(url.query.p).name
- version = parsePackageString(url.query.p).version
+ name = parsePackageString(query.p).name
+ version = parsePackageString(query.p).version
}
const packageString = version ? `${name}@${version}` : name
diff --git a/pages/result/ResultPage.scss b/pages/result/ResultPage.scss
index 4a139477..40afbac2 100644
--- a/pages/result/ResultPage.scss
+++ b/pages/result/ResultPage.scss
@@ -248,6 +248,7 @@
background: #ffbc40;
border-radius: 2px;
line-height: 1.2;
+ color: black;
}
}
@@ -328,3 +329,13 @@
margin: $global-spacing * 3 0 0 0;
line-height: 1.2;
}
+
+.dark-theme {
+ .result-error__img {
+ fill: $spun-pearl;
+ }
+
+ .time-container {
+ border-color: darken($spun-pearl, 25%);
+ }
+}
diff --git a/pages/result/components/ExportAnalysisSection/ExportAnalysisSection.scss b/pages/result/components/ExportAnalysisSection/ExportAnalysisSection.scss
index fa9d6c3c..5f48fae1 100644
--- a/pages/result/components/ExportAnalysisSection/ExportAnalysisSection.scss
+++ b/pages/result/components/ExportAnalysisSection/ExportAnalysisSection.scss
@@ -232,3 +232,32 @@
color: $raven;
}
}
+
+.dark-theme {
+ .export-analysis-section__list {
+ background-image: none;
+ }
+
+ .export-analysis-section__filter-input {
+ background-color: $black-russian;
+ color: $spun-pearl;
+ border-color: $autocomplete-border-color-dark;
+
+ &:focus {
+ background: lighten($black-russian, 5%);
+ border-color: lighten($autocomplete-border-color-dark, 48%);
+ }
+ }
+
+ .export-analysis-section__pill {
+ color: $black-russian;
+
+ &::after {
+ background: $black-russian;
+ }
+ }
+
+ .export-analysis-section__pill-spinner {
+ background-color: $spun-pearl;
+ }
+}
diff --git a/pages/scan-results/ScanResults.scss b/pages/scan-results/ScanResults.scss
index 321ac1bb..e1baea29 100644
--- a/pages/scan-results/ScanResults.scss
+++ b/pages/scan-results/ScanResults.scss
@@ -221,3 +221,22 @@ $index-width: 4rem;
text-decoration: line-through;
}
}
+
+.dark-theme {
+ .scan-results__container {
+ border-color: $spun-pearl;
+ box-shadow: 0 0 4px lighten($black-russian, 20%);
+ }
+
+ .scan-results__item {
+ background: $black-russian;
+ }
+
+ .scan-results__item--total {
+ background: lighten($black-russian, 5%);
+ }
+
+ .scan-results__index {
+ color: lighten($black-russian, 10%);
+ }
+}
diff --git a/pages/scan/Scan.scss b/pages/scan/Scan.scss
index c3d25cb1..4472c7e2 100644
--- a/pages/scan/Scan.scss
+++ b/pages/scan/Scan.scss
@@ -103,3 +103,22 @@
}
}
}
+
+.dark-theme {
+ .scan__btn {
+ background-color: $spun-pearl;
+ color: $black-russian;
+
+ &:hover {
+ background: lighten($spun-pearl, 20%);
+ }
+ }
+
+ .scan__selection-header {
+ .scan__btn {
+ &:hover {
+ color: $black-russian;
+ }
+ }
+ }
+}
diff --git a/stylesheets/base.scss b/stylesheets/base.scss
index 17f07165..c284968b 100644
--- a/stylesheets/base.scss
+++ b/stylesheets/base.scss
@@ -16,6 +16,11 @@ html {
color: #212121;
}
+body.dark-theme {
+ background-color: $black-russian;
+ color: $spun-pearl;
+}
+
code {
font-family: $font-family-code;
}
diff --git a/stylesheets/colors.scss b/stylesheets/colors.scss
index 29db39c0..68f0ab3d 100644
--- a/stylesheets/colors.scss
+++ b/stylesheets/colors.scss
@@ -8,4 +8,9 @@ $dandelion: #fff3cf;
$carrot-orange: #eb841f;
$raven: #666e78;
+$black-russian: #1c1c1e;
+$spun-pearl: #a9a9b3;
+$ghost-white: #f1f1f2;
+
$autocomplete-border-color: transparentize(black, 0.93);
+$autocomplete-border-color-dark: transparentize($spun-pearl, 0.6);