Skip to content

Commit 1dcb91a

Browse files
author
Dimitri POSTOLOV
authoredOct 10, 2023
[v3] export useThemeConfig (#2415)

22 files changed

+226
-234
lines changed
 

Diff for: ‎.changeset/fifty-rockets-kick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'nextra-theme-docs': minor
3+
---
4+
5+
export `useThemeConfig`

Diff for: ‎packages/nextra-theme-docs/src/components/404.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { useRouter } from 'next/router'
22
import { useMounted } from 'nextra/hooks'
33
import type { ReactElement } from 'react'
4-
import { useConfig } from '../contexts'
4+
import { useThemeConfig } from '../contexts'
55
import { getGitIssueUrl, renderComponent } from '../utils'
66
import { Anchor } from './anchor'
77

88
export function NotFoundPage(): ReactElement | null {
9-
const config = useConfig()
9+
const themeConfig = useThemeConfig()
10+
1011
const mounted = useMounted()
1112
const { asPath } = useRouter()
12-
const { content, labels } = config.notFound
13+
const { content, labels } = themeConfig.notFound
1314
if (!content) {
1415
return null
1516
}
@@ -18,7 +19,7 @@ export function NotFoundPage(): ReactElement | null {
1819
<p className="_text-center">
1920
<Anchor
2021
href={getGitIssueUrl({
21-
repository: config.docsRepositoryBase,
22+
repository: themeConfig.docsRepositoryBase,
2223
title: `Found broken \`${mounted ? asPath : ''}\` link. Please fix!`,
2324
labels
2425
})}

Diff for: ‎packages/nextra-theme-docs/src/components/banner.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import cn from 'clsx'
22
import { XIcon } from 'nextra/icons'
33
import type { ReactElement } from 'react'
4-
import { useConfig } from '../contexts'
4+
import { useThemeConfig } from '../contexts'
55
import { renderComponent } from '../utils'
66

77
export function Banner(): ReactElement | null {
8-
const { banner } = useConfig()
8+
const { banner } = useThemeConfig()
99
if (!banner.content) {
1010
return null
1111
}

Diff for: ‎packages/nextra-theme-docs/src/components/footer.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import cn from 'clsx'
22
import type { ReactElement } from 'react'
3-
import { useConfig } from '../contexts'
3+
import { useThemeConfig } from '../contexts'
44
import { renderComponent } from '../utils'
55
import { LocaleSwitch } from './locale-switch'
66

77
export function Footer({ menu }: { menu?: boolean }): ReactElement {
8-
const config = useConfig()
8+
const themeConfig = useThemeConfig()
99
return (
1010
<footer className="_bg-gray-100 _pb-[env(safe-area-inset-bottom)] dark:_bg-neutral-900 print:_bg-transparent">
1111
<div
1212
className={cn(
1313
'_mx-auto _flex _max-w-[90rem] _gap-2 _py-2 _px-4',
14-
menu && (config.i18n.length > 0 || config.darkMode)
14+
menu && (themeConfig.i18n.length > 0 || themeConfig.darkMode)
1515
? '_flex'
1616
: '_hidden'
1717
)}
1818
>
1919
<LocaleSwitch />
20-
{config.darkMode && renderComponent(config.themeSwitch.component)}
20+
{themeConfig.darkMode &&
21+
renderComponent(themeConfig.themeSwitch.component)}
2122
</div>
2223
<hr className="dark:_border-neutral-800" />
2324
<div
@@ -26,7 +27,7 @@ export function Footer({ menu }: { menu?: boolean }): ReactElement {
2627
'_pl-[max(env(safe-area-inset-left),1.5rem)] _pr-[max(env(safe-area-inset-right),1.5rem)]'
2728
)}
2829
>
29-
{renderComponent(config.footer.content)}
30+
{renderComponent(themeConfig.footer.content)}
3031
</div>
3132
</footer>
3233
)

Diff for: ‎packages/nextra-theme-docs/src/components/head.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import { useTheme } from 'next-themes'
22
import NextHead from 'next/head'
33
import { useMounted } from 'nextra/hooks'
44
import type { ReactElement } from 'react'
5-
import { useConfig } from '../contexts'
5+
import { useThemeConfig } from '../contexts'
66

77
export function Head(): ReactElement {
8-
const config = useConfig()
98
const { resolvedTheme } = useTheme()
109
const mounted = useMounted()
10+
const themeConfig = useThemeConfig()
1111

1212
// `head` can be either FC or ReactNode. We have to directly call it if it's an
1313
// FC because hooks like Next.js' `useRouter` aren't allowed inside NextHead.
14-
const head = typeof config.head === 'function' ? config.head({}) : config.head
15-
const { hue, saturation } = config.color
14+
const head =
15+
typeof themeConfig.head === 'function'
16+
? themeConfig.head({})
17+
: themeConfig.head
18+
const { hue, saturation } = themeConfig.color
1619
const { dark: darkHue, light: lightHue } =
1720
typeof hue === 'number' ? { dark: hue, light: hue } : hue
1821
const { dark: darkSaturation, light: lightSaturation } =
@@ -22,10 +25,10 @@ export function Head(): ReactElement {
2225

2326
return (
2427
<NextHead>
25-
{config.faviconGlyph ? (
28+
{themeConfig.faviconGlyph ? (
2629
<link
2730
rel="icon"
28-
href={`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text x='50' y='.9em' font-size='90' text-anchor='middle'>${config.faviconGlyph}</text><style>text{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";fill:black}@media(prefers-color-scheme:dark){text{fill:white}}</style></svg>`}
31+
href={`data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text x='50' y='.9em' font-size='90' text-anchor='middle'>${themeConfig.faviconGlyph}</text><style>text{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";fill:black}@media(prefers-color-scheme:dark){text{fill:white}}</style></svg>`}
2932
/>
3033
) : null}
3134
{mounted ? (

Diff for: ‎packages/nextra-theme-docs/src/components/locale-switch.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { addBasePath } from 'next/dist/client/add-base-path'
22
import { useRouter } from 'nextra/hooks'
33
import { GlobeIcon } from 'nextra/icons'
44
import type { ReactElement } from 'react'
5-
import { useConfig } from '../contexts'
5+
import { useThemeConfig } from '../contexts'
66
import { Select } from './select'
77

88
const ONE_YEAR = 365 * 24 * 60 * 60 * 1000
@@ -16,10 +16,11 @@ export function LocaleSwitch({
1616
lite,
1717
className
1818
}: LocaleSwitchProps): ReactElement | null {
19-
const config = useConfig()
19+
const themeConfig = useThemeConfig()
20+
2021
const { locale, asPath } = useRouter()
2122

22-
const options = config.i18n
23+
const options = themeConfig.i18n
2324
if (!options.length) return null
2425

2526
const selected = options.find(l => locale === l.locale)

Diff for: ‎packages/nextra-theme-docs/src/components/nav-links.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import NextLink from 'next/link'
44
import { ArrowRightIcon } from 'nextra/icons'
55
import type { Item } from 'nextra/normalize-pages'
66
import type { ReactElement } from 'react'
7-
import { useConfig } from '../contexts'
7+
import { useThemeConfig } from '../contexts'
88
import type { DocsThemeConfig } from '../index'
99

1010
interface NavLinkProps {
@@ -23,8 +23,8 @@ export const NavLinks = ({
2323
flatDirectories,
2424
currentIndex
2525
}: NavLinkProps): ReactElement | null => {
26-
const config = useConfig()
27-
const nav = config.navigation
26+
const themeConfig = useThemeConfig()
27+
const nav = themeConfig.navigation
2828
const navigation: Exclude<DocsThemeConfig['navigation'], boolean> =
2929
typeof nav === 'boolean' ? { prev: nav, next: nav } : nav
3030
let prev = navigation.prev && flatDirectories[currentIndex - 1]

Diff for: ‎packages/nextra-theme-docs/src/components/navbar.tsx

+19-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useFSRoute } from 'nextra/hooks'
66
import { ArrowRightIcon, MenuIcon } from 'nextra/icons'
77
import type { MenuItem, PageItem } from 'nextra/normalize-pages'
88
import type { ReactElement, ReactNode } from 'react'
9-
import { useConfig, useMenu } from '../contexts'
9+
import { useMenu, useThemeConfig } from '../contexts'
1010
import { renderComponent } from '../utils'
1111
import { Anchor } from './anchor'
1212

@@ -79,7 +79,8 @@ function NavbarMenu({
7979
}
8080

8181
export function Navbar({ items }: NavBarProps): ReactElement {
82-
const config = useConfig()
82+
const themeConfig = useThemeConfig()
83+
8384
const activeRoute = useFSRoute()
8485
const { menu, setMenu } = useMenu()
8586

@@ -94,16 +95,20 @@ export function Navbar({ items }: NavBarProps): ReactElement {
9495
)}
9596
/>
9697
<nav className="_mx-auto _flex _h-[var(--nextra-navbar-height)] _max-w-[90rem] _items-center _justify-end _gap-4 _pl-[max(env(safe-area-inset-left),1.5rem)] _pr-[max(env(safe-area-inset-right),1.5rem)]">
97-
{config.logoLink ? (
98+
{themeConfig.logoLink ? (
9899
<NextLink
99-
href={typeof config.logoLink === 'string' ? config.logoLink : '/'}
100+
href={
101+
typeof themeConfig.logoLink === 'string'
102+
? themeConfig.logoLink
103+
: '/'
104+
}
100105
className="_flex _items-center hover:_opacity-75 ltr:_mr-auto rtl:_ml-auto"
101106
>
102-
{renderComponent(config.logo)}
107+
{renderComponent(themeConfig.logo)}
103108
</NextLink>
104109
) : (
105110
<div className="_flex _items-center ltr:_mr-auto rtl:_ml-auto">
106-
{renderComponent(config.logo)}
111+
{renderComponent(themeConfig.logo)}
107112
</div>
108113
)}
109114
{items.map(pageOrMenu => {
@@ -152,23 +157,23 @@ export function Navbar({ items }: NavBarProps): ReactElement {
152157
})}
153158

154159
{process.env.NEXTRA_SEARCH &&
155-
renderComponent(config.search.component, {
160+
renderComponent(themeConfig.search.component, {
156161
className: 'max-md:_hidden'
157162
})}
158163

159-
{config.project.link ? (
160-
<Anchor href={config.project.link} newWindow>
161-
{renderComponent(config.project.icon)}
164+
{themeConfig.project.link ? (
165+
<Anchor href={themeConfig.project.link} newWindow>
166+
{renderComponent(themeConfig.project.icon)}
162167
</Anchor>
163168
) : null}
164169

165-
{config.chat.link ? (
166-
<Anchor href={config.chat.link} newWindow>
167-
{renderComponent(config.chat.icon)}
170+
{themeConfig.chat.link ? (
171+
<Anchor href={themeConfig.chat.link} newWindow>
172+
{renderComponent(themeConfig.chat.icon)}
168173
</Anchor>
169174
) : null}
170175

171-
{renderComponent(config.navbar.extraContent)}
176+
{renderComponent(themeConfig.navbar.extraContent)}
172177

173178
<button
174179
type="button"

Diff for: ‎packages/nextra-theme-docs/src/components/search.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useMounted } from 'nextra/hooks'
77
import { InformationCircleIcon, SpinnerIcon } from 'nextra/icons'
88
import type { CompositionEvent, KeyboardEvent, ReactElement } from 'react'
99
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
10-
import { useConfig, useMenu } from '../contexts'
10+
import { useMenu, useThemeConfig } from '../contexts'
1111
import type { SearchResult } from '../types'
1212
import { renderComponent, renderString } from '../utils'
1313
import { Input } from './input'
@@ -35,8 +35,8 @@ export function Search({
3535
error,
3636
results
3737
}: SearchProps): ReactElement {
38+
const themeConfig = useThemeConfig()
3839
const [show, setShow] = useState(false)
39-
const config = useConfig()
4040
const [active, setActive] = useState(0)
4141
const router = useRouter()
4242
const { setMenu } = useMenu()
@@ -217,7 +217,7 @@ export function Search({
217217
onCompositionStart={handleComposition}
218218
onCompositionEnd={handleComposition}
219219
type="search"
220-
placeholder={renderString(config.search.placeholder)}
220+
placeholder={renderString(themeConfig.search.placeholder)}
221221
onKeyDown={handleKeyDown}
222222
suffix={icon}
223223
/>
@@ -250,12 +250,12 @@ export function Search({
250250
{error ? (
251251
<span className="_flex _select-none _justify-center _gap-2 _p-8 _text-center _text-sm _text-red-500">
252252
<InformationCircleIcon className="_h-5 _w-5" />
253-
{renderString(config.search.error)}
253+
{renderString(themeConfig.search.error)}
254254
</span>
255255
) : loading ? (
256256
<span className="_flex _select-none _justify-center _gap-2 _p-8 _text-center _text-sm _text-gray-400">
257257
<SpinnerIcon className="_h-5 _w-5 _animate-spin" />
258-
{renderComponent(config.search.loading)}
258+
{renderComponent(themeConfig.search.loading)}
259259
</span>
260260
) : results.length > 0 ? (
261261
results.map(({ route, prefix, children, id }, i) => (
@@ -285,7 +285,7 @@ export function Search({
285285
</Fragment>
286286
))
287287
) : (
288-
renderComponent(config.search.emptyResult)
288+
renderComponent(themeConfig.search.emptyResult)
289289
)}
290290
</ul>
291291
</Transition>

Diff for: ‎packages/nextra-theme-docs/src/components/sidebar.tsx

+17-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
useState
1515
} from 'react'
1616
import scrollIntoView from 'scroll-into-view-if-needed'
17-
import { useActiveAnchor, useConfig, useMenu } from '../contexts'
17+
import { useActiveAnchor, useMenu, useThemeConfig } from '../contexts'
1818
import { renderComponent } from '../utils'
1919
import { Anchor } from './anchor'
2020
import { Collapse } from './collapse'
@@ -79,16 +79,17 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
7979
const level = useContext(FolderLevelContext)
8080

8181
const { setMenu } = useMenu()
82-
const config = useConfig()
8382
const { theme } = item as Item
83+
const themeConfig = useThemeConfig()
84+
8485
const open =
8586
TreeState[item.route] === undefined
8687
? active ||
8788
activeRouteInside ||
8889
focusedRouteInside ||
8990
(theme && 'collapsed' in theme
9091
? !theme.collapsed
91-
: level < config.sidebar.defaultMenuCollapseLevel)
92+
: level < themeConfig.sidebar.defaultMenuCollapseLevel)
9293
: TreeState[item.route] || focusedRouteInside
9394

9495
const rerender = useState({})[1]
@@ -106,12 +107,14 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
106107
delete TreeState[item.route]
107108
}
108109
}
109-
config.sidebar.autoCollapse ? updateAndPruneTreeState() : updateTreeState()
110+
themeConfig.sidebar.autoCollapse
111+
? updateAndPruneTreeState()
112+
: updateTreeState()
110113
}, [
111114
activeRouteInside,
112115
focusedRouteInside,
113116
item.route,
114-
config.sidebar.autoCollapse
117+
themeConfig.sidebar.autoCollapse
115118
])
116119

117120
if (item.type === 'menu') {
@@ -318,7 +321,6 @@ export function Sidebar({
318321
toc,
319322
includePlaceholder
320323
}: SideBarProps): ReactElement {
321-
const config = useConfig()
322324
const { menu, setMenu } = useMenu()
323325
const [focused, setFocused] = useState<null | string>(null)
324326
const [showSidebar, setSidebar] = useState(true)
@@ -357,8 +359,10 @@ export function Sidebar({
357359
}
358360
}, [menu])
359361

360-
const hasI18n = config.i18n.length > 0
361-
const hasMenu = config.darkMode || hasI18n || config.sidebar.toggleButton
362+
const themeConfig = useThemeConfig()
363+
const hasI18n = themeConfig.i18n.length > 0
364+
const hasMenu =
365+
themeConfig.darkMode || hasI18n || themeConfig.sidebar.toggleButton
362366

363367
return (
364368
<>
@@ -390,7 +394,7 @@ export function Sidebar({
390394
>
391395
{process.env.NEXTRA_SEARCH && (
392396
<div className="_px-4 _pt-4 md:_hidden">
393-
{renderComponent(config.search.component)}
397+
{renderComponent(themeConfig.search.component)}
394398
</div>
395399
)}
396400
<FocusedItemContext.Provider value={focused}>
@@ -416,7 +420,7 @@ export function Sidebar({
416420
directories={docsDirectories}
417421
// When the viewport size is larger than `md`, hide the anchors in
418422
// the sidebar when `floatTOC` is enabled.
419-
anchors={config.toc.float ? [] : anchors}
423+
anchors={themeConfig.toc.float ? [] : anchors}
420424
onlyCurrentDocs
421425
/>
422426
</Collapse>
@@ -455,18 +459,18 @@ export function Sidebar({
455459
lite={!showSidebar}
456460
className={showSidebar ? '_grow' : 'max-md:_grow'}
457461
/>
458-
{config.darkMode && (
462+
{themeConfig.darkMode && (
459463
<div
460464
className={
461465
showSidebar && !hasI18n ? '_grow _flex _flex-col' : ''
462466
}
463467
>
464-
{renderComponent(config.themeSwitch.component, {
468+
{renderComponent(themeConfig.themeSwitch.component, {
465469
lite: !showSidebar || hasI18n
466470
})}
467471
</div>
468472
)}
469-
{config.sidebar.toggleButton && (
473+
{themeConfig.sidebar.toggleButton && (
470474
<button
471475
title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}
472476
className="max-md:_hidden _h-7 _rounded-md _transition-colors _text-gray-600 dark:_text-gray-400 _px-2 hover:_bg-gray-100 hover:_text-gray-900 dark:hover:_bg-primary-100/5 dark:hover:_text-gray-50"

Diff for: ‎packages/nextra-theme-docs/src/components/theme-switch.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useMounted } from 'nextra/hooks'
33
import { MoonIcon, SunIcon } from 'nextra/icons'
44
import type { ReactElement } from 'react'
55
import type { z } from 'zod'
6-
import { useConfig } from '../contexts'
6+
import { useThemeConfig } from '../contexts'
77
import type { themeOptionsSchema } from '../schemas'
88
import { Select } from './select'
99

@@ -20,7 +20,7 @@ export function ThemeSwitch({
2020
}: ThemeSwitchProps): ReactElement {
2121
const { setTheme, resolvedTheme, theme = '' } = useTheme()
2222
const mounted = useMounted()
23-
const config = useConfig().themeSwitch
23+
const config = useThemeConfig().themeSwitch
2424

2525
const IconToUse = mounted && resolvedTheme === 'dark' ? MoonIcon : SunIcon
2626
const options: ThemeOptions =

Diff for: ‎packages/nextra-theme-docs/src/components/toc.tsx

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Heading } from 'nextra'
33
import type { ReactElement } from 'react'
44
import { useEffect, useRef } from 'react'
55
import scrollIntoView from 'scroll-into-view-if-needed'
6-
import { useActiveAnchor, useConfig } from '../contexts'
6+
import { useActiveAnchor, useThemeConfig } from '../contexts'
77
import { renderComponent } from '../utils'
88
import { Anchor } from './anchor'
99
import { BackToTop } from './back-to-top'
@@ -20,14 +20,14 @@ const linkClassName = cn(
2020

2121
export function TOC({ toc, filePath }: TOCProps): ReactElement {
2222
const activeAnchor = useActiveAnchor()
23-
const config = useConfig()
2423
const tocRef = useRef<HTMLDivElement>(null)
24+
const themeConfig = useThemeConfig()
2525

2626
const hasHeadings = toc.length > 0
2727
const hasMetaInfo = Boolean(
28-
config.feedback.content ||
29-
config.editLink.component ||
30-
config.toc.extraContent
28+
themeConfig.feedback.content ||
29+
themeConfig.editLink.component ||
30+
themeConfig.toc.extraContent
3131
)
3232

3333
const activeSlug = Object.entries(activeAnchor).find(
@@ -62,7 +62,7 @@ export function TOC({ toc, filePath }: TOCProps): ReactElement {
6262
{hasHeadings && (
6363
<>
6464
<p className="_mb-4 _font-semibold _tracking-tight">
65-
{renderComponent(config.toc.title)}
65+
{renderComponent(themeConfig.toc.title)}
6666
</p>
6767
<ul>
6868
{toc.map(({ id, value, depth }) => (
@@ -101,25 +101,25 @@ export function TOC({ toc, filePath }: TOCProps): ReactElement {
101101
'contrast-more:_border-t contrast-more:_border-neutral-400 contrast-more:_shadow-none contrast-more:dark:_border-neutral-400'
102102
)}
103103
>
104-
{config.feedback.content ? (
104+
{themeConfig.feedback.content ? (
105105
<Anchor
106106
className={linkClassName}
107-
href={config.feedback.useLink()}
107+
href={themeConfig.feedback.useLink()}
108108
newWindow
109109
>
110-
{renderComponent(config.feedback.content)}
110+
{renderComponent(themeConfig.feedback.content)}
111111
</Anchor>
112112
) : null}
113113

114-
{renderComponent(config.editLink.component, {
114+
{renderComponent(themeConfig.editLink.component, {
115115
filePath,
116116
className: linkClassName,
117-
children: renderComponent(config.editLink.content)
117+
children: renderComponent(themeConfig.editLink.content)
118118
})}
119119

120-
{renderComponent(config.toc.extraContent)}
120+
{renderComponent(themeConfig.toc.extraContent)}
121121

122-
{config.toc.backToTop && <BackToTop className={linkClassName} />}
122+
{themeConfig.toc.backToTop && <BackToTop className={linkClassName} />}
123123
</div>
124124
)}
125125
</div>

Diff for: ‎packages/nextra-theme-docs/src/constants.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
ThemeSwitch,
1212
TOC
1313
} from './components'
14-
import { useConfig } from './contexts'
14+
import { useConfig, useThemeConfig } from './contexts'
1515
import type { publicThemeSchema, themeSchema } from './schemas'
1616
import { getGitIssueUrl, useGitEditUrl } from './utils'
1717

@@ -78,9 +78,10 @@ export const DEFAULT_THEME: DocsThemeConfig = {
7878
labels: 'feedback',
7979
useLink() {
8080
const config = useConfig()
81+
const themeConfig = useThemeConfig()
8182
return getGitIssueUrl({
82-
labels: config.feedback.labels,
83-
repository: config.docsRepositoryBase,
83+
labels: themeConfig.feedback.labels,
84+
repository: themeConfig.docsRepositoryBase,
8485
title: `Feedback for “${config.title}”`
8586
})
8687
}

Diff for: ‎packages/nextra-theme-docs/src/contexts/config.tsx

+14-99
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,25 @@
1-
import { ThemeProvider } from 'next-themes'
21
import { useRouter } from 'next/router'
3-
import type { FrontMatter, PageMapItem, PageOpts } from 'nextra'
2+
import type { FrontMatter, PageOpts } from 'nextra'
43
import { useFSRoute } from 'nextra/hooks'
54
import { normalizePages } from 'nextra/normalize-pages'
6-
import { metaSchema } from 'nextra/schemas'
75
import type { ReactElement, ReactNode } from 'react'
86
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
9-
import type { ZodError } from 'zod'
10-
import type { DocsThemeConfig } from '../constants'
11-
import { DEEP_OBJECT_KEYS, DEFAULT_THEME } from '../constants'
12-
import { themeSchema } from '../schemas'
13-
import type { Context } from '../types'
147
import { MenuProvider } from './menu'
158

16-
type Config<FrontMatterType = FrontMatter> = DocsThemeConfig &
17-
Pick<
18-
PageOpts<FrontMatterType>,
19-
'title' | 'frontMatter' | 'filePath' | 'timestamp'
20-
> & {
21-
hideSidebar: boolean
22-
normalizePagesResult: ReturnType<typeof normalizePages>
23-
}
9+
type Config<FrontMatterType = FrontMatter> = Pick<
10+
PageOpts<FrontMatterType>,
11+
'title' | 'frontMatter' | 'filePath' | 'timestamp'
12+
> & {
13+
hideSidebar: boolean
14+
normalizePagesResult: ReturnType<typeof normalizePages>
15+
}
2416

2517
const ConfigContext = createContext<Config>({
2618
title: '',
2719
frontMatter: {},
2820
filePath: '',
2921
hideSidebar: false,
30-
normalizePagesResult: {} as ReturnType<typeof normalizePages>,
31-
...DEFAULT_THEME
22+
normalizePagesResult: {} as ReturnType<typeof normalizePages>
3223
})
3324
ConfigContext.displayName = 'Config'
3425

@@ -37,80 +28,15 @@ export function useConfig<FrontMatterType = FrontMatter>() {
3728
return useContext<Config<FrontMatterType>>(ConfigContext)
3829
}
3930

40-
let theme: DocsThemeConfig
41-
let isValidated = false
42-
43-
function normalizeZodMessage(error: unknown): string {
44-
return (error as ZodError).issues
45-
.flatMap(issue => {
46-
const themePath =
47-
issue.path.length > 0 && `Path: "${issue.path.join('.')}"`
48-
const unionErrors =
49-
'unionErrors' in issue ? issue.unionErrors.map(normalizeZodMessage) : []
50-
return [
51-
[issue.message, themePath].filter(Boolean).join('. '),
52-
...unionErrors
53-
]
54-
})
55-
.join('\n')
56-
}
57-
58-
function _validateMeta(pageMap: PageMapItem[]) {
59-
for (const pageMapItem of pageMap) {
60-
if ('data' in pageMapItem) {
61-
for (const [key, data] of Object.entries(pageMapItem.data)) {
62-
try {
63-
metaSchema.parse(data)
64-
} catch (error) {
65-
console.error(
66-
`[nextra-theme-docs] Error validating _meta.json file for "${key}" property.\n\n${normalizeZodMessage(
67-
error
68-
)}`
69-
)
70-
}
71-
}
72-
} else if ('children' in pageMapItem) {
73-
_validateMeta(pageMapItem.children)
74-
}
75-
}
76-
}
77-
7831
export function ConfigProvider({
7932
children,
80-
value: { themeConfig, pageOpts }
33+
value: pageOpts
8134
}: {
8235
children: ReactNode
83-
value: Context
36+
value: PageOpts
8437
}): ReactElement {
8538
const [menu, setMenu] = useState(false)
8639
const { asPath } = useRouter()
87-
// Merge only on first load
88-
theme ||= {
89-
...DEFAULT_THEME,
90-
...(themeConfig &&
91-
Object.fromEntries(
92-
Object.entries(themeConfig).map(([key, value]) => [
93-
key,
94-
value && typeof value === 'object' && DEEP_OBJECT_KEYS.includes(key)
95-
? // @ts-expect-error -- key has always object value
96-
{ ...DEFAULT_THEME[key], ...value }
97-
: value
98-
])
99-
))
100-
}
101-
if (process.env.NODE_ENV !== 'production' && !isValidated) {
102-
try {
103-
themeSchema.parse(theme)
104-
} catch (error) {
105-
console.error(
106-
`[nextra-theme-docs] Error validating theme config file.\n\n${normalizeZodMessage(
107-
error
108-
)}`
109-
)
110-
}
111-
// validateMeta(pageOpts.pageMap)
112-
isValidated = true
113-
}
11440

11541
const fsPath = useFSRoute()
11642

@@ -122,7 +48,6 @@ export function ConfigProvider({
12248
const { activeType, activeThemeContext: themeContext } = normalizePagesResult
12349

12450
const extendedConfig: Config = {
125-
...theme,
12651
title: pageOpts.title,
12752
frontMatter: pageOpts.frontMatter,
12853
filePath: pageOpts.filePath,
@@ -134,24 +59,14 @@ export function ConfigProvider({
13459
normalizePagesResult
13560
}
13661

137-
const { nextThemes } = extendedConfig
138-
13962
// Always close mobile nav when route was changed (e.g. logo click)
14063
useEffect(() => {
14164
setMenu(false)
14265
}, [asPath])
14366

14467
return (
145-
<ThemeProvider
146-
attribute="class"
147-
disableTransitionOnChange
148-
defaultTheme={nextThemes.defaultTheme}
149-
storageKey={nextThemes.storageKey}
150-
forcedTheme={nextThemes.forcedTheme}
151-
>
152-
<ConfigContext.Provider value={extendedConfig}>
153-
<MenuProvider value={{ menu, setMenu }}>{children}</MenuProvider>
154-
</ConfigContext.Provider>
155-
</ThemeProvider>
68+
<ConfigContext.Provider value={extendedConfig}>
69+
<MenuProvider value={{ menu, setMenu }}>{children}</MenuProvider>
70+
</ConfigContext.Provider>
15671
)
15772
}

Diff for: ‎packages/nextra-theme-docs/src/contexts/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export {
55
} from './active-anchor'
66
export { useConfig, ConfigProvider } from './config'
77
export { useMenu } from './menu'
8+
export { ThemeConfigProvider, useThemeConfig } from './theme-config'
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ReactElement, ReactNode } from 'react'
2+
import { createContext, useContext, useRef } from 'react'
3+
import type { DocsThemeConfig } from '../constants'
4+
import { DEEP_OBJECT_KEYS, DEFAULT_THEME } from '../constants'
5+
6+
const ThemeConfigContext = createContext<DocsThemeConfig>(DEFAULT_THEME)
7+
ThemeConfigContext.displayName = 'ThemeConfig'
8+
export const useThemeConfig = () => useContext(ThemeConfigContext)
9+
10+
export function ThemeConfigProvider({
11+
value,
12+
children
13+
}: {
14+
value: DocsThemeConfig
15+
children: ReactNode
16+
}): ReactElement {
17+
const storeRef = useRef<DocsThemeConfig>()
18+
storeRef.current ||= {
19+
...DEFAULT_THEME,
20+
...(value &&
21+
Object.fromEntries(
22+
Object.entries(value).map(([key, value]) => [
23+
key,
24+
value && typeof value === 'object' && DEEP_OBJECT_KEYS.includes(key)
25+
? // @ts-expect-error -- key has always object value
26+
{ ...DEFAULT_THEME[key], ...value }
27+
: value
28+
])
29+
))
30+
}
31+
32+
return (
33+
<ThemeConfigContext.Provider value={storeRef.current}>
34+
{children}
35+
</ThemeConfigContext.Provider>
36+
)
37+
}

Diff for: ‎packages/nextra-theme-docs/src/env.d.ts

Whitespace-only changes.

Diff for: ‎packages/nextra-theme-docs/src/index.tsx

+57-34
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,92 @@
1-
import type { NextraThemeLayoutProps } from 'nextra'
2-
import type { ReactElement, ReactNode } from 'react'
31
import 'focus-visible'
2+
import './polyfill'
3+
import { ThemeProvider } from 'next-themes'
4+
import type { NextraThemeLayoutProps } from 'nextra'
45
import { useRouter } from 'nextra/hooks'
56
import { MDXProvider } from 'nextra/mdx'
6-
import './polyfill'
7+
import type { ReactElement, ReactNode } from 'react'
78
import { Banner, Head } from './components'
89
import { PartialDocsThemeConfig } from './constants'
9-
import { ActiveAnchorProvider, ConfigProvider, useConfig } from './contexts'
10+
import {
11+
ActiveAnchorProvider,
12+
ConfigProvider,
13+
ThemeConfigProvider,
14+
useConfig,
15+
useThemeConfig
16+
} from './contexts'
1017
import { getComponents } from './mdx-components'
1118
import { renderComponent } from './utils'
1219

1320
function InnerLayout({ children }: { children: ReactNode }): ReactElement {
21+
const themeConfig = useThemeConfig()
22+
1423
const config = useConfig()
1524
const { locale } = useRouter()
1625

17-
const { direction } = config.i18n.find(l => l.locale === locale) || config
26+
const { direction } =
27+
themeConfig.i18n.find(l => l.locale === locale) || themeConfig
1828
const dir = direction === 'rtl' ? 'rtl' : 'ltr'
1929

2030
const { activeThemeContext: themeContext, topLevelNavbarItems } =
2131
config.normalizePagesResult
2232

2333
const components = getComponents({
2434
isRawLayout: themeContext.layout === 'raw',
25-
components: config.components
35+
components: themeConfig.components
2636
})
2737

2838
return (
29-
// This makes sure that selectors like `[dir=ltr] .nextra-container` work
30-
// before hydration as Tailwind expects the `dir` attribute to exist on the
31-
// `html` element.
32-
<div dir={dir}>
33-
<script
34-
dangerouslySetInnerHTML={{
35-
__html: `document.documentElement.setAttribute('dir','${dir}')`
36-
}}
37-
/>
38-
<Head />
39-
<Banner />
40-
{themeContext.navbar &&
41-
renderComponent(config.navbar.component, {
42-
items: topLevelNavbarItems
43-
})}
44-
<ActiveAnchorProvider>
45-
<MDXProvider disableParentContext components={components}>
46-
{children}
47-
</MDXProvider>
48-
</ActiveAnchorProvider>
49-
{themeContext.footer &&
50-
renderComponent(config.footer.component, { menu: config.hideSidebar })}
51-
</div>
39+
<ThemeProvider
40+
attribute="class"
41+
disableTransitionOnChange
42+
{...themeConfig.nextThemes}
43+
>
44+
{/*
45+
This makes sure that selectors like `[dir=ltr] .nextra-container` work
46+
before hydration as Tailwind expects the `dir` attribute to exist on the
47+
`html` element.
48+
*/}
49+
<div dir={dir}>
50+
<script
51+
dangerouslySetInnerHTML={{
52+
__html: `document.documentElement.setAttribute('dir','${dir}')`
53+
}}
54+
/>
55+
<Head />
56+
<Banner />
57+
{themeContext.navbar &&
58+
renderComponent(themeConfig.navbar.component, {
59+
items: topLevelNavbarItems
60+
})}
61+
<ActiveAnchorProvider>
62+
<MDXProvider disableParentContext components={components}>
63+
{children}
64+
</MDXProvider>
65+
</ActiveAnchorProvider>
66+
{themeContext.footer &&
67+
renderComponent(themeConfig.footer.component, {
68+
menu: config.hideSidebar
69+
})}
70+
</div>
71+
</ThemeProvider>
5272
)
5373
}
5474

5575
export default function Layout({
5676
children,
57-
...context
77+
themeConfig,
78+
pageOpts
5879
}: NextraThemeLayoutProps): ReactElement {
5980
return (
60-
<ConfigProvider value={context}>
61-
<InnerLayout>{children}</InnerLayout>
62-
</ConfigProvider>
81+
<ThemeConfigProvider value={themeConfig}>
82+
<ConfigProvider value={pageOpts}>
83+
<InnerLayout>{children}</InnerLayout>
84+
</ConfigProvider>
85+
</ThemeConfigProvider>
6386
)
6487
}
6588

66-
export { useConfig, PartialDocsThemeConfig as DocsThemeConfig }
89+
export { useThemeConfig, useConfig, PartialDocsThemeConfig as DocsThemeConfig }
6790
export { useTheme } from 'next-themes'
6891
export { Link } from './mdx-components'
6992
export {

Diff for: ‎packages/nextra-theme-docs/src/mdx-components.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from './components'
2424
import type { AnchorProps } from './components/anchor'
2525
import type { DocsThemeConfig } from './constants'
26-
import { useConfig, useSetActiveAnchor } from './contexts'
26+
import { useConfig, useSetActiveAnchor, useThemeConfig } from './contexts'
2727
import { useIntersectionObserver, useSlugs } from './contexts/active-anchor'
2828
import { renderComponent } from './utils'
2929

@@ -200,6 +200,7 @@ const classes = {
200200

201201
function Body({ breadcrumb, navigation, children }: BodyProps): ReactElement {
202202
const config = useConfig()
203+
const themeConfig = useThemeConfig()
203204
const mounted = useMounted()
204205
const themeContext = config.normalizePagesResult.activeThemeContext
205206

@@ -208,15 +209,15 @@ function Body({ breadcrumb, navigation, children }: BodyProps): ReactElement {
208209
}
209210

210211
const date =
211-
themeContext.timestamp && config.gitTimestamp && config.timestamp
212+
themeContext.timestamp && themeConfig.gitTimestamp && config.timestamp
212213
? new Date(config.timestamp)
213214
: null
214215

215216
const gitTimestampEl =
216217
// Because a user's time zone may be different from the server page
217218
mounted && date ? (
218219
<div className="_mt-12 _mb-8 _block _text-xs _text-gray-500 ltr:_text-right rtl:_text-left dark:_text-gray-400">
219-
{renderComponent(config.gitTimestamp, { timestamp: date })}
220+
{renderComponent(themeConfig.gitTimestamp, { timestamp: date })}
220221
</div>
221222
) : (
222223
<div className="_mt-16" />
@@ -230,7 +231,7 @@ function Body({ breadcrumb, navigation, children }: BodyProps): ReactElement {
230231
</>
231232
)
232233

233-
const body = config.main?.({ children: content }) || content
234+
const body = themeConfig.main?.({ children: content }) || content
234235

235236
if (themeContext.layout === 'full') {
236237
return (
@@ -320,6 +321,7 @@ const DEFAULT_COMPONENTS: Components = {
320321
flatDocsDirectories,
321322
activeIndex
322323
} = config.normalizePagesResult
324+
const themeConfig = useThemeConfig()
323325

324326
const tocEl =
325327
activeType === 'page' ||
@@ -334,8 +336,8 @@ const DEFAULT_COMPONENTS: Components = {
334336
className={cn(classes.toc, '_px-4')}
335337
aria-label="table of contents"
336338
>
337-
{renderComponent(config.toc.component, {
338-
toc: config.toc.float ? toc : [],
339+
{renderComponent(themeConfig.toc.component, {
340+
toc: themeConfig.toc.float ? toc : [],
339341
filePath: config.filePath
340342
})}
341343
</nav>

Diff for: ‎packages/nextra-theme-docs/src/types.ts

-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
/* eslint typescript-sort-keys/interface: error */
2-
import type { PageOpts } from 'nextra'
32
import type { ReactNode } from 'react'
4-
import type { DocsThemeConfig } from './constants'
5-
6-
export type Context = {
7-
pageOpts: PageOpts
8-
themeConfig: DocsThemeConfig
9-
}
103

114
export type SearchResult = {
125
children: ReactNode

Diff for: ‎packages/nextra-theme-docs/src/utils/use-git-edit-url.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useConfig } from '../contexts'
1+
import { useThemeConfig } from '../contexts'
22
import { gitUrlParse } from './git-url-parse'
33

44
export function useGitEditUrl(filePath = ''): string {
5-
const config = useConfig()
5+
const config = useThemeConfig()
66
const repo = gitUrlParse(config.docsRepositoryBase || '')
77

88
if (!repo) throw new Error('Invalid `docsRepositoryBase` URL!')

Diff for: ‎pnpm-lock.yaml

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.