mirror of
https://github.com/zhigang1992/react.git
synced 2026-03-26 06:55:07 +08:00
docs: add menu bar and optimize navigation
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Button, useTheme, Spacer } from 'components'
|
||||
import { Button, useTheme, Select, Spacer } from 'components'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import MoonIcon from './icons/moon'
|
||||
import SunIcon from './icons/sun'
|
||||
import GithubIcon from './icons/github'
|
||||
|
||||
const Controls: React.FC<{}> = React.memo(({
|
||||
}) => {
|
||||
@@ -12,14 +11,14 @@ const Controls: React.FC<{}> = React.memo(({
|
||||
const { onChange, updateChineseState } = useConfigs()
|
||||
const { pathname } = useRouter()
|
||||
const currentLocaleText = useMemo(() => {
|
||||
return pathname.toLowerCase().includes('zh-cn') ? 'EN' : '中'
|
||||
return pathname.toLowerCase().includes('zh-cn') ? 'English' : '中文文档'
|
||||
}, [pathname])
|
||||
const isDark = useMemo(() => theme.type === 'dark', [theme.type])
|
||||
const switchThemes = useCallback(() => {
|
||||
const isDark = theme.type === 'dark'
|
||||
onChange && onChange(!isDark)
|
||||
}, [theme.type])
|
||||
|
||||
const switchThemes = (val: string) => {
|
||||
const isDark = val === 'dark'
|
||||
onChange && onChange(isDark)
|
||||
}
|
||||
|
||||
const switchLanguages = useCallback(() => {
|
||||
const currentIsChinese = pathname.toLowerCase().includes('zh-cn')
|
||||
const nextPath = `/${currentIsChinese ? 'en-us' : 'zh-cn'}`
|
||||
@@ -36,39 +35,45 @@ const Controls: React.FC<{}> = React.memo(({
|
||||
return (
|
||||
<div className="controls">
|
||||
<div className="tools">
|
||||
<Spacer x={.5} />
|
||||
<Button className="button" auto type="abort"
|
||||
onClick={switchThemes}>
|
||||
{isDark ? <SunIcon width={16} height={16} /> : <MoonIcon width={16} height={16} />}
|
||||
</Button>
|
||||
<Button className="button" auto type="abort"
|
||||
onClick={redirectGithub}>
|
||||
<GithubIcon width={16} height={16} />
|
||||
</Button>
|
||||
<Button className="button" auto type="abort"
|
||||
onClick={switchLanguages}>
|
||||
<span>{currentLocaleText}</span>
|
||||
</Button>
|
||||
<Button auto type="abort" size="small" onClick={redirectGithub}>Github</Button>
|
||||
<Spacer x={.25} />
|
||||
<Button auto type="abort" size="small" onClick={switchLanguages}>{currentLocaleText}</Button>
|
||||
<Spacer x={.75} />
|
||||
<Select size="small" pure onChange={switchThemes} initialValue={isDark ? 'dark' : 'light'}>
|
||||
<Select.Option value="light">
|
||||
<div className="select-content">
|
||||
<SunIcon width={16} height={16} /> Light
|
||||
</div>
|
||||
</Select.Option>
|
||||
<Select.Option value="dark">
|
||||
<div className="select-content">
|
||||
<MoonIcon width={16} height={16} /> Dark
|
||||
</div>
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.controls {
|
||||
height: 110px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
margin: 0;
|
||||
padding-bottom: ${theme.layout.gapHalf};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.controls :global(.button) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
.controls :global(.select) {
|
||||
width: min-content;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.select-content {
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.select-content :global(svg) {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.tools {
|
||||
@@ -78,25 +83,6 @@ const Controls: React.FC<{}> = React.memo(({
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tools:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 1.25rem;
|
||||
width: .3125rem;
|
||||
background-color: ${theme.palette.accents_2};
|
||||
}
|
||||
|
||||
.controls :global(.line) {
|
||||
width: 150px;
|
||||
height: 55px;
|
||||
cursor: pointer;
|
||||
background-color: ${theme.palette.background};
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
transition: all 200ms ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
.controls {
|
||||
display: none;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { useTheme } from 'components'
|
||||
import Controls from './controls'
|
||||
import Sidebar from './sidebar'
|
||||
import { Sides } from './sidebar/side-item'
|
||||
import TabbarMobile from './sidebar/tabbar-mobile'
|
||||
import useBodyScroll from 'components/utils/use-body-scroll'
|
||||
import sides from 'lib/data/metadata.json'
|
||||
import { useConfigs } from '../config-context'
|
||||
|
||||
export interface Meta {
|
||||
title: string
|
||||
@@ -23,27 +21,19 @@ export interface MultilLocaleMetaInformation {
|
||||
|
||||
export const Layout: React.FC<React.PropsWithChildren<Props>> = React.memo(({ children }) => {
|
||||
const theme = useTheme()
|
||||
const { pathname } = useRouter()
|
||||
const { sides, tabbarFixed } = useConfigs()
|
||||
const [, setBodyScroll] = useBodyScroll(null, { scrollLayer: true })
|
||||
const [expanded, setExpanded] = useState<boolean>(false)
|
||||
const mobileTabbarClickHandler = () => {
|
||||
setExpanded(!expanded)
|
||||
setBodyScroll(!expanded)
|
||||
}
|
||||
const sideData = useMemo(() => {
|
||||
const language = pathname
|
||||
.split('/')
|
||||
.filter(r => !!r)
|
||||
const locale: string = language[0] || 'en-us'
|
||||
return (sides as MultilLocaleMetaInformation)[locale]
|
||||
}, [pathname, sides])
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<TabbarMobile onClick={mobileTabbarClickHandler} />
|
||||
<aside className="sidebar">
|
||||
<Controls />
|
||||
<Sidebar sides={sideData}/>
|
||||
<Sidebar sides={sides}/>
|
||||
</aside>
|
||||
<div className="side-shadow" />
|
||||
<main className="main">
|
||||
@@ -52,7 +42,7 @@ export const Layout: React.FC<React.PropsWithChildren<Props>> = React.memo(({ ch
|
||||
|
||||
<style jsx>{`
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
min-height: calc(100vh - 108px);
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${theme.layout.gap};
|
||||
@@ -65,10 +55,15 @@ export const Layout: React.FC<React.PropsWithChildren<Props>> = React.memo(({ ch
|
||||
margin-right: 20px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-flex-shrink: 0;
|
||||
height: 100%;
|
||||
height: calc(100% - 2rem - 140px + ${tabbarFixed ? '60px' : 0});
|
||||
position: fixed;
|
||||
top: 140px;
|
||||
bottom: 2rem;
|
||||
transform: translateY(${tabbarFixed ? '-60px' : 0});
|
||||
transition: transform 200ms ease-out;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.side-shadow {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
|
||||
14
lib/components/menu/index.tsx
Normal file
14
lib/components/menu/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import MenuLinks from './menu-links'
|
||||
import MenuSticker from './menu-sticker'
|
||||
|
||||
const Menu = () => {
|
||||
return (
|
||||
<div>
|
||||
<MenuLinks />
|
||||
<MenuSticker />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
33
lib/components/menu/menu-links.tsx
Normal file
33
lib/components/menu/menu-links.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import { useTheme } from 'components'
|
||||
import Controls from 'lib/components/controls'
|
||||
|
||||
|
||||
const MenuLinks = () => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<nav>
|
||||
<div className="site-name">
|
||||
<h3>ZEIT UI - React</h3>
|
||||
</div>
|
||||
<div className="links">
|
||||
<Controls />
|
||||
</div>
|
||||
<style jsx>{`
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 1000px;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${theme.layout.gap};
|
||||
height: 60px;
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuLinks
|
||||
170
lib/components/menu/menu-sticker.tsx
Normal file
170
lib/components/menu/menu-sticker.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { Tabs, useTheme } from 'components'
|
||||
import useCurrentState from 'components/utils/use-current-state'
|
||||
import sides from 'lib/data/metadata.json'
|
||||
import { Sides } from 'lib/components/sidebar/side-item'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useConfigs } from '../../config-context'
|
||||
|
||||
export interface MultilLocaleMetaInformation {
|
||||
[key: string]: Sides[]
|
||||
}
|
||||
|
||||
const MenuSticker = () => {
|
||||
const theme = useTheme()
|
||||
const { pathname } = useRouter()
|
||||
const { updateSides, updateTabbarFixed } = useConfigs()
|
||||
const [fixed, setFixed, fixedRef] = useCurrentState<boolean>(false)
|
||||
|
||||
useEffect(() => updateTabbarFixed(fixed), [fixed])
|
||||
|
||||
const tabbarData = useMemo(() => {
|
||||
const language = pathname
|
||||
.split('/')
|
||||
.filter(r => !!r)
|
||||
const locale: string = language[0] || 'en-us'
|
||||
return (sides as MultilLocaleMetaInformation)[locale]
|
||||
}, [pathname, sides])
|
||||
|
||||
const currentTabValue = useMemo(() => {
|
||||
const language = pathname
|
||||
.split('/')
|
||||
.filter(r => !!r)
|
||||
return language[1]
|
||||
}, [pathname])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const scrollHandler = () => {
|
||||
const shouldFixed = document.documentElement.scrollTop > 60
|
||||
if (shouldFixed === fixedRef.current) return
|
||||
setFixed(shouldFixed)
|
||||
}
|
||||
document.addEventListener('scroll', scrollHandler)
|
||||
return () => document.removeEventListener('scroll', scrollHandler)
|
||||
}, [])
|
||||
|
||||
const tabChangeHandler = (value: string) => {
|
||||
const currentTab = tabbarData.find(tab => tab.name === value)
|
||||
if (!currentTab || !Array.isArray(currentTab.children)) return
|
||||
|
||||
let firstChildren = currentTab.children
|
||||
if (Array.isArray(firstChildren[0].children)) {
|
||||
firstChildren = firstChildren[0].children
|
||||
}
|
||||
|
||||
const defaultPath = firstChildren[0].url
|
||||
if (!defaultPath) return
|
||||
updateSides(currentTab.children)
|
||||
Router.push(defaultPath)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabbarData && tabChangeHandler(currentTabValue)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`nav-fill ${fixed ? 'active' : ''}`} />
|
||||
<nav className={fixed ? 'fixed' : ''}>
|
||||
<div className="sticker">
|
||||
<div className="inner">
|
||||
<Tabs value={currentTabValue} onChange={tabChangeHandler}>
|
||||
{tabbarData ? tabbarData.map(tab => (
|
||||
<Tabs.Item label={tab.localeName || tab.name}
|
||||
value={tab.name}
|
||||
key={tab.name} />
|
||||
)) : null}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<style jsx>{`
|
||||
.nav-fill {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-fill.active {
|
||||
height: 48px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
nav {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: ${theme.palette.background};
|
||||
}
|
||||
|
||||
nav.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 3000;
|
||||
background-color: ${theme.palette.background};
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 0 15px 0;
|
||||
}
|
||||
|
||||
.sticker {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sticker:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${theme.palette.border};
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: 1000px;
|
||||
padding: 0 ${theme.layout.gap};
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
z-index: 1000;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.inner :global(.content) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner :global(.tabs), .inner :global(header) {
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inner :global(.tab) {
|
||||
height: calc(100% - 2px);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
color: ${theme.palette.accents_5};
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.inner :global(.tab):hover {
|
||||
color: ${theme.palette.foreground};
|
||||
}
|
||||
|
||||
.inner :global(.active) {
|
||||
color: ${theme.palette.foreground};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuSticker
|
||||
@@ -56,7 +56,7 @@ export const Sidebar: React.FC<SideGroupProps> = React.memo(({ sides }) => {
|
||||
.box {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 140px);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Sides } from 'lib/components/sidebar/side-item'
|
||||
|
||||
export interface Configs {
|
||||
onChange?: Function
|
||||
@@ -6,12 +7,24 @@ export interface Configs {
|
||||
updateChineseState: Function
|
||||
sidebarScrollHeight: number
|
||||
updateSidebarScrollHeight: Function
|
||||
|
||||
sides: Sides[]
|
||||
updateSides: Function
|
||||
|
||||
tabbarFixed: boolean
|
||||
updateTabbarFixed: Function
|
||||
}
|
||||
|
||||
export const defaultConfigs: Configs = {
|
||||
sidebarScrollHeight: 0,
|
||||
updateSidebarScrollHeight: () => {},
|
||||
updateChineseState: () => {},
|
||||
|
||||
sides: [],
|
||||
updateSides: () => {},
|
||||
|
||||
tabbarFixed: false,
|
||||
updateTabbarFixed: () => {},
|
||||
}
|
||||
|
||||
export const ConfigContext = React.createContext<Configs>(defaultConfigs)
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react'
|
||||
import withDefaults from 'components/utils/with-defaults'
|
||||
import { ConfigContext, Configs } from 'lib/config-context'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Sides } from 'lib/components/sidebar/side-item'
|
||||
|
||||
interface Props {
|
||||
onChange?: Function
|
||||
@@ -18,16 +19,21 @@ const ConfigProvider: React.FC<React.PropsWithChildren<ConfigProviderProps>> = R
|
||||
const { pathname } = useRouter()
|
||||
const [isChinese, setIsChinese] = useState<boolean>(() => pathname.includes('zh-cn'))
|
||||
const [scrollHeight, setScrollHeight] = useState<number>(0)
|
||||
const [sides, setSides] = useState<Sides[]>([] as Sides[])
|
||||
const [tabbarFixed, setTabbarFixed] = useState<boolean>(false)
|
||||
const updateSidebarScrollHeight = (height: number) => setScrollHeight(height)
|
||||
const updateChineseState = (state: boolean) => setIsChinese(state)
|
||||
const updateSides = (sides: Sides[]) => setSides(sides)
|
||||
const updateTabbarFixed = (state: boolean) => setTabbarFixed(state)
|
||||
|
||||
const initialValue = useMemo<Configs>(() => ({
|
||||
onChange,
|
||||
isChinese,
|
||||
onChange, sides, isChinese, tabbarFixed,
|
||||
updateSides,
|
||||
updateTabbarFixed,
|
||||
updateChineseState,
|
||||
sidebarScrollHeight: scrollHeight,
|
||||
updateSidebarScrollHeight,
|
||||
}), [onChange, scrollHeight])
|
||||
}), [onChange, scrollHeight, sides, tabbarFixed])
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={initialValue}>
|
||||
|
||||
Reference in New Issue
Block a user