@umijs/plugin-docs 4.0.0-beta.18 → 4.0.0-rc.3

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.
Files changed (34) hide show
  1. package/client/theme-blog/index.ts +1 -0
  2. package/client/theme-doc/Github.tsx +18 -0
  3. package/client/theme-doc/Head.tsx +72 -0
  4. package/client/theme-doc/LangSwitch.tsx +55 -0
  5. package/client/theme-doc/Layout.tsx +116 -0
  6. package/client/theme-doc/Logo.tsx +18 -0
  7. package/client/theme-doc/NavBar.tsx +27 -0
  8. package/client/theme-doc/Search.tsx +178 -0
  9. package/client/theme-doc/Sidebar.tsx +84 -0
  10. package/client/theme-doc/ThemeSwitch.tsx +60 -0
  11. package/client/theme-doc/Tip.tsx +5 -0
  12. package/client/theme-doc/Toc.tsx +63 -0
  13. package/client/theme-doc/VersionSwitch.tsx +5 -0
  14. package/client/theme-doc/components/Announcement.tsx +62 -0
  15. package/client/theme-doc/components/FeatureItem.tsx +48 -0
  16. package/client/theme-doc/components/Features.tsx +44 -0
  17. package/client/theme-doc/components/Hero.tsx +152 -0
  18. package/client/theme-doc/components/Message.tsx +57 -0
  19. package/client/theme-doc/context.ts +40 -0
  20. package/client/theme-doc/firefox-polyfill.css +31 -0
  21. package/client/theme-doc/icons/github.svg +1 -0
  22. package/client/theme-doc/icons/hero-bg.svg +1 -0
  23. package/client/theme-doc/icons/moon.png +0 -0
  24. package/client/theme-doc/icons/star.png +0 -0
  25. package/client/theme-doc/icons/sun.png +0 -0
  26. package/client/theme-doc/icons/umi.png +0 -0
  27. package/client/theme-doc/index.ts +8 -0
  28. package/client/theme-doc/tailwind.css +153 -0
  29. package/client/theme-doc/tailwind.out.css +2031 -0
  30. package/client/theme-doc/useLanguage.ts +69 -0
  31. package/dist/compiler.d.ts +1 -1
  32. package/dist/compiler.js +22 -11
  33. package/dist/index.js +18 -3
  34. package/package.json +23 -8
@@ -0,0 +1 @@
1
+ export function $Layout() {}
@@ -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,116 @@
1
+ import cx from 'classnames';
2
+ import React, { Fragment, useEffect, useState } from 'react';
3
+ import { Helmet } from 'react-helmet';
4
+ import Announcement from './components/Announcement';
5
+ import { ThemeContext } from './context';
6
+ import Head from './Head';
7
+ import Sidebar from './Sidebar';
8
+ import Toc from './Toc';
9
+
10
+ export default (props: any) => {
11
+ const [isMenuOpened, setIsMenuOpened] = useState(false);
12
+
13
+ /**
14
+ FireFox CSS backdrop-filter polyfill
15
+ https://www.cnblogs.com/coco1s/p/14953143.html
16
+ */
17
+ useEffect(() => {
18
+ let blur = document.getElementById('firefox-head-bg')?.style;
19
+ let offset = document.getElementById('head-container');
20
+
21
+ function updateBlur() {
22
+ if (!offset || !blur) return;
23
+ blur.backgroundPosition = `0px ` + `${-window.scrollY + 100}px`;
24
+ }
25
+
26
+ document.addEventListener('scroll', updateBlur, false), updateBlur();
27
+ return () => {
28
+ document.removeEventListener('scroll', updateBlur, false);
29
+ };
30
+ }, []);
31
+
32
+ const { title, description } = props.themeConfig;
33
+
34
+ return (
35
+ <ThemeContext.Provider
36
+ value={{
37
+ appData: props.appData,
38
+ components: props.components,
39
+ themeConfig: props.themeConfig,
40
+ location: props.location,
41
+ }}
42
+ >
43
+ <div className="flex flex-col dark:bg-gray-900 min-h-screen transition-all">
44
+ <div
45
+ id="head-container"
46
+ className="z-30 sticky top-0 dark:before:bg-gray-800 before:bg-white before:bg-opacity-[.85]
47
+ before:backdrop-blur-md before:absolute before:block dark:before:bg-opacity-[.85]
48
+ before:w-full before:h-full before:z-[-1]"
49
+ >
50
+ <Announcement />
51
+ <Head setMenuOpened={setIsMenuOpened} isMenuOpened={isMenuOpened} />
52
+ </div>
53
+
54
+ <div className="g-glossy-firefox-cover" />
55
+ <div className="g-glossy-firefox" id="firefox-head-bg" />
56
+
57
+ {window.location.pathname === '/' ? (
58
+ <div id="article-body">
59
+ <Helmet>
60
+ <title>
61
+ {title}
62
+ {description ? ` - ${description}` : ''}
63
+ </title>
64
+ </Helmet>
65
+ {props.children}
66
+ </div>
67
+ ) : (
68
+ <Fragment>
69
+ <div
70
+ id="article-body"
71
+ className="w-full flex flex-row justify-center overflow-x-hidden"
72
+ >
73
+ <div className="container flex flex-row justify-center">
74
+ <div className="w-full lg:w-1/2 px-4 lg:px-0 m-8 z-20 lg:py-12">
75
+ <article className="flex-1">{props.children}</article>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div
81
+ className="fixed left-0 top-0 w-1/4 flex flex-row
82
+ justify-center h-screen z-10 pt-20"
83
+ >
84
+ <div className="container flex flex-row justify-end">
85
+ <div className="hidden lg:block">
86
+ <Sidebar />
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <div
92
+ className="fixed right-0 top-0 w-1/4 flex flex-row
93
+ justify-center h-screen z-10 pt-20 hidden lg:block"
94
+ >
95
+ <div className="container flex flex-row justify-start">
96
+ <div className="w-2/3 top-32">
97
+ <Toc />
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </Fragment>
102
+ )}
103
+ </div>
104
+
105
+ <div
106
+ className={cx(
107
+ 'fixed top-12 w-screen bg-white z-20 dark:bg-gray-800',
108
+ 'overflow-hidden transition-all duration-500',
109
+ isMenuOpened ? 'max-h-screen' : 'max-h-0',
110
+ )}
111
+ >
112
+ <Sidebar setMenuOpened={setIsMenuOpened} />
113
+ </div>
114
+ </ThemeContext.Provider>
115
+ );
116
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { useThemeContext } from './context';
3
+
4
+ export default () => {
5
+ const { themeConfig, components } = useThemeContext()!;
6
+ // @ts-ignore
7
+ const { logo } = themeConfig;
8
+ return (
9
+ <components.Link to="/">
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
+ </components.Link>
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,5 @@
1
+ import React from 'react';
2
+
3
+ export default () => {
4
+ return <div>Tip</div>;
5
+ };