boltdocs 2.5.5 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/bin/boltdocs.js +2 -2
  2. package/dist/client/index.cjs +6 -0
  3. package/dist/client/index.d.cts +1560 -0
  4. package/dist/client/index.d.ts +1219 -922
  5. package/dist/client/index.js +6 -1
  6. package/dist/client/theme/neutral.css +428 -0
  7. package/dist/node/cli-entry.cjs +8 -0
  8. package/dist/node/cli-entry.d.cts +2 -0
  9. package/dist/node/cli-entry.d.mts +2 -1
  10. package/dist/node/cli-entry.mjs +7 -5
  11. package/dist/node/index.cjs +6 -0
  12. package/dist/node/index.d.cts +519 -0
  13. package/dist/node/index.d.mts +374 -422
  14. package/dist/node/index.mjs +6 -1
  15. package/dist/node-BgvNl2Ay.mjs +89 -0
  16. package/dist/node-vkbb0MK7.cjs +89 -0
  17. package/dist/package-CR0HF9x3.mjs +6 -0
  18. package/dist/package-Dgmsc_l5.cjs +6 -0
  19. package/dist/search-dialog-3lvKsbVG.js +6 -0
  20. package/dist/search-dialog-DMK5OpgH.cjs +6 -0
  21. package/dist/use-search-C9bxCqfF.js +6 -0
  22. package/dist/use-search-DcfZSunO.cjs +6 -0
  23. package/package.json +26 -25
  24. package/src/client/app/config-context.tsx +38 -5
  25. package/src/client/app/doc-page.tsx +34 -0
  26. package/src/client/app/mdx-component.tsx +2 -3
  27. package/src/client/app/mdx-components-context.tsx +27 -2
  28. package/src/client/app/routes-context.tsx +34 -0
  29. package/src/client/app/scroll-handler.tsx +7 -4
  30. package/src/client/app/theme-context.tsx +71 -67
  31. package/src/client/components/default-layout.tsx +34 -33
  32. package/src/client/components/docs-layout.tsx +1 -2
  33. package/src/client/components/icons-dev.tsx +36 -5
  34. package/src/client/components/mdx/admonition.tsx +11 -27
  35. package/src/client/components/mdx/badge.tsx +1 -1
  36. package/src/client/components/mdx/button.tsx +3 -3
  37. package/src/client/components/mdx/card.tsx +1 -1
  38. package/src/client/components/mdx/code-block.tsx +90 -80
  39. package/src/client/components/mdx/component-preview.tsx +1 -5
  40. package/src/client/components/mdx/component-props.tsx +1 -1
  41. package/src/client/components/mdx/field.tsx +4 -5
  42. package/src/client/components/mdx/file-tree.tsx +6 -3
  43. package/src/client/components/mdx/hooks/use-code-block.ts +2 -2
  44. package/src/client/components/mdx/image.tsx +1 -1
  45. package/src/client/components/mdx/link.tsx +2 -2
  46. package/src/client/components/mdx/list.tsx +1 -1
  47. package/src/client/components/mdx/table.tsx +1 -1
  48. package/src/client/components/mdx/tabs.tsx +1 -1
  49. package/src/client/components/primitives/breadcrumbs.tsx +1 -7
  50. package/src/client/components/primitives/button-group.tsx +1 -1
  51. package/src/client/components/primitives/button.tsx +1 -1
  52. package/src/client/components/primitives/code-block.tsx +113 -0
  53. package/src/client/components/primitives/link.tsx +23 -41
  54. package/src/client/components/primitives/menu.tsx +5 -6
  55. package/src/client/components/primitives/navbar.tsx +6 -18
  56. package/src/client/components/primitives/navigation-menu.tsx +4 -4
  57. package/src/client/components/primitives/on-this-page.tsx +6 -10
  58. package/src/client/components/primitives/page-nav.tsx +4 -9
  59. package/src/client/components/primitives/popover.tsx +1 -1
  60. package/src/client/components/primitives/search-dialog.tsx +3 -6
  61. package/src/client/components/primitives/sidebar.tsx +80 -22
  62. package/src/client/components/primitives/skeleton.tsx +1 -1
  63. package/src/client/components/primitives/tabs.tsx +4 -11
  64. package/src/client/components/primitives/tooltip.tsx +3 -3
  65. package/src/client/components/ui-base/breadcrumbs.tsx +4 -6
  66. package/src/client/components/ui-base/copy-markdown.tsx +2 -7
  67. package/src/client/components/ui-base/github-stars.tsx +2 -2
  68. package/src/client/components/ui-base/head.tsx +58 -51
  69. package/src/client/components/ui-base/loading.tsx +2 -2
  70. package/src/client/components/ui-base/navbar.tsx +12 -14
  71. package/src/client/components/ui-base/not-found.tsx +1 -1
  72. package/src/client/components/ui-base/on-this-page.tsx +6 -6
  73. package/src/client/components/ui-base/page-nav.tsx +4 -8
  74. package/src/client/components/ui-base/search-dialog.tsx +10 -8
  75. package/src/client/components/ui-base/sidebar.tsx +76 -23
  76. package/src/client/components/ui-base/tabs.tsx +9 -8
  77. package/src/client/components/ui-base/theme-toggle.tsx +2 -2
  78. package/src/client/hooks/use-i18n.ts +3 -3
  79. package/src/client/hooks/use-localized-to.ts +1 -1
  80. package/src/client/hooks/use-navbar.ts +8 -6
  81. package/src/client/hooks/use-routes.ts +19 -11
  82. package/src/client/hooks/use-search.ts +1 -1
  83. package/src/client/hooks/use-sidebar.ts +48 -2
  84. package/src/client/hooks/use-tabs.ts +6 -2
  85. package/src/client/hooks/use-version.ts +3 -3
  86. package/src/client/index.ts +22 -22
  87. package/src/client/ssg/boltdocs-shell.tsx +127 -0
  88. package/src/client/ssg/create-routes.tsx +179 -0
  89. package/src/client/ssg/index.ts +3 -0
  90. package/src/client/ssg/mdx-page.tsx +37 -0
  91. package/src/client/store/boltdocs-context.tsx +66 -0
  92. package/src/client/theme/neutral.css +90 -50
  93. package/src/client/types.ts +5 -33
  94. package/src/client/utils/react-to-text.ts +34 -0
  95. package/CHANGELOG.md +0 -98
  96. package/dist/cache-3FOEPC2P.mjs +0 -1
  97. package/dist/chunk-5B5NKOW6.mjs +0 -77
  98. package/dist/chunk-J2PTDWZM.mjs +0 -1
  99. package/dist/chunk-TP5KMRD3.mjs +0 -1
  100. package/dist/chunk-Y4RE5KI7.mjs +0 -1
  101. package/dist/client/index.d.mts +0 -1263
  102. package/dist/client/index.mjs +0 -1
  103. package/dist/client/ssr.d.mts +0 -78
  104. package/dist/client/ssr.d.ts +0 -78
  105. package/dist/client/ssr.js +0 -1
  106. package/dist/client/ssr.mjs +0 -1
  107. package/dist/node/cli-entry.d.ts +0 -1
  108. package/dist/node/cli-entry.js +0 -82
  109. package/dist/node/index.d.ts +0 -567
  110. package/dist/node/index.js +0 -77
  111. package/dist/package-QFIAETHR.mjs +0 -1
  112. package/dist/search-dialog-O6VLVSOA.mjs +0 -1
  113. package/src/client/app/index.tsx +0 -345
  114. package/src/client/app/mdx-page.tsx +0 -15
  115. package/src/client/app/preload.tsx +0 -66
  116. package/src/client/app/router.tsx +0 -30
  117. package/src/client/integrations/codesandbox.ts +0 -179
  118. package/src/client/integrations/index.ts +0 -1
  119. package/src/client/ssr.tsx +0 -65
  120. package/src/client/store/use-boltdocs-store.ts +0 -44
  121. package/src/node/cache.ts +0 -408
  122. package/src/node/cli/build.ts +0 -53
  123. package/src/node/cli/dev.ts +0 -22
  124. package/src/node/cli/doctor.ts +0 -243
  125. package/src/node/cli/index.ts +0 -9
  126. package/src/node/cli/ui.ts +0 -54
  127. package/src/node/cli-entry.ts +0 -24
  128. package/src/node/config.ts +0 -382
  129. package/src/node/errors.ts +0 -44
  130. package/src/node/index.ts +0 -84
  131. package/src/node/mdx/cache.ts +0 -12
  132. package/src/node/mdx/highlighter.ts +0 -47
  133. package/src/node/mdx/index.ts +0 -122
  134. package/src/node/mdx/rehype-shiki.ts +0 -62
  135. package/src/node/mdx/remark-code-meta.ts +0 -35
  136. package/src/node/mdx/remark-shiki.ts +0 -61
  137. package/src/node/plugin/entry.ts +0 -87
  138. package/src/node/plugin/html.ts +0 -99
  139. package/src/node/plugin/index.ts +0 -478
  140. package/src/node/plugin/types.ts +0 -9
  141. package/src/node/plugins/index.ts +0 -17
  142. package/src/node/plugins/plugin-errors.ts +0 -62
  143. package/src/node/plugins/plugin-lifecycle.ts +0 -117
  144. package/src/node/plugins/plugin-sandbox.ts +0 -59
  145. package/src/node/plugins/plugin-store.ts +0 -54
  146. package/src/node/plugins/plugin-types.ts +0 -107
  147. package/src/node/plugins/plugin-validator.ts +0 -105
  148. package/src/node/routes/cache.ts +0 -28
  149. package/src/node/routes/index.ts +0 -293
  150. package/src/node/routes/parser.ts +0 -262
  151. package/src/node/routes/sorter.ts +0 -42
  152. package/src/node/routes/types.ts +0 -61
  153. package/src/node/schema/config.ts +0 -195
  154. package/src/node/schema/frontmatter.ts +0 -17
  155. package/src/node/search/index.ts +0 -55
  156. package/src/node/security/constants/index.ts +0 -10
  157. package/src/node/security/csp.ts +0 -31
  158. package/src/node/security/headers.ts +0 -27
  159. package/src/node/ssg/index.ts +0 -205
  160. package/src/node/ssg/meta.ts +0 -33
  161. package/src/node/ssg/options.ts +0 -15
  162. package/src/node/ssg/robots.ts +0 -53
  163. package/src/node/ssg/sitemap.ts +0 -55
  164. package/src/node/utils.ts +0 -349
  165. package/tsconfig.json +0 -26
  166. package/tsup.config.ts +0 -56
