boltdocs 2.1.1 → 2.3.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 (133) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/boltdocs.js +2 -2
  3. package/dist/base-ui/index.d.mts +25 -0
  4. package/dist/base-ui/index.d.ts +25 -0
  5. package/dist/base-ui/index.js +1 -0
  6. package/dist/base-ui/index.mjs +1 -0
  7. package/dist/{cache-Q4T6VAUL.mjs → cache-P6WK424C.mjs} +1 -1
  8. package/dist/chunk-22NXDNP4.mjs +74 -0
  9. package/dist/chunk-2HUVMMJU.mjs +1 -0
  10. package/dist/chunk-2Z5T6EAU.mjs +1 -0
  11. package/dist/chunk-CRZGOE32.mjs +1 -0
  12. package/dist/chunk-HA6543SL.mjs +1 -0
  13. package/dist/chunk-JD3RSDE4.mjs +1 -0
  14. package/dist/chunk-JZXLCA2E.mjs +1 -0
  15. package/dist/chunk-NBCYHLAA.mjs +1 -0
  16. package/dist/chunk-RPUERTVC.mjs +1 -0
  17. package/dist/chunk-T3W44KWY.mjs +1 -0
  18. package/dist/chunk-URTD6E6S.mjs +1 -0
  19. package/dist/chunk-W2NB4T6V.mjs +1 -0
  20. package/dist/chunk-Y4RRHPXC.mjs +1 -0
  21. package/dist/client/index.d.mts +13 -115
  22. package/dist/client/index.d.ts +13 -115
  23. package/dist/client/index.js +1 -1
  24. package/dist/client/index.mjs +1 -1
  25. package/dist/client/ssr.js +1 -1
  26. package/dist/client/ssr.mjs +1 -1
  27. package/dist/client/types.d.mts +3 -0
  28. package/dist/client/types.d.ts +3 -0
  29. package/dist/client/types.js +1 -0
  30. package/dist/client/types.mjs +0 -0
  31. package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
  32. package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
  33. package/dist/{client/hooks → hooks}/index.d.mts +16 -11
  34. package/dist/{client/hooks → hooks}/index.d.ts +16 -11
  35. package/dist/hooks/index.js +1 -0
  36. package/dist/hooks/index.mjs +1 -0
  37. package/dist/integrations/index.d.mts +48 -0
  38. package/dist/integrations/index.d.ts +48 -0
  39. package/dist/integrations/index.js +1 -0
  40. package/dist/integrations/index.mjs +1 -0
  41. package/dist/link-DfBwCeZc.d.mts +68 -0
  42. package/dist/link-DfBwCeZc.d.ts +68 -0
  43. package/dist/loading-B7X5Wchs.d.ts +66 -0
  44. package/dist/loading-WuaQbsKb.d.mts +66 -0
  45. package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
  46. package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
  47. package/dist/mdx/index.js +1 -0
  48. package/dist/mdx/index.mjs +1 -0
  49. package/dist/node/cli-entry.js +31 -27
  50. package/dist/node/cli-entry.mjs +5 -1
  51. package/dist/node/index.d.mts +44 -14
  52. package/dist/node/index.d.ts +44 -14
  53. package/dist/node/index.js +24 -24
  54. package/dist/node/index.mjs +1 -1
  55. package/dist/primitives/index.d.mts +301 -0
  56. package/dist/primitives/index.d.ts +301 -0
  57. package/dist/primitives/index.js +1 -0
  58. package/dist/primitives/index.mjs +1 -0
  59. package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
  60. package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
  61. package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
  62. package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
  63. package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
  64. package/package.json +35 -8
  65. package/src/client/app/index.tsx +155 -35
  66. package/src/client/app/mdx-component.tsx +7 -3
  67. package/src/client/app/theme-context.tsx +47 -23
  68. package/src/client/components/default-layout.tsx +16 -6
  69. package/src/client/components/primitives/breadcrumbs.tsx +1 -1
  70. package/src/client/components/primitives/navbar.tsx +8 -5
  71. package/src/client/components/primitives/search-dialog.tsx +15 -6
  72. package/src/client/components/primitives/sidebar.tsx +3 -2
  73. package/src/client/components/primitives/skeleton.tsx +26 -0
  74. package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
  75. package/src/client/components/ui-base/index.ts +17 -0
  76. package/src/client/components/ui-base/loading.tsx +43 -73
  77. package/src/client/components/ui-base/navbar.tsx +74 -39
  78. package/src/client/components/ui-base/page-nav.tsx +2 -1
  79. package/src/client/components/ui-base/powered-by.tsx +11 -5
  80. package/src/client/components/ui-base/search-dialog.tsx +16 -5
  81. package/src/client/components/ui-base/sidebar.tsx +33 -22
  82. package/src/client/components/ui-base/tabs.tsx +4 -1
  83. package/src/client/components/ui-base/theme-toggle.tsx +35 -15
  84. package/src/client/hooks/use-i18n.ts +38 -7
  85. package/src/client/hooks/use-localized-to.ts +51 -73
  86. package/src/client/hooks/use-navbar.ts +10 -3
  87. package/src/client/hooks/use-page-nav.ts +27 -6
  88. package/src/client/hooks/use-routes.ts +62 -17
  89. package/src/client/hooks/use-search.ts +84 -46
  90. package/src/client/hooks/use-sidebar.ts +6 -2
  91. package/src/client/hooks/use-version.ts +5 -0
  92. package/src/client/integrations/index.ts +1 -0
  93. package/src/client/store/use-boltdocs-store.ts +44 -0
  94. package/src/client/theme/neutral.css +29 -0
  95. package/src/client/types.ts +4 -2
  96. package/src/client/utils/i18n.ts +23 -0
  97. package/src/node/{cli.ts → cli/build.ts} +17 -23
  98. package/src/node/cli/dev.ts +22 -0
  99. package/src/node/cli/doctor.ts +243 -0
  100. package/src/node/cli/index.ts +9 -0
  101. package/src/node/cli/ui.ts +54 -0
  102. package/src/node/cli-entry.ts +16 -16
  103. package/src/node/config.ts +54 -17
  104. package/src/node/index.ts +1 -1
  105. package/src/node/mdx/cache.ts +12 -0
  106. package/src/node/mdx/highlighter.ts +47 -0
  107. package/src/node/mdx/index.ts +114 -0
  108. package/src/node/mdx/rehype-shiki.ts +53 -0
  109. package/src/node/mdx/remark-shiki.ts +61 -0
  110. package/src/node/plugin/entry.ts +1 -1
  111. package/src/node/plugin/html.ts +8 -4
  112. package/src/node/plugin/index.ts +135 -72
  113. package/src/node/routes/index.ts +34 -13
  114. package/src/node/routes/parser.ts +13 -5
  115. package/src/node/search/index.ts +55 -0
  116. package/src/node/ssg/index.ts +15 -7
  117. package/src/node/ssg/robots.ts +7 -4
  118. package/src/node/utils.ts +32 -2
  119. package/tsup.config.ts +7 -2
  120. package/dist/chunk-52MVMZWS.mjs +0 -1
  121. package/dist/chunk-BVWWKXJH.mjs +0 -1
  122. package/dist/chunk-DVY3RDXD.mjs +0 -1
  123. package/dist/chunk-FUVYCYWC.mjs +0 -1
  124. package/dist/chunk-GBLMDJ2B.mjs +0 -1
  125. package/dist/chunk-ISPX45DF.mjs +0 -1
  126. package/dist/chunk-PNXZMUCO.mjs +0 -1
  127. package/dist/chunk-V2ZHKQSP.mjs +0 -74
  128. package/dist/client/components/mdx/index.js +0 -1
  129. package/dist/client/components/mdx/index.mjs +0 -1
  130. package/dist/client/hooks/index.js +0 -1
  131. package/dist/client/hooks/index.mjs +0 -1
  132. package/dist/search-dialog-TWGYKF2D.mjs +0 -1
  133. package/src/node/mdx.ts +0 -279
@@ -8,7 +8,8 @@ import type { BoltdocsConfig } from '@node/config'
8
8
 
9
9
  function getIcon(iconName?: string): React.ElementType | undefined {
10
10
  if (!iconName) return undefined
11
- const IconComponent = (LucideIcons as Record<string, any>)[iconName]
11
+ const icons = LucideIcons as unknown as Record<string, React.ElementType>
12
+ const IconComponent = icons[iconName]
12
13
  return IconComponent || undefined
13
14
  }
14
15
 
@@ -45,16 +46,21 @@ function CollapsibleSidebarGroup({
45
46
  isOpen={isOpen}
46
47
  onToggle={() => setIsOpen(!isOpen)}
47
48
  >
48
- {group.routes.map((route: ComponentRoute) => (
49
- <SidebarPrimitive.SidebarLink
50
- key={route.path}
51
- label={route.title}
52
- href={route.path}
53
- active={activePath === route.path}
54
- icon={getIcon(route.icon)}
55
- badge={route.badge}
56
- />
57
- ))}
49
+ {group.routes.map((route: ComponentRoute) => {
50
+ const isCurrent =
51
+ activePath ===
52
+ (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
53
+ return (
54
+ <SidebarPrimitive.SidebarLink
55
+ key={route.path}
56
+ label={route.title}
57
+ href={route.path}
58
+ active={isCurrent}
59
+ icon={getIcon(route.icon)}
60
+ badge={route.badge}
61
+ />
62
+ )
63
+ })}
58
64
  </SidebarPrimitive.SidebarGroup>
59
65
  )
60
66
  }
@@ -67,22 +73,27 @@ export function Sidebar({
67
73
  config: BoltdocsConfig
68
74
  }) {
69
75
  const { groups, ungrouped, activePath } = useSidebar(routes)
70
- const themeConfig = config.theme || config.themeConfig || {}
76
+ const themeConfig = config.theme || {}
71
77
 
72
78
  return (
73
79
  <SidebarPrimitive.SidebarRoot>
74
80
  {ungrouped.length > 0 && (
75
81
  <SidebarPrimitive.SidebarGroup className="mb-6">
76
- {ungrouped.map((route) => (
77
- <SidebarPrimitive.SidebarLink
78
- key={route.path}
79
- label={route.title}
80
- href={route.path}
81
- active={activePath === route.path}
82
- icon={getIcon(route.icon)}
83
- badge={route.badge}
84
- />
85
- ))}
82
+ {ungrouped.map((route) => {
83
+ const isCurrent =
84
+ activePath ===
85
+ (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
86
+ return (
87
+ <SidebarPrimitive.SidebarLink
88
+ key={route.path}
89
+ label={route.title}
90
+ href={route.path}
91
+ active={isCurrent}
92
+ icon={getIcon(route.icon)}
93
+ badge={route.badge}
94
+ />
95
+ )
96
+ })}
86
97
  </SidebarPrimitive.SidebarGroup>
87
98
  )}
88
99
 
@@ -3,6 +3,8 @@ import T from '@components/primitives/tabs'
3
3
  import { Link } from '@components/primitives/link'
4
4
  import type { BoltdocsTab, ComponentRoute } from '@client/types'
5
5
  import * as Icons from 'lucide-react'
6
+ import { getTranslated } from '@client/utils/i18n'
7
+ import { useRoutes } from '@hooks/use-routes'
6
8
 
7
9
  export function Tabs({
8
10
  tabs,
@@ -11,6 +13,7 @@ export function Tabs({
11
13
  tabs: BoltdocsTab[]
12
14
  routes: ComponentRoute[]
13
15
  }) {
16
+ const { currentLocale } = useRoutes()
14
17
  const { indicatorStyle, tabRefs, activeIndex } = useTabsHook(tabs, routes)
15
18
 
16
19
  const renderTabIcon = (iconName?: string) => {
@@ -54,7 +57,7 @@ export function Tabs({
54
57
  }`}
55
58
  >
56
59
  {renderTabIcon(tab.icon)}
57
- <span>{tab.text}</span>
60
+ <span>{getTranslated(tab.text, currentLocale)}</span>
58
61
  </Link>
59
62
  )
60
63
  })}
@@ -1,10 +1,11 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { Sun, Moon } from 'lucide-react'
2
+ import { Sun, Moon, Monitor } from 'lucide-react'
3
3
  import { useTheme } from '@client/app/theme-context'
4
- import { ToggleButton } from 'react-aria-components'
4
+ import { Button } from 'react-aria-components'
5
+ import { Menu, MenuItem, MenuTrigger } from '@components/primitives/menu'
5
6
 
6
7
  export function ThemeToggle() {
7
- const { theme, toggleTheme } = useTheme()
8
+ const { theme, setTheme } = useTheme()
8
9
  const [mounted, setMounted] = useState(false)
9
10
 
10
11
  useEffect(() => {
@@ -15,18 +16,37 @@ export function ThemeToggle() {
15
16
  return <div className="h-9 w-9" />
16
17
  }
17
18
 
19
+ const Icon = theme === 'system' ? Monitor : theme === 'dark' ? Moon : Sun
20
+
18
21
  return (
19
- <ToggleButton
20
- onChange={toggleTheme}
21
- className="flex h-9 w-9 items-center justify-center rounded-md text-text-muted transition-colors hover:bg-bg-surface hover:text-text-main"
22
- aria-label="Toggle theme"
23
- isSelected={theme === 'dark'}
24
- >
25
- {theme === 'dark' ? (
26
- <Sun size={20} className="animate-in fade-in zoom-in duration-300" />
27
- ) : (
28
- <Moon size={20} className="animate-in fade-in zoom-in duration-300" />
29
- )}
30
- </ToggleButton>
22
+ <MenuTrigger placement="bottom right">
23
+ <Button
24
+ className="flex h-9 w-9 items-center justify-center rounded-md text-text-muted transition-colors hover:bg-bg-surface hover:text-text-main outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
25
+ aria-label="Selection theme"
26
+ >
27
+ <Icon size={20} className="animate-in fade-in zoom-in duration-300" />
28
+ </Button>
29
+ <Menu
30
+ selectionMode="single"
31
+ selectedKeys={[theme]}
32
+ onSelectionChange={(keys) => {
33
+ const newTheme = Array.from(keys)[0] as 'light' | 'dark' | 'system'
34
+ setTheme(newTheme)
35
+ }}
36
+ >
37
+ <MenuItem id="light">
38
+ <Sun size={16} />
39
+ <span>Light</span>
40
+ </MenuItem>
41
+ <MenuItem id="dark">
42
+ <Moon size={16} />
43
+ <span>Dark</span>
44
+ </MenuItem>
45
+ <MenuItem id="system">
46
+ <Monitor size={16} />
47
+ <span>System</span>
48
+ </MenuItem>
49
+ </Menu>
50
+ </MenuTrigger>
31
51
  )
32
52
  }
@@ -1,6 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom'
2
2
  import { getBaseFilePath } from '@client/utils/get-base-file-path'
3
3
  import { useRoutes } from './use-routes'
4
+ import { useBoltdocsStore } from '../store/use-boltdocs-store'
4
5
 
5
6
  export interface LocaleOption {
6
7
  key: string
@@ -24,10 +25,14 @@ export function useI18n(): UseI18nReturn {
24
25
  const routeContext = useRoutes()
25
26
  const { allRoutes, currentRoute, currentLocale, config } = routeContext
26
27
  const i18n = config.i18n
28
+ const setLocale = useBoltdocsStore((s) => s.setLocale)
27
29
 
28
30
  const handleLocaleChange = (locale: string) => {
29
31
  if (!i18n || locale === currentLocale) return
30
32
 
33
+ // Update store
34
+ setLocale(locale)
35
+
31
36
  let targetPath = '/'
32
37
 
33
38
  if (currentRoute) {
@@ -63,21 +68,47 @@ export function useI18n(): UseI18nReturn {
63
68
  : `/${locale}`
64
69
  }
65
70
  } else {
66
- targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
71
+ // Fallback for when we don't have a current route (e.g. 404 page)
72
+ // Try to find the root documentation page for the target locale
73
+ const targetRoute = allRoutes.find(
74
+ (r) =>
75
+ (r.filePath === 'index.mdx' || r.filePath === 'index.md') &&
76
+ (r.locale || i18n.defaultLocale) === locale &&
77
+ !r.version, // Prefer non-versioned root
78
+ )
79
+
80
+ if (targetRoute) {
81
+ targetPath = targetRoute.path
82
+ } else {
83
+ targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
84
+ }
67
85
  }
68
86
 
69
87
  navigate(targetPath)
70
88
  }
71
89
 
72
- const availableLocales = routeContext.availableLocales.map((l) => ({
73
- ...l,
74
- label: l.label as string,
75
- value: l.key,
76
- }))
90
+ const currentLocaleConfig =
91
+ config.i18n?.localeConfigs?.[currentLocale as string]
92
+ const currentLocaleLabel =
93
+ currentLocaleConfig?.label ||
94
+ config.i18n?.locales[currentLocale as string] ||
95
+ currentLocale
96
+
97
+ const availableLocales = config.i18n
98
+ ? Object.entries(config.i18n.locales).map(([key, defaultLabel]) => {
99
+ const localeConfig = config.i18n?.localeConfigs?.[key]
100
+ return {
101
+ key,
102
+ label: localeConfig?.label || defaultLabel,
103
+ value: key,
104
+ isCurrent: key === currentLocale,
105
+ }
106
+ })
107
+ : []
77
108
 
78
109
  return {
79
110
  currentLocale,
80
- currentLocaleLabel: routeContext.currentLocaleLabel,
111
+ currentLocaleLabel,
81
112
  availableLocales,
82
113
  handleLocaleChange,
83
114
  }
@@ -1,95 +1,73 @@
1
- import { useLocation } from 'react-router-dom'
2
1
  import { useConfig } from '@client/app/config-context'
3
2
  import type { LinkProps as RouterLinkProps } from 'react-router-dom'
3
+ import { useRoutes } from './use-routes'
4
4
 
5
5
  /**
6
6
  * Hook to automatically localize a path based on the current version and locale context.
7
- * It ensures that navigation within the /docs path preserves the active version and language.
7
+ * It ensures that navigation preserves the active version and language across the entire site.
8
8
  */
9
9
  export function useLocalizedTo(to: RouterLinkProps['to']) {
10
- const location = useLocation()
11
10
  const config = useConfig()
11
+ const { currentLocale: activeLocale, currentVersion: activeVersion } =
12
+ useRoutes()
12
13
 
13
14
  if (!config || typeof to !== 'string') return to
14
- if (!config.i18n && !config.versions) return to
15
-
16
- const basePath = '/docs'
17
- if (!to.startsWith(basePath)) return to
18
-
19
- // 1. Detect current context from location
20
- const curSub = location.pathname.substring(basePath.length)
21
- const curParts = curSub.split('/').filter(Boolean)
22
-
23
- let currentVersion = config.versions?.defaultVersion
24
- let currentLocale = config.i18n?.defaultLocale
25
-
26
- let cIdx = 0
27
- if (
28
- config.versions &&
29
- curParts.length > cIdx &&
30
- config.versions.versions[curParts[cIdx]]
31
- ) {
32
- currentVersion = curParts[cIdx]
33
- cIdx++
34
- }
35
- if (
36
- config.i18n &&
37
- curParts.length > cIdx &&
38
- config.i18n.locales[curParts[cIdx]]
39
- ) {
40
- currentLocale = curParts[cIdx]
41
- }
42
15
 
43
- // 2. Parse the target `to` path
44
- const toSub = to.substring(basePath.length)
45
- const toParts = toSub.split('/').filter(Boolean)
46
-
47
- let tIdx = 0
48
- let hasVersion = false
49
- let hasLocale = false
50
-
51
- if (
52
- config.versions &&
53
- toParts.length > tIdx &&
54
- config.versions.versions[toParts[tIdx]]
55
- ) {
56
- hasVersion = true
57
- tIdx++
58
- }
59
- if (
60
- config.i18n &&
61
- toParts.length > tIdx &&
62
- config.i18n.locales[toParts[tIdx]]
63
- ) {
64
- hasLocale = true
65
- tIdx++
16
+ // External or absolute links don't need localization
17
+ if (to.startsWith('http') || to.startsWith('//')) return to
18
+
19
+ const i18n = config.i18n
20
+ const versions = config.versions
21
+
22
+ if (!i18n && !versions) return to
23
+
24
+ // 1. Identify the input intent
25
+ const isDocLink = to.startsWith('/docs')
26
+
27
+ // 3. Clean the 'to' path of ANY existing prefixes to avoid stacking
28
+ const parts = to.split('/').filter(Boolean)
29
+ let pIdx = 0
30
+
31
+ // Strip '/docs' if present at start
32
+ if (parts[pIdx] === 'docs') pIdx++
33
+
34
+ // Strip versions if present
35
+ if (versions && parts.length > pIdx) {
36
+ const vMatch = versions.versions.find((v) => v.path === parts[pIdx])
37
+ if (vMatch) pIdx++
66
38
  }
67
39
 
68
- // Extract just the actual route parts
69
- const routeParts = toParts.slice(tIdx)
40
+ // Strip locales if present
41
+ if (i18n && parts.length > pIdx && i18n.locales[parts[pIdx]]) pIdx++
42
+
43
+ // The actual relative route remaining
44
+ const routeContent = parts.slice(pIdx)
45
+
46
+ // 4. Reconstruct strictly from base
47
+ const resultParts: string[] = []
70
48
 
71
- // Reconstruct path
72
- const finalParts = []
73
- if (config.versions) {
74
- if (hasVersion) {
75
- finalParts.push(toParts[0])
76
- } else if (currentVersion) {
77
- finalParts.push(currentVersion)
49
+ if (isDocLink) {
50
+ resultParts.push('docs')
51
+ if (versions && activeVersion) {
52
+ resultParts.push(activeVersion)
78
53
  }
79
54
  }
80
- if (config.i18n) {
81
- if (hasLocale) {
82
- finalParts.push(toParts[hasVersion ? 1 : 0])
83
- } else if (currentLocale) {
84
- finalParts.push(currentLocale)
55
+
56
+ if (i18n && activeLocale) {
57
+ // Only prefix if it's NOT the default locale (cleaner URLs)
58
+ if (activeLocale !== i18n.defaultLocale) {
59
+ resultParts.push(activeLocale)
85
60
  }
86
61
  }
87
62
 
88
- finalParts.push(...routeParts)
63
+ resultParts.push(...routeContent)
64
+
65
+ const finalPath = `/${resultParts.join('/')}`
89
66
 
90
- let finalPath = `${basePath}/${finalParts.join('/')}`
91
- if (finalPath.endsWith('/')) {
92
- finalPath = finalPath.slice(0, -1)
67
+ // Cleanup trailing slashes unless it's just root
68
+ if (finalPath.length > 1 && finalPath.endsWith('/')) {
69
+ return finalPath.slice(0, -1)
93
70
  }
94
- return finalPath === basePath ? basePath : finalPath
71
+
72
+ return finalPath || '/'
95
73
  }
@@ -2,14 +2,17 @@ import { useLocation } from 'react-router-dom'
2
2
  import { useConfig } from '@client/app/config-context'
3
3
  import { useTheme } from '@client/app/theme-context'
4
4
  import type { NavbarLink } from '@client/types'
5
+ import { getTranslated } from '@client/utils/i18n'
6
+ import { useRoutes } from './use-routes'
5
7
 
6
8
  export function useNavbar() {
7
9
  const config = useConfig()
8
10
  const { theme } = useTheme()
9
11
  const location = useLocation()
12
+ const { currentLocale } = useRoutes()
10
13
 
11
- const themeConfig = config.theme || config.themeConfig || {}
12
- const title = themeConfig.title || 'Boltdocs'
14
+ const themeConfig = config.theme || {}
15
+ const title = getTranslated(themeConfig.title, currentLocale) || 'Boltdocs'
13
16
  const rawLinks = themeConfig.navbar || []
14
17
  const socialLinks = themeConfig.socialLinks || []
15
18
  const githubRepo = themeConfig.githubRepo
@@ -18,13 +21,17 @@ export function useNavbar() {
18
21
  const links: NavbarLink[] = rawLinks.map((item: any) => {
19
22
  const href = item.href || item.to || item.link || ''
20
23
  return {
21
- label: item.label || item.text || '',
24
+ label: getTranslated(item.label || item.text, currentLocale),
22
25
  href,
23
26
  active: location.pathname === href,
24
27
  to:
25
28
  href.startsWith('http') || href.startsWith('//')
26
29
  ? 'external'
27
30
  : undefined,
31
+ items: item.items?.map((sub: any) => ({
32
+ label: getTranslated(sub.label || sub.text, currentLocale),
33
+ href: sub.href || sub.link || sub.to || '',
34
+ })),
28
35
  }
29
36
  })
30
37
 
@@ -1,18 +1,39 @@
1
+ import { useLocation } from 'react-router-dom'
1
2
  import { useRoutes } from './use-routes'
2
3
 
3
4
  /**
4
5
  * Hook to manage the previous and next button functionality for documentation pages.
6
+ * Intelligent: respects current locale, version, and tab to keep navigation logical.
5
7
  */
6
8
  export function usePageNav() {
7
- const { routes } = useRoutes()
8
- const currentPath = window.location.pathname
9
+ const { routes, currentRoute } = useRoutes()
10
+ const location = useLocation()
9
11
 
10
- const currentIndex = routes.findIndex((r) => r.path === currentPath)
11
- const currentRoute = routes[currentIndex]
12
+ if (!currentRoute) {
13
+ return {
14
+ prevPage: null,
15
+ nextPage: null,
16
+ currentRoute: null,
17
+ }
18
+ }
19
+
20
+ const activeTabId = currentRoute.tab?.toLowerCase()
21
+
22
+ // Subset of routes that match the current context (locale and version are already filtered by useRoutes)
23
+ // We further filter by tab to keep the user in the same logical section
24
+ const contextRoutes = activeTabId
25
+ ? routes.filter((r) => r.tab?.toLowerCase() === activeTabId)
26
+ : routes.filter((r) => !r.tab)
27
+
28
+ const currentIndex = contextRoutes.findIndex(
29
+ (r) => r.path === location.pathname,
30
+ )
12
31
 
13
- const prevPage = currentIndex > 0 ? routes[currentIndex - 1] : null
32
+ const prevPage = currentIndex > 0 ? contextRoutes[currentIndex - 1] : null
14
33
  const nextPage =
15
- currentIndex < routes.length - 1 ? routes[currentIndex + 1] : null
34
+ currentIndex !== -1 && currentIndex < contextRoutes.length - 1
35
+ ? contextRoutes[currentIndex + 1]
36
+ : null
16
37
 
17
38
  return {
18
39
  prevPage,
@@ -1,6 +1,7 @@
1
1
  import { useLocation } from 'react-router-dom'
2
2
  import { useConfig } from '@client/app/config-context'
3
3
  import { usePreload } from '@client/app/preload'
4
+ import { useBoltdocsStore } from '../store/use-boltdocs-store'
4
5
 
5
6
  /**
6
7
  * Hook to access the framework's routing state.
@@ -12,16 +13,26 @@ export function useRoutes() {
12
13
  const config = useConfig()
13
14
  const location = useLocation()
14
15
 
15
- // Find the current route exactly matching the pathname
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)
20
+
21
+ // Find the current route matching the pathname
16
22
  const currentRoute = allRoutes.find((r) => r.path === location.pathname)
17
23
 
18
- // Derive current locale and version from the route or defaults
24
+ // Derive current locale and version
25
+ // Priority: URL (currentRoute) > Zustand Store (Persistence) > Config Default
19
26
  const currentLocale = config.i18n
20
- ? currentRoute?.locale || config.i18n.defaultLocale
27
+ ? currentRoute?.locale ||
28
+ (hasHydrated ? currentLocaleStore : undefined) ||
29
+ config.i18n.defaultLocale
21
30
  : undefined
22
31
 
23
32
  const currentVersion = config.versions
24
- ? currentRoute?.version || config.versions.defaultVersion
33
+ ? currentRoute?.version ||
34
+ (hasHydrated ? currentVersionStore : undefined) ||
35
+ config.versions.defaultVersion
25
36
  : undefined
26
37
 
27
38
  // Filter routes to those matching the current version and locale
@@ -32,28 +43,62 @@ export function useRoutes() {
32
43
  const versionMatch = config.versions
33
44
  ? (r.version || config.versions.defaultVersion) === currentVersion
34
45
  : true
35
- return localeMatch && versionMatch
46
+
47
+ if (!(localeMatch && versionMatch)) return false
48
+
49
+ // Resolve duplicate paths (aliases) like /docs vs /docs/en
50
+ // We prefer the version that matches the current route's prefix style
51
+ const i18n = config.i18n
52
+ if (i18n) {
53
+ const isCurrentRoutePrefixed = !!currentRoute?.locale
54
+ const isRoutePrefixed = !!r.locale
55
+
56
+ const hasAlternate = allRoutes.some(
57
+ (alt) =>
58
+ alt !== r &&
59
+ alt.filePath === r.filePath &&
60
+ alt.version === r.version &&
61
+ (alt.locale || i18n.defaultLocale) ===
62
+ (r.locale || i18n.defaultLocale),
63
+ )
64
+
65
+ if (hasAlternate && isCurrentRoutePrefixed !== isRoutePrefixed) {
66
+ return false
67
+ }
68
+ }
69
+
70
+ return true
36
71
  })
37
72
 
38
73
  // Labels and lists for UI convenience
74
+ const currentLocaleConfig =
75
+ config.i18n?.localeConfigs?.[currentLocale as string]
39
76
  const currentLocaleLabel =
40
- config.i18n?.locales[currentLocale as string] || currentLocale
41
- const currentVersionLabel =
42
- config.versions?.versions[currentVersion as string] || currentVersion
77
+ currentLocaleConfig?.label ||
78
+ config.i18n?.locales[currentLocale as string] ||
79
+ currentLocale
80
+
81
+ const currentVersionConfig = config.versions?.versions.find(
82
+ (v) => v.path === currentVersion,
83
+ )
84
+ const currentVersionLabel = currentVersionConfig?.label || currentVersion
43
85
 
44
86
  const availableLocales = config.i18n
45
- ? Object.entries(config.i18n.locales).map(([key, label]) => ({
46
- key,
47
- label,
48
- isCurrent: key === currentLocale,
49
- }))
87
+ ? Object.entries(config.i18n.locales).map(([key, defaultLabel]) => {
88
+ const localeConfig = config.i18n?.localeConfigs?.[key]
89
+ return {
90
+ key,
91
+ label: localeConfig?.label || defaultLabel,
92
+ isCurrent: key === currentLocale,
93
+ }
94
+ })
50
95
  : []
51
96
 
52
97
  const availableVersions = config.versions
53
- ? Object.entries(config.versions.versions).map(([key, label]) => ({
54
- key,
55
- label,
56
- isCurrent: key === currentVersion,
98
+ ? config.versions.versions.map((v) => ({
99
+ key: v.path,
100
+ label: v.label,
101
+ isCurrent: v.path === currentVersion,
57
102
  }))
58
103
  : []
59
104