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.
- package/CHANGELOG.md +19 -0
- package/bin/boltdocs.js +2 -2
- package/dist/base-ui/index.d.mts +25 -0
- package/dist/base-ui/index.d.ts +25 -0
- package/dist/base-ui/index.js +1 -0
- package/dist/base-ui/index.mjs +1 -0
- package/dist/{cache-Q4T6VAUL.mjs → cache-P6WK424C.mjs} +1 -1
- package/dist/chunk-22NXDNP4.mjs +74 -0
- package/dist/chunk-2HUVMMJU.mjs +1 -0
- package/dist/chunk-2Z5T6EAU.mjs +1 -0
- package/dist/chunk-CRZGOE32.mjs +1 -0
- package/dist/chunk-HA6543SL.mjs +1 -0
- package/dist/chunk-JD3RSDE4.mjs +1 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-NBCYHLAA.mjs +1 -0
- package/dist/chunk-RPUERTVC.mjs +1 -0
- package/dist/chunk-T3W44KWY.mjs +1 -0
- package/dist/chunk-URTD6E6S.mjs +1 -0
- package/dist/chunk-W2NB4T6V.mjs +1 -0
- package/dist/chunk-Y4RRHPXC.mjs +1 -0
- package/dist/client/index.d.mts +13 -115
- package/dist/client/index.d.ts +13 -115
- package/dist/client/index.js +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/client/ssr.js +1 -1
- package/dist/client/ssr.mjs +1 -1
- package/dist/client/types.d.mts +3 -0
- package/dist/client/types.d.ts +3 -0
- package/dist/client/types.js +1 -0
- package/dist/client/types.mjs +0 -0
- package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
- package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
- package/dist/{client/hooks → hooks}/index.d.mts +16 -11
- package/dist/{client/hooks → hooks}/index.d.ts +16 -11
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.mjs +1 -0
- package/dist/integrations/index.d.mts +48 -0
- package/dist/integrations/index.d.ts +48 -0
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/link-DfBwCeZc.d.mts +68 -0
- package/dist/link-DfBwCeZc.d.ts +68 -0
- package/dist/loading-B7X5Wchs.d.ts +66 -0
- package/dist/loading-WuaQbsKb.d.mts +66 -0
- package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
- package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
- package/dist/mdx/index.js +1 -0
- package/dist/mdx/index.mjs +1 -0
- package/dist/node/cli-entry.js +31 -27
- package/dist/node/cli-entry.mjs +5 -1
- package/dist/node/index.d.mts +44 -14
- package/dist/node/index.d.ts +44 -14
- package/dist/node/index.js +24 -24
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +301 -0
- package/dist/primitives/index.d.ts +301 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/index.mjs +1 -0
- package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
- package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
- package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
- package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
- package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
- package/package.json +35 -8
- package/src/client/app/index.tsx +155 -35
- package/src/client/app/mdx-component.tsx +7 -3
- package/src/client/app/theme-context.tsx +47 -23
- package/src/client/components/default-layout.tsx +16 -6
- package/src/client/components/primitives/breadcrumbs.tsx +1 -1
- package/src/client/components/primitives/navbar.tsx +8 -5
- package/src/client/components/primitives/search-dialog.tsx +15 -6
- package/src/client/components/primitives/sidebar.tsx +3 -2
- package/src/client/components/primitives/skeleton.tsx +26 -0
- package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
- package/src/client/components/ui-base/index.ts +17 -0
- package/src/client/components/ui-base/loading.tsx +43 -73
- package/src/client/components/ui-base/navbar.tsx +74 -39
- package/src/client/components/ui-base/page-nav.tsx +2 -1
- package/src/client/components/ui-base/powered-by.tsx +11 -5
- package/src/client/components/ui-base/search-dialog.tsx +16 -5
- package/src/client/components/ui-base/sidebar.tsx +33 -22
- package/src/client/components/ui-base/tabs.tsx +4 -1
- package/src/client/components/ui-base/theme-toggle.tsx +35 -15
- package/src/client/hooks/use-i18n.ts +38 -7
- package/src/client/hooks/use-localized-to.ts +51 -73
- package/src/client/hooks/use-navbar.ts +10 -3
- package/src/client/hooks/use-page-nav.ts +27 -6
- package/src/client/hooks/use-routes.ts +62 -17
- package/src/client/hooks/use-search.ts +84 -46
- package/src/client/hooks/use-sidebar.ts +6 -2
- package/src/client/hooks/use-version.ts +5 -0
- package/src/client/integrations/index.ts +1 -0
- package/src/client/store/use-boltdocs-store.ts +44 -0
- package/src/client/theme/neutral.css +29 -0
- package/src/client/types.ts +4 -2
- package/src/client/utils/i18n.ts +23 -0
- package/src/node/{cli.ts → cli/build.ts} +17 -23
- package/src/node/cli/dev.ts +22 -0
- package/src/node/cli/doctor.ts +243 -0
- package/src/node/cli/index.ts +9 -0
- package/src/node/cli/ui.ts +54 -0
- package/src/node/cli-entry.ts +16 -16
- package/src/node/config.ts +54 -17
- package/src/node/index.ts +1 -1
- package/src/node/mdx/cache.ts +12 -0
- package/src/node/mdx/highlighter.ts +47 -0
- package/src/node/mdx/index.ts +114 -0
- package/src/node/mdx/rehype-shiki.ts +53 -0
- package/src/node/mdx/remark-shiki.ts +61 -0
- package/src/node/plugin/entry.ts +1 -1
- package/src/node/plugin/html.ts +8 -4
- package/src/node/plugin/index.ts +135 -72
- package/src/node/routes/index.ts +34 -13
- package/src/node/routes/parser.ts +13 -5
- package/src/node/search/index.ts +55 -0
- package/src/node/ssg/index.ts +15 -7
- package/src/node/ssg/robots.ts +7 -4
- package/src/node/utils.ts +32 -2
- package/tsup.config.ts +7 -2
- package/dist/chunk-52MVMZWS.mjs +0 -1
- package/dist/chunk-BVWWKXJH.mjs +0 -1
- package/dist/chunk-DVY3RDXD.mjs +0 -1
- package/dist/chunk-FUVYCYWC.mjs +0 -1
- package/dist/chunk-GBLMDJ2B.mjs +0 -1
- package/dist/chunk-ISPX45DF.mjs +0 -1
- package/dist/chunk-PNXZMUCO.mjs +0 -1
- package/dist/chunk-V2ZHKQSP.mjs +0 -74
- package/dist/client/components/mdx/index.js +0 -1
- package/dist/client/components/mdx/index.mjs +0 -1
- package/dist/client/hooks/index.js +0 -1
- package/dist/client/hooks/index.mjs +0 -1
- package/dist/search-dialog-TWGYKF2D.mjs +0 -1
- 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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 ||
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
63
|
+
resultParts.push(...routeContent)
|
|
64
|
+
|
|
65
|
+
const finalPath = `/${resultParts.join('/')}`
|
|
89
66
|
|
|
90
|
-
|
|
91
|
-
if (finalPath.endsWith('/')) {
|
|
92
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
9
|
+
const { routes, currentRoute } = useRoutes()
|
|
10
|
+
const location = useLocation()
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
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 ?
|
|
32
|
+
const prevPage = currentIndex > 0 ? contextRoutes[currentIndex - 1] : null
|
|
14
33
|
const nextPage =
|
|
15
|
-
currentIndex
|
|
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
|
-
//
|
|
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
|
|
24
|
+
// Derive current locale and version
|
|
25
|
+
// Priority: URL (currentRoute) > Zustand Store (Persistence) > Config Default
|
|
19
26
|
const currentLocale = config.i18n
|
|
20
|
-
? currentRoute?.locale ||
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
46
|
-
key
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
?
|
|
54
|
-
key,
|
|
55
|
-
label,
|
|
56
|
-
isCurrent:
|
|
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
|
|