-
Notifications
You must be signed in to change notification settings - Fork 525
feat: add dark mode #2322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add dark mode #2322
Changes from 1 commit
8fcc565
e8d30c3
4a61e2a
09ec2ca
180dd54
80ae8f2
ea53fa6
86e3fee
f0c5ad4
b4b1c95
06cb705
17a8263
f8ecade
27920ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
.theme-toggle { | ||
--size: 2rem; | ||
--icon-fill: hsl(210, 22%, 22%); | ||
--icon-fill-hover: hsl(210, 22%, 12%); | ||
|
||
background: none; | ||
border: none; | ||
padding: 0; | ||
inline-size: var(--size); | ||
block-size: var(--size); | ||
aspect-ratio: 1; | ||
border-radius: 50%; | ||
cursor: pointer; | ||
touch-action: manipulation; | ||
-webkit-tap-highlight-color: transparent; | ||
outline-offset: 5px; | ||
} | ||
|
||
.theme-toggle > svg { | ||
inline-size: 100%; | ||
block-size: 100%; | ||
stroke-linecap: round; | ||
} | ||
|
||
[data-theme='dark'] .theme-toggle { | ||
--icon-fill: hsl(25, 100%, 50%); | ||
--icon-fill-hover: hsl(25, 100%, 40%); | ||
} | ||
|
||
.theme-toggle:hover, | ||
.theme-toggle:focus-visible { | ||
background: hsl(0 0% 50% / 0.1); | ||
} | ||
|
||
@media (prefers-reduced-motion: no-preference) { | ||
.theme-toggle { | ||
transition: background-color 0.3s ease; | ||
} | ||
|
||
.theme-toggle > svg { | ||
transition: transform 0.5s ease; | ||
} | ||
|
||
.theme-toggle:hover > svg, | ||
.theme-toggle:focus-visible > svg { | ||
transform: scale(1.1); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react' | ||
import './theme-toggle.css' | ||
import { useTheme } from '../../hooks/theme' | ||
|
||
export const ThemeToggle = () => { | ||
const { currentTheme: isDarkTheme, toggleTheme, toggleThemeWithKey } = useTheme() | ||
return ( | ||
<button | ||
className="theme-toggle" | ||
onClick={toggleTheme} | ||
onKeyDown={toggleThemeWithKey} | ||
tabIndex={0} | ||
aria-label={`Toggle ${isDarkTheme === 'light' ? 'light' : 'dark'} mode`} | ||
role="switch" | ||
aria-checked={isDarkTheme === 'dark'} | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
aria-hidden="true" | ||
color="#fff" | ||
> | ||
{isDarkTheme | ||
? ( | ||
<> | ||
<circle cx="12" cy="12" r="5" /> | ||
<line x1="12" y1="1" x2="12" y2="3" /> | ||
<line x1="12" y1="21" x2="12" y2="23" /> | ||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /> | ||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" /> | ||
<line x1="1" y1="12" x2="3" y2="12" /> | ||
<line x1="21" y1="12" x2="23" y2="12" /> | ||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /> | ||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" /> | ||
</> | ||
) | ||
: ( | ||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /> | ||
)} | ||
</svg> | ||
</button> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import React from 'react' | ||
|
||
export interface ThemeProviderProps { | ||
children: React.ReactNode | ||
} | ||
SgtPooki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export type Theme = 'light' | 'dark' | ||
|
||
export type ThemeContextValues = { | ||
currentTheme: Theme, | ||
toggleTheme: () => void; | ||
toggleThemeWithKey: (event: React.KeyboardEvent<HTMLButtonElement>) => void; | ||
|
||
} | ||
|
||
const createThemeContext = () => React.createContext<ThemeContextValues | null>(null) | ||
export const ThemeContext = createThemeContext() | ||
kaf-lamed-beyt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
export const ThemeProvider = ({ children }: ThemeProviderProps) => { | ||
SgtPooki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const [theme, setTheme] = React.useState<boolean>(() => { | ||
const savedTheme = | ||
typeof window !== 'undefined' && localStorage.getItem('theme') | ||
if (savedTheme) return savedTheme === 'dark' | ||
return window.matchMedia('prefers-color-scheme: dark').matches | ||
}) | ||
React.useEffect(() => { | ||
const htmlElem = document.documentElement | ||
const currentTheme = theme ? 'dark' : 'light' | ||
|
||
htmlElem.setAttribute('data-theme', currentTheme) | ||
localStorage.setItem('theme', currentTheme) | ||
htmlElem.setAttribute('aria-label', `Current theme: ${currentTheme}`) | ||
}, [theme]) | ||
const toggleTheme = () => setTheme((prevTheme) => !prevTheme) | ||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => { | ||
if (event.key === 'Enter' || event.key === ' ') { | ||
event.preventDefault() | ||
toggleTheme() | ||
} | ||
} | ||
const values: ThemeContextValues = { | ||
currentTheme: theme as unknown as Theme, | ||
|
||
toggleTheme, | ||
toggleThemeWithKey: handleKeyDown | ||
} | ||
return ( | ||
<ThemeContext.Provider value={values}> | ||
{children} | ||
</ThemeContext.Provider> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import React from 'react' | ||
import { ThemeContext, ThemeContextValues } from '../context/theme-provider' | ||
|
||
export const useTheme = () => { | ||
kaf-lamed-beyt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
const context = React.useContext(ThemeContext) | ||
if(context === null) { | ||
kaf-lamed-beyt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
throw new Error('Theme context is missing You probably forgot to wrap the component depending on theme in <ThemeProvider />') | ||
} | ||
return context as ThemeContextValues | ||
kaf-lamed-beyt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import StrokeIpld from '../icons/StrokeIpld.js' | |
|
||
// Styles | ||
import './NavBar.css' | ||
import { ThemeToggle } from '../components/theme-toggle/toggle' | ||
|
||
const NavLink = ({ | ||
to, | ||
|
@@ -73,6 +74,9 @@ export const NavBar = ({ t }) => { | |
</div> | ||
</div> | ||
<div className='dn db-l navbar-footer mb2 tc center f7 o-80 glow'> | ||
<div className='mb4'> | ||
<ThemeToggle /> | ||
</div> | ||
|
||
{ gitRevision && <div className='mb1'> | ||
<a className='link white' href={revisionUrl} target='_blank' rel='noopener noreferrer'>{t('app:terms.revision')} {gitRevision}</a> | ||
</div> } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want the background to be red here? I haven't ran the code to see for myself yet but this seems wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh! do not mind that. i was using it to test