Skip to content

Commit

Permalink
Enhancements made to ColorModeToggle for docs
Browse files Browse the repository at this point in the history
  • Loading branch information
glyph-cat committed Jun 3, 2024
1 parent 5b7fb6d commit b349bfb
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 33 deletions.
4 changes: 3 additions & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"@mdx-js/react": "^3.0.1",
"@monaco-editor/react": "^4.6.0",
"clsx": "^2.1.0",
"cotton-box": "^0.1.0",
"cotton-box-react": "^0.1.0",
"prism-react-renderer": "^2.3.1",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
Expand Down Expand Up @@ -63,4 +65,4 @@
"engines": {
"node": ">=18.0"
}
}
}
2 changes: 1 addition & 1 deletion packages/docs/src/components/homepage-features/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const features: Array<FeatureItem> = [
Svg: require('@site/static/img/acute.svg').default,
description: (
<>
An escape hatch for when you need to handle complicated data-fetching logic.
An escape hatch for when you need to set state in conjunction with complicated data-fetching logic.
</>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

.featureSvg {
height: 96px;
fill: #8db8fa;
fill: #2b80ff;
width: 96px;
}

[data-theme="dark"] {
.featureSvg {
fill: #4b90ff;
}
}
3 changes: 2 additions & 1 deletion packages/docs/src/css/custom.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap");

/**
* Any CSS included here will be global. The classic template
Expand All @@ -9,6 +9,7 @@
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2b80ff;
/* TODO: [High priority] Set all other css variables based on the main/primary color */
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
Expand Down
57 changes: 57 additions & 0 deletions packages/docs/src/hooks/theming/index.ts
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
}
36 changes: 36 additions & 0 deletions packages/docs/src/services/site-settings/index.ts
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 packages/docs/src/theme/Navbar/ColorModeToggle/index.tsx
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 packages/docs/src/theme/Navbar/ColorModeToggle/styles.module.css
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);
}
15 changes: 11 additions & 4 deletions packages/docs/src/theme/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import React from 'react'
import { useColorMode } from '@docusaurus/theme-common'
import type { WrapperProps } from '@docusaurus/types'
import Navbar from '@theme-original/Navbar'
import type NavbarType from '@theme/Navbar'
import type { WrapperProps } from '@docusaurus/types'
import React from 'react'

type Props = WrapperProps<typeof NavbarType>

export default function NavbarWrapper(props: Props): JSX.Element {
const { colorMode } = useColorMode()
return (
<>
<Navbar {...props} />
<div style={{
backgroundColor: '#ffeecc',
color: '#aa804a',
fontWeight: 600,
padding: '5px 10px',
position: 'sticky',
textAlign: 'center',
top: 60,
zIndex: 1,
...(colorMode === 'light' ? {
backgroundColor: '#ffeecc',
color: '#aa804a',
} : {
backgroundColor: '#804a00',
color: '#ffeecc',
}),
}}>
NOTE: This library is still experimental and documentations might be incomplete.
</div>
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/utils/type-check/index.ts
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'
}
1 change: 1 addition & 0 deletions packages/docs/static/img/brightness_4.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/docs/static/img/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b349bfb

Please sign in to comment.