@@ -1,69 +1,76 @@
1
- import { useEffect } from 'react'
2
1
  import { useLocation } from 'react-router-dom'
2
+ import { Helmet } from 'react-helmet-async'
3
+ import { useConfig } from '../../app/config-context'
3
4
 
4
5
  interface HeadProps {
5
6
  siteTitle: string
6
7
  siteDescription?: string
7
- routes: Array<{ path: string; title: string; description?: string }>
8
+ routes: Array<{ path: string; title: string; description?: string; seo?: Record<string, any> }>
8
9
  }
9
10
 
10
11
  export function Head({ siteTitle, siteDescription, routes }: HeadProps) {
11
12
  const location = useLocation()
13
+ const config = useConfig()
12
14
 
13
- useEffect(() => {
14
- // Find the current route's metadata
15
- const currentRoute = routes.find((r) => r.path === location.pathname)
16
- const pageTitle = currentRoute?.title
17
- const pageDescription = currentRoute?.description || siteDescription || ''
15
+ // Find the current route's metadata
16
+ const currentRoute = routes?.find?.((r) => r.path === location.pathname)
17
+ const pageTitle = currentRoute?.title
18
+ const pageDescription = currentRoute?.description || siteDescription || ''
18
19
 
19
- // Update document title
20
- document.title = pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle
20
+ const finalTitle = pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle
21
21
 
22
- // Update or create meta description
23
- let metaDesc = document.querySelector(
24
- 'meta[name="description"]',
25
- ) as HTMLMetaElement | null
26
- if (!metaDesc) {
27
- metaDesc = document.createElement('meta')
28
- metaDesc.name = 'description'
29
- document.head.appendChild(metaDesc)
30
- }
31
- metaDesc.content = pageDescription
22
+ const seo = currentRoute?.seo || {}
32
23
 
33
- // Update OG tags
34
- setMetaTag('property', 'og:title', document.title)
35
- setMetaTag('property', 'og:description', pageDescription)
36
- setMetaTag('property', 'og:type', 'article')
37
- setMetaTag('property', 'og:url', window.location.href)
24
+ // Merge custom global metatags
25
+ const globalMetatags = config?.seo?.metatags || {}
38
26
 
39
- // Twitter card
40
- setMetaTag('name', 'twitter:card', 'summary')
41
- setMetaTag('name', 'twitter:title', document.title)
42
- setMetaTag('name', 'twitter:description', pageDescription)
27
+ // Calculate specific ones
28
+ const defaultOgImage = config?.seo?.thumbnails?.background
29
+ const ogImage = seo['og:image'] || defaultOgImage
43
30
 
44
- // Canonical URL
45
- let canonical = document.querySelector(
46
- 'link[rel="canonical"]',
47
- ) as HTMLLinkElement | null
48
- if (!canonical) {
49
- canonical = document.createElement('link')
50
- canonical.rel = 'canonical'
51
- document.head.appendChild(canonical)
52
- }
53
- canonical.href = window.location.origin + location.pathname
54
- }, [location.pathname, siteTitle, siteDescription, routes])
31
+ return (
32
+ // @ts-ignore
33
+ <Helmet>
34
+ <title>{finalTitle}</title>
35
+ <meta name="description" content={pageDescription} />
55
36
 
56
- return null // This component only manages <head>, no visual output
57
- }
37
+ {/* Default OG Tags */}
38
+ <meta property="og:title" content={finalTitle} />
39
+ <meta property="og:description" content={pageDescription} />
40
+ <meta property="og:type" content="article" />
41
+ {/* Canonical URL for both <link> and og:url */}
42
+ {typeof window !== 'undefined' && <meta property="og:url" content={window.location.href} />}
43
+ {typeof window !== 'undefined' && <link rel="canonical" href={window.location.origin + location.pathname} />}
44
+
45
+ {/* Default Twitter Card */}
46
+ <meta name="twitter:card" content="summary" />
47
+ <meta name="twitter:title" content={finalTitle} />
48
+ <meta name="twitter:description" content={pageDescription} />
49
+ {ogImage && <meta name="twitter:image" content={ogImage} />}
50
+ {ogImage && <meta property="og:image" content={ogImage} />}
51
+
52
+ {/* Generator */}
53
+ <meta name="generator" content="Boltdocs" />
54
+
55
+ {/* User-defined global metatags */}
56
+ {Object.entries(globalMetatags).map(([key, value]) => {
57
+ const isProperty = key.startsWith('og:') || key.startsWith('music:') || key.startsWith('video:') || key.startsWith('article:') || key.startsWith('book:') || key.startsWith('profile:')
58
+ return isProperty
59
+ ? <meta key={key} property={key} content={value as string} />
60
+ : <meta key={key} name={key} content={value as string} />
61
+ })}
62
+
63
+ {/* Page granular SEO tags (override global) */}
64
+ {Object.entries(seo).map(([key, value]) => {
65
+ if (key === 'noindex' && value === true) return <meta key="noindex" name="robots" content="noindex" />
66
+ if (key === 'robots') return <meta key="robots" name="robots" content={value as string} />
67
+ if (key === 'canonical') return <link key="canonical" rel="canonical" href={value as string} />
58
68
 
59
- function setMetaTag(attr: 'name' | 'property', key: string, content: string) {
60
- let tag = document.querySelector(
61
- `meta[${attr}="${key}"]`,
62
- ) as HTMLMetaElement | null
63
- if (!tag) {
64
- tag = document.createElement('meta')
65
- tag.setAttribute(attr, key)
66
- document.head.appendChild(tag)
67
- }
68
- tag.content = content
69
+ const isProperty = key.startsWith('og:') || key.startsWith('music:') || key.startsWith('video:') || key.startsWith('article:') || key.startsWith('book:') || key.startsWith('profile:')
70
+ return isProperty
71
+ ? <meta key={key} property={key} content={value as string} />
72
+ : <meta key={key} name={key} content={value as string} />
73
+ })}
74
+ </Helmet>
75
+ )
69
76
  }
