@primer/doctocat-nextjs 0.4.1-rc.afcbafc → 0.5.0-rc.6ee882c
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/CHANGELOG.md +29 -0
- package/components/context/useConfig.tsx +31 -0
- package/components/layout/global-search/GlobalSearch.module.css +6 -0
- package/components/layout/global-search/GlobalSearch.tsx +2 -2
- package/components/layout/header/Header.module.css +61 -4
- package/components/layout/header/Header.tsx +94 -51
- package/components/layout/links-dropdown/LinksDropdown.module.css +121 -0
- package/components/layout/links-dropdown/LinksDropdown.tsx +135 -0
- package/components/layout/nav-drawer/NavDrawer.tsx +8 -10
- package/components/layout/root-layout/Theme.tsx +2 -2
- package/components/layout/root-layout/index.tsx +13 -5
- package/components/layout/sidebar/Sidebar.tsx +21 -19
- package/package.json +1 -1
- package/types.ts +2 -0
- /package/components/{highlight-search-term → layout/highlight-search-term}/HighlightSearchTerm.tsx +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @primer/doctocat-nextjs
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#30](https://github.com/primer/doctocat-nextjs/pull/30) [`cdcb65e`](https://github.com/primer/doctocat-nextjs/commit/cdcb65e087d647a6d61c87d9122f105dcda64e35) Thanks [@joshfarrant](https://github.com/joshfarrant)! - - Aribitrary links can now be added to the sidebar and header using the `Theme` component's `headerLinks` and `sidebarLinks` props.
|
|
8
|
+
- Updated the header navigation to more closely visually align it with the existing Primer docs navigation.
|
|
9
|
+
- Removed `_meta.global.ts` and instead directly pass header and sidebar links into the doctocat `Theme` component.
|
|
10
|
+
```diff
|
|
11
|
+
- // _meta.global.ts
|
|
12
|
+
- export default {
|
|
13
|
+
- type: 'page',
|
|
14
|
+
- href: 'https://github.com/primer/doctocat-nextjs',
|
|
15
|
+
- title: 'Doctocat',
|
|
16
|
+
- }
|
|
17
|
+
```
|
|
18
|
+
```diff
|
|
19
|
+
+ // app/layout.tsx
|
|
20
|
+
+
|
|
21
|
+
+ const sidebarLinks: ThemeProps['sidebarLinks'] = [
|
|
22
|
+
+ {
|
|
23
|
+
+ href: 'https://github.com/',
|
|
24
|
+
+ title: 'GitHub',
|
|
25
|
+
+ isExternal: true,
|
|
26
|
+
+ },
|
|
27
|
+
+ ]
|
|
28
|
+
+
|
|
29
|
+
+ <Theme sidebarLinks={sidebarLinks} {...rest} />
|
|
30
|
+
```
|
|
31
|
+
|
|
3
32
|
## 0.4.1
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, {createContext, PropsWithChildren, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
export type ConfigContextLink = {
|
|
4
|
+
title: string
|
|
5
|
+
href: string
|
|
6
|
+
isActive?: boolean
|
|
7
|
+
isExternal?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ConfigContextValue = {
|
|
11
|
+
headerLinks: ConfigContextLink[]
|
|
12
|
+
sidebarLinks: ConfigContextLink[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ConfigContext = createContext<ConfigContextValue | null>(null)
|
|
16
|
+
|
|
17
|
+
export const useConfig = (): ConfigContextValue => {
|
|
18
|
+
const context = useContext(ConfigContext)
|
|
19
|
+
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error('useConfig must be used within a ConfigContextProvider')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return context
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ConfigContextProviderProps = PropsWithChildren<{value: ConfigContextValue}>
|
|
28
|
+
|
|
29
|
+
export const ConfigContextProvider = ({children, value}: ConfigContextProviderProps) => {
|
|
30
|
+
return <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>
|
|
31
|
+
}
|
|
@@ -9,7 +9,7 @@ import {useRouter} from 'next/navigation'
|
|
|
9
9
|
|
|
10
10
|
import styles from './GlobalSearch.module.css'
|
|
11
11
|
import type {DocsItem} from '../../../types'
|
|
12
|
-
import {HighlightSearchTerm} from '
|
|
12
|
+
import {HighlightSearchTerm} from '../highlight-search-term/HighlightSearchTerm'
|
|
13
13
|
|
|
14
14
|
type GlobalSearchProps = {
|
|
15
15
|
flatDocsDirectories: DocsItem[]
|
|
@@ -173,7 +173,7 @@ export const GlobalSearch = forwardRef<HTMLInputElement, GlobalSearchProps>(
|
|
|
173
173
|
contrast
|
|
174
174
|
type="search"
|
|
175
175
|
className={styles.GlobalSearch__searchInput}
|
|
176
|
-
leadingVisual={<SearchIcon />}
|
|
176
|
+
leadingVisual={<SearchIcon className={styles.GlobalSearch__searchIcon} />}
|
|
177
177
|
placeholder={`Search ${siteTitle}`}
|
|
178
178
|
ref={forwardedRef}
|
|
179
179
|
value={searchTerm}
|
|
@@ -3,12 +3,31 @@
|
|
|
3
3
|
display: flex;
|
|
4
4
|
align-items: center;
|
|
5
5
|
justify-content: space-between;
|
|
6
|
-
padding: var(--base-size-
|
|
6
|
+
padding-block: var(--base-size-12);
|
|
7
|
+
padding-inline: var(--base-size-16) var(--base-size-16);
|
|
7
8
|
border-bottom: var(--brand-borderWidth-thin) solid var(--brand-color-border-default);
|
|
8
9
|
background: var(--brand-color-canvas-default);
|
|
9
10
|
z-index: 20;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
.Header__start {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.Header__separator {
|
|
19
|
+
color: var(--borderColor-default);
|
|
20
|
+
font-weight: var(--base-text-weight-light);
|
|
21
|
+
margin-inline-start: var(--base-size-12);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.Header__end {
|
|
25
|
+
flex: 1;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: flex-end;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
@media (max-width: 767px) {
|
|
13
32
|
.Header__searchArea {
|
|
14
33
|
display: none;
|
|
@@ -61,13 +80,12 @@
|
|
|
61
80
|
|
|
62
81
|
@media (min-width: 768px) {
|
|
63
82
|
.Header {
|
|
64
|
-
padding: var(--base-size-
|
|
83
|
+
padding-inline: var(--base-size-24) var(--base-size-24);
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
.Header__searchArea {
|
|
68
87
|
width: 100%;
|
|
69
88
|
max-width: 350px;
|
|
70
|
-
margin-inline-start: auto;
|
|
71
89
|
margin-inline-end: var(--base-size-16);
|
|
72
90
|
}
|
|
73
91
|
|
|
@@ -82,10 +100,49 @@
|
|
|
82
100
|
display: flex;
|
|
83
101
|
gap: var(--base-size-8);
|
|
84
102
|
align-items: center;
|
|
85
|
-
|
|
103
|
+
margin-inline-end: auto;
|
|
86
104
|
text-decoration: none;
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
.Header__siteTitle svg {
|
|
90
108
|
fill: var(--brand-color-text-default);
|
|
91
109
|
}
|
|
110
|
+
|
|
111
|
+
.Header__siteTitleText {
|
|
112
|
+
@media (max-width: 25rem) {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.Header__links {
|
|
118
|
+
display: none;
|
|
119
|
+
margin-inline: var(--base-size-24);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.Header__link {
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
font-size: var(--base-font-size-16);
|
|
125
|
+
font-weight: var(--base-font-weight-500);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.Header__externalLinkIcon {
|
|
129
|
+
margin-block-end: var(--base-size-8);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (min-width: 72rem) {
|
|
133
|
+
.Header {
|
|
134
|
+
padding-block: var(--base-size-16);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.Header__separator {
|
|
138
|
+
display: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.Header__linksDropdown {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.Header__links {
|
|
146
|
+
display: flex;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
import React, {useEffect, useRef, useState} from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ArrowUpRightIcon,
|
|
4
|
+
MarkGithubIcon,
|
|
5
|
+
MoonIcon,
|
|
6
|
+
SearchIcon,
|
|
7
|
+
SunIcon,
|
|
8
|
+
ThreeBarsIcon,
|
|
9
|
+
XIcon,
|
|
10
|
+
} from '@primer/octicons-react'
|
|
3
11
|
import {IconButton} from '@primer/react'
|
|
4
12
|
import {Stack, Text} from '@primer/react-brand'
|
|
5
13
|
import {clsx} from 'clsx'
|
|
6
|
-
import {PageMapItem} from 'nextra'
|
|
14
|
+
import type {PageMapItem} from 'nextra'
|
|
7
15
|
|
|
8
16
|
import Link from 'next/link'
|
|
9
17
|
import styles from './Header.module.css'
|
|
10
18
|
import {NavDrawer} from '../nav-drawer/NavDrawer'
|
|
11
19
|
import {useNavDrawerState} from '../nav-drawer/useNavDrawerState'
|
|
12
20
|
import {useColorMode} from '../../context/color-modes/useColorMode'
|
|
13
|
-
|
|
21
|
+
|
|
22
|
+
import type {DocsItem} from '../../../types'
|
|
23
|
+
|
|
14
24
|
import {GlobalSearch} from '../global-search/GlobalSearch'
|
|
15
25
|
import {FocusOn} from 'react-focus-on'
|
|
26
|
+
import {LinksDropdown} from '../links-dropdown/LinksDropdown'
|
|
27
|
+
import {useConfig} from '../../context/useConfig'
|
|
16
28
|
|
|
17
29
|
type HeaderProps = {
|
|
18
|
-
pageMap: PageMapItem[]
|
|
19
30
|
flatDocsDirectories: DocsItem[]
|
|
20
31
|
siteTitle: string
|
|
32
|
+
pageMap: PageMapItem[]
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
export function Header({
|
|
35
|
+
export function Header({siteTitle, flatDocsDirectories, pageMap}: HeaderProps) {
|
|
24
36
|
const searchRef = useRef<HTMLInputElement | null>(null)
|
|
25
37
|
const {colorMode, setColorMode} = useColorMode()
|
|
26
38
|
const searchTriggerRef = useRef<HTMLButtonElement | null>(null)
|
|
27
39
|
const [isNavDrawerOpen, setIsNavDrawerOpen] = useNavDrawerState('768')
|
|
28
40
|
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
41
|
+
const {headerLinks} = useConfig()
|
|
29
42
|
|
|
30
43
|
useEffect(() => {
|
|
31
44
|
document.documentElement.setAttribute('data-color-mode', colorMode)
|
|
@@ -44,57 +57,87 @@ export function Header({pageMap, siteTitle, flatDocsDirectories}: HeaderProps) {
|
|
|
44
57
|
role="navigation"
|
|
45
58
|
aria-label="Header Navigation"
|
|
46
59
|
>
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
{
|
|
60
|
+
<div className={styles.Header__start}>
|
|
61
|
+
<Link href="/" className={styles.Header__siteTitle}>
|
|
62
|
+
<MarkGithubIcon size={24} />
|
|
63
|
+
<Text className={styles.Header__siteTitleText} as="p" size="300" weight="semibold">
|
|
64
|
+
{siteTitle}
|
|
65
|
+
</Text>
|
|
66
|
+
</Link>
|
|
67
|
+
<Text as="span" className={styles.Header__separator} weight="semibold" aria-hidden>
|
|
68
|
+
/
|
|
51
69
|
</Text>
|
|
52
|
-
|
|
53
|
-
<div className={clsx(styles.Header__searchArea, isSearchOpen && styles['Header__searchArea--open'])}>
|
|
54
|
-
<FocusOn enabled={isSearchOpen} onEscapeKey={closeSearch} onClickOutside={closeSearch}>
|
|
55
|
-
<GlobalSearch
|
|
56
|
-
ref={searchRef}
|
|
57
|
-
siteTitle={siteTitle}
|
|
58
|
-
flatDocsDirectories={flatDocsDirectories}
|
|
59
|
-
onNavigate={() => closeSearch()}
|
|
60
|
-
/>
|
|
61
|
-
<div className={styles.Header__searchHeaderBanner}>
|
|
62
|
-
<Stack direction="horizontal" padding="none" gap={4} alignItems="center" justifyContent="space-between">
|
|
63
|
-
<Text as="p" size="300" weight="semibold">
|
|
64
|
-
Search
|
|
65
|
-
</Text>
|
|
66
|
-
<IconButton icon={XIcon} variant="invisible" aria-label="Close search" onClick={closeSearch} />
|
|
67
|
-
</Stack>
|
|
68
|
-
</div>
|
|
69
|
-
</FocusOn>
|
|
70
|
+
{headerLinks.length > 0 && <LinksDropdown className={styles.Header__linksDropdown} items={headerLinks} />}
|
|
70
71
|
</div>
|
|
71
|
-
<div>
|
|
72
|
-
<Stack direction="horizontal" padding="none" gap={
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
<div className={styles.Header__end}>
|
|
73
|
+
<Stack className={styles.Header__links} direction="horizontal" padding="none" gap={24}>
|
|
74
|
+
{headerLinks.map(link => (
|
|
75
|
+
<Link
|
|
76
|
+
key={link.href}
|
|
77
|
+
className={styles.Header__link}
|
|
78
|
+
href={link.href}
|
|
79
|
+
aria-current={link.isActive ? 'page' : undefined}
|
|
80
|
+
{...(link.isExternal && {target: '_blank', rel: 'noopener noreferrer'})}
|
|
81
|
+
>
|
|
82
|
+
<Text
|
|
83
|
+
size="200"
|
|
84
|
+
variant={link.isActive ? 'default' : 'muted'}
|
|
85
|
+
weight={link.isActive ? 'semibold' : 'normal'}
|
|
86
|
+
>
|
|
87
|
+
{link.title}
|
|
88
|
+
{link.isExternal && (
|
|
89
|
+
<ArrowUpRightIcon className={styles.Header__externalLinkIcon} size={10} aria-label="External link" />
|
|
90
|
+
)}
|
|
91
|
+
</Text>
|
|
92
|
+
</Link>
|
|
93
|
+
))}
|
|
94
|
+
</Stack>
|
|
95
|
+
<div className={clsx(styles.Header__searchArea, isSearchOpen && styles['Header__searchArea--open'])}>
|
|
96
|
+
<FocusOn enabled={isSearchOpen} onEscapeKey={closeSearch} onClickOutside={closeSearch}>
|
|
97
|
+
<GlobalSearch
|
|
98
|
+
ref={searchRef}
|
|
99
|
+
siteTitle={siteTitle}
|
|
100
|
+
flatDocsDirectories={flatDocsDirectories}
|
|
101
|
+
onNavigate={() => closeSearch()}
|
|
102
|
+
/>
|
|
103
|
+
<div className={styles.Header__searchHeaderBanner}>
|
|
104
|
+
<Stack direction="horizontal" padding="none" gap={4} alignItems="center" justifyContent="space-between">
|
|
105
|
+
<Text as="p" size="300" weight="semibold">
|
|
106
|
+
Search
|
|
107
|
+
</Text>
|
|
108
|
+
<IconButton icon={XIcon} variant="invisible" aria-label="Close search" onClick={closeSearch} />
|
|
109
|
+
</Stack>
|
|
110
|
+
</div>
|
|
111
|
+
</FocusOn>
|
|
112
|
+
</div>
|
|
113
|
+
<div>
|
|
114
|
+
<Stack direction="horizontal" padding="none" gap={4}>
|
|
88
115
|
<IconButton
|
|
89
|
-
icon={
|
|
116
|
+
icon={colorMode === 'light' ? SunIcon : MoonIcon}
|
|
90
117
|
variant="invisible"
|
|
91
|
-
aria-label=
|
|
92
|
-
|
|
93
|
-
onClick={() => setIsNavDrawerOpen(true)}
|
|
118
|
+
aria-label={`Change color mode. Active mode is ${colorMode}.`}
|
|
119
|
+
onClick={() => setColorMode(colorMode === 'light' ? 'dark' : 'light')}
|
|
94
120
|
/>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
<IconButton
|
|
122
|
+
ref={searchTriggerRef}
|
|
123
|
+
className={styles.Header__searchButton}
|
|
124
|
+
icon={SearchIcon}
|
|
125
|
+
variant="invisible"
|
|
126
|
+
aria-label={`Open search`}
|
|
127
|
+
onClick={() => setIsSearchOpen(true)}
|
|
128
|
+
/>
|
|
129
|
+
<div className={styles.Header__navDrawerContainer}>
|
|
130
|
+
<IconButton
|
|
131
|
+
icon={ThreeBarsIcon}
|
|
132
|
+
variant="invisible"
|
|
133
|
+
aria-label="Menu"
|
|
134
|
+
aria-expanded={isNavDrawerOpen}
|
|
135
|
+
onClick={() => setIsNavDrawerOpen(true)}
|
|
136
|
+
/>
|
|
137
|
+
<NavDrawer isOpen={isNavDrawerOpen} onDismiss={() => setIsNavDrawerOpen(false)} pageMap={pageMap} />
|
|
138
|
+
</div>
|
|
139
|
+
</Stack>
|
|
140
|
+
</div>
|
|
98
141
|
</div>
|
|
99
142
|
</nav>
|
|
100
143
|
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
.dropdown {
|
|
2
|
+
position: relative;
|
|
3
|
+
margin-inline-end: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.dropdownButton {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
background: none;
|
|
10
|
+
border: none;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
font-size: var(--base-font-size-16);
|
|
13
|
+
font-weight: var(--base-font-weight-500);
|
|
14
|
+
padding: var(--base-size-8) var(--base-size-12);
|
|
15
|
+
border-radius: var(--brand-borderRadius-medium);
|
|
16
|
+
color: var(--brand-color-text-default);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dropdownButton:focus-visible {
|
|
20
|
+
outline: var(--base-size-4) solid var(--brand-color-focus);
|
|
21
|
+
outline-radius: var(--base-size-8);
|
|
22
|
+
outline-offset: var(--base-size-2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.buttonText {
|
|
26
|
+
margin-right: var(--base-size-8);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.chevron {
|
|
30
|
+
color: var(--brand-color-text-muted);
|
|
31
|
+
transition: transform 0.2s ease;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.dropdownButton[aria-expanded='true'] .chevron {
|
|
35
|
+
transform: rotate(180deg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dropdownMenu {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 100%;
|
|
41
|
+
left: 0;
|
|
42
|
+
min-width: 200px;
|
|
43
|
+
z-index: 100;
|
|
44
|
+
background-color: var(--brand-color-canvas-default);
|
|
45
|
+
border-radius: var(--brand-borderRadius-medium);
|
|
46
|
+
box-shadow: var(--shadow-floating-small);
|
|
47
|
+
margin-top: var(--base-size-4);
|
|
48
|
+
opacity: 0;
|
|
49
|
+
visibility: hidden;
|
|
50
|
+
transform: translateY(-10px);
|
|
51
|
+
transition:
|
|
52
|
+
opacity 0.2s ease,
|
|
53
|
+
transform 0.2s ease,
|
|
54
|
+
visibility 0s linear 0.2s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dropdownMenu.open {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
visibility: visible;
|
|
60
|
+
transform: translateY(0);
|
|
61
|
+
transition:
|
|
62
|
+
opacity 0.2s ease,
|
|
63
|
+
transform 0.2s ease,
|
|
64
|
+
visibility 0s linear 0s;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dropdownMenu ul {
|
|
68
|
+
list-style: none;
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: var(--base-size-8) 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.menuItem {
|
|
74
|
+
margin-inline: var(--base-size-8);
|
|
75
|
+
padding: 0;
|
|
76
|
+
position: relative;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.menuItemActive {
|
|
80
|
+
background-color: var(--brand-color-border-subtle);
|
|
81
|
+
border-radius: var(--brand-borderRadius-medium);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.menuItemActive::after {
|
|
85
|
+
content: '';
|
|
86
|
+
position: absolute;
|
|
87
|
+
top: calc(50% - var(--base-size-12));
|
|
88
|
+
left: calc(var(--base-size-8) * -1);
|
|
89
|
+
width: var(--base-size-4);
|
|
90
|
+
height: var(--base-size-24);
|
|
91
|
+
background: var(--base-color-scale-blue-5);
|
|
92
|
+
border-radius: var(--brand-borderRadius-medium);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.link:focus-visible {
|
|
96
|
+
position: relative;
|
|
97
|
+
outline: var(--base-size-4) solid var(--brand-color-focus);
|
|
98
|
+
border-radius: var(--brand-borderRadius-small);
|
|
99
|
+
z-index: 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.link {
|
|
103
|
+
display: block;
|
|
104
|
+
padding: var(--base-size-6) var(--base-size-8);
|
|
105
|
+
text-decoration: none;
|
|
106
|
+
color: var(--brand-color-text-default);
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.link:hover {
|
|
111
|
+
background-color: var(--brand-color-canvas-subtle);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.linkText {
|
|
115
|
+
display: block;
|
|
116
|
+
line-height: var(--base-size-20);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.externalLinkIcon {
|
|
120
|
+
margin-block-end: var(--base-size-8);
|
|
121
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, {useState, useRef, useEffect, type HTMLProps, useCallback} from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import {ArrowUpRightIcon, TriangleDownIcon} from '@primer/octicons-react'
|
|
5
|
+
import {Text} from '@primer/react-brand'
|
|
6
|
+
|
|
7
|
+
import styles from './LinksDropdown.module.css'
|
|
8
|
+
import type {ConfigContextLink} from '../../context/useConfig'
|
|
9
|
+
|
|
10
|
+
export type LinksDropdownProps = {
|
|
11
|
+
items: ConfigContextLink[]
|
|
12
|
+
} & HTMLProps<HTMLDivElement>
|
|
13
|
+
|
|
14
|
+
export const LinksDropdown = ({items, className, ...props}: LinksDropdownProps) => {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
16
|
+
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
17
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const closeDropdown = (event: MouseEvent) => {
|
|
21
|
+
if (
|
|
22
|
+
menuRef.current &&
|
|
23
|
+
!menuRef.current.contains(event.target as Node) &&
|
|
24
|
+
buttonRef.current &&
|
|
25
|
+
!buttonRef.current.contains(event.target as Node)
|
|
26
|
+
) {
|
|
27
|
+
setIsOpen(false)
|
|
28
|
+
buttonRef.current.focus()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
document.addEventListener('mousedown', closeDropdown)
|
|
33
|
+
return () => {
|
|
34
|
+
document.removeEventListener('mousedown', closeDropdown)
|
|
35
|
+
}
|
|
36
|
+
}, [menuRef, buttonRef])
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const menu = menuRef.current
|
|
40
|
+
|
|
41
|
+
if (!menu) return
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
44
|
+
switch (e.key) {
|
|
45
|
+
case 'Escape':
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
setIsOpen(false)
|
|
48
|
+
buttonRef.current?.focus()
|
|
49
|
+
break
|
|
50
|
+
case 'Tab':
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (!menu.contains(document.activeElement)) {
|
|
53
|
+
setIsOpen(false)
|
|
54
|
+
}
|
|
55
|
+
}, 0)
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
menu.addEventListener('keydown', handleKeyDown)
|
|
61
|
+
return () => {
|
|
62
|
+
menu.removeEventListener('keydown', handleKeyDown)
|
|
63
|
+
}
|
|
64
|
+
}, [menuRef])
|
|
65
|
+
|
|
66
|
+
const handleToggle = useCallback(() => {
|
|
67
|
+
const nextIsOpen = !isOpen
|
|
68
|
+
setIsOpen(nextIsOpen)
|
|
69
|
+
|
|
70
|
+
if (nextIsOpen) {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
document.querySelector<HTMLAnchorElement>(`#dropdown-item-0 a`)?.focus()
|
|
73
|
+
}, 0)
|
|
74
|
+
} else {
|
|
75
|
+
buttonRef.current?.focus()
|
|
76
|
+
}
|
|
77
|
+
}, [isOpen])
|
|
78
|
+
|
|
79
|
+
const activeItem = items.find(item => item.isActive) || items[0]
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={clsx(styles.dropdown, className)} {...props}>
|
|
83
|
+
<button
|
|
84
|
+
ref={buttonRef}
|
|
85
|
+
className={styles.dropdownButton}
|
|
86
|
+
aria-haspopup="true"
|
|
87
|
+
aria-expanded={isOpen}
|
|
88
|
+
aria-controls="links-dropdown-menu"
|
|
89
|
+
onClick={handleToggle}
|
|
90
|
+
>
|
|
91
|
+
<Text size="200" className={styles.buttonText} variant="muted">
|
|
92
|
+
{activeItem.title}
|
|
93
|
+
</Text>
|
|
94
|
+
<TriangleDownIcon className={styles.chevron} size={16} />
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
<div
|
|
98
|
+
ref={menuRef}
|
|
99
|
+
id="links-dropdown-menu"
|
|
100
|
+
className={`${styles.dropdownMenu} ${isOpen ? styles.open : ''}`}
|
|
101
|
+
role="menu"
|
|
102
|
+
aria-labelledby="links-dropdown-button"
|
|
103
|
+
>
|
|
104
|
+
<ul role="menu">
|
|
105
|
+
{items.map((link, index) => (
|
|
106
|
+
<li
|
|
107
|
+
key={link.href}
|
|
108
|
+
className={clsx(styles.menuItem, link.isActive && styles.menuItemActive)}
|
|
109
|
+
role="menuitem"
|
|
110
|
+
id={`dropdown-item-${index}`}
|
|
111
|
+
>
|
|
112
|
+
<Link
|
|
113
|
+
href={link.href}
|
|
114
|
+
className={clsx(styles.link, link.isActive && styles.linkActive)}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setIsOpen(false)
|
|
117
|
+
}}
|
|
118
|
+
tabIndex={isOpen ? 0 : -1}
|
|
119
|
+
aria-current={link.isActive ? 'page' : undefined}
|
|
120
|
+
{...(link.isExternal && {target: '_blank', rel: 'noopener noreferrer'})}
|
|
121
|
+
>
|
|
122
|
+
<Text className={styles.linkText} size="100" weight={link.isActive ? 'semibold' : 'normal'}>
|
|
123
|
+
{link.title}
|
|
124
|
+
{link.isExternal && (
|
|
125
|
+
<ArrowUpRightIcon className={styles.externalLinkIcon} size={10} aria-label="External link" />
|
|
126
|
+
)}
|
|
127
|
+
</Text>
|
|
128
|
+
</Link>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -4,18 +4,18 @@ import {IconButton, Link, ThemeProvider} from '@primer/react'
|
|
|
4
4
|
import {XIcon} from '@primer/octicons-react'
|
|
5
5
|
|
|
6
6
|
import {Drawer} from './Drawer'
|
|
7
|
-
import type {PageMapItem} from 'nextra'
|
|
8
7
|
import {Sidebar} from '../sidebar/Sidebar'
|
|
9
8
|
import {useColorMode} from '../../context/color-modes/useColorMode'
|
|
10
9
|
import styles from './NavDrawer.module.css'
|
|
10
|
+
import type {PageMapItem} from 'nextra'
|
|
11
11
|
|
|
12
12
|
type NavDrawerProps = {
|
|
13
13
|
isOpen: boolean
|
|
14
14
|
onDismiss: () => void
|
|
15
|
-
|
|
15
|
+
pageMap: PageMapItem[]
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function NavDrawer({isOpen, onDismiss,
|
|
18
|
+
export function NavDrawer({isOpen, onDismiss, pageMap}: NavDrawerProps) {
|
|
19
19
|
const {colorMode} = useColorMode()
|
|
20
20
|
return (
|
|
21
21
|
<Drawer isOpen={isOpen} onDismiss={onDismiss}>
|
|
@@ -31,13 +31,11 @@ export function NavDrawer({isOpen, onDismiss, navItems}: NavDrawerProps) {
|
|
|
31
31
|
</div>
|
|
32
32
|
<div className={styles.navContainer}>{/* <PrimerNavItems items={primerNavItems} /> */}</div>
|
|
33
33
|
</div>
|
|
34
|
-
{
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</ThemeProvider>
|
|
40
|
-
) : null}
|
|
34
|
+
<ThemeProvider colorMode={colorMode}>
|
|
35
|
+
<div className={styles.sidebarWrapper}>
|
|
36
|
+
<Sidebar pageMap={pageMap} />
|
|
37
|
+
</div>
|
|
38
|
+
</ThemeProvider>
|
|
41
39
|
</div>
|
|
42
40
|
</Drawer>
|
|
43
41
|
)
|
|
@@ -51,7 +51,7 @@ export type ThemeProps = PropsWithChildren<{
|
|
|
51
51
|
pageMap: PageMapItem[]
|
|
52
52
|
}>
|
|
53
53
|
|
|
54
|
-
export function Theme({
|
|
54
|
+
export function Theme({pageMap, children}: ThemeProps) {
|
|
55
55
|
const pathname = usePathname()
|
|
56
56
|
|
|
57
57
|
const normalizedPages = normalizePages({
|
|
@@ -116,7 +116,7 @@ export function Theme({children, pageMap}: ThemeProps) {
|
|
|
116
116
|
}}
|
|
117
117
|
>
|
|
118
118
|
<SkipToMainContent href="#main">Skip to main content</SkipToMainContent>
|
|
119
|
-
<Header
|
|
119
|
+
<Header flatDocsDirectories={flatDocsDirectories} siteTitle={siteTitle} pageMap={pageMap} />
|
|
120
120
|
</PRCBox>
|
|
121
121
|
<PageLayout rowGap="none" columnGap="none" padding="none" containerWidth="full">
|
|
122
122
|
<PageLayout.Content padding="normal">
|
|
@@ -3,10 +3,13 @@ import React from 'react'
|
|
|
3
3
|
|
|
4
4
|
import {ColorModeProvider} from '../../context/color-modes/ColorModeProvider'
|
|
5
5
|
import {Theme, ThemeProps} from './Theme'
|
|
6
|
-
import {
|
|
6
|
+
import {type ConfigContextLink, ConfigContextProvider} from '../../context/useConfig'
|
|
7
|
+
import type {PageMapItem} from 'nextra'
|
|
7
8
|
|
|
8
9
|
type Props = {
|
|
9
10
|
pageMap: PageMapItem[]
|
|
11
|
+
headerLinks?: ConfigContextLink[]
|
|
12
|
+
sidebarLinks?: ConfigContextLink[]
|
|
10
13
|
} & ThemeProps
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -15,12 +18,17 @@ type Props = {
|
|
|
15
18
|
* To add custom layouts, create a new file in `pages/_layouts`
|
|
16
19
|
* and export a component with the same name as the layout file
|
|
17
20
|
*/
|
|
18
|
-
export default function Shell({children,
|
|
21
|
+
export default function Shell({children, headerLinks = [], sidebarLinks = [], ...rest}: Props) {
|
|
19
22
|
return (
|
|
20
23
|
<ColorModeProvider>
|
|
21
|
-
<
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
+
<ConfigContextProvider
|
|
25
|
+
value={{
|
|
26
|
+
headerLinks,
|
|
27
|
+
sidebarLinks,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<Theme {...rest}>{children}</Theme>
|
|
31
|
+
</ConfigContextProvider>
|
|
24
32
|
</ColorModeProvider>
|
|
25
33
|
)
|
|
26
34
|
}
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
import React, {useMemo} from 'react'
|
|
2
2
|
import NextLink from 'next/link'
|
|
3
3
|
import {NavList} from '@primer/react'
|
|
4
|
-
import {Folder, MdxFile, PageMapItem} from 'nextra'
|
|
4
|
+
import type {Folder, MdxFile, PageMapItem} from 'nextra'
|
|
5
5
|
|
|
6
6
|
import styles from './Sidebar.module.css'
|
|
7
7
|
import {LinkExternalIcon} from '@primer/octicons-react'
|
|
8
8
|
import type {DocsItem, ExtendedPageItem} from '../../../index'
|
|
9
9
|
import {hasChildren} from '../../../helpers/hasChildren'
|
|
10
10
|
import {usePathname} from 'next/navigation'
|
|
11
|
-
|
|
12
|
-
type SidebarProps = {
|
|
13
|
-
pageMap: PageMapItem[]
|
|
14
|
-
}
|
|
11
|
+
import {useConfig} from '../../context/useConfig'
|
|
15
12
|
|
|
16
13
|
const hasShowTabs = (child: ExtendedPageItem): boolean => {
|
|
17
14
|
return child.name === 'index' && (child as MdxFile).frontMatter?.['show-tabs'] === true
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
type SidebarProps = {
|
|
18
|
+
pageMap: PageMapItem[]
|
|
19
|
+
}
|
|
22
20
|
|
|
21
|
+
export function Sidebar({pageMap}: SidebarProps) {
|
|
23
22
|
const pathname = usePathname()
|
|
24
|
-
|
|
25
|
-
const externalLinks = pageMap.filter(page => {
|
|
26
|
-
if (page.href && page.href.startsWith('http')) {
|
|
27
|
-
return page
|
|
28
|
-
}
|
|
29
|
-
})
|
|
23
|
+
const {sidebarLinks} = useConfig()
|
|
30
24
|
|
|
31
25
|
/**
|
|
32
26
|
* Sorts the incoming data so that folders with a menu-position frontmatter value
|
|
@@ -112,15 +106,23 @@ export function Sidebar({pageMap: pageMapIn}: SidebarProps) {
|
|
|
112
106
|
</NavList.Group>
|
|
113
107
|
)
|
|
114
108
|
})}
|
|
115
|
-
{
|
|
109
|
+
{sidebarLinks.length > 0 && (
|
|
116
110
|
<NavList.Group title="" sx={{mb: 24}}>
|
|
117
|
-
{
|
|
111
|
+
{sidebarLinks.map(link => {
|
|
118
112
|
return (
|
|
119
|
-
<NavList.Item
|
|
113
|
+
<NavList.Item
|
|
114
|
+
as={NextLink}
|
|
115
|
+
key={link.title}
|
|
116
|
+
href={link.href}
|
|
117
|
+
{...(link.isExternal && {target: '_blank', rel: 'noopener noreferrer'})}
|
|
118
|
+
aria-current={link.isActive ? 'page' : undefined}
|
|
119
|
+
>
|
|
120
120
|
{link.title}
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
121
|
+
{link.isExternal && (
|
|
122
|
+
<NavList.TrailingVisual>
|
|
123
|
+
<LinkExternalIcon />
|
|
124
|
+
</NavList.TrailingVisual>
|
|
125
|
+
)}
|
|
124
126
|
</NavList.Item>
|
|
125
127
|
)
|
|
126
128
|
})}
|
package/package.json
CHANGED
package/types.ts
CHANGED
/package/components/{highlight-search-term → layout/highlight-search-term}/HighlightSearchTerm.tsx
RENAMED
|
File without changes
|