boltdocs 2.1.0 → 2.2.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 (116) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/base-ui/index.d.mts +25 -0
  3. package/dist/base-ui/index.d.ts +25 -0
  4. package/dist/base-ui/index.js +1 -0
  5. package/dist/base-ui/index.mjs +1 -0
  6. package/dist/{cache-Q4T6VAUL.mjs → cache-CRAZ55X7.mjs} +1 -1
  7. package/dist/chunk-5D6XPYQ3.mjs +74 -0
  8. package/dist/chunk-6QXCKZAT.mjs +1 -0
  9. package/dist/chunk-H4M6P3DM.mjs +1 -0
  10. package/dist/chunk-JD3RSDE4.mjs +1 -0
  11. package/dist/chunk-JXHNX2WN.mjs +1 -0
  12. package/dist/chunk-JZXLCA2E.mjs +1 -0
  13. package/dist/chunk-MZBG4N4W.mjs +1 -0
  14. package/dist/chunk-NBCYHLAA.mjs +1 -0
  15. package/dist/chunk-Q3MLYTIQ.mjs +1 -0
  16. package/dist/chunk-RSII2UPE.mjs +1 -0
  17. package/dist/chunk-T3W44KWY.mjs +1 -0
  18. package/dist/chunk-ZK2266IZ.mjs +1 -0
  19. package/dist/chunk-ZRJ55GGF.mjs +1 -0
  20. package/dist/client/index.d.mts +13 -115
  21. package/dist/client/index.d.ts +13 -115
  22. package/dist/client/index.js +1 -1
  23. package/dist/client/index.mjs +1 -1
  24. package/dist/client/ssr.js +1 -1
  25. package/dist/client/ssr.mjs +1 -1
  26. package/dist/client/types.d.mts +3 -0
  27. package/dist/client/types.d.ts +3 -0
  28. package/dist/client/types.js +1 -0
  29. package/dist/client/types.mjs +0 -0
  30. package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
  31. package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
  32. package/dist/{client/hooks → hooks}/index.d.mts +25 -12
  33. package/dist/{client/hooks → hooks}/index.d.ts +25 -12
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/index.mjs +1 -0
  36. package/dist/integrations/index.d.mts +48 -0
  37. package/dist/integrations/index.d.ts +48 -0
  38. package/dist/integrations/index.js +1 -0
  39. package/dist/integrations/index.mjs +1 -0
  40. package/dist/link-DfBwCeZc.d.mts +68 -0
  41. package/dist/link-DfBwCeZc.d.ts +68 -0
  42. package/dist/loading-BqGrFWO5.d.mts +68 -0
  43. package/dist/loading-chS3pm9W.d.ts +68 -0
  44. package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
  45. package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
  46. package/dist/mdx/index.js +1 -0
  47. package/dist/mdx/index.mjs +1 -0
  48. package/dist/node/cli-entry.js +27 -27
  49. package/dist/node/cli-entry.mjs +1 -1
  50. package/dist/node/index.d.mts +44 -14
  51. package/dist/node/index.d.ts +44 -14
  52. package/dist/node/index.js +23 -23
  53. package/dist/node/index.mjs +1 -1
  54. package/dist/primitives/index.d.mts +301 -0
  55. package/dist/primitives/index.d.ts +301 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/index.mjs +1 -0
  58. package/dist/search-dialog-MA5AISC7.mjs +1 -0
  59. package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
  60. package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
  61. package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
  62. package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
  63. package/package.json +34 -8
  64. package/src/client/app/index.tsx +155 -35
  65. package/src/client/app/mdx-component.tsx +7 -3
  66. package/src/client/app/theme-context.tsx +40 -23
  67. package/src/client/components/default-layout.tsx +12 -6
  68. package/src/client/components/primitives/breadcrumbs.tsx +1 -1
  69. package/src/client/components/primitives/navbar.tsx +5 -2
  70. package/src/client/components/primitives/search-dialog.tsx +12 -3
  71. package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
  72. package/src/client/components/ui-base/index.ts +17 -0
  73. package/src/client/components/ui-base/navbar.tsx +66 -33
  74. package/src/client/components/ui-base/powered-by.tsx +8 -5
  75. package/src/client/components/ui-base/sidebar.tsx +31 -22
  76. package/src/client/components/ui-base/tabs.tsx +4 -1
  77. package/src/client/components/ui-base/theme-toggle.tsx +35 -15
  78. package/src/client/hooks/use-i18n.ts +37 -7
  79. package/src/client/hooks/use-localized-to.ts +45 -68
  80. package/src/client/hooks/use-navbar.ts +10 -3
  81. package/src/client/hooks/use-routes.ts +61 -17
  82. package/src/client/hooks/use-search.ts +21 -5
  83. package/src/client/hooks/use-sidebar.ts +5 -2
  84. package/src/client/hooks/use-version.ts +5 -0
  85. package/src/client/integrations/index.ts +1 -0
  86. package/src/client/store/use-boltdocs-store.ts +43 -0
  87. package/src/client/types.ts +4 -2
  88. package/src/client/utils/i18n.ts +23 -0
  89. package/src/node/config.ts +54 -17
  90. package/src/node/index.ts +1 -1
  91. package/src/node/mdx/cache.ts +12 -0
  92. package/src/node/mdx/highlighter.ts +47 -0
  93. package/src/node/mdx/index.ts +114 -0
  94. package/src/node/mdx/rehype-shiki.ts +53 -0
  95. package/src/node/mdx/remark-shiki.ts +61 -0
  96. package/src/node/plugin/html.ts +8 -4
  97. package/src/node/plugin/index.ts +117 -68
  98. package/src/node/routes/index.ts +34 -13
  99. package/src/node/routes/parser.ts +12 -4
  100. package/src/node/ssg/index.ts +3 -3
  101. package/src/node/utils.ts +32 -2
  102. package/tsup.config.ts +7 -2
  103. package/dist/chunk-52MVMZWS.mjs +0 -1
  104. package/dist/chunk-BVWWKXJH.mjs +0 -1
  105. package/dist/chunk-DVY3RDXD.mjs +0 -1
  106. package/dist/chunk-FUVYCYWC.mjs +0 -1
  107. package/dist/chunk-GBLMDJ2B.mjs +0 -1
  108. package/dist/chunk-ISPX45DF.mjs +0 -1
  109. package/dist/chunk-PNXZMUCO.mjs +0 -1
  110. package/dist/chunk-V2ZHKQSP.mjs +0 -74
  111. package/dist/client/components/mdx/index.js +0 -1
  112. package/dist/client/components/mdx/index.mjs +0 -1
  113. package/dist/client/hooks/index.js +0 -1
  114. package/dist/client/hooks/index.mjs +0 -1
  115. package/dist/search-dialog-TWGYKF2D.mjs +0 -1
  116. package/src/node/mdx.ts +0 -279