@@ -1,5 +1,5 @@
1
- import { cn } from '@client/utils/cn'
2
- import { Skeleton } from '@primitives/skeleton'
1
+ import { cn } from '../../utils/cn'
2
+ import { Skeleton } from '../primitives/skeleton'
3
3
 
4
4
  /**
5
5
  * A premium loading component that only skeletons the markdown content area.
@@ -1,19 +1,19 @@
1
1
  import { Suspense, lazy } from 'react'
2
- import { useNavbar } from '@hooks/use-navbar'
3
- import { useVersion } from '@hooks/use-version'
4
- import { useI18n } from '@hooks/use-i18n'
5
- import { useRoutes } from '@hooks/use-routes'
6
- import NavbarPrimitive from '@components/primitives/navbar'
2
+ import { useNavbar } from '../../hooks/use-navbar'
3
+ import { useVersion } from '../../hooks/use-version'
4
+ import { useI18n } from '../../hooks/use-i18n'
5
+ import { useRoutes } from '../../hooks/use-routes'
6
+ import NavbarPrimitive from '../primitives/navbar'
7
7
  import { ThemeToggle } from './theme-toggle'
8
8
  import { GithubStars } from './github-stars'
9
9
  import { Tabs } from './tabs'
10
10
  import { useLocation } from 'react-router-dom'
11
- import type { BoltdocsSocialLink } from '@node/config'
12
- import { Menu } from '@components/primitives/menu'
13
- import { Button } from '@components/primitives/button'
11
+ import type { BoltdocsSocialLink } from '../../../shared/types'
12
+ import { Menu } from '../primitives/menu'
13
+ import { Button } from '../primitives/button'
14
14
  import { ChevronDown, Languages } from 'lucide-react'
15
- import { useLocalizedTo } from '@hooks/use-localized-to'
16
- import type { NavbarLink as NavbarLinkType } from '@client/types'
15
+ import { useLocalizedTo } from '../../hooks/use-localized-to'
16
+ import type { NavbarLink as NavbarLinkType } from '../../types'
17
17
 
18
18
  const SearchDialog = lazy(() =>
19
19
  import('./search-dialog').then((m) => ({
@@ -57,9 +57,7 @@ export function Navbar() {
57
57
  <NavbarPrimitive.Right>
58
58
  <NavbarPrimitive.Links>
59
59
  {links.map((link) => (
60
- <>
61
- <NavbarLinkItem key={link.href} link={link} />
62
- </>
60
+ <NavbarLinkItem key={link.href} link={link} />
63
61
  ))}
64
62
  </NavbarPrimitive.Links>
65
63
  {config.i18n && currentLocale && <NavbarI18n />}
@@ -147,7 +145,7 @@ function NavbarI18n() {
147
145
  >
148
146
  <div className="flex items-center gap-1.5">
149
147
  <Languages className="w-3.5 h-3.5 text-primary-500" />
150
- <span className="font-bold text-[0.75rem] tracking-wider uppercase opacity-90">
148
+ <span className="font-bold text-[0.75rem] uppercase opacity-90">
151
149
  {currentLocale || 'en'}
152
150
  </span>
153
151
  </div>
@@ -1,5 +1,5 @@
1
1
  import { ArrowLeft } from 'lucide-react'
2
- import { Link } from '@components/primitives/link'
2
+ import { Link } from '../primitives/link'
3
3
 
4
4
  export function NotFound() {
5
5
  return (
@@ -3,10 +3,10 @@ import {
3
3
  AnchorProvider,
4
4
  ScrollProvider,
5
5
  useActiveAnchor,
6
- } from '@components/primitives/on-this-page'
7
- import React, { useRef, useEffect, useState, useCallback } from 'react'
8
- import { useOnThisPage } from '@hooks/use-onthispage'
9
- import type { OnThisPageProps } from '@client/types'
6
+ } from '../primitives/on-this-page'
7
+ import { useRef, useEffect, useState, useCallback, useMemo } from 'react'
8
+ import { useOnThisPage } from '../../hooks/use-onthispage'
9
+ import type { OnThisPageProps } from '../../types'
10
10
  import { Pencil, CircleHelp, TextAlignStart } from 'lucide-react'
11
11
 
12
12
  export function OnThisPage({
@@ -17,7 +17,7 @@ export function OnThisPage({
17
17
  }: OnThisPageProps) {
18
18
  const { headings } = useOnThisPage(rawHeadings)
19
19
 
20
- const toc = React.useMemo(
20
+ const toc = useMemo(
21
21
  () =>
22
22
  headings.map((h) => ({ title: h.text, url: `#${h.id}`, depth: h.level })),
23
23
  [headings],
@@ -115,7 +115,7 @@ function OnThisPageInner({
115
115
 
116
116
  {(editLink || communityHelp) && (
117
117
  <div className="mt-8 pt-8 border-t border-border-subtle space-y-4">
118
- <p className="text-xs font-bold uppercase tracking-wider text-text-main">
118
+ <p className="text-xs font-bold uppercase text-text-main">
119
119
  Need help?
120
120
  </p>
121
121
  <ul className="space-y-3">
@@ -1,5 +1,5 @@
1
- import { usePageNav } from '@hooks/use-page-nav'
2
- import { PageNav as PageNavPrimitive } from '@components/primitives/page-nav'
1
+ import { usePageNav } from '../../hooks/use-page-nav'
2
+ import { PageNav as PageNavPrimitive } from '../primitives/page-nav'
3
3
 
4
4
  /**
5
5
  * Component to display the previous and next page navigation buttons.
@@ -14,9 +14,7 @@ export function PageNav() {
14
14
  <PageNavPrimitive.Root className="animate-in fade-in slide-in-from-bottom-4 duration-700">
15
15
  {prevPage ? (
16
16
  <PageNavPrimitive.Link to={prevPage.path} direction="prev">
17
- <PageNavPrimitive.Title>
18
- Previous
19
- </PageNavPrimitive.Title>
17
+ <PageNavPrimitive.Title>Previous</PageNavPrimitive.Title>
20
18
  <PageNavPrimitive.Description>
21
19
  {prevPage.title}
22
20
  </PageNavPrimitive.Description>
@@ -27,9 +25,7 @@ export function PageNav() {
27
25
 
28
26
  {nextPage && (
29
27
  <PageNavPrimitive.Link to={nextPage.path} direction="next">
30
- <PageNavPrimitive.Title>
31
- Next
32
- </PageNavPrimitive.Title>
28
+ <PageNavPrimitive.Title>Next</PageNavPrimitive.Title>
33
29
  <PageNavPrimitive.Description>
34
30
  {nextPage.title}
35
31
  </PageNavPrimitive.Description>
@@ -1,11 +1,9 @@
1
1
  import { useEffect, useCallback } from 'react'
2
- import { useSearch } from '@hooks/use-search'
3
- import {
4
- SearchDialog as SearchDialogPrimitive,
5
- } from '@components/primitives/search-dialog'
6
- import Navbar from '@components/primitives/navbar'
2
+ import { useSearch } from '../../hooks/use-search'
3
+ import { SearchDialog as SearchDialogPrimitive } from '../primitives/search-dialog'
4
+ import Navbar from '../primitives/navbar'
7
5
  import { useNavigate } from 'react-router-dom'
8
- import type { ComponentRoute } from '@client/types'
6
+ import type { ComponentRoute } from '../../types'
9
7
  interface SearchResult {
10
8
  id: string
11
9
  title: string
@@ -73,8 +71,12 @@ export function SearchDialog({ routes }: { routes: ComponentRoute[] }) {
73
71
  >
74
72
  <SearchDialogPrimitive.Item.Icon isHeading={item.isHeading} />
75
73
  <div className="flex flex-col justify-center gap-0.5">
76
- <SearchDialogPrimitive.Item.Title>{item.title}</SearchDialogPrimitive.Item.Title>
77
- <SearchDialogPrimitive.Item.Bio>{item.bio}</SearchDialogPrimitive.Item.Bio>
74
+ <SearchDialogPrimitive.Item.Title>
75
+ {item.title}
76
+ </SearchDialogPrimitive.Item.Title>
77
+ <SearchDialogPrimitive.Item.Bio>
78
+ {item.bio}
79
+ </SearchDialogPrimitive.Item.Bio>
78
80
  </div>
79
81
  </SearchDialogPrimitive.Item>
80
82
  )}
@@ -1,52 +1,105 @@
1
1
  import { useState, useEffect, useMemo } from 'react'
2
- import { useSidebar } from '@hooks/use-sidebar'
3
- import { Sidebar as SidebarPrimitive } from '@components/primitives/sidebar'
2
+ import { useSidebar } from '../../hooks/use-sidebar'
3
+ import { Sidebar as SidebarPrimitive } from '../primitives/sidebar'
4
4
  import { PoweredBy } from './powered-by'
5
5
  import * as LucideIcons from 'lucide-react'
6
- import type { ComponentRoute } from '@client/types'
7
- import type { BoltdocsConfig } from '@node/config'
6
+ import virtualIcons from 'virtual:boltdocs-icons'
7
+ import type { ComponentRoute } from '../../types'
8
+ import type { BoltdocsConfig } from '../../../shared/types'
8
9
 
9
10
  function getIcon(iconName?: string): React.ElementType | undefined {
10
11
  if (!iconName) return undefined
11
- const icons = LucideIcons as unknown as Record<string, React.ElementType>
12
- const IconComponent = icons[iconName]
12
+ const icons = { ...LucideIcons, ...virtualIcons } as unknown as Record<
13
+ string,
14
+ React.ElementType
15
+ >
16
+ const IconComponent = icons[iconName] || icons[iconName + 'Icon']
13
17
  return IconComponent || undefined
14
18
  }
15
19
 
16
- function CollapsibleSidebarGroup({
17
- group,
20
+ function SidebarSubRouteGroup({
21
+ route,
18
22
  activePath,
19
23
  getIcon,
20
24
  }: {
21
- group: {
22
- slug: string
23
- title: string
24
- routes: ComponentRoute[]
25
- icon?: string
26
- }
25
+ route: ComponentRoute
27
26
  activePath: string
28
27
  getIcon: (iconName?: string) => React.ElementType | undefined
29
28
  }) {
30
- const hasActiveRoute = useMemo(
31
- () => group.routes.some((r) => r.path === activePath),
32
- [group.routes, activePath],
29
+ const isCurrent =
30
+ activePath ===
31
+ (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
32
+
33
+ const hasActiveSubRoute = useMemo(
34
+ () => route.subRoutes?.some((r) => r.path === activePath),
35
+ [route.subRoutes, activePath],
33
36
  )
34
37
 
35
- const [isOpen, setIsOpen] = useState(true)
38
+ const [isOpen, setIsOpen] = useState(hasActiveSubRoute || isCurrent)
36
39
 
37
40
  useEffect(() => {
38
- if (hasActiveRoute) {
41
+ if (hasActiveSubRoute || isCurrent) {
39
42
  setIsOpen(true)
40
43
  }
41
- }, [hasActiveRoute])
44
+ }, [hasActiveSubRoute, isCurrent])
42
45
 
43
46
  return (
44
- <SidebarPrimitive.Group
45
- title={group.title}
47
+ <SidebarPrimitive.SubGroup
48
+ label={route.title}
49
+ href={route.path}
50
+ active={isCurrent}
51
+ icon={getIcon(route.icon)}
52
+ badge={route.badge}
46
53
  isOpen={isOpen}
47
54
  onToggle={() => setIsOpen(!isOpen)}
48
55
  >
56
+ {route.subRoutes?.map((subRoute: ComponentRoute) => {
57
+ const isSubCurrent =
58
+ activePath ===
59
+ (subRoute.path.endsWith('/') ? subRoute.path.slice(0, -1) : subRoute.path)
60
+ return (
61
+ <SidebarPrimitive.Link
62
+ key={subRoute.path}
63
+ label={subRoute.title}
64
+ href={subRoute.path}
65
+ active={isSubCurrent}
66
+ icon={getIcon(subRoute.icon)}
67
+ badge={subRoute.badge}
68
+ />
69
+ )
70
+ })}
71
+ </SidebarPrimitive.SubGroup>
72
+ )
73
+ }
74
+
75
+ function SidebarGroupSection({
76
+ group,
77
+ activePath,
78
+ getIcon,
79
+ }: {
80
+ group: {
81
+ slug: string
82
+ title: string
83
+ routes: ComponentRoute[]
84
+ icon?: string
85
+ }
86
+ activePath: string
87
+ getIcon: (iconName?: string) => React.ElementType | undefined
88
+ }) {
89
+ return (
90
+ <SidebarPrimitive.Group title={group.title} icon={getIcon(group.icon)}>
49
91
  {group.routes.map((route: ComponentRoute) => {
92
+ if (route.subRoutes && route.subRoutes.length > 0) {
93
+ return (
94
+ <SidebarSubRouteGroup
95
+ key={route.path}
96
+ route={route}
97
+ activePath={activePath}
98
+ getIcon={getIcon}
99
+ />
100
+ )
101
+ }
102
+
50
103
  const isCurrent =
51
104
  activePath ===
52
105
  (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
@@ -98,7 +151,7 @@ export function Sidebar({
98
151
  )}
99
152
 
100
153
  {groups.map((group) => (
101
- <CollapsibleSidebarGroup
154
+ <SidebarGroupSection
102
155
  key={group.slug}
103
156
  group={group}
104
157
  activePath={activePath}
@@ -1,10 +1,10 @@
1
- import { useTabs as useTabsHook } from '@hooks/use-tabs'
2
- import { Tabs as T } from '@components/primitives/tabs'
3
- import { Link } from '@components/primitives/link'
4
- import type { BoltdocsTab, ComponentRoute } from '@client/types'
1
+ import { useTabs as useTabsHook } from '../../hooks/use-tabs'
2
+ import { Tabs as T } from '../primitives/tabs'
3
+ import { Link } from '../primitives/link'
4
+ import type { BoltdocsTab, ComponentRoute } from '../../types'
5
5
  import * as Icons from 'lucide-react'
6
- import { getTranslated } from '@client/utils/i18n'
7
- import { useRoutes } from '@hooks/use-routes'
6
+ import { getTranslated } from '../../utils/i18n'
7
+ import { useRoutes } from '../../hooks/use-routes'
8
8
 
9
9
  export function Tabs({
10
10
  tabs,
@@ -50,10 +50,11 @@ export function Tabs({
50
50
  ref={(el: HTMLAnchorElement | null) => {
51
51
  tabRefs.current[index] = el
52
52
  }}
53
- className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors outline-none ${isActive
53
+ className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors outline-none ${
54
+ isActive
54
55
  ? 'text-primary-500'
55
56
  : 'text-text-muted hover:text-text-main'
56
- }`}
57
+ }`}
57
58
  >
58
59
  {renderTabIcon(tab.icon)}
59
60
  <span>{getTranslated(tab.text, currentLocale)}</span>
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { Sun, Moon, Monitor } from 'lucide-react'
3
- import { useTheme } from '@client/app/theme-context'
3
+ import { useTheme } from '../../app/theme-context'
4
4
  import { Button } from 'react-aria-components'
5
- import { Menu } from '@components/primitives/menu'
5
+ import { Menu } from '../primitives/menu'
6
6
 
7
7
  export function ThemeToggle() {
8
8
  const { theme, setTheme } = useTheme()
@@ -1,7 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom'
2
- import { getBaseFilePath } from '@client/utils/get-base-file-path'
2
+ import { getBaseFilePath } from '../utils/get-base-file-path'
3
3
  import { useRoutes } from './use-routes'
4
- import { useBoltdocsStore } from '../store/use-boltdocs-store'
4
+ import { useBoltdocsContext } from '../store/boltdocs-context'
5
5
 
6
6
  export interface LocaleOption {
7
7
  key: string
@@ -25,7 +25,7 @@ export function useI18n(): UseI18nReturn {
25
25
  const routeContext = useRoutes()
26
26
  const { allRoutes, currentRoute, currentLocale, config } = routeContext
27
27
  const i18n = config.i18n
28
- const setLocale = useBoltdocsStore((s) => s.setLocale)
28
+ const { setLocale } = useBoltdocsContext()
29
29
 
30
30
  const handleLocaleChange = (locale: string) => {
31
31
  if (!i18n || locale === currentLocale) return
@@ -1,4 +1,4 @@
1
- import { useConfig } from '@client/app/config-context'
1
+ import { useConfig } from '../app/config-context'
2
2
  import type { LinkProps as RouterLinkProps } from 'react-router-dom'
3
3
  import { useRoutes } from './use-routes'
4
4
 
@@ -1,13 +1,13 @@
1
1
  import { useLocation } from 'react-router-dom'
2
- import { useConfig } from '@client/app/config-context'
3
- import { useTheme } from '@client/app/theme-context'
4
- import type { NavbarLink } from '@client/types'
5
- import { getTranslated } from '@client/utils/i18n'
2
+ import { useConfig } from '../app/config-context'
3
+ import { useTheme } from '../app/theme-context'
4
+ import type { NavbarLink } from '../types'
5
+ import { getTranslated } from '../utils/i18n'
6
6
  import { useRoutes } from './use-routes'
7
7
 
8
8
  export function useNavbar() {
9
9
  const config = useConfig()
10
- const { theme } = useTheme()
10
+ const { theme, resolvedTheme } = useTheme()
11
11
  const location = useLocation()
12
12
  const { currentLocale } = useRoutes()
13
13
 
@@ -67,11 +67,13 @@ export function useNavbar() {
67
67
  })
68
68
 
69
69
  const logo = themeConfig.logo
70
+ // Use resolvedTheme so 'system' correctly maps to 'dark' or 'light'
71
+ // based on the OS preference, instead of always falling back to 'light'.
70
72
  const logoSrc = !logo
71
73
  ? null
72
74
  : typeof logo === 'string'
73
75
  ? logo
74
- : theme === 'dark'
76
+ : resolvedTheme === 'dark'
75
77
  ? (logo as any).dark
76
78
  : (logo as any).light
77
79
 
@@ -1,7 +1,7 @@
1
1
  import { useLocation } from 'react-router-dom'
2
- import { useConfig } from '@client/app/config-context'
3
- import { usePreload } from '@client/app/preload'
4
- import { useBoltdocsStore } from '../store/use-boltdocs-store'
2
+ import { useConfig } from '../app/config-context'
3
+ import { useRoutesContext } from '../app/routes-context'
4
+ import { useBoltdocsContext } from '../store/boltdocs-context'
5
5
 
6
6
  /**
7
7
  * Hook to access the framework's routing state.
@@ -9,17 +9,25 @@ import { useBoltdocsStore } from '../store/use-boltdocs-store'
9
9
  * version and locale.
10
10
  */
