Merge pull request #19 from acid-info/theme/header

Theme/header
This commit is contained in:
amir houieh
2022-10-07 20:00:34 +02:00
committed by GitHub
26 changed files with 732 additions and 6 deletions
+3 -5
View File
@@ -59,7 +59,7 @@ const config = {
({
docs: { sidebar: { hideable: true } },
navbar: {
title: 'My Site',
title: '',
logo: {
alt: 'My Site Logo',
src: 'img/logo.svg',
@@ -69,12 +69,10 @@ const config = {
type: 'doc',
docId: 'intro',
position: 'left',
label: 'Tutorial',
label: 'Docs',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{
href: 'https://github.com/facebook/docusaurus',
label: 'GitHub',
type: 'localeDropdown',
position: 'right',
},
],
+1
View File
@@ -22,6 +22,7 @@
"@docusaurus/preset-classic": "2.1.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.2",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
@@ -0,0 +1,17 @@
.root {
-webkit-tap-highlight-color: transparent;
align-items: center;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 50%;
transition: background var(--ifm-transition-fast);
width: 2rem;
height: 2rem;
}
.root:hover {
background: var(--ifm-color-emphasis-200);
}
+20
View File
@@ -0,0 +1,20 @@
import clsx from 'clsx'
import React from 'react'
import styles from './IconButton.module.scss'
export const IconButton: React.FC<
React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {}
> = ({ children, className, ...props }) => {
return (
<button
type="button"
className={clsx('clean-btn', styles.root, className)}
{...props}
>
{children}
</button>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './IconButton'
@@ -6,6 +6,7 @@ export namespace GlobalStore {
}
export type Actions = {
toggleSidebar: () => void
setHiddenSidebar: (payload: any) => void
}
@@ -14,6 +15,9 @@ export namespace GlobalStore {
export const globalStore = new GlobalStore.Store(() => {
return {
toggleSidebar: (payload, state, setState, dispatch) => {
setState((state) => void (state.hiddenSidebar = !state.hiddenSidebar))
},
setHiddenSidebar: (payload, state, setState, dispatch) => {
setState((state) => {
state.hiddenSidebar =
+4
View File
@@ -351,7 +351,11 @@
--ifm-hero-text-color: var(--ifm-color-black);
}
/* sidebar */
:root {
--doc-sidebar-width: 16.66vw !important;
--doc-sidebar-max-width: 320px;
--ifm-navbar-item-padding-vertical: 8px;
--ifm-navbar-item-padding-horizontal: 12px;
}
+56
View File
@@ -0,0 +1,56 @@
import { translate } from '@docusaurus/Translate'
import useIsBrowser from '@docusaurus/useIsBrowser'
import { IconButton } from '@site/src/components/IconButton'
import type { Props } from '@theme/ColorModeToggle'
import IconDarkMode from '@theme/Icon/DarkMode'
import IconLightMode from '@theme/Icon/LightMode'
import clsx from 'clsx'
import React from 'react'
import styles from './styles.module.css'
function ColorModeToggle({ className, value, onChange }: Props): JSX.Element {
const isBrowser = useIsBrowser()
const title = translate(
{
message: 'Switch between dark and light mode (currently {mode})',
id: 'theme.colorToggle.ariaLabel',
description: 'The ARIA label for the navbar color mode toggle',
},
{
mode:
value === 'dark'
? translate({
message: 'dark mode',
id: 'theme.colorToggle.ariaLabel.mode.dark',
description: 'The name for the dark color mode',
})
: translate({
message: 'light mode',
id: 'theme.colorToggle.ariaLabel.mode.light',
description: 'The name for the light color mode',
}),
},
)
return (
<div className={clsx(styles.toggle, className)}>
<IconButton
title={title}
aria-label={title}
disabled={!isBrowser}
className={clsx(!isBrowser && styles.toggleButtonDisabled)}
onClick={() => onChange(value === 'dark' ? 'light' : 'dark')}
>
<IconLightMode
className={clsx(styles.toggleIcon, styles.lightToggleIcon)}
/>
<IconDarkMode
className={clsx(styles.toggleIcon, styles.darkToggleIcon)}
/>
</IconButton>
</div>
)
}
export default React.memo(ColorModeToggle)
@@ -0,0 +1,8 @@
[data-theme='light'] .darkToggleIcon,
[data-theme='dark'] .lightToggleIcon {
display: none;
}
.toggleButtonDisabled {
cursor: not-allowed;
}
+7
View File
@@ -0,0 +1,7 @@
import React from 'react'
import type { Props } from '@theme/Icon/LightMode'
import Icon from '@site/static/icons/day.svg'
export default function IconLightMode(props: Props): JSX.Element {
return <Icon {...props} />
}
+21
View File
@@ -0,0 +1,21 @@
import { IconButton } from '@site/src/components/IconButton'
import React from 'react'
import Icon from '@site/static/icons/share.svg'
import styles from './styles.module.scss'
import copy2clipboard from 'copy-to-clipboard'
export const ShareButton: React.FC<{}> = ({}) => {
return (
<IconButton
title="share this page"
aria-label="share this page"
onClick={() => {
if (typeof navigator?.share === 'function') navigator.share()
else copy2clipboard(window.location.href)
}}
className={styles.shareButton}
>
<Icon />
</IconButton>
)
}
+97
View File
@@ -0,0 +1,97 @@
import { useThemeConfig } from '@docusaurus/theme-common'
import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'
import {
globalStore,
selectHiddenSidebar,
} from '@site/src/containers/GlobalStore'
import ChevronIcon from '@site/static/icons/chevron-left.svg'
import HamburgerIcon from '@site/static/icons/hamburger-menu.svg'
import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'
import NavbarLogo from '@theme/Navbar/Logo'
import NavbarSearch from '@theme/Navbar/Search'
import NavbarItem, { Props as NavbarItemConfig } from '@theme/NavbarItem'
import SearchBar from '@theme/SearchBar'
import clsx from 'clsx'
import React from 'react'
import { ShareButton } from './ShareButton'
import styles from './styles.module.scss'
function useNavbarItems() {
// TODO temporary casting until ThemeConfig type is improved
return useThemeConfig().navbar.items as NavbarItemConfig[]
}
function NavbarItems({ items }: { items: NavbarItemConfig[] }): JSX.Element {
return (
<>
{items.map((item, i) => (
<NavbarItem {...item} key={i} />
))}
</>
)
}
export default function NavbarContent(): JSX.Element {
const mobileSidebar = useNavbarMobileSidebar()
const items = useNavbarItems()
const dispatch = globalStore.useDispatch()
const hiddenDesktopSidebar = globalStore.useSelector(selectHiddenSidebar)
const localeDropdown = items.find((item) => item.type === 'localeDropdown')
const links = items.filter(
(item) => item.type === 'doc' || !!(item as any).to,
)
return (
<div className={clsx('row', styles.root)}>
<div className="col col--2">
<button
onClick={dispatch.toggleSidebar}
className={clsx(
styles.sidebarButton,
hiddenDesktopSidebar && styles.expand,
)}
>
{!hiddenDesktopSidebar ? <ChevronIcon /> : <HamburgerIcon />}
</button>
</div>
<div className={clsx('col', styles.headerMiddle)}>
<div
className={clsx(
styles.menu,
styles.leftContainer,
hiddenDesktopSidebar && styles.shifted,
)}
>
<div className={styles.navbarLogoWrapper}>
<NavbarLogo />
</div>
<div>
<NavbarSearch>
<SearchBar />
</NavbarSearch>
</div>
</div>
<div>
<nav className={styles.menu}>
<NavbarItems items={links} />
</nav>
</div>
</div>
<div className={clsx('col col--5', styles.headerRight)}>
<ShareButton />
<NavbarColorModeToggle />
{localeDropdown && (
<>
<div className={styles.divider} />
<NavbarItem {...localeDropdown} />
</>
)}
</div>
</div>
)
}
+147
View File
@@ -0,0 +1,147 @@
@use '@site/src/css/utils';
.root {
width: 100%;
margin: 0;
& > div {
display: flex;
flex-direction: row;
align-items: center;
flex-grow: 0;
}
& > div:first-child {
max-width: var(--doc-sidebar-max-width);
}
& > .headerMiddle {
justify-content: space-between;
flex-grow: 1;
}
}
.leftContainer {
display: flex;
flex-direction: row;
align-items: center;
transition: 0.2s;
transform: translateX(0);
&.shifted {
transform: translateX(
calc(-1 * min(var(--doc-sidebar-max-width), 16.66vw) + 64px)
);
}
}
.headerRight {
justify-content: flex-end;
}
.navbarLogoWrapper {
& > * {
margin: 0;
}
}
.menu {
@extend %menu-common;
display: flex;
flex-direction: row;
}
.sidebarButton {
border: none;
background: none;
cursor: pointer;
@extend %menu-common;
width: 52px;
height: 46px;
&.expand {
svg,
svg * {
fill: currentColor;
}
}
&:not(.expand) {
svg,
svg * {
stroke: currentColor;
}
}
}
%menu-common {
padding: 6px;
background-color: var(--ifm-background-surface-color);
border-radius: 1rem;
}
.headerRight {
& > *:not(:last-child) {
margin-right: 34px;
}
}
.divider {
height: 11px;
width: 1px;
background-color: #d9d9d9; // TODO: use infima variables
}
.shareButton {
svg,
svg * {
fill: currentColor;
stroke: currentColor;
}
}
@include utils.responsive('lg', 'down') {
.root {
padding: 0 var(--ifm-spacing-horizontal);
}
.root > * {
width: auto;
flex: 0 0 auto !important;
padding: 0;
}
.headerLeft {
}
.headerMiddle {
margin-left: 20px;
.leftContainer.shifted {
transform: none;
}
nav {
display: none;
}
}
.headerRight {
display: none !important;
}
}
/*
Hide color mode toggle in small viewports
*/
@media (max-width: 996px) {
.colorModeToggle {
display: none;
}
}
+10
View File
@@ -0,0 +1,10 @@
.navbar {
--ifm-navbar-height: 3.875rem;
--ifm-navbar-background-color: none;
--ifm-navbar-shadow: none;
--ifm-navbar-padding-horizontal: 0px;
--ifm-navbar-padding-vertical: 8px;
}
:root {
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import Navbar from '@theme-original/Navbar'
import type NavbarType from '@theme/Navbar'
import type { WrapperProps } from '@docusaurus/types'
import './index.scss'
type Props = WrapperProps<typeof NavbarType>
export default function NavbarWrapper(props: Props): JSX.Element {
return (
<>
<Navbar {...props} />
</>
)
}
@@ -0,0 +1,24 @@
.navbar__item.dropdown {
background-color: #eeeef1;
border-radius: 0.5rem;
& > .navbar__link {
&::after {
display: none;
}
& + svg {
margin-left: 0.75rem;
}
}
ul {
margin-top: 5px;
}
}
[data-theme='dark'] {
.navbar__item.dropdown {
background-color: var(--ifm-background-color);
}
}
+190
View File
@@ -0,0 +1,190 @@
import React, { useState, useRef, useEffect } from 'react'
import clsx from 'clsx'
import {
isRegexpStringMatch,
useCollapsible,
Collapsible,
} from '@docusaurus/theme-common'
import { isSamePath, useLocalPathname } from '@docusaurus/theme-common/internal'
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'
import NavbarItem, { LinkLikeNavbarItemProps } from '@theme/NavbarItem'
import type {
DesktopOrMobileNavBarItemProps,
Props,
} from '@theme/NavbarItem/DropdownNavbarItem'
import './DropdownNavbarItem.scss'
import DropdownIcon from '@site/static/icons/dropdown.svg'
function isItemActive(
item: LinkLikeNavbarItemProps,
localPathname: string,
): boolean {
if (isSamePath(item.to, localPathname)) {
return true
}
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
return true
}
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
return true
}
return false
}
function containsActiveItems(
items: readonly LinkLikeNavbarItemProps[],
localPathname: string,
): boolean {
return items.some((item) => isItemActive(item, localPathname))
}
function DropdownNavbarItemDesktop({
items,
position,
className,
onClick,
...props
}: DesktopOrMobileNavBarItemProps) {
const dropdownRef = useRef<HTMLDivElement>(null)
const [showDropdown, setShowDropdown] = useState(false)
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (
!dropdownRef.current ||
dropdownRef.current.contains(event.target as Node)
) {
return
}
setShowDropdown(false)
}
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [dropdownRef])
return (
<div
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--right': position === 'right',
'dropdown--show': showDropdown,
})}
>
<NavbarNavLink
aria-haspopup="true"
aria-expanded={showDropdown}
role="button"
href={props.to ? undefined : '#'}
className={clsx('navbar__link', className)}
{...props}
onClick={props.to ? undefined : (e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
setShowDropdown(!showDropdown)
}
}}
>
{props.children ?? props.label}
</NavbarNavLink>
<DropdownIcon />
<ul className="dropdown__menu">
{items.map((childItemProps, i) => (
<NavbarItem
isDropdownItem
onKeyDown={(e) => {
if (i === items.length - 1 && e.key === 'Tab') {
e.preventDefault()
setShowDropdown(false)
const nextNavbarItem = dropdownRef.current!.nextElementSibling
if (nextNavbarItem) {
const targetItem =
nextNavbarItem instanceof HTMLAnchorElement
? nextNavbarItem
: // Next item is another dropdown; focus on the inner
// anchor element instead so there's outline
nextNavbarItem.querySelector('a')!
targetItem.focus()
}
}
}}
activeClassName="dropdown__link--active"
{...childItemProps}
key={i}
/>
))}
</ul>
</div>
)
}
function DropdownNavbarItemMobile({
items,
className,
position, // Need to destructure position from props so that it doesn't get passed on.
onClick,
...props
}: DesktopOrMobileNavBarItemProps) {
const localPathname = useLocalPathname()
const containsActive = containsActiveItems(items, localPathname)
const { collapsed, toggleCollapsed, setCollapsed } = useCollapsible({
initialState: () => !containsActive,
})
// Expand/collapse if any item active after a navigation
useEffect(() => {
if (containsActive) {
setCollapsed(!containsActive)
}
}, [localPathname, containsActive, setCollapsed])
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}
>
<NavbarNavLink
role="button"
className={clsx(
'menu__link menu__link--sublist menu__link--sublist-caret',
className,
)}
{...props}
onClick={(e) => {
e.preventDefault()
toggleCollapsed()
}}
>
{props.children ?? props.label}
</NavbarNavLink>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
{items.map((childItemProps, i) => (
<NavbarItem
mobile
isDropdownItem
onClick={onClick}
activeClassName="menu__link--active"
{...childItemProps}
key={i}
/>
))}
</Collapsible>
</li>
)
}
export default function DropdownNavbarItem({
mobile = false,
...props
}: Props): JSX.Element {
const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop
return <Comp {...props} />
}
@@ -0,0 +1,66 @@
import React from 'react'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import { useAlternatePageUtils } from '@docusaurus/theme-common/internal'
import { translate } from '@docusaurus/Translate'
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'
import IconLanguage from '@theme/Icon/Language'
import type { LinkLikeNavbarItemProps } from '@theme/NavbarItem'
import type { Props } from '@theme/NavbarItem/LocaleDropdownNavbarItem'
import styles from './styles.module.css'
export default function LocaleDropdownNavbarItem({
mobile,
dropdownItemsBefore,
dropdownItemsAfter,
...props
}: Props): JSX.Element {
const {
i18n: { currentLocale, locales, localeConfigs },
} = useDocusaurusContext()
const alternatePageUtils = useAlternatePageUtils()
const localeItems = locales.map((locale): LinkLikeNavbarItemProps => {
const to = `pathname://${alternatePageUtils.createUrl({
locale,
fullyQualified: false,
})}`
return {
label: localeConfigs[locale]!.label,
lang: localeConfigs[locale]!.htmlLang,
to,
target: '_self',
autoAddBaseUrl: false,
className:
// eslint-disable-next-line no-nested-ternary
locale === currentLocale
? // Similar idea as DefaultNavbarItem: select the right Infima active
// class name. This cannot be substituted with isActive, because the
// target URLs contain `pathname://` and therefore are not NavLinks!
mobile
? 'menu__link--active'
: 'dropdown__link--active'
: '',
}
})
const items = [...dropdownItemsBefore, ...localeItems, ...dropdownItemsAfter]
// Mobile is handled a bit differently
const dropdownLabel = mobile
? translate({
message: 'Languages',
id: 'theme.navbar.mobileLanguageDropdown.label',
description: 'The label for the mobile language switcher dropdown',
})
: localeConfigs[currentLocale]!.label
return (
<DropdownNavbarItem
{...props}
mobile={mobile}
label={<>{dropdownLabel}</>}
items={items}
/>
)
}
@@ -0,0 +1,4 @@
.iconLanguage {
vertical-align: text-bottom;
margin-right: 5px;
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7647 5.29407L7.05884 9.99995L11.7647 14.7058" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

Some files were not shown because too many files have changed in this diff Show More