@umijs/plugin-docs 4.0.0-beta.18 → 4.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/theme-doc/Github.tsx +18 -0
- package/client/theme-doc/Head.tsx +72 -0
- package/client/theme-doc/LangSwitch.tsx +55 -0
- package/client/theme-doc/Layout.tsx +79 -0
- package/client/theme-doc/Logo.tsx +18 -0
- package/client/theme-doc/NavBar.tsx +27 -0
- package/client/theme-doc/Search.tsx +178 -0
- package/client/theme-doc/Sidebar.tsx +84 -0
- package/client/theme-doc/ThemeSwitch.tsx +60 -0
- package/client/theme-doc/Tip.tsx +5 -0
- package/client/theme-doc/Toc.tsx +57 -0
- package/client/theme-doc/VersionSwitch.tsx +5 -0
- package/client/theme-doc/components/Announcement.tsx +55 -0
- package/client/theme-doc/components/Hero.tsx +149 -0
- package/client/theme-doc/components/Message.tsx +50 -0
- package/client/theme-doc/context.ts +39 -0
- package/client/theme-doc/icons/github.svg +1 -0
- package/client/theme-doc/icons/hero-bg.svg +1 -0
- package/client/theme-doc/icons/moon.png +0 -0
- package/client/theme-doc/icons/star.png +0 -0
- package/client/theme-doc/icons/sun.png +0 -0
- package/client/theme-doc/icons/umi.png +0 -0
- package/client/theme-doc/index.ts +5 -0
- package/client/theme-doc/tailwind.css +115 -0
- package/client/theme-doc/tailwind.out.css +1825 -0
- package/client/theme-doc/useLanguage.ts +69 -0
- package/dist/compiler.js +12 -1
- package/dist/index.js +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useThemeContext } from './context';
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import GithubIcon from './icons/github.svg';
|
|
5
|
+
|
|
6
|
+
export default () => {
|
|
7
|
+
const ctx = useThemeContext();
|
|
8
|
+
|
|
9
|
+
if (!ctx?.themeConfig.github) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<a href={ctx.themeConfig.github}>
|
|
15
|
+
<img className="dark:invert" src={GithubIcon} alt="Github" />
|
|
16
|
+
</a>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Github from './Github';
|
|
4
|
+
import LangSwitch from './LangSwitch';
|
|
5
|
+
import Logo from './Logo';
|
|
6
|
+
import NavBar from './NavBar';
|
|
7
|
+
import Search from './Search';
|
|
8
|
+
import ThemeSwitch from './ThemeSwitch';
|
|
9
|
+
|
|
10
|
+
interface HeadProps {
|
|
11
|
+
isMenuOpened: boolean;
|
|
12
|
+
setMenuOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default (props: HeadProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className="w-full flex flex-row items-center justify-between
|
|
19
|
+
border-b-gray-100 border-b-2 pt-4 pb-4 px-8 dark:border-b-gray-800"
|
|
20
|
+
>
|
|
21
|
+
<Logo />
|
|
22
|
+
<div className="flex flex-row items-center">
|
|
23
|
+
<Search />
|
|
24
|
+
<HamburgerButton {...props} />
|
|
25
|
+
<div className="hidden lg:block">
|
|
26
|
+
<NavBar />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="ml-4 hidden lg:block">
|
|
29
|
+
<LangSwitch />
|
|
30
|
+
</div>
|
|
31
|
+
<div className="ml-4 hidden lg:block">
|
|
32
|
+
<ThemeSwitch />
|
|
33
|
+
</div>
|
|
34
|
+
<div className="ml-4 hidden lg:block">
|
|
35
|
+
<Github />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface HamburgerButtonProps {
|
|
43
|
+
isMenuOpened: boolean;
|
|
44
|
+
setMenuOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function HamburgerButton(props: HamburgerButtonProps) {
|
|
48
|
+
const barClass =
|
|
49
|
+
'block absolute h-0.5 w-5 bg-current transform dark:bg-white' +
|
|
50
|
+
' transition duration-500 ease-in-out';
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className="relative py-3 sm:max-w-xl mx-auto mx-5 lg:hidden"
|
|
55
|
+
onClick={() => props.setMenuOpened((o) => !o)}
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
className={cx(
|
|
59
|
+
barClass,
|
|
60
|
+
props.isMenuOpened ? 'rotate-45 ' : '-translate-y-1.5',
|
|
61
|
+
)}
|
|
62
|
+
/>
|
|
63
|
+
<span className={cx(barClass, props.isMenuOpened && 'opacity-0')} />
|
|
64
|
+
<span
|
|
65
|
+
className={cx(
|
|
66
|
+
barClass,
|
|
67
|
+
props.isMenuOpened ? '-rotate-45' : 'translate-y-1.5',
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import useLanguage from './useLanguage';
|
|
3
|
+
|
|
4
|
+
export default () => {
|
|
5
|
+
const { currentLanguage, languages, switchLanguage } = useLanguage();
|
|
6
|
+
const [isExpanded, setExpanded] = useState(false);
|
|
7
|
+
|
|
8
|
+
if (!currentLanguage) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function handleClick() {
|
|
13
|
+
if (!currentLanguage) return;
|
|
14
|
+
if (languages.length === 2) {
|
|
15
|
+
switchLanguage(
|
|
16
|
+
languages[0].locale === currentLanguage.locale
|
|
17
|
+
? languages[1].locale
|
|
18
|
+
: languages[0].locale,
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setExpanded((e) => !e);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<div
|
|
28
|
+
className="w-24 rounded-lg overflow-hidden cursor-pointer border
|
|
29
|
+
border-white hover:border-gray-100 dark:border-gray-800"
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
>
|
|
32
|
+
<p className="px-2 py-1 dark:text-white">{currentLanguage.text}</p>
|
|
33
|
+
</div>
|
|
34
|
+
<div
|
|
35
|
+
className={
|
|
36
|
+
'absolute transition-all duration-300 bottom-[-12] w-24 rounded-lg' +
|
|
37
|
+
' cursor-pointer shadow overflow-hidden ' +
|
|
38
|
+
(isExpanded ? ` max-h-${(languages.length - 1) * 12}` : ' max-h-0 ')
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
{languages
|
|
42
|
+
.filter((l) => l.locale !== currentLanguage.locale)
|
|
43
|
+
.map((lang) => (
|
|
44
|
+
<p
|
|
45
|
+
onClick={() => switchLanguage(lang.locale)}
|
|
46
|
+
key={lang.locale}
|
|
47
|
+
className="p-2 bg-white dark:bg-gray-700 dark:text-white hover:bg-gray-50 transition duration-300"
|
|
48
|
+
>
|
|
49
|
+
{lang.text}
|
|
50
|
+
</p>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import React, { Fragment, useState } from 'react';
|
|
3
|
+
import Announcement from './components/Announcement';
|
|
4
|
+
import { ThemeContext } from './context';
|
|
5
|
+
import Head from './Head';
|
|
6
|
+
import Sidebar from './Sidebar';
|
|
7
|
+
import Toc from './Toc';
|
|
8
|
+
|
|
9
|
+
export default (props: any) => {
|
|
10
|
+
const [isMenuOpened, setIsMenuOpened] = useState(false);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ThemeContext.Provider
|
|
14
|
+
value={{
|
|
15
|
+
appData: props.appData,
|
|
16
|
+
components: props.components,
|
|
17
|
+
themeConfig: props.themeConfig,
|
|
18
|
+
location: props.location,
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<div className="flex flex-col dark:bg-gray-900 min-h-screen transition-all">
|
|
22
|
+
<div
|
|
23
|
+
className="z-30 sticky top-0 dark:before:bg-gray-800 before:bg-white before:bg-opacity-[.85]
|
|
24
|
+
before:backdrop-blur-md before:absolute before:block dark:before:bg-opacity-[.85]
|
|
25
|
+
before:w-full before:h-full before:z-[-1]"
|
|
26
|
+
>
|
|
27
|
+
<Announcement />
|
|
28
|
+
<Head setMenuOpened={setIsMenuOpened} isMenuOpened={isMenuOpened} />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{window.location.pathname === '/' ? (
|
|
32
|
+
<div>{props.children}</div>
|
|
33
|
+
) : (
|
|
34
|
+
<Fragment>
|
|
35
|
+
<div className="w-full flex flex-row justify-center overflow-x-hidden">
|
|
36
|
+
<div className="container flex flex-row justify-center">
|
|
37
|
+
<div className="w-full lg:w-1/2 px-4 lg:px-0 m-8 z-20 lg:py-12">
|
|
38
|
+
<article className="flex-1">{props.children}</article>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
className="fixed left-0 top-0 w-1/4 flex flex-row
|
|
45
|
+
justify-center h-screen z-10 pt-20"
|
|
46
|
+
>
|
|
47
|
+
<div className="container flex flex-row justify-end">
|
|
48
|
+
<div className="hidden lg:block">
|
|
49
|
+
<Sidebar />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div
|
|
55
|
+
className="fixed right-0 top-0 w-1/4 flex flex-row
|
|
56
|
+
justify-center h-screen z-10 pt-20 hidden lg:block"
|
|
57
|
+
>
|
|
58
|
+
<div className="container flex flex-row justify-start">
|
|
59
|
+
<div className="w-2/3 top-32">
|
|
60
|
+
<Toc />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</Fragment>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
className={cx(
|
|
70
|
+
'fixed top-12 w-screen bg-white z-20 dark:bg-gray-800',
|
|
71
|
+
'overflow-hidden transition-all duration-500',
|
|
72
|
+
isMenuOpened ? 'max-h-screen' : 'max-h-0',
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<Sidebar setMenuOpened={setIsMenuOpened} />
|
|
76
|
+
</div>
|
|
77
|
+
</ThemeContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useThemeContext } from './context';
|
|
3
|
+
|
|
4
|
+
export default () => {
|
|
5
|
+
const { themeConfig } = useThemeContext()!;
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
const { logo } = themeConfig;
|
|
8
|
+
return (
|
|
9
|
+
<a href="/">
|
|
10
|
+
<div className="flex flex-row items-center">
|
|
11
|
+
<img src={logo} className="w-8 h-8" alt="logo" />
|
|
12
|
+
<div className="text-xl font-extrabold ml-2 dark:text-white">
|
|
13
|
+
{themeConfig.title}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</a>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useThemeContext } from './context';
|
|
3
|
+
import useLanguage from './useLanguage';
|
|
4
|
+
|
|
5
|
+
export default () => {
|
|
6
|
+
const { components, themeConfig } = useThemeContext()!;
|
|
7
|
+
const lang = useLanguage();
|
|
8
|
+
return (
|
|
9
|
+
<ul className="flex">
|
|
10
|
+
{themeConfig.navs.map((nav: any) => {
|
|
11
|
+
return (
|
|
12
|
+
<li key={nav.path} className="ml-4 dark:text-white">
|
|
13
|
+
<components.Link
|
|
14
|
+
to={
|
|
15
|
+
lang.isFromPath
|
|
16
|
+
? lang.currentLanguage?.locale + nav.path
|
|
17
|
+
: nav.path
|
|
18
|
+
}
|
|
19
|
+
>
|
|
20
|
+
{lang.render(nav.title)}
|
|
21
|
+
</components.Link>
|
|
22
|
+
</li>
|
|
23
|
+
);
|
|
24
|
+
})}
|
|
25
|
+
</ul>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import key from 'keymaster';
|
|
3
|
+
import React, { Fragment, useEffect, useState } from 'react';
|
|
4
|
+
import { useThemeContext } from './context';
|
|
5
|
+
import useLanguage from './useLanguage';
|
|
6
|
+
|
|
7
|
+
export default () => {
|
|
8
|
+
const { render } = useLanguage();
|
|
9
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
10
|
+
const [keyword, setKeyword] = useState('');
|
|
11
|
+
|
|
12
|
+
const { appData, themeConfig } = useThemeContext()!;
|
|
13
|
+
|
|
14
|
+
const isMac = /(Mac|iPad)/i.test(navigator.userAgent);
|
|
15
|
+
|
|
16
|
+
let searchHotKey = '⌘+k, ctrl+k';
|
|
17
|
+
let macSearchKey = '⌘+k';
|
|
18
|
+
let windowsSearchKey = 'ctrl+k';
|
|
19
|
+
|
|
20
|
+
if (themeConfig.searchHotKey) {
|
|
21
|
+
if (typeof themeConfig.searchHotKey === 'string') {
|
|
22
|
+
searchHotKey = themeConfig.searchHotKey;
|
|
23
|
+
macSearchKey = themeConfig.searchHotKey;
|
|
24
|
+
windowsSearchKey = themeConfig.searchHotKey;
|
|
25
|
+
}
|
|
26
|
+
if (typeof themeConfig.searchHotKey === 'object') {
|
|
27
|
+
searchHotKey =
|
|
28
|
+
themeConfig.searchHotKey.macos +
|
|
29
|
+
', ' +
|
|
30
|
+
themeConfig.searchHotKey.windows;
|
|
31
|
+
macSearchKey = themeConfig.searchHotKey.macos;
|
|
32
|
+
windowsSearchKey = themeConfig.searchHotKey.windows;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
key.filter = () => true;
|
|
38
|
+
|
|
39
|
+
// 在页面中按下 ⌘+k 可以打开搜索框
|
|
40
|
+
key(searchHotKey, (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
document.getElementById('search-input')?.focus();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 在搜索框中按下 'Escape' 键可以关闭搜索框
|
|
46
|
+
key('escape', () => {
|
|
47
|
+
(document.activeElement as HTMLElement).blur();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
key('up', handleKeyUp);
|
|
51
|
+
key('down', handleKeyDown);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
key.unbind(searchHotKey);
|
|
55
|
+
key.unbind('escape');
|
|
56
|
+
key.unbind('up');
|
|
57
|
+
key.unbind('down');
|
|
58
|
+
};
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const result = search(appData.routes, keyword);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Fragment>
|
|
65
|
+
<div
|
|
66
|
+
className="rounded-lg w-40 lg:w-64 flex items-center pr-2 flex-row hover:bg-gray-50
|
|
67
|
+
transition duration-300 bg-gray-100 border border-white focus-within:border-gray-100
|
|
68
|
+
focus-within:bg-white dark:bg-gray-700 dark:border-gray-700
|
|
69
|
+
dark:focus-within:border-gray-700 dark:focus-within:bg-gray-800 dark:text-gray-100"
|
|
70
|
+
>
|
|
71
|
+
<input
|
|
72
|
+
onFocus={() => setIsFocused(true)}
|
|
73
|
+
onBlur={() => setIsFocused(false)}
|
|
74
|
+
value={keyword}
|
|
75
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
76
|
+
id="search-input"
|
|
77
|
+
className="w-full bg-transparent outline-0 text-sm px-4 py-2 "
|
|
78
|
+
placeholder={render('Search anything ...')}
|
|
79
|
+
/>
|
|
80
|
+
<div
|
|
81
|
+
className="bg-gray-200 rounded px-2 h-6 flex flex-row text-gray-400
|
|
82
|
+
items-center justify-center border border-gray-300 text-xs"
|
|
83
|
+
>
|
|
84
|
+
{isMac ? macSearchKey : windowsSearchKey}
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
className={cx(
|
|
88
|
+
'absolute transition-all duration-500 top-16 w-96 rounded-lg',
|
|
89
|
+
'cursor-pointer shadow overflow-hidden',
|
|
90
|
+
result.length > 0 && isFocused ? 'max-h-80' : 'max-h-0',
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{result.map((r, i) => (
|
|
94
|
+
<a
|
|
95
|
+
href={r.href}
|
|
96
|
+
key={i}
|
|
97
|
+
className="group outline-none search-result"
|
|
98
|
+
onFocus={() => setIsFocused(true)}
|
|
99
|
+
onBlur={() => setIsFocused(false)}
|
|
100
|
+
>
|
|
101
|
+
<p
|
|
102
|
+
className="p-2 bg-white hover:bg-gray-50 transition
|
|
103
|
+
duration-300 group-focus:bg-blue-200 dark:bg-gray-700"
|
|
104
|
+
>
|
|
105
|
+
{r.path}
|
|
106
|
+
</p>
|
|
107
|
+
</a>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Fragment>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
interface SearchResultItem {
|
|
116
|
+
path: string;
|
|
117
|
+
href: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function search(routes: any, keyword: string): SearchResultItem[] {
|
|
121
|
+
if (!keyword) return [];
|
|
122
|
+
|
|
123
|
+
const result: SearchResultItem[] = [];
|
|
124
|
+
|
|
125
|
+
Object.keys(routes).map((path) => {
|
|
126
|
+
if (path.toLowerCase().includes(keyword.toLowerCase())) {
|
|
127
|
+
result.push({
|
|
128
|
+
path: path.split('/').slice(1).join(' > '),
|
|
129
|
+
href: '/' + path,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const route = routes[path];
|
|
134
|
+
if (!route.titles) return;
|
|
135
|
+
route.titles
|
|
136
|
+
.filter((t: any) => t.level <= 2)
|
|
137
|
+
.map((title: any) => {
|
|
138
|
+
if (title.title.toLowerCase().includes(keyword.toLowerCase())) {
|
|
139
|
+
result.push({
|
|
140
|
+
path: path.split('/').slice(1).join(' > ') + ' > ' + title.title,
|
|
141
|
+
href: '/' + path + '#' + title.title,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (result.length > 8) return result.slice(0, 8);
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
153
|
+
if (!document.activeElement) return;
|
|
154
|
+
|
|
155
|
+
if (document.activeElement.id === 'search-input') {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
(
|
|
158
|
+
document.getElementsByClassName('search-result')[0] as
|
|
159
|
+
| HTMLDivElement
|
|
160
|
+
| undefined
|
|
161
|
+
)?.focus();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (document.activeElement.className.indexOf('search-result') === -1) return;
|
|
166
|
+
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
(document.activeElement?.nextSibling as HTMLDivElement | undefined)?.focus();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleKeyUp(e: KeyboardEvent) {
|
|
172
|
+
if (!document.activeElement) return;
|
|
173
|
+
if (document.activeElement.className.indexOf('search-result') === -1) return;
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
(
|
|
176
|
+
document.activeElement?.previousSibling as HTMLDivElement | undefined
|
|
177
|
+
)?.focus();
|
|
178
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useThemeContext } from './context';
|
|
4
|
+
import useLanguage from './useLanguage';
|
|
5
|
+
|
|
6
|
+
interface SidebarProps {
|
|
7
|
+
setMenuOpened?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default (props: SidebarProps) => {
|
|
11
|
+
const { currentLanguage, isFromPath } = useLanguage();
|
|
12
|
+
const { appData, components, themeConfig, location } = useThemeContext()!;
|
|
13
|
+
const matchedNav = themeConfig.navs.filter((nav) =>
|
|
14
|
+
location.pathname.startsWith(
|
|
15
|
+
(isFromPath && currentLanguage ? '/' + currentLanguage.locale : '') +
|
|
16
|
+
nav.path,
|
|
17
|
+
),
|
|
18
|
+
)[0];
|
|
19
|
+
|
|
20
|
+
if (!matchedNav) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let locale = currentLanguage?.locale;
|
|
25
|
+
if (!isFromPath) locale = '';
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ul
|
|
29
|
+
className={cx(
|
|
30
|
+
'h-screen lg:h-[calc(100vh-8rem)] overflow-y-scroll',
|
|
31
|
+
'lg:w-64 p-8 pb-12 fadeout w-full',
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{(matchedNav.children || []).map((item) => {
|
|
35
|
+
return (
|
|
36
|
+
<li key={item.title}>
|
|
37
|
+
<div>
|
|
38
|
+
<p className="text-xl font-extrabold my-6 dark:text-white">
|
|
39
|
+
{item.title}
|
|
40
|
+
</p>
|
|
41
|
+
{item.children.map((child: any) => {
|
|
42
|
+
const to =
|
|
43
|
+
(locale ? `/${locale}` : '') + `${matchedNav.path}/${child}`;
|
|
44
|
+
const id = `${matchedNav.path}/${child}`.slice(1);
|
|
45
|
+
const route =
|
|
46
|
+
appData.routes[id + '.' + locale] || appData.routes[id];
|
|
47
|
+
const title = route.titles[0]?.title || null;
|
|
48
|
+
|
|
49
|
+
if (to === window.location.pathname) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={child}
|
|
53
|
+
className="my-2 hover:text-blue-400 transition-all
|
|
54
|
+
bg-blue-50 text-blue-400 px-4 py-1
|
|
55
|
+
rounded-lg cursor-default dark:bg-blue-900 dark:text-blue-200"
|
|
56
|
+
>
|
|
57
|
+
{title}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<components.Link
|
|
64
|
+
to={route.path}
|
|
65
|
+
onClick={() =>
|
|
66
|
+
props.setMenuOpened && props.setMenuOpened((o) => !o)
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
key={child}
|
|
71
|
+
className="text-gray-700 my-2 hover:text-blue-400 transition-all px-4 py-1 dark:text-blue-200 dark:hover:text-blue-400"
|
|
72
|
+
>
|
|
73
|
+
{title}
|
|
74
|
+
</div>
|
|
75
|
+
</components.Link>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</ul>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import MoonIcon from './icons/moon.png';
|
|
4
|
+
import SunIcon from './icons/sun.png';
|
|
5
|
+
|
|
6
|
+
export default () => {
|
|
7
|
+
const [toggle, setToggle] = useState<Boolean>();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// 初始化,获取过去曾经设定过的主题,或是系统当前的主题
|
|
11
|
+
if (toggle === undefined) {
|
|
12
|
+
if (localStorage.getItem('theme') === 'dark') {
|
|
13
|
+
setToggle(false);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (localStorage.getItem('theme') === 'light') {
|
|
17
|
+
setToggle(true);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
21
|
+
setToggle(false);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setToggle(true);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (toggle) {
|
|
28
|
+
document.body.classList.remove('dark');
|
|
29
|
+
localStorage.setItem('theme', 'light');
|
|
30
|
+
} else {
|
|
31
|
+
document.body.classList.add('dark');
|
|
32
|
+
localStorage.setItem('theme', 'dark');
|
|
33
|
+
}
|
|
34
|
+
}, [toggle]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cx(
|
|
39
|
+
'md:w-12 md:h-6 w-12 h-4 flex items-center bg-gray-300 rounded-full ',
|
|
40
|
+
'py-1 px-1.5 cursor-pointer',
|
|
41
|
+
toggle ? 'bg-blue-300' : 'bg-gray-700',
|
|
42
|
+
)}
|
|
43
|
+
onClick={() => setToggle(!toggle)}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
className={cx(
|
|
47
|
+
'md:w-4 md:h-4 h-3 w-3 rounded-full shadow-md ',
|
|
48
|
+
'transition transform',
|
|
49
|
+
toggle && 'translate-x-5',
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<img
|
|
53
|
+
src={toggle ? SunIcon : MoonIcon}
|
|
54
|
+
alt="toggle"
|
|
55
|
+
className="w-full h-full"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useThemeContext } from './context';
|
|
3
|
+
import useLanguage from './useLanguage';
|
|
4
|
+
|
|
5
|
+
function getLinkFromTitle(title: string) {
|
|
6
|
+
return title
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/\s/g, '-')
|
|
9
|
+
.replace(/[()]/g, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default () => {
|
|
13
|
+
const { location, appData } = useThemeContext()!;
|
|
14
|
+
const lang = useLanguage();
|
|
15
|
+
const route =
|
|
16
|
+
appData.routes[
|
|
17
|
+
lang.isFromPath
|
|
18
|
+
? location.pathname.split('/').slice(2).join('/') +
|
|
19
|
+
'.' +
|
|
20
|
+
lang.currentLanguage?.locale
|
|
21
|
+
: location.pathname.slice(1)
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
if (!route) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const titles = route.titles.filter((t: any) => t.level > 1);
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className="w-full lg:m-12 mb-12 border
|
|
32
|
+
border-gray-100 p-8 rounded-xl z-20"
|
|
33
|
+
>
|
|
34
|
+
<p className="text-lg font-extrabold dark:text-white">
|
|
35
|
+
{route.titles[0].title}
|
|
36
|
+
</p>
|
|
37
|
+
<ul className="max-h-[calc(100vh-360px)] overflow-y-scroll py-2">
|
|
38
|
+
{titles.map((item: any) => {
|
|
39
|
+
return (
|
|
40
|
+
<li
|
|
41
|
+
style={{ paddingLeft: `${item.level - 2}rem` }}
|
|
42
|
+
className="mt-3 text-gray-600 cursor-pointer dark:text-gray-400
|
|
43
|
+
hover:text-blue-500 transition duration-300 dark:hover:text-blue-500"
|
|
44
|
+
>
|
|
45
|
+
<a
|
|
46
|
+
className={item.level > 2 ? 'text-sm' : ''}
|
|
47
|
+
href={'#' + getLinkFromTitle(item.title)}
|
|
48
|
+
>
|
|
49
|
+
{item.title}
|
|
50
|
+
</a>
|
|
51
|
+
</li>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|