11
11
  export function useRoutes() {
12
- const { routes: allRoutes } = usePreload()
12
+ const { routes: allRoutes } = useRoutesContext()
13
13
  const config = useConfig()
14
14
  const location = useLocation()
15
15
 
16
16
  // Use Zustand store for active state
17
- const currentLocaleStore = useBoltdocsStore((s) => s.currentLocale)
18
- const currentVersionStore = useBoltdocsStore((s) => s.currentVersion)
19
- const hasHydrated = useBoltdocsStore((s) => s.hasHydrated)
17
+ const {
18
+ hasHydrated,
19
+ currentLocale: currentLocaleStore,
20
+ currentVersion: currentVersionStore,
21
+ } = useBoltdocsContext()
22
+
23
+ const normalize = (p: string) =>
24
+ p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
25
+ const currentPath = normalize(location.pathname)
20
26
 
21
27
  // Find the current route matching the pathname
22
- const currentRoute = allRoutes.find((r) => r.path === location.pathname)
28
+ const currentRoute = allRoutes?.find?.(
29
+ (r) => normalize(r.path) === currentPath,
30
+ )
23
31
 
24
32
  // Derive current locale and version
25
33
  // Priority: URL (currentRoute) > Zustand Store (Persistence) > Config Default
@@ -36,7 +44,7 @@ export function useRoutes() {
36
44
  : undefined
37
45
 
38
46
  // Filter routes to those matching the current version and locale
39
- const routes = allRoutes.filter((r) => {
47
+ const routes = allRoutes?.filter?.((r) => {
40
48
  const localeMatch = config.i18n
41
49
  ? (r.locale || config.i18n.defaultLocale) === currentLocale
42
50
  : true
@@ -53,7 +61,7 @@ export function useRoutes() {
53
61
  const isCurrentRoutePrefixed = !!currentRoute?.locale
54
62
  const isRoutePrefixed = !!r.locale
55
63
 
56
- const hasAlternate = allRoutes.some(
64
+ const hasAlternate = allRoutes?.some?.(
57
65
  (alt) =>
58
66
  alt !== r &&
59
67
  alt.filePath === r.filePath &&
@@ -78,7 +86,7 @@ export function useRoutes() {
78
86
  config.i18n?.locales[currentLocale as string] ||
79
87
  currentLocale
80
88
 
81
- const currentVersionConfig = config.versions?.versions.find(
89
+ const currentVersionConfig = config.versions?.versions?.find?.(
82
90
  (v) => v.path === currentVersion,
83
91
  )
84
92
  const currentVersionLabel = currentVersionConfig?.label || currentVersion
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo, useEffect } from 'react'
2
2
  import { Index } from 'flexsearch'
3
3
  import { useRoutes } from './use-routes'
4
- import type { ComponentRoute } from '@client/types'
4
+ import type { ComponentRoute } from '../types'
5
5
  // @ts-ignore
6
6
  import searchData from 'virtual:boltdocs-search'
7
7