diff --git a/components/icons.tsx b/components/icons.tsx index 99624f3e..ffb10ae3 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -7,17 +7,21 @@ type Props = { children: ReactNode; className?: string; viewBox?: string; + width?: number | `${number}`; + height?: number | `${number}`; }; const Icon: FC = ({ children, className = "", viewBox = DEFAULT_VIEW_BOX, + width = "45", + height = "45" }) => ( @@ -66,3 +70,25 @@ export function DiscordIcon({ className = "" }) { ); } + +export function LightThemeIcon({ className = "" }) { + return ( + + + + ); +} + +export function DarkThemeIcon({ className = "" }) { + return ( + + + + ); +} diff --git a/components/nav.tsx b/components/nav.tsx index b722bd3a..d2270436 100644 --- a/components/nav.tsx +++ b/components/nav.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useState } from "react"; import classnames from "classnames"; import SocialLinks from "./social-links"; +import ToggleTheme from "./toggle-theme"; // TODO: what is this thing?? type Blog = any; @@ -68,6 +69,8 @@ const Navigation: FC<{ blog: Blog }> = ({ blog }) => {
+ +
diff --git a/components/toggle-theme.tsx b/components/toggle-theme.tsx new file mode 100644 index 00000000..3a7e8094 --- /dev/null +++ b/components/toggle-theme.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { DarkThemeIcon, LightThemeIcon } from "./icons"; + +const ToggleTheme: React.FC = () => { + /** + * Handles theme change + */ + const toggleTheme = () => { + const THEME_KEY = 'data-theme'; + + // getting current theme from body element + const currentTheme = document.body.getAttribute(THEME_KEY); + + // new theme, opposite to current theme + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + + // set new theme on body element + document.body.setAttribute(THEME_KEY, newTheme); + // set new theme in local storage + localStorage.setItem(THEME_KEY, newTheme); + }; + + return ( +
+ +
+ ); +}; + +export default ToggleTheme; diff --git a/hooks/use-theme.ts b/hooks/use-theme.ts new file mode 100644 index 00000000..07dc2d2d --- /dev/null +++ b/hooks/use-theme.ts @@ -0,0 +1,54 @@ +import React from "react"; + +// This supresses the `useLayoutEffect` usage warning on server side +if (typeof window === 'undefined') { + React.useLayoutEffect = () => {} +} + +let initDone = false; + +export const useTheme = () => { + + // This effect selects the theme from preferences or storage + React.useLayoutEffect(() => { + if (initDone) { + // return early if the effect has ran once + return; + } + initDone = true; + + // getting theme value from local storage + const savedTheme = localStorage.getItem('data-theme'); + if (savedTheme) { + // if user has theme in localStorage, set it on body element + document.body.setAttribute('data-theme', savedTheme); + return; + } + + // When localStorage does not contain theme value + // Read user color preference on device + const darkModePreferred = matchMedia('(prefers-color-scheme: dark)').matches; + // set theme value on body element as per device preference + document.body.setAttribute('data-theme', darkModePreferred ? 'dark' : 'light'); + }, []); + + + // This effects adds the change listener on "prefers-color-scheme: dark" media query + React.useEffect(() => { + // Preferred Theme Media Query + const themeMediaQuery = matchMedia('(prefers-color-scheme: dark)'); + + // Handles preferred color scheme change + function onPreferColorSchemeChange(event: MediaQueryListEvent) { + // Remove saved theme data in localStorage on change of theme preference + localStorage.removeItem('data-theme'); + // Setting new preferred theme on body element + document.body.setAttribute('data-theme', event.matches ? 'dark' : 'light'); + } + + themeMediaQuery.addEventListener("change", onPreferColorSchemeChange); + return () => { + themeMediaQuery.removeEventListener("change", onPreferColorSchemeChange); + } + }, []); +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 8c1e9645..84a17011 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,8 +2,12 @@ import React from "react"; import { AppProps } from "next/app"; import "../styles/styles.scss"; +import { useTheme } from "../hooks/use-theme"; function MyApp({ Component, pageProps }: AppProps) { + // Handle Theme Selection on client side + useTheme(); + return ; } diff --git a/styles/dark-styles.scss b/styles/dark-styles.scss new file mode 100644 index 00000000..2bb0648e --- /dev/null +++ b/styles/dark-styles.scss @@ -0,0 +1,457 @@ +/** COLOR VALUES */ +$surface-100: #1B1B1B; // For body background color +$surface-200: #1F1F1F; // For cards, aside, sidebars background color +$surface-250: #242424; +$surface-300: #2D2D2D; // Navbar - For chips buttons, dropdowns background color +$surface-400: #353535; // For sidebars, navbar background color +$surface-500: #3D3D3D; // For modal, dialogs background color +$surface-600: #404040; // divider, hr, border +$surface-700: #696969; // For on background texts color | 267 0 41 + +$light-gray: #D6D6D6; // hover on secondary text +$gray: #A0A0A0; // secondary text +$dark-gray: #939393; // code snippet text + +$magenta-000: #C63995; +$magenta-100: #DE46A9; +$magenta-200: #E25AB2; +$magenta-300: #E670BD; + +$pure-white: #ffffff; +$white: #F0F0F0; // text, title +$red: #e7544c; +$orange: #f5a623; +$black-pink: #211E21; +$purple: #B284EB; +$green: #81C42C; +$yellow: #DDB218; +$blue: #4197E3; +$lightblue: #56A3E6; + +// Each color is associated with a library +$tk-lib-runtime: $magenta-200; +$tk-lib-hyper: $orange; +$tk-lib-tonic: $yellow; +$tk-lib-tower: $green; +$tk-lib-mio: $red; +$tk-lib-tracing: $purple; +$tk-lib-bytes: $blue; + + +// Per lib styling: [ name, bg-color, color, size, logo-adjust ] +$libs: "tokio" $white $surface-100 3.8rem -60%, "runtime" $tk-lib-runtime $surface-100 2.2rem -22%, + "hyper" $tk-lib-hyper $surface-100 100% -50%, "tonic" $tk-lib-tonic $surface-100 100% -25%, + "tower" $tk-lib-tower $surface-100 100% -52%, "mio" $tk-lib-mio $surface-100 100% -41%, + "tracing" $tk-lib-tracing $surface-100 100% -31%, "bytes" $tk-lib-bytes $surface-100 100% -30%; + +body { + background-color: $surface-100; + color: $white; +} + +.navbar { + background-color: $surface-400; + + .navbar-burger { + color: $white; + } + + .navbar-link, + .navbar-item { + color: $white; + + &:hover, + &:focus, + &:focus-within, + &.is-active { + background-color: initial; + color: $magenta-200; + } + } + .navbar-brand, .navbar-menu { + background-color: $surface-400; + } + hr { + background-color: $gray; + } +} + +.toggle-theme__btn { + &:hover { + background-color: $black-pink; + } +} + +.hero { + &.is-primary { + background-color: $surface-200; + .title, .subtitle { + color: $white; + } + } + &.tk-intro { + .button { + background-color: $magenta-000; + } + .button:hover { + background-color: $magenta-100; + } + .title, .subtitle { + color: $white; + } + } + &.tk-users { + background-color: $surface-100; + + .title, .subtitle { + color: $white; + } + + .image { + img { + filter: invert(0.95); + } + } + } +} + +.tk-features, +.tk-stack { + code { + background-color: $surface-400; + color: $white; + } +} + +.tk-features { + background-color: $surface-200; + + .card { + background-color: $surface-250; + border: 2px solid $white; + } + + @each $name, $bgColor, $color, $size, $adjust in $libs { + .tk-lib-#{$name} { + .title, + .learn-more a { + color: $bgColor; + } + .learn-more a:hover { + color: darken($bgColor); + } + .card { + border-color: $bgColor; + } + } + } +} + +.card { + background-color: $surface-200; +} + +.tk-stack { + background-color: $surface-100; + + .menu { + @each $name, $bgColor, $color, $size, $adjust in $libs { + .tk-lib-#{$name} { + a:hover { + color: $bgColor; + } + + a { + color: $gray; + } + + &.is-active { + background-color: $bgColor; + + a { + color: $color; + } + } + } + } + } + + // Per lib styling + @each $name, $bgColor, $color, $size, $adjust in $libs { + .tk-lib-#{$name} { + .title, + .learn-more a { + color: $bgColor; + } + .title img { + vertical-align: $adjust; + height: $size; + } + } + } + + .tk-lib-tokio { + .title img { + filter: invert(1); + } + } +} + +.tk-docs { + background-color: $surface-100; + + .tk-docs-nav { + background-color: $surface-200; + } + + .menu { + @include mobile { + .tk-toc { + background-color: $surface-200; + + a { + color: $white; + } + + .tk-arrow { + &:before, + &:after { + background-color: $white; + } + } + } + + .tk-menu-body { + background-color: $surface-200; + } + } + + .menu-label { + color: $gray; + } + .menu-list { + a { + color: $gray; + + &:hover { + color: $magenta-200; + } + } + + } + + li.is-active { + background-color: $surface-100; + + > a { + color: $white; + } + } + } + + .tk-content { + background-color: $surface-100; + color: $white; + + .tk-content-summary { + a { + color: $gray; + + &:hover { + color: $light-gray; + } + } + + > ul { + border-left: solid 1px $surface-600; + } + } + + h1:not(.title) { + border-bottom: solid 1px $surface-600; + } + + a { + color: $magenta-100; + } + a:hover { + color: $magenta-200; + } + + .tk-markdown { + .is-warning { + background: $black-pink; + color: $light-gray;// $magenta-100; + + strong, + code { + color: $magenta-200; + } + } + + blockquote.is-warning { + border-left-color: $magenta-200; + } + } + + code { + color: $white; + } + + a code { + color: $magenta-200; + } + a:hover code { + color: $magenta-300; + } + } +} + +.content h1, +.content h2, +.content h3, +.content h4, +.content h5, +.content h6, +strong { + color: $white; +} + +.content blockquote, +.content blockquote.is-info { + background-color: $surface-200; + border-color: $surface-600; + + a { + color: $magenta-200; + + &:hover { + color: $magenta-300; + } + } +} + +.footer { + color: $white; + background-color: $surface-400; + + a { + color: $lightblue; + + &:hover { + color: $blue; + } + } + + .tk-footer-libs { + a:hover { + color: $magenta-100; + } + } + + .tk-footer-libs, + .tk-footer-libs a, + .tk-sponsored, + .tk-sponsored a { + color: $white; + } +} + +.tk-doc-footer { + border-top: 1px solid $surface-600; + + .tk-help-links { + .tk-svg-path { + fill: $white !important; + } + } +} + +/* + * + * Syntax highlighting + * + */ + +pre, code { + background-color: $surface-250; +} + +.hljs-comment, +.hljs-quote { + color: $dark-gray; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { + color: $magenta-200; +} + +.hljs-number { + color: $green; +} + +.hljs-string, +.hljs-doctag { + color: $green; +} + +.hljs-selector-id, +.hljs-selector-class, +.hljs-section, +.hljs-type { + color: $blue; +} + +.hljs-params { + color: $purple; +} + +.hljs-title { + color: $purple; +} + +.hljs-tag, +.hljs-name, +.hljs-attribute { + color: $lightblue; +} + +.hljs-variable, +.hljs-template-variable { + color: $green; +} + +.hljs-regexp, +.hljs-link { + color: $green; +} + +.hljs-symbol, +.hljs-bullet { + color: $purple; +} + +.hljs-built_in, +.hljs-builtin-name { + color: $yellow; +} + +.hljs-meta { + color: $dark-gray; +} + +.hljs-deletion { + background: $red; +} + +.hljs-addition { + background: $green; +} + +.all-posts-link { + a { + color: $magenta-100; + } +} diff --git a/styles/styles.scss b/styles/styles.scss index 5053ab90..2645d1b2 100644 --- a/styles/styles.scss +++ b/styles/styles.scss @@ -96,6 +96,61 @@ a:active { overscroll-behavior-y: none; } +.toggle-theme { + &__container { + width: 2rem; + height: 2rem; + margin: 0 1.5rem; + } + + // Reducing whitespace around toggle button to make it consistent + // with other navbar links and icons on breakpoint: mobile < 800px < tablet/desktop + @include until(800px) { + &__container { + margin: 0 0.85rem; + } + } + + @include mobile { + &__container { + margin: 0 1.5rem; + margin-bottom: 1.5rem; + } + } + + &__btn { + // button style reset/cleanup + background: none; + border: none; + color: inherit; + cursor: pointer; + font-family: inherit; + padding: 0; + -webkit-tap-highlight-color: transparent; + + // custom styles + width: 100%; + height: 100%; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background 200ms; + + &:hover { + background-color: darken($dark-gray); + } + } + + body[data-theme=dark] &__icon--light { + display: none; + } + + body:not([data-theme=dark]) &__icon--dark { + display: none; + } +} + .navbar { .navbar-brand { background-color: black; @@ -125,6 +180,22 @@ a:active { } } + // Reducing whitespace in navbar to fit links and icons + // on breakpoint: mobile < 800px < tablet/desktop + @include until(800px) { + .navbar-item { + &.navbar-text { + padding: 0 0.85rem; + } + &.navbar-icon { + padding: 0 0.85rem; + } + } + hr { + margin: 1rem 0.85rem; + } + } + @include mobile { .navbar-menu { display: block; @@ -150,6 +221,10 @@ a:active { &:first-child { margin-top: 0.5rem; } + // Resetting reduced whitespace to normal + &.navbar-text, &.navbar-icon { + padding: 0 1.5rem; + } } .tk-social, @@ -1033,7 +1108,7 @@ Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine a { font-weight: 500; - color: #c83895; + color: $magenta; } @include mobile { @@ -1044,3 +1119,19 @@ Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine .blog-year-posts ul { margin-bottom: 2rem; } + +//#region Dark Theme Styles + +// Applying dark theme when body has dark atribute, not matter the theme preference +body[data-theme=dark] { + @import "./dark-styles.scss"; +} + +// When body element has light theme data and the theme preference is set to dark, Use light theme +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) { + @import "./dark-styles.scss"; + } +} + +//#endregion Dark Theme Styles