-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enhancements made to
ColorModeToggle
for docs
- Loading branch information
glyph-cat
committed
Jun 3, 2024
1 parent
5b7fb6d
commit b349bfb
Showing
13 changed files
with
297 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { ColorMode, useColorMode } from '@docusaurus/theme-common' | ||
import { SiteSettingsState, UIAppearance } from '@site/src/services/site-settings' | ||
import { isFunction } from '@site/src/utils/type-check' | ||
import { useStateValue } from 'cotton-box-react' | ||
import { useEffect, useLayoutEffect, useReducer, useState } from 'react' | ||
|
||
const forceUpdateReducer = (c: number): number => c + 1 | ||
|
||
const IS_MEDIA_QUERY_SUPPORTED = typeof window !== 'undefined' && | ||
typeof window.matchMedia !== 'undefined' | ||
|
||
export function useMediaQuery(query: string): boolean { | ||
const [, forceUpdate] = useReducer(forceUpdateReducer, 0) | ||
const [mediaQuery] = useState(() => { | ||
return IS_MEDIA_QUERY_SUPPORTED | ||
? window.matchMedia(query) | ||
: null | ||
}) | ||
useLayoutEffect(() => { | ||
// The ref value `mq.current will likely have changed by the time this | ||
// effect's cleanup function runs. So a copy by value is made inside this | ||
// effect. | ||
if (IS_MEDIA_QUERY_SUPPORTED) { | ||
if (isFunction(mediaQuery.addEventListener)) { | ||
// New API | ||
mediaQuery.addEventListener('change', forceUpdate) | ||
return () => { mediaQuery.removeEventListener('change', forceUpdate) } | ||
} else if (isFunction(mediaQuery.addListener)) { | ||
// Deprecated API (fallback for old systems) | ||
mediaQuery.addListener(forceUpdate) | ||
return () => { mediaQuery.removeListener(forceUpdate) } | ||
} | ||
} | ||
}, [forceUpdate, mediaQuery]) | ||
return IS_MEDIA_QUERY_SUPPORTED ? mediaQuery.matches || false : false | ||
} | ||
|
||
export function useDerivedColorMode(): ColorMode { | ||
const { appearance: appearanceConfig } = useStateValue(SiteSettingsState) | ||
const systemPrefersDarkColorScheme = useMediaQuery('(prefers-color-scheme: dark)') | ||
if (appearanceConfig === UIAppearance.AUTO) { | ||
return systemPrefersDarkColorScheme ? 'dark' : 'light' | ||
} else { | ||
return appearanceConfig === UIAppearance.LIGHT ? 'light' : 'dark' | ||
} | ||
} | ||
|
||
export function ThemeInterception(): JSX.Element { | ||
const { colorMode: currentColorMode, setColorMode } = useColorMode() | ||
const derivedColorMode = useDerivedColorMode() | ||
useEffect(() => { | ||
if (derivedColorMode !== currentColorMode) { | ||
setColorMode(derivedColorMode) | ||
} | ||
}, [currentColorMode, derivedColorMode, setColorMode]) | ||
return null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { StateManager } from 'cotton-box' | ||
|
||
const STORAGE_KEY = 'cotton-box-site-settings' | ||
|
||
export enum UIAppearance { | ||
AUTO, | ||
LIGHT, | ||
DARK, | ||
} | ||
|
||
export interface ISiteSettingsState { | ||
appearance: UIAppearance | ||
} | ||
|
||
export const SiteSettingsState = new StateManager<ISiteSettingsState>({ | ||
appearance: UIAppearance.AUTO, | ||
}, { | ||
lifecycle: typeof window === 'undefined' ? {} : { | ||
init({ commit, commitNoop }) { | ||
const rawState = localStorage.getItem(STORAGE_KEY) | ||
if (rawState) { | ||
try { | ||
const parsedState = JSON.parse(rawState) | ||
return commit(parsedState) | ||
} catch (e) { /* ... */ } | ||
} | ||
return commitNoop() | ||
}, | ||
didSet({ state }) { | ||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) | ||
}, | ||
didReset() { | ||
localStorage.removeItem(STORAGE_KEY) | ||
}, | ||
}, | ||
}) |
125 changes: 100 additions & 25 deletions
125
packages/docs/src/theme/Navbar/ColorModeToggle/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,110 @@ | ||
import React from 'react' | ||
import { useColorMode, useThemeConfig } from '@docusaurus/theme-common' | ||
import ColorModeToggle from '@theme/ColorModeToggle' | ||
import type { Props } from '@theme/Navbar/ColorModeToggle' | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
import { useColorMode } from '@docusaurus/theme-common' | ||
import { ThemeInterception } from '@site/src/hooks/theming' | ||
import { SiteSettingsState, UIAppearance } from '@site/src/services/site-settings' | ||
import { useStateValue } from 'cotton-box-react' | ||
import { JSX, useCallback, useEffect, useRef, useState } from 'react' | ||
import styles from './styles.module.css' | ||
|
||
export default function NavbarColorModeToggle({ | ||
className, | ||
}: Props): JSX.Element | null { | ||
const navbarStyle = useThemeConfig().navbar.style | ||
const disabled = useThemeConfig().colorMode.disableSwitch | ||
const { colorMode, setColorMode } = useColorMode() | ||
const AppearanceIcon = require('@site/static/img/brightness_4.svg').default | ||
const CheckIcon = require('@site/static/img/check.svg').default | ||
|
||
if (disabled) { | ||
return null | ||
} | ||
const setToAuto = () => { | ||
SiteSettingsState.set((prevState) => ({ | ||
...prevState, | ||
appearance: UIAppearance.AUTO, | ||
})) | ||
} | ||
|
||
// TODO: [Low priority] `useMediaQuery` to find out if system prefers dark theme (when set to `system`) | ||
const setToLight = () => { | ||
SiteSettingsState.set((prevState) => ({ | ||
...prevState, | ||
appearance: UIAppearance.LIGHT, | ||
})) | ||
} | ||
|
||
// TODO: [Low priority] Dropdown menu | ||
// - System | ||
// - Light | ||
// - Dark | ||
const setToDark = () => { | ||
SiteSettingsState.set((prevState) => ({ | ||
...prevState, | ||
appearance: UIAppearance.DARK, | ||
})) | ||
} | ||
|
||
function pointerIsInBound(event: MouseEvent, div: HTMLDivElement): boolean { | ||
const { top, left, height, width } = div.getBoundingClientRect() | ||
return ( | ||
<ColorModeToggle | ||
className={className} | ||
buttonClassName={ | ||
navbarStyle === 'dark' ? styles.darkNavbarColorModeToggle : undefined | ||
event.clientX >= left && | ||
event.clientX <= left + width && | ||
event.clientY >= top && | ||
event.clientY <= top + height | ||
) | ||
} | ||
|
||
export default function NavbarColorModeToggle(): JSX.Element { | ||
const { colorMode } = useColorMode() | ||
const { appearance } = useStateValue(SiteSettingsState) | ||
const [showMenu, setMenuVisibility] = useState(false) | ||
const toggleMenuVisibility = useCallback(() => { | ||
setMenuVisibility(v => !v) | ||
}, []) | ||
const iconColor = colorMode === 'light' ? '#000000' : '#ffffff' | ||
|
||
const buttonRef = useRef<HTMLDivElement>(null) | ||
const menuContainerRef = useRef<HTMLDivElement>(null) | ||
useEffect(() => { | ||
// Click-away handling | ||
if (!showMenu) { return } | ||
const onClick = (event: MouseEvent) => { | ||
if (pointerIsInBound(event, buttonRef.current)) { return } | ||
if (!pointerIsInBound(event, menuContainerRef.current)) { | ||
setMenuVisibility(false) | ||
} | ||
value={colorMode} | ||
onChange={setColorMode} | ||
/> | ||
} | ||
window.addEventListener('click', onClick) | ||
return () => { window.removeEventListener('click', onClick) } | ||
}, [showMenu]) | ||
|
||
return ( | ||
<> | ||
<div | ||
ref={buttonRef} | ||
className={styles.colorModeButton} | ||
onClick={toggleMenuVisibility} | ||
> | ||
<AppearanceIcon fill={iconColor} /> | ||
</div> | ||
{showMenu && ( | ||
<div ref={menuContainerRef} className={styles.menuContainer}> | ||
<div className={styles.menuItem} onClick={setToAuto}> | ||
<div | ||
className={styles.checkIconContainer} | ||
style={appearance !== UIAppearance.AUTO ? { opacity: 0 } : {}} | ||
> | ||
<CheckIcon fill={iconColor} /> | ||
</div> | ||
{'System'} | ||
</div> | ||
<div className={styles.menuItem} onClick={setToLight}> | ||
<div | ||
className={styles.checkIconContainer} | ||
style={appearance !== UIAppearance.LIGHT ? { opacity: 0 } : {}} | ||
> | ||
<CheckIcon fill={iconColor} /> | ||
</div> | ||
{'Light'} | ||
</div> | ||
<div className={styles.menuItem} onClick={setToDark}> | ||
<div | ||
className={styles.checkIconContainer} | ||
style={appearance !== UIAppearance.DARK ? { opacity: 0 } : {}} | ||
> | ||
<CheckIcon fill={iconColor} /> | ||
</div> | ||
{'Dark'} | ||
</div> | ||
</div> | ||
)} | ||
<ThemeInterception /> | ||
</> | ||
) | ||
} |
61 changes: 61 additions & 0 deletions
61
packages/docs/src/theme/Navbar/ColorModeToggle/styles.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,64 @@ | ||
.darkNavbarColorModeToggle:hover { | ||
background: var(--ifm-color-gray-800); | ||
} | ||
|
||
.colorModeButton { | ||
aspect-ratio: 1; | ||
border-radius: 50%; | ||
cursor: pointer; | ||
display: grid; | ||
height: 44px; | ||
place-items: center; | ||
transition: background-color 0.1s; | ||
} | ||
|
||
.colorModeButton:hover { | ||
background-color: #80808040; | ||
} | ||
|
||
.colorModeButton:active { | ||
background-color: #80808020; | ||
transition: background-color 0s; | ||
} | ||
|
||
.menuContainer { | ||
--menuItemSize: 48px; | ||
background-color: #ffffff; | ||
border: solid 1px #80808040; | ||
box-shadow: 0px 10px 20px 0px #00000010; | ||
display: grid; | ||
grid-auto-rows: var(--menuItemSize); | ||
position: absolute; | ||
right: 0; | ||
top: 60px; | ||
min-width: 200px; | ||
} | ||
|
||
[data-theme="dark"] { | ||
.menuContainer { | ||
background-color: #202428; | ||
} | ||
} | ||
|
||
.menuItem { | ||
align-items: center; | ||
cursor: pointer; | ||
display: grid; | ||
grid-auto-columns: max-content; | ||
grid-auto-flow: column; | ||
user-select: none; | ||
} | ||
|
||
.menuItem:hover { | ||
background-color: #80808040; | ||
} | ||
|
||
.menuItem:active { | ||
background-color: #00000040; | ||
} | ||
|
||
.checkIconContainer { | ||
display: grid; | ||
place-items: center; | ||
width: var(--menuItemSize); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* Determine if a value is a function. | ||
* @param value - The value to check. | ||
* @returns A boolean indicating whether the value is a function. | ||
*/ | ||
export function isFunction(value: unknown): value is ((args: unknown[]) => unknown) { | ||
return typeof value === 'function' | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.