Update ThemeSelector to include system option
This commit is contained in:
parent
5f608c23f9
commit
d7a405eea1
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
96
src/components/ThemeSelector.jsx
Normal file
96
src/components/ThemeSelector.jsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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"
|
||||
|
@ -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,9 +111,8 @@ 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}>
|
||||
{role.start.label ?? role.start}
|
||||
|
@ -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=""
|
||||
|
Loading…
x
Reference in New Issue
Block a user