@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 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
+ }
@@ -3,6 +3,12 @@
3
3
  z-index: 1;
4
4
  }
5
5
 
6
+ .GlobalSearch__searchIcon {
7
+ vertical-align: middle;
8
+ position: relative;
9
+ top: calc(var(--base-size-2) * -1);
10
+ }
11
+
6
12
  .GlobalSearch__searchResultsContainer {
7
13
  display: none;
8
14
  margin-top: var(--base-size-4);
@@ -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 '../../highlight-search-term/HighlightSearchTerm'
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-20) var(--base-size-12) var(--base-size-20) var(--base-size-16);
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-20) var(--base-size-12) var(--base-size-20) var(--base-size-24);
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
- min-width: 230px;
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 {MarkGithubIcon, MoonIcon, SearchIcon, SunIcon, ThreeBarsIcon, XIcon} from '@primer/octicons-react'
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
- import {DocsItem} from '../../../types'
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({pageMap, siteTitle, flatDocsDirectories}: HeaderProps) {
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
- <Link href="/" className={styles.Header__siteTitle}>
48
- <MarkGithubIcon size={24} />
49
- <Text as="p" size="300" weight="semibold">
50
- {siteTitle}
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
+ &#47;
51
69
  </Text>
52
- </Link>
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={4}>
73
- <IconButton
74
- icon={colorMode === 'light' ? SunIcon : MoonIcon}
75
- variant="invisible"
76
- aria-label={`Change color mode. Active mode is ${colorMode}.`}
77
- onClick={() => setColorMode(colorMode === 'light' ? 'dark' : 'light')}
78
- />
79
- <IconButton
80
- ref={searchTriggerRef}
81
- className={styles.Header__searchButton}
82
- icon={SearchIcon}
83
- variant="invisible"
84
- aria-label={`Open search`}
85
- onClick={() => setIsSearchOpen(true)}
86
- />
87
- <div className={styles.Header__navDrawerContainer}>
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={ThreeBarsIcon}
116
+ icon={colorMode === 'light' ? SunIcon : MoonIcon}
90
117
  variant="invisible"
91
- aria-label="Menu"
92
- aria-expanded={isNavDrawerOpen}
93
- onClick={() => setIsNavDrawerOpen(true)}
118
+ aria-label={`Change color mode. Active mode is ${colorMode}.`}
119
+ onClick={() => setColorMode(colorMode === 'light' ? 'dark' : 'light')}
94
120
  />
95
- <NavDrawer isOpen={isNavDrawerOpen} onDismiss={() => setIsNavDrawerOpen(false)} navItems={pageMap} />
96
- </div>
97
- </Stack>
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
- navItems?: PageMapItem[]
15
+ pageMap: PageMapItem[]
16
16
  }
17
17
 
18
- export function NavDrawer({isOpen, onDismiss, navItems}: NavDrawerProps) {
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
- {navItems && navItems.length > 0 ? (
35
- <ThemeProvider colorMode={colorMode}>
36
- <div className={styles.sidebarWrapper}>
37
- <Sidebar pageMap={navItems} />
38
- </div>
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({children, pageMap}: ThemeProps) {
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 pageMap={pageMap} flatDocsDirectories={flatDocsDirectories} siteTitle={siteTitle} />
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 {PageMapItem} from 'nextra'
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, pageMap, ...rest}: Props) {
21
+ export default function Shell({children, headerLinks = [], sidebarLinks = [], ...rest}: Props) {
19
22
  return (
20
23
  <ColorModeProvider>
21
- <Theme {...rest} pageMap={pageMap}>
22
- {children}
23
- </Theme>
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
- export function Sidebar({pageMap: pageMapIn}: SidebarProps) {
21
- const pageMap = pageMapIn as ExtendedPageItem[]
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
- {externalLinks.length > 0 && (
109
+ {sidebarLinks.length > 0 && (
116
110
  <NavList.Group title="" sx={{mb: 24}}>
117
- {externalLinks.map(link => {
111
+ {sidebarLinks.map(link => {
118
112
  return (
119
- <NavList.Item as={NextLink} key={link.title} href={link.href}>
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
- <NavList.TrailingVisual>
122
- <LinkExternalIcon />
123
- </NavList.TrailingVisual>
121
+ {link.isExternal && (
122
+ <NavList.TrailingVisual>
123
+ <LinkExternalIcon />
124
+ </NavList.TrailingVisual>
125
+ )}
124
126
  </NavList.Item>
125
127
  )
126
128
  })}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/doctocat-nextjs",
3
- "version": "0.4.1-rc.afcbafc",
3
+ "version": "0.5.0-rc.6ee882c",
4
4
  "description": "A Next.js theme for building Primer documentation sites",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -12,6 +12,8 @@ export type ExtendedPageItem = PageMapItem & {
12
12
  name: string
13
13
  title: string
14
14
  href: string
15
+ type?: string
16
+ active?: boolean
15
17
  }
16
18
 
17
19
  export type FolderWithoutChildren = Omit<Folder, 'children'>