@@ -1,95 +1,72 @@
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 } = useRoutes()
12
12
 
13
13
  if (!config || typeof to !== 'string') return to
14
- if (!config.i18n && !config.versions) return to
14
+
15
+ // External or absolute links don't need localization
16
+ if (to.startsWith('http') || to.startsWith('//')) return to
15
17
 
16
- const basePath = '/docs'
17
- if (!to.startsWith(basePath)) return to
18
+ const i18n = config.i18n
19
+ const versions = config.versions
18
20
 
19
- // 1. Detect current context from location
20
- const curSub = location.pathname.substring(basePath.length)
21
- const curParts = curSub.split('/').filter(Boolean)
21
+ if (!i18n && !versions) return to
22
22
 
23
- let currentVersion = config.versions?.defaultVersion
24
- let currentLocale = config.i18n?.defaultLocale
23
+ // 1. Identify the input intent
24
+ const isDocLink = to.startsWith('/docs')
25
+
26
+ // 3. Clean the 'to' path of ANY existing prefixes to avoid stacking
27
+ const parts = to.split('/').filter(Boolean)
28
+ let pIdx = 0
25
29
 
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]
30
+ // Strip '/docs' if present at start
31
+ if (parts[pIdx] === 'docs') pIdx++
32
+
33
+ // Strip versions if present
34
+ if (versions && parts.length > pIdx) {
35
+ const vMatch = versions.versions.find(v => v.path === parts[pIdx])
36
+ if (vMatch) pIdx++
41
37
  }
42
38
 
43
- // 2. Parse the target `to` path
44
- const toSub = to.substring(basePath.length)
45
- const toParts = toSub.split('/').filter(Boolean)
39
+ // Strip locales if present
40
+ if (i18n && parts.length > pIdx && i18n.locales[parts[pIdx]]) pIdx++
46
41
 
47
- let tIdx = 0
48
- let hasVersion = false
49
- let hasLocale = false
42
+ // The actual relative route remaining
43
+ const routeContent = parts.slice(pIdx)
50
44
 
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++
66
- }
45
+ // 4. Reconstruct strictly from base
46
+ const resultParts: string[] = []
67
47
 
68
- // Extract just the actual route parts
69
- const routeParts = toParts.slice(tIdx)
70
-
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)
48
+ if (isDocLink) {
49
+ resultParts.push('docs')
50
+ if (versions && activeVersion) {
51
+ resultParts.push(activeVersion)
78
52
  }
79
53
  }
80
- if (config.i18n) {
81
- if (hasLocale) {
82
- finalParts.push(toParts[hasVersion ? 1 : 0])
83
- } else if (currentLocale) {
84
- finalParts.push(currentLocale)
54
+
55
+ if (i18n && activeLocale) {
56
+ // Only prefix if it's NOT the default locale (cleaner URLs)
57
+ if (activeLocale !== i18n.defaultLocale) {
58
+ resultParts.push(activeLocale)
85
59
  }
86
60
  }
87
61
 
88
- finalParts.push(...routeParts)
62
+ resultParts.push(...routeContent)
89
63
 
90
- let finalPath = `${basePath}/${finalParts.join('/')}`
91
- if (finalPath.endsWith('/')) {
92
- finalPath = finalPath.slice(0, -1)
64
+ const finalPath = `/${resultParts.join('/')}`
65
+
66
+ // Cleanup trailing slashes unless it's just root
67
+ if (finalPath.length > 1 && finalPath.endsWith('/')) {
68
+ return finalPath.slice(0, -1)
93
69
  }
94
- return finalPath === basePath ? basePath : finalPath
70
+
71
+ return finalPath || '/'
95
72
  }
@@ -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,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,61 @@ 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 = config.i18n?.localeConfigs?.[currentLocale as string]
39
75
  const currentLocaleLabel =
40
- config.i18n?.locales[currentLocale as string] || currentLocale
41
- const currentVersionLabel =
42
- config.versions?.versions[currentVersion as string] || currentVersion
76
+ currentLocaleConfig?.label ||
77
+ config.i18n?.locales[currentLocale as string] ||
78
+ currentLocale
79
+
80
+ const currentVersionConfig = config.versions?.versions.find(
81
+ (v) => v.path === currentVersion,
82
+ )
83
+ const currentVersionLabel = currentVersionConfig?.label || currentVersion
43
84
 
44
85
  const availableLocales = config.i18n
45
- ? Object.entries(config.i18n.locales).map(([key, label]) => ({
46
- key,
47
- label,
48
- isCurrent: key === currentLocale,
49
- }))
86
+ ? Object.entries(config.i18n.locales).map(([key, defaultLabel]) => {
87
+ const localeConfig = config.i18n?.localeConfigs?.[key]
88
+ return {
89
+ key,
90
+ label: localeConfig?.label || defaultLabel,
91
+ isCurrent: key === currentLocale,
92
+ }
93
+ })
50
94
  : []
51
95
 
52
96
  const availableVersions = config.versions
53
- ? Object.entries(config.versions.versions).map(([key, label]) => ({
54
- key,
55
- label,
56
- isCurrent: key === currentVersion,
97
+ ? config.versions.versions.map((v) => ({
98
+ key: v.path,
99
+ label: v.label,
100
+ isCurrent: v.path === currentVersion,
57
101
  }))
58
102
  : []
59
103
 
@@ -1,13 +1,22 @@
1
1
  import { useState, useMemo } from 'react'
2
+ import { useRoutes } from './use-routes'
2
3
  import type { ComponentRoute } from '@client/types'
3
4
 
4
5
  export function useSearch(routes: ComponentRoute[]) {
6
+ const { currentLocale, currentVersion } = useRoutes()
5
7
  const [isOpen, setIsOpen] = useState(false)
6
8
  const [query, setQuery] = useState('')
7
9
 
8
10
  const list = useMemo(() => {
11
+ // 0. Filter routes by active context
12
+ const activeRoutes = routes.filter((r) => {
13
+ const localeMatch = !currentLocale || r.locale === currentLocale
14
+ const versionMatch = !currentVersion || r.version === currentVersion
15
+ return localeMatch && versionMatch
16
+ })
17
+
9
18
  if (!query) {
10
- return routes.slice(0, 10).map((r) => ({
19
+ return activeRoutes.slice(0, 10).map((r) => ({
11
20
  id: r.path,
12
21
  title: r.title,
13
22
  path: r.path,
@@ -16,10 +25,17 @@ export function useSearch(routes: ComponentRoute[]) {
16
25
  }))
17
26
  }
18
27
 
19
- const results: any[] = []
28
+ const results: {
29
+ id: string
30
+ title: string | undefined
31
+ path: string
32
+ bio: string
33
+ groupTitle: string | undefined
34
+ isHeading?: boolean
35
+ }[] = []
20
36
  const lowerQuery = query.toLowerCase()
21
37
 
22
- for (const route of routes) {
38
+ for (const route of activeRoutes) {
23
39
  if (route.title?.toLowerCase().includes(lowerQuery)) {
24
40
  results.push({
25
41
  id: route.path,
@@ -55,7 +71,7 @@ export function useSearch(routes: ComponentRoute[]) {
55
71
  return true
56
72
  })
57
73
  .slice(0, 10)
58
- }, [routes, query])
74
+ }, [routes, query, currentLocale, currentVersion])
59
75
 
60
76
  return {
61
77
  isOpen,
@@ -65,7 +81,7 @@ export function useSearch(routes: ComponentRoute[]) {
65
81
  list,
66
82
  input: {
67
83
  value: query,
68
- onChange: (e: any) => setQuery(e.target.value),
84
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),
69
85
  },
70
86
  }
71
87
  }
@@ -7,7 +7,10 @@ export function useSidebar(routes: ComponentRoute[]) {
7
7
  const location = useLocation()
8
8
 
9
9
  // Find active route and tab
10
- const activeRoute = routes.find((r) => r.path === location.pathname)
10
+ const normalize = (p: string) => (p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p)
11
+ const currentPath = normalize(location.pathname)
12
+
13
+ const activeRoute = routes.find((r) => normalize(r.path) === currentPath)
11
14
  const activeTabId = activeRoute?.tab?.toLowerCase()
12
15
 
13
16
  // Filter routes by active tab if any
@@ -43,7 +46,7 @@ export function useSidebar(routes: ComponentRoute[]) {
43
46
  groups,
44
47
  ungrouped,
45
48
  activeRoute,
46
- activePath: location.pathname,
49
+ activePath: currentPath,
47
50
  config,
48
51
  }
49
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 VersionOption {
6
7
  key: string
@@ -25,10 +26,14 @@ export function useVersion(): UseVersionReturn {
25
26
  const { allRoutes, currentRoute, currentVersion, currentLocale, config } =
26
27
  routeContext
27
28
  const versions = config.versions
29
+ const setVersion = useBoltdocsStore((s) => s.setVersion)
28
30
 
29
31
  const handleVersionChange = (version: string) => {
30
32
  if (!versions || version === currentVersion) return
31
33
 
34
+ // Update store
35
+ setVersion(version)
36
+
32
37
  let targetPath = `/docs/${version}`
33
38
 
34
39
  if (currentRoute) {
@@ -0,0 +1 @@
1
+ export * from './codesandbox'
@@ -0,0 +1,43 @@
1
+ import { create } from 'zustand'
2
+ import { persist, createJSONStorage } from 'zustand/middleware'
3
+
4
+ interface BoltdocsState {
5
+ currentLocale: string | undefined
6
+ currentVersion: string | undefined
7
+ hasHydrated: boolean
8
+
9
+ // Actions
10
+ setLocale: (locale: string | undefined) => void
11
+ setVersion: (version: string | undefined) => void
12
+ setHasHydrated: (val: boolean) => void
13
+ }
14
+
15
+ /**
16
+ * Global store for Boltdocs documentation state.
17
+ * Uses localStorage persistence to remember user preferences across sessions.
18
+ */
19
+ export const useBoltdocsStore = create<BoltdocsState>()(
20
+ persist(
21
+ (set) => ({
22
+ currentLocale: undefined,
23
+ currentVersion: undefined,
24
+ hasHydrated: false,
25
+
26
+ setLocale: (locale: string | undefined) => set({ currentLocale: locale }),
27
+ setVersion: (version: string | undefined) => set({ currentVersion: version }),
28
+ setHasHydrated: (val: boolean) => set({ hasHydrated: val }),
29
+ }),
30
+ {
31
+ name: 'boltdocs-storage',
32
+ storage: createJSONStorage(() => localStorage),
33
+ // Only persist identifying state
34
+ partialize: (state: BoltdocsState) => ({
35
+ currentLocale: state.currentLocale,
36
+ currentVersion: state.currentVersion,
37
+ }),
38
+ onRehydrateStorage: () => (state?: BoltdocsState) => {
39
+ state?.setHasHydrated(true)
40
+ },
41
+ }
42
+ )
43
+ )
@@ -119,7 +119,8 @@ export interface SandboxEmbedOptions {
119
119
  */
120
120
  export interface BoltdocsTab {
121
121
  id: string
122
- text: string
122
+ /** Text to display (can be a string or a map of translations) */
123
+ text: string | Record<string, string>
123
124
  icon?: string
124
125
  }
125
126
 
@@ -160,7 +161,8 @@ export interface LayoutProps {
160
161
  * Unified type for navbar links.
161
162
  */
162
163
  export interface NavbarLink {
163
- label: string
164
+ /** Label to display (can be a string or a map of translations) */
165
+ label: string | Record<string, string>
164
166
  href: string
165
167
  active: boolean
166
168
  /** Optional icon or string for external link indication */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Retrieves the correct translation from a value that can be either
3
+ * a simple string or a map of locale-specific strings.
4
+ *
5
+ * @param value - The text to translate
6
+ * @param locale - The current active locale (e.g., 'en', 'es')
7
+ * @returns The translated string
8
+ */
9
+ export function getTranslated(
10
+ value: string | Record<string, string> | undefined,
11
+ locale?: string,
12
+ ): string {
13
+ if (!value) return ''
14
+ if (typeof value === 'string') return value
15
+
16
+ if (locale && value[locale]) {
17
+ return value[locale]
18
+ }
19
+
20
+ // Fallback: Use the first available translation or an empty string
21
+ const firstValue = Object.values(value)[0]
22
+ return firstValue || ''
23
+ }
@@ -24,10 +24,10 @@ export interface BoltdocsFooterConfig {
24
24
  * Theme-specific configuration options governing the appearance and navigation of the site.
25
25
  */
26
26
  export interface BoltdocsThemeConfig {
27
- /** The global title of the documentation site */
28
- title?: string
29
- /** The global description of the site (used for SEO) */
30
- description?: string
27
+ /** The global title of the documentation site (can be translated) */
28
+ title?: string | Record<string, string>
29
+ /** The global description of the site (can be translated) */
30
+ description?: string | Record<string, string>
31
31
  /** URL path to the site logo or an object for light/dark versions */
32
32
  logo?:
33
33
  | string
@@ -40,12 +40,17 @@ export interface BoltdocsThemeConfig {
40
40
  }
41
41
  /** Items to display in the top navigation bar */
42
42
  navbar?: Array<{
43
- /** Text to display */
44
- label: string
43
+ /** Text to display (can be a string or a map of translations) */
44
+ label: string | Record<string, string>
45
45
  /** URL path or external link */
46
46
  href: string
47
47
  /** Nested items for NavigationMenu */
48
- items?: Array<{ label: string; href: string }>
48
+ items?: Array<{
49
+ /** Text to display (can be a string or a map of translations) */
50
+ label: string | Record<string, string>
51
+ /** URL path or external link */
52
+ href: string
53
+ }>
49
54
  }>
50
55
  /** Items to display in the sidebar, organized optionally by group URLs */
51
56
  sidebar?: Record<string, Array<{ text: string; link: string }>>
@@ -78,7 +83,12 @@ export interface BoltdocsThemeConfig {
78
83
  * Top-level tabs for organizing documentation groups.
79
84
  * Tab discovery uses the (tab-id) directory syntax.
80
85
  */
81
- tabs?: Array<{ id: string; text: string; icon?: string }>
86
+ tabs?: Array<{
87
+ id: string
88
+ /** Text to display (can be a string or a map of translations) */
89
+ text: string | Record<string, string>
90
+ icon?: string
91
+ }>
82
92
  /**
83
93
  * The syntax highlighting theme for code blocks.
84
94
  * Supports any Shiki theme name (e.g., 'github-dark', 'one-dark-pro', 'aurora-x').
@@ -112,24 +122,55 @@ export type BoltdocsRobotsConfig =
112
122
  sitemaps?: string[]
113
123
  }
114
124
 
125
+ /**
126
+ * Configuration for a specific locale.
127
+ */
128
+ export interface BoltdocsLocaleConfig {
129
+ /** The display name of the locale */
130
+ label?: string
131
+ /** The text direction (ltr or rtl) */
132
+ direction?: 'ltr' | 'rtl'
133
+ /** The HTML lang attribute value (e.g., 'en-US') */
134
+ htmlLang?: string
135
+ /** The calendar system to use (e.g., 'gregory') */
136
+ calendar?: string
137
+ }
138
+
115
139
  /**
116
140
  * Configuration for internationalization (i18n).
117
141
  */
118
142
  export interface BoltdocsI18nConfig {
119
143
  /** The default locale (e.g., 'en') */
120
144
  defaultLocale: string
121
- /** Available locales and their display names (e.g., { en: 'English', es: 'Español' }) */
145
+ /** Available locales and their basic display names (e.g., { en: 'English', es: 'Español' }) */
122
146
  locales: Record<string, string>
147
+ /** Detailed configuration for each locale */
148
+ localeConfigs?: Record<string, BoltdocsLocaleConfig>
149
+ }
150
+
151
+ /**
152
+ * Configuration for a specific documentation version.
153
+ */
154
+ export interface BoltdocsVersionConfig {
155
+ /** The display name of the version (e.g., 'v2.0') */
156
+ label: string
157
+ /** The URL path prefix for the version (e.g., '2.0') */
158
+ path: string
123
159
  }
124
160
 
125
161
  /**
126
162
  * Configuration for documentation versioning.
127
163
  */
128
164
  export interface BoltdocsVersionsConfig {
129
- /** The default version (e.g., 'v2') */
165
+ /** The default version path (e.g., 'v2') */
130
166
  defaultVersion: string
131
- /** Available versions and their display names (e.g., { v1: 'Version 1.x', v2: 'Version 2.x' }) */
132
- versions: Record<string, string>
167
+ /**
168
+ * Optional prefix for all version paths (e.g., 'v').
169
+ * If set to 'v', version '1.1' will be available at '/docs/v1.1'.
170
+ */
171
+ prefix?: string
172
+ /** Available versions configurations */
173
+ versions: BoltdocsVersionConfig[]
133
174
  }
134
175
 
135
176
  /**
@@ -189,8 +230,6 @@ export interface BoltdocsConfig {
189
230
  robots?: BoltdocsRobotsConfig
190
231
  /** Low-level Vite configuration overrides */
191
232
  vite?: import('vite').InlineConfig
192
- /** @deprecated Use theme instead */
193
- themeConfig?: BoltdocsThemeConfig
194
233
  }
195
234
 
196
235
  export function defineConfig(config: BoltdocsConfig): BoltdocsConfig {
@@ -286,13 +325,12 @@ export async function resolveConfig(
286
325
  poweredBy: userConfig.poweredBy,
287
326
  communityHelp: userConfig.communityHelp,
288
327
  version: userConfig.version,
289
- editLink: userConfig.editLink
328
+ editLink: userConfig.editLink,
290
329
  }
291
330
 
292
331
  // User can define properties at top level or inside themeConfig/theme
293
332
  const userThemeConfig: BoltdocsThemeConfig = {
294
333
  ...themeConfigFromTop,
295
- ...(userConfig.themeConfig || {}),
296
334
  ...(userConfig.theme || {}),
297
335
  }
298
336
 
@@ -330,4 +368,3 @@ export async function resolveConfig(
330
368
  vite: userConfig.vite,
331
369
  }
332
370
  }
333
-
package/src/node/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { Plugin, InlineConfig } from 'vite'
2
2
  import react from '@vitejs/plugin-react'
3
3
  import tailwindcss from '@tailwindcss/vite'
4
4
  import { boltdocsPlugin } from './plugin/index'
5
- import { boltdocsMdxPlugin } from './mdx'
5
+ import { boltdocsMdxPlugin } from './mdx/index'
6
6
  import type { BoltdocsPluginOptions } from './plugin/index'
7
7
 
8
8
  import { resolveConfig, type BoltdocsConfig } from './config'
@@ -0,0 +1,12 @@
1
+ import { TransformCache } from '../cache'
2
+
3
+ /**
4
+ * Version identifier for the MDX plugin to invalidate cache if logic changes.
5
+ */
6
+ export const MDX_PLUGIN_VERSION = 'v3'
7
+
8
+ /**
9
+ * Persistent cache for MDX transformations.
10
+ * Saves results to `.boltdocs/transform-mdx.json.gz`.
11
+ */
12
+ export const mdxCache = new TransformCache('mdx')