Update ThemeSelector to include system option

This commit is contained in:
Jip J. Dekker 2023-06-26 14:24:05 +10:00
parent 5f608c23f9
commit d7a405eea1
No known key found for this signature in database
8 changed files with 145 additions and 69 deletions

View File

@ -32,7 +32,7 @@ export function ArticleLayout({
type="button"
onClick={() => router.back()}
aria-label="Go back to articles"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
>
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
</button>
@ -46,7 +46,7 @@ export function ArticleLayout({
dateTime={meta.date}
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
<span className="ml-3">{formatDate(meta.date)}</span>
</time>
</header>

View File

@ -74,7 +74,7 @@ Card.Eyebrow = function CardEyebrow({
className="absolute inset-y-0 left-0 flex items-center"
aria-hidden="true"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
</span>
)}
{children}

View File

@ -7,7 +7,8 @@ import clsx from 'clsx'
import { Container } from '@/components/Container'
import avatarImage from '@/images/avatar.jpg'
import { ChevronDownIcon, CloseIcon, MoonIcon, SunIcon } from '@/components/SVGIcons'
import { ChevronDownIcon, CloseIcon } from '@/components/SVGIcons'
import { ThemeSelector } from './ThemeSelector'
function MobileNavItem({ href, children }) {
return (
@ -22,7 +23,7 @@ function MobileNavItem({ href, children }) {
function MobileNavigation(props) {
return (
<Popover {...props}>
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
<Popover.Button className="group flex items-center rounded-xl bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
Menu
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
</Popover.Button>
@ -101,7 +102,7 @@ function NavItem({ href, children }) {
function DesktopNavigation(props) {
return (
<nav {...props}>
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<ul className="flex rounded-xl bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<NavItem href="/about">About</NavItem>
<NavItem href="/articles">Articles</NavItem>
<NavItem href="/projects">Projects</NavItem>
@ -112,41 +113,6 @@ function DesktopNavigation(props) {
)
}
function ModeToggle() {
function disableTransitionsTemporarily() {
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
document.documentElement.classList.remove('[&_*]:!transition-none')
}, 0)
}
function toggleMode() {
disableTransitionsTemporarily()
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
let isSystemDarkMode = darkModeMediaQuery.matches
let isDarkMode = document.documentElement.classList.toggle('dark')
if (isDarkMode === isSystemDarkMode) {
delete window.localStorage.isDarkMode
} else {
window.localStorage.isDarkMode = isDarkMode
}
}
return (
<button
type="button"
aria-label="Toggle dark mode"
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
onClick={toggleMode}
>
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
</button>
)
}
function clamp(number, a, b) {
let min = Math.min(a, b)
let max = Math.max(a, b)
@ -158,7 +124,7 @@ function AvatarContainer({ className, ...props }) {
<div
className={clsx(
className,
'h-10 w-10 rounded-full bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10'
'h-10 w-10 rounded-xl bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10'
)}
{...props}
/>
@ -178,7 +144,7 @@ function Avatar({ large = false, className, ...props }) {
alt=""
sizes={large ? '4rem' : '2.25rem'}
className={clsx(
'rounded-full bg-zinc-100 object-cover dark:bg-zinc-800',
'rounded-xl bg-zinc-100 object-cover dark:bg-zinc-800',
large ? 'h-16 w-16' : 'h-9 w-9'
)}
priority
@ -354,7 +320,7 @@ export function Header() {
</div>
<div className="flex justify-end md:flex-1">
<div className="pointer-events-auto">
<ModeToggle />
<ThemeSelector className="relative z-10" />
</div>
</div>
</div>

View File

@ -216,3 +216,15 @@ export function SunIcon(props) {
</svg>
)
}
export function SystemIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
/>
</svg>
)
}

View File

@ -0,0 +1,96 @@
import { useEffect, useState } from 'react'
import { Listbox } from '@headlessui/react'
import clsx from 'clsx'
import { MoonIcon, SunIcon, SystemIcon } from '@/components//SVGIcons'
const themes = [
{ name: 'Light', value: 'light', icon: SunIcon },
{ name: 'Dark', value: 'dark', icon: MoonIcon },
{ name: 'System', value: 'system', icon: SystemIcon },
]
export function ThemeSelector(props) {
let [selectedTheme, setSelectedTheme] = useState(null)
useEffect(() => {
if (selectedTheme) {
document.documentElement.setAttribute('data-theme', selectedTheme.value)
} else {
setSelectedTheme(
themes.find(
(theme) =>
theme.value === document.documentElement.getAttribute('data-theme')
)
)
}
}, [selectedTheme])
useEffect(() => {
let handler = () =>
setSelectedTheme(
themes.find(
(theme) => theme.value === (window.localStorage.theme ?? 'system')
)
)
window.addEventListener('storage', handler)
return () => window.removeEventListener('storage', handler)
}, [])
return (
<Listbox
as="div"
value={selectedTheme}
onChange={setSelectedTheme}
{...props}
>
<Listbox.Label className="sr-only">Theme</Listbox.Label>
<Listbox.Button
className="group rounded-xl bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
aria-label={selectedTheme?.name}
>
<SunIcon className="hidden h-6 w-6 fill-teal-50 stroke-teal-500 [[data-theme=light]_&]:block" />
<MoonIcon className="hidden h-6 w-6 fill-teal-400/10 stroke-teal-500 [[data-theme=dark]_&]:block" />
<SunIcon className="hidden h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 [:not(.dark)[data-theme=system]_&]:block" />
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition group-hover:stroke-zinc-400 [.dark[data-theme=system]_&]:block" />
</Listbox.Button>
<Listbox.Options className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
{themes.map((theme) => (
<Listbox.Option
key={theme.value}
value={theme}
className={({ active, selected }) =>
clsx(
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
{
'text-teal-400': selected,
'text-zinc-900 dark:text-white': active && !selected,
'text-zinc-700 dark:text-zinc-400': !active && !selected,
'bg-zinc-100 dark:bg-zinc-900/40': active,
}
)
}
>
{({ selected }) => (
<>
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
<theme.icon
className={clsx(
'h-4 w-4',
selected
? 'fill-teal-400 stroke-teal-400'
: 'fill-zinc-500 stroke-zinc-500'
)}
/>
</div>
<div className="ml-3">{theme.name}</div>
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
)
}

View File

@ -1,45 +1,48 @@
import { Head, Html, Main, NextScript } from 'next/document'
const modeScript = `
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const themeScript = `
let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
updateMode()
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
window.addEventListener('storage', updateModeWithoutTransitions)
function updateTheme(theme) {
theme = theme ?? window.localStorage.theme ?? 'system'
function updateMode() {
let isSystemDarkMode = darkModeMediaQuery.matches
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
if (isDarkMode) {
if (theme === 'dark' || (theme === 'system' && isDarkMode.matches)) {
document.documentElement.classList.add('dark')
} else {
} else if (theme === 'light' || (theme === 'system' && !isDarkMode.matches)) {
document.documentElement.classList.remove('dark')
}
if (isDarkMode === isSystemDarkMode) {
delete window.localStorage.isDarkMode
}
return theme
}
function disableTransitionsTemporarily() {
function updateThemeWithoutTransitions(theme) {
updateTheme(theme)
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
document.documentElement.classList.remove('[&_*]:!transition-none')
}, 0)
}
function updateModeWithoutTransitions() {
disableTransitionsTemporarily()
updateMode()
document.documentElement.setAttribute('data-theme', updateTheme())
new MutationObserver(([{ oldValue }]) => {
let newValue = document.documentElement.getAttribute('data-theme')
if (newValue !== oldValue) {
try {
window.localStorage.setItem('theme', newValue)
} catch {}
updateThemeWithoutTransitions(newValue)
}
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
isDarkMode.addEventListener('change', () => updateThemeWithoutTransitions())
`
export default function Document() {
return (
<Html className="h-full antialiased" lang="en">
<Head>
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<link
rel="alternate"
type="application/rss+xml"

View File

@ -96,7 +96,7 @@ function Resume() {
<ol className="mt-6 space-y-4">
{resume.map((role, roleIndex) => (
<li key={roleIndex} className="flex gap-4">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-full shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-xl shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image src={role.logo} alt="" className="h-7 w-7" unoptimized />
</div>
<dl className="flex flex-auto flex-wrap gap-x-2">
@ -111,8 +111,7 @@ function Resume() {
<dt className="sr-only">Date</dt>
<dd
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
aria-label={`${role.start.label ?? role.start} until ${
role.end.label ?? role.end
aria-label={`${role.start.label ?? role.start} until ${role.end.label ?? role.end
}`}
>
<time dateTime={role.start.dateTime ?? role.start}>

View File

@ -68,7 +68,7 @@ export default function Projects() {
>
{projects.map((project) => (
<Card as="li" key={project.name}>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image
src={project.logo}
alt=""