boltdocs 2.2.0 → 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 +10 -0
- package/bin/boltdocs.js +2 -2
- package/dist/base-ui/index.d.mts +3 -3
- package/dist/base-ui/index.d.ts +3 -3
- package/dist/base-ui/index.js +1 -1
- package/dist/base-ui/index.mjs +1 -1
- package/dist/{cache-CRAZ55X7.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-ZK2266IZ.mjs → chunk-RPUERTVC.mjs} +1 -1
- package/dist/{chunk-MZBG4N4W.mjs → chunk-URTD6E6S.mjs} +1 -1
- package/dist/chunk-W2NB4T6V.mjs +1 -0
- package/dist/chunk-Y4RRHPXC.mjs +1 -0
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- 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/hooks/index.d.mts +7 -15
- package/dist/hooks/index.d.ts +7 -15
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/{loading-chS3pm9W.d.ts → loading-B7X5Wchs.d.ts} +3 -5
- package/dist/{loading-BqGrFWO5.d.mts → loading-WuaQbsKb.d.mts} +3 -5
- package/dist/mdx/index.js +1 -1
- package/dist/mdx/index.mjs +1 -1
- package/dist/node/cli-entry.js +27 -23
- package/dist/node/cli-entry.mjs +5 -1
- package/dist/node/index.js +10 -10
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +1 -1
- package/dist/primitives/index.mjs +1 -1
- package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
- package/package.json +2 -1
- package/src/client/app/theme-context.tsx +14 -7
- package/src/client/components/default-layout.tsx +6 -2
- package/src/client/components/primitives/navbar.tsx +3 -3
- package/src/client/components/primitives/search-dialog.tsx +4 -4
- package/src/client/components/primitives/sidebar.tsx +3 -2
- package/src/client/components/primitives/skeleton.tsx +26 -0
- package/src/client/components/ui-base/loading.tsx +43 -73
- package/src/client/components/ui-base/navbar.tsx +8 -6
- package/src/client/components/ui-base/page-nav.tsx +2 -1
- package/src/client/components/ui-base/powered-by.tsx +4 -1
- package/src/client/components/ui-base/search-dialog.tsx +16 -5
- package/src/client/components/ui-base/sidebar.tsx +4 -2
- package/src/client/hooks/use-i18n.ts +3 -2
- package/src/client/hooks/use-localized-to.ts +6 -5
- package/src/client/hooks/use-page-nav.ts +27 -6
- package/src/client/hooks/use-routes.ts +2 -1
- package/src/client/hooks/use-search.ts +81 -59
- package/src/client/hooks/use-sidebar.ts +2 -1
- package/src/client/store/use-boltdocs-store.ts +6 -5
- package/src/client/theme/neutral.css +29 -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 +1 -1
- package/src/node/plugin/entry.ts +1 -1
- package/src/node/plugin/index.ts +24 -10
- package/src/node/routes/parser.ts +9 -9
- package/src/node/search/index.ts +55 -0
- package/src/node/ssg/index.ts +14 -6
- package/src/node/ssg/robots.ts +7 -4
- package/dist/chunk-5D6XPYQ3.mjs +0 -74
- package/dist/chunk-6QXCKZAT.mjs +0 -1
- package/dist/chunk-H4M6P3DM.mjs +0 -1
- package/dist/chunk-JXHNX2WN.mjs +0 -1
- package/dist/chunk-Q3MLYTIQ.mjs +0 -1
- package/dist/chunk-RSII2UPE.mjs +0 -1
- package/dist/chunk-ZRJ55GGF.mjs +0 -1
- package/dist/search-dialog-MA5AISC7.mjs +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from '@client/utils/cn'
|
|
2
|
+
|
|
3
|
+
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
variant?: 'rect' | 'circle'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A flexible skeleton component that mimics the shape of content
|
|
9
|
+
* while it is loading. Features a smooth pulse animation.
|
|
10
|
+
*/
|
|
11
|
+
export function Skeleton({
|
|
12
|
+
className,
|
|
13
|
+
variant = 'rect',
|
|
14
|
+
...props
|
|
15
|
+
}: SkeletonProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
'animate-pulse bg-bg-muted',
|
|
20
|
+
variant === 'circle' ? 'rounded-full' : 'rounded-md',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -1,85 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cn } from '@client/utils/cn'
|
|
2
|
+
import { Skeleton } from '@primitives/skeleton'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* A premium loading component that
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* It features a glassmorphism container and a Bolt-style SVG logo
|
|
8
|
-
* with a dynamic fill effect.
|
|
5
|
+
* A premium loading component that only skeletons the markdown content area.
|
|
6
|
+
* Designed to be used as a Suspense fallback within a persistent layout.
|
|
9
7
|
*/
|
|
10
8
|
export function Loading() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
'w-full h-full relative overflow-y-auto transition-opacity duration-300 animate-fade-in',
|
|
13
|
+
)}
|
|
14
|
+
>
|
|
15
|
+
<div className="mx-auto max-w-(--spacing-content-max) px-4 py-8 space-y-10">
|
|
16
|
+
{/* Breadcrumbs */}
|
|
17
|
+
<div className="flex gap-2">
|
|
18
|
+
<Skeleton className="h-3 w-16" />
|
|
19
|
+
<Skeleton className="h-3 w-24" />
|
|
20
|
+
</div>
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
currentProgress += 1
|
|
20
|
-
if (currentProgress >= 100) {
|
|
21
|
-
currentProgress = 100
|
|
22
|
-
up = false
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
currentProgress -= 1
|
|
26
|
-
if (currentProgress <= 0) {
|
|
27
|
-
currentProgress = 0
|
|
28
|
-
up = true
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
setProgress(currentProgress)
|
|
32
|
-
}, 20)
|
|
22
|
+
{/* Page Title */}
|
|
23
|
+
<Skeleton className="h-10 w-[60%] sm:h-12" />
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
{/* Intro Paragraph */}
|
|
26
|
+
<div className="space-y-3">
|
|
27
|
+
<Skeleton className="h-4 w-full" />
|
|
28
|
+
<Skeleton className="h-4 w-[95%]" />
|
|
29
|
+
<Skeleton className="h-4 w-[40%]" />
|
|
30
|
+
</div>
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
{/* Section 1 */}
|
|
33
|
+
<div className="space-y-6 pt-4">
|
|
34
|
+
<Skeleton className="h-7 w-32" />
|
|
35
|
+
<div className="space-y-3">
|
|
36
|
+
<Skeleton className="h-4 w-full" />
|
|
37
|
+
<Skeleton className="h-4 w-[98%]" />
|
|
38
|
+
<Skeleton className="h-4 w-[92%]" />
|
|
39
|
+
<Skeleton className="h-4 w-[60%]" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<div className="relative group">
|
|
42
|
-
<div className="relative inline-block">
|
|
43
|
-
{/* SVG Background (Dimmed Base) */}
|
|
44
|
-
<svg
|
|
45
|
-
className="w-24 h-auto opacity-10 filter grayscale brightness-50"
|
|
46
|
-
viewBox="0 0 60 51"
|
|
47
|
-
fill="none"
|
|
48
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
-
role="img"
|
|
50
|
-
aria-hidden="true"
|
|
51
|
-
>
|
|
52
|
-
<title>Loading indicator background</title>
|
|
53
|
-
<path
|
|
54
|
-
d="M29.4449 0H19.4449V16.5L29.4449 6.5V0Z"
|
|
55
|
-
fill="currentColor"
|
|
56
|
-
/>
|
|
57
|
-
<path
|
|
58
|
-
d="M26.9449 22.7265C26.9449 22.5077 21.2201 27.0658 16.9449 28.5C13.7491 29.5721 12.3156 29.5038 8.94486 29.5C5.59532 29.4963 0 28.5 0 28.5C0 28.5 5.57953 28.5146 8.94486 27.5C12.5409 26.4158 14.8203 25.5843 17.9449 23.5C23.3445 19.898 29.4449 11.5 29.4449 11.5L29.9449 18C29.9449 18 33.5825 15.8308 36.4449 15C39.4452 14.1291 44.4449 14 44.4449 14C44.4449 14 36.9449 19 34.4449 21.5C31.5322 24.4126 29.8582 26.9017 29.4449 31C29.1217 34.2041 29.4771 36.4508 31.4449 39C33.5792 41.765 35.952 43.0183 39.4449 43C42.677 42.9831 45.3003 42.4182 47.4449 40C49.7406 37.4113 50.2495 34.4466 49.9449 31C49.6603 27.7804 48.4876 25.4953 45.9449 23.5C43.2931 21.4191 36.4449 24 36.4449 24L47.9449 15C47.9449 15 51.5761 16.771 53.4449 18.5C55.711 20.5967 56.7467 22.1546 57.9449 25C59.1784 27.9295 59.4832 29.8216 59.4449 33C59.4089 35.9867 59.179 37.78 57.9449 40.5C56.8475 42.9185 55.8511 44.6507 53.9449 46.5C51.9236 48.4609 50.5803 49.0076 47.9449 50C45.5414 50.9051 44.0131 51 41.4449 51C38.8766 51 37.3235 50.9685 34.9449 50C32.4851 48.9985 29.4449 46 29.4449 46V51H19.4449V37.9904L22.9449 31.4226L26.9449 22.7265Z"
|
|
59
|
-
fill="currentColor"
|
|
60
|
-
/>
|
|
61
|
-
</svg>
|
|
43
|
+
{/* Code Block Placeholder */}
|
|
44
|
+
<Skeleton className="h-32 w-full rounded-lg bg-bg-muted/50" />
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
role="img"
|
|
71
|
-
aria-hidden="true"
|
|
72
|
-
>
|
|
73
|
-
<title>Loading indicator animated fill</title>
|
|
74
|
-
<path
|
|
75
|
-
d="M29.4449 0H19.4449V16.5L29.4449 6.5V0Z"
|
|
76
|
-
fill="currentColor"
|
|
77
|
-
/>
|
|
78
|
-
<path
|
|
79
|
-
d="M26.9449 22.7265C26.9449 22.5077 21.2201 27.0658 16.9449 28.5C13.7491 29.5721 12.3156 29.5038 8.94486 29.5C5.59532 29.4963 0 28.5 0 28.5C0 28.5 5.57953 28.5146 8.94486 27.5C12.5409 26.4158 14.8203 25.5843 17.9449 23.5C23.3445 19.898 29.4449 11.5 29.4449 11.5L29.9449 18C29.9449 18 33.5825 15.8308 36.4449 15C39.4452 14.1291 44.4449 14 44.4449 14C44.4449 14 36.9449 19 34.4449 21.5C31.5322 24.4126 29.8582 26.9017 29.4449 31C29.1217 34.2041 29.4771 36.4508 31.4449 39C33.5792 41.765 35.952 43.0183 39.4449 43C42.677 42.9831 45.3003 42.4182 47.4449 40C49.7406 37.4113 50.2495 34.4466 49.9449 31C49.6603 27.7804 48.4876 25.4953 45.9449 23.5C43.2931 21.4191 36.4449 24 36.4449 24L47.9449 15C47.9449 15 51.5761 16.771 53.4449 18.5C55.711 20.5967 56.7467 22.1546 57.9449 25C59.1784 27.9295 59.4832 29.8216 59.4449 33C59.4089 35.9867 59.179 37.78 57.9449 40.5C56.8475 42.9185 55.8511 44.6507 53.9449 46.5C51.9236 48.4609 50.5803 49.0076 47.9449 50C45.5414 50.9051 44.0131 51 41.4449 51C38.8766 51 37.3235 50.9685 34.9449 50C32.4851 48.9985 29.4449 46 29.4449 46V51H19.4449V37.9904L22.9449 31.4226L26.9449 22.7265Z"
|
|
80
|
-
fill="currentColor"
|
|
81
|
-
/>
|
|
82
|
-
</svg>
|
|
46
|
+
{/* Section 2 */}
|
|
47
|
+
<div className="space-y-6 pt-4">
|
|
48
|
+
<Skeleton className="h-7 w-48" />
|
|
49
|
+
<div className="space-y-3">
|
|
50
|
+
<Skeleton className="h-4 w-full" />
|
|
51
|
+
<Skeleton className="h-4 w-[85%]" />
|
|
52
|
+
</div>
|
|
83
53
|
</div>
|
|
84
54
|
</div>
|
|
85
55
|
</div>
|
|
@@ -33,12 +33,14 @@ export function Navbar() {
|
|
|
33
33
|
<NavbarPrimitive.NavbarRoot className={hasTabs ? 'border-b-0' : ''}>
|
|
34
34
|
<NavbarPrimitive.Content>
|
|
35
35
|
<NavbarPrimitive.NavbarLeft>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
{logo && (
|
|
37
|
+
<NavbarPrimitive.NavbarLogo
|
|
38
|
+
src={logo}
|
|
39
|
+
alt={logoProps?.alt || title}
|
|
40
|
+
width={logoProps?.width ?? 24}
|
|
41
|
+
height={logoProps?.height ?? 24}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
42
44
|
<NavbarPrimitive.Title>{title}</NavbarPrimitive.Title>
|
|
43
45
|
|
|
44
46
|
{config.versions && currentVersion && <NavbarVersion />}
|
|
@@ -3,6 +3,7 @@ import PageNavPrimitive from '@components/primitives/page-nav'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Component to display the previous and next page navigation buttons.
|
|
6
|
+
* Enhanced with subtle entrance animations and a modern card layout.
|
|
6
7
|
*/
|
|
7
8
|
export function PageNav() {
|
|
8
9
|
const { prevPage, nextPage } = usePageNav()
|
|
@@ -10,7 +11,7 @@ export function PageNav() {
|
|
|
10
11
|
if (!prevPage && !nextPage) return null
|
|
11
12
|
|
|
12
13
|
return (
|
|
13
|
-
<PageNavPrimitive.PageNavRoot>
|
|
14
|
+
<PageNavPrimitive.PageNavRoot className="animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
14
15
|
{prevPage ? (
|
|
15
16
|
<PageNavPrimitive.PageNavLink to={prevPage.path} direction="prev">
|
|
16
17
|
<PageNavPrimitive.PageNavLink.Title>
|
|
@@ -14,7 +14,10 @@ export function PoweredBy() {
|
|
|
14
14
|
fill="currentColor"
|
|
15
15
|
/>
|
|
16
16
|
<span className="text-[11px] font-medium text-text-muted group-hover:text-text-main transition-colors duration-300 tracking-wide">
|
|
17
|
-
Powered by
|
|
17
|
+
Powered by{' '}
|
|
18
|
+
<strong className="font-bold text-text-main/80 group-hover:text-text-main">
|
|
19
|
+
Boltdocs
|
|
20
|
+
</strong>
|
|
18
21
|
</span>
|
|
19
22
|
</a>
|
|
20
23
|
</div>
|
|
@@ -12,8 +12,17 @@ import {
|
|
|
12
12
|
} from '@components/primitives/search-dialog'
|
|
13
13
|
import Navbar from '@components/primitives/navbar'
|
|
14
14
|
import { useNavigate } from 'react-router-dom'
|
|
15
|
+
import type { ComponentRoute } from '@client/types'
|
|
16
|
+
interface SearchResult {
|
|
17
|
+
id: string
|
|
18
|
+
title: string
|
|
19
|
+
path: string
|
|
20
|
+
bio: string
|
|
21
|
+
groupTitle?: string
|
|
22
|
+
isHeading?: boolean
|
|
23
|
+
}
|
|
15
24
|
|
|
16
|
-
export function SearchDialog({ routes }: { routes:
|
|
25
|
+
export function SearchDialog({ routes }: { routes: ComponentRoute[] }) {
|
|
17
26
|
const { isOpen, setIsOpen, query, setQuery, list } = useSearch(routes)
|
|
18
27
|
const navigate = useNavigate()
|
|
19
28
|
|
|
@@ -58,10 +67,12 @@ export function SearchDialog({ routes }: { routes: any[] }) {
|
|
|
58
67
|
<SearchDialogAutocomplete onSelectionChange={handleSelect}>
|
|
59
68
|
<SearchDialogInput
|
|
60
69
|
value={query}
|
|
61
|
-
onChange={(e:
|
|
70
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
71
|
+
setQuery(e.target.value)
|
|
72
|
+
}
|
|
62
73
|
/>
|
|
63
|
-
<SearchDialogList items={list}>
|
|
64
|
-
{(item:
|
|
74
|
+
<SearchDialogList items={list as SearchResult[]}>
|
|
75
|
+
{(item: SearchResult) => (
|
|
65
76
|
<SearchDialogItemRoot
|
|
66
77
|
key={item.id}
|
|
67
78
|
onPress={() => handleSelect(item.id)}
|
|
@@ -70,7 +81,7 @@ export function SearchDialog({ routes }: { routes: any[] }) {
|
|
|
70
81
|
<SearchDialogItemIcon isHeading={item.isHeading} />
|
|
71
82
|
<div className="flex flex-col justify-center gap-0.5">
|
|
72
83
|
<SearchDialogItemTitle>{item.title}</SearchDialogItemTitle>
|
|
73
|
-
<SearchDialogItemBio>{item.
|
|
84
|
+
<SearchDialogItemBio>{item.bio}</SearchDialogItemBio>
|
|
74
85
|
</div>
|
|
75
86
|
</SearchDialogItemRoot>
|
|
76
87
|
)}
|
|
@@ -48,7 +48,8 @@ function CollapsibleSidebarGroup({
|
|
|
48
48
|
>
|
|
49
49
|
{group.routes.map((route: ComponentRoute) => {
|
|
50
50
|
const isCurrent =
|
|
51
|
-
activePath ===
|
|
51
|
+
activePath ===
|
|
52
|
+
(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
|
|
52
53
|
return (
|
|
53
54
|
<SidebarPrimitive.SidebarLink
|
|
54
55
|
key={route.path}
|
|
@@ -80,7 +81,8 @@ export function Sidebar({
|
|
|
80
81
|
<SidebarPrimitive.SidebarGroup className="mb-6">
|
|
81
82
|
{ungrouped.map((route) => {
|
|
82
83
|
const isCurrent =
|
|
83
|
-
activePath ===
|
|
84
|
+
activePath ===
|
|
85
|
+
(route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
|
|
84
86
|
return (
|
|
85
87
|
<SidebarPrimitive.SidebarLink
|
|
86
88
|
key={route.path}
|
|
@@ -29,7 +29,7 @@ export function useI18n(): UseI18nReturn {
|
|
|
29
29
|
|
|
30
30
|
const handleLocaleChange = (locale: string) => {
|
|
31
31
|
if (!i18n || locale === currentLocale) return
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
// Update store
|
|
34
34
|
setLocale(locale)
|
|
35
35
|
|
|
@@ -87,7 +87,8 @@ export function useI18n(): UseI18nReturn {
|
|
|
87
87
|
navigate(targetPath)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const currentLocaleConfig =
|
|
90
|
+
const currentLocaleConfig =
|
|
91
|
+
config.i18n?.localeConfigs?.[currentLocale as string]
|
|
91
92
|
const currentLocaleLabel =
|
|
92
93
|
currentLocaleConfig?.label ||
|
|
93
94
|
config.i18n?.locales[currentLocale as string] ||
|
|
@@ -8,10 +8,11 @@ import { useRoutes } from './use-routes'
|
|
|
8
8
|
*/
|
|
9
9
|
export function useLocalizedTo(to: RouterLinkProps['to']) {
|
|
10
10
|
const config = useConfig()
|
|
11
|
-
const { currentLocale: activeLocale, currentVersion: activeVersion } =
|
|
11
|
+
const { currentLocale: activeLocale, currentVersion: activeVersion } =
|
|
12
|
+
useRoutes()
|
|
12
13
|
|
|
13
14
|
if (!config || typeof to !== 'string') return to
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
// External or absolute links don't need localization
|
|
16
17
|
if (to.startsWith('http') || to.startsWith('//')) return to
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ export function useLocalizedTo(to: RouterLinkProps['to']) {
|
|
|
22
23
|
|
|
23
24
|
// 1. Identify the input intent
|
|
24
25
|
const isDocLink = to.startsWith('/docs')
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
// 3. Clean the 'to' path of ANY existing prefixes to avoid stacking
|
|
27
28
|
const parts = to.split('/').filter(Boolean)
|
|
28
29
|
let pIdx = 0
|
|
@@ -32,7 +33,7 @@ export function useLocalizedTo(to: RouterLinkProps['to']) {
|
|
|
32
33
|
|
|
33
34
|
// Strip versions if present
|
|
34
35
|
if (versions && parts.length > pIdx) {
|
|
35
|
-
const vMatch = versions.versions.find(v => v.path === parts[pIdx])
|
|
36
|
+
const vMatch = versions.versions.find((v) => v.path === parts[pIdx])
|
|
36
37
|
if (vMatch) pIdx++
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -62,7 +63,7 @@ export function useLocalizedTo(to: RouterLinkProps['to']) {
|
|
|
62
63
|
resultParts.push(...routeContent)
|
|
63
64
|
|
|
64
65
|
const finalPath = `/${resultParts.join('/')}`
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
// Cleanup trailing slashes unless it's just root
|
|
67
68
|
if (finalPath.length > 1 && finalPath.endsWith('/')) {
|
|
68
69
|
return finalPath.slice(0, -1)
|
|
@@ -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,
|
|
@@ -71,7 +71,8 @@ export function useRoutes() {
|
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
// Labels and lists for UI convenience
|
|
74
|
-
const currentLocaleConfig =
|
|
74
|
+
const currentLocaleConfig =
|
|
75
|
+
config.i18n?.localeConfigs?.[currentLocale as string]
|
|
75
76
|
const currentLocaleLabel =
|
|
76
77
|
currentLocaleConfig?.label ||
|
|
77
78
|
config.i18n?.locales[currentLocale as string] ||
|
|
@@ -1,77 +1,98 @@
|
|
|
1
|
-
import { useState, useMemo } from 'react'
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react'
|
|
2
|
+
import { Index } from 'flexsearch'
|
|
2
3
|
import { useRoutes } from './use-routes'
|
|
3
4
|
import type { ComponentRoute } from '@client/types'
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import searchData from 'virtual:boltdocs-search'
|
|
7
|
+
|
|
8
|
+
interface SearchDataItem {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
content: string
|
|
12
|
+
url: string
|
|
13
|
+
display: string
|
|
14
|
+
locale?: string
|
|
15
|
+
version?: string
|
|
16
|
+
}
|
|
4
17
|
|
|
5
18
|
export function useSearch(routes: ComponentRoute[]) {
|
|
6
19
|
const { currentLocale, currentVersion } = useRoutes()
|
|
7
20
|
const [isOpen, setIsOpen] = useState(false)
|
|
8
21
|
const [query, setQuery] = useState('')
|
|
22
|
+
const [index, setIndex] = useState<Index | null>(null)
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
// Initialize FlexSearch index once
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!isOpen || index) return
|
|
27
|
+
|
|
28
|
+
const newIndex = new Index({
|
|
29
|
+
preset: 'match',
|
|
30
|
+
tokenize: 'full',
|
|
31
|
+
resolution: 9,
|
|
32
|
+
cache: true,
|
|
16
33
|
})
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
title: r.title,
|
|
22
|
-
path: r.path,
|
|
23
|
-
bio: r.description || '',
|
|
24
|
-
groupTitle: r.groupTitle,
|
|
25
|
-
}))
|
|
35
|
+
// Index all documents
|
|
36
|
+
for (const doc of searchData as SearchDataItem[]) {
|
|
37
|
+
newIndex.add(doc.id, `${doc.title} ${doc.content}`)
|
|
26
38
|
}
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (route.title?.toLowerCase().includes(lowerQuery)) {
|
|
40
|
-
results.push({
|
|
41
|
-
id: route.path,
|
|
42
|
-
title: route.title,
|
|
43
|
-
path: route.path,
|
|
44
|
-
bio: route.description || '',
|
|
45
|
-
groupTitle: route.groupTitle,
|
|
40
|
+
setIndex(newIndex)
|
|
41
|
+
}, [isOpen, index])
|
|
42
|
+
|
|
43
|
+
const list = useMemo(() => {
|
|
44
|
+
if (!query) {
|
|
45
|
+
// Default results: just active routes
|
|
46
|
+
return routes
|
|
47
|
+
.filter((r) => {
|
|
48
|
+
const localeMatch = !currentLocale || r.locale === currentLocale
|
|
49
|
+
const versionMatch = !currentVersion || r.version === currentVersion
|
|
50
|
+
return localeMatch && versionMatch
|
|
46
51
|
})
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
path: `${route.path}#${heading.id}`,
|
|
56
|
-
bio: `Heading in ${route.title}`,
|
|
57
|
-
groupTitle: route.title,
|
|
58
|
-
isHeading: true,
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
52
|
+
.slice(0, 10)
|
|
53
|
+
.map((r) => ({
|
|
54
|
+
id: r.path,
|
|
55
|
+
title: r.title,
|
|
56
|
+
path: r.path,
|
|
57
|
+
bio: r.description || '',
|
|
58
|
+
groupTitle: r.groupTitle,
|
|
59
|
+
}))
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
if (!index) return []
|
|
63
|
+
|
|
64
|
+
const searchResults = index.search(query, {
|
|
65
|
+
limit: 20,
|
|
66
|
+
suggest: true,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const results: any[] = []
|
|
70
|
+
const seen = new Set<string>()
|
|
71
|
+
|
|
72
|
+
for (const id of searchResults) {
|
|
73
|
+
const doc = (searchData as SearchDataItem[]).find((d) => d.id === id)
|
|
74
|
+
if (!doc) continue
|
|
75
|
+
|
|
76
|
+
// Filter by locale and version
|
|
77
|
+
const localeMatch = !currentLocale || doc.locale === currentLocale
|
|
78
|
+
const versionMatch = !currentVersion || doc.version === currentVersion
|
|
79
|
+
if (!localeMatch || !versionMatch) continue
|
|
80
|
+
|
|
81
|
+
if (seen.has(doc.url)) continue
|
|
82
|
+
seen.add(doc.url)
|
|
83
|
+
|
|
84
|
+
results.push({
|
|
85
|
+
id: doc.url,
|
|
86
|
+
title: doc.title,
|
|
87
|
+
path: doc.url,
|
|
88
|
+
bio: doc.display,
|
|
89
|
+
groupTitle: doc.display.split(' > ')[0],
|
|
90
|
+
isHeading: doc.url.includes('#'),
|
|
72
91
|
})
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results.slice(0, 10)
|
|
95
|
+
}, [query, index, currentLocale, currentVersion, routes])
|
|
75
96
|
|
|
76
97
|
return {
|
|
77
98
|
isOpen,
|
|
@@ -81,7 +102,8 @@ export function useSearch(routes: ComponentRoute[]) {
|
|
|
81
102
|
list,
|
|
82
103
|
input: {
|
|
83
104
|
value: query,
|
|
84
|
-
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
105
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
106
|
+
setQuery(e.target.value),
|
|
85
107
|
},
|
|
86
108
|
}
|
|
87
109
|
}
|
|
@@ -7,7 +7,8 @@ export function useSidebar(routes: ComponentRoute[]) {
|
|
|
7
7
|
const location = useLocation()
|
|
8
8
|
|
|
9
9
|
// Find active route and tab
|
|
10
|
-
const normalize = (p: string) =>
|
|
10
|
+
const normalize = (p: string) =>
|
|
11
|
+
p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
|
|
11
12
|
const currentPath = normalize(location.pathname)
|
|
12
13
|
|
|
13
14
|
const activeRoute = routes.find((r) => normalize(r.path) === currentPath)
|
|
@@ -5,7 +5,7 @@ interface BoltdocsState {
|
|
|
5
5
|
currentLocale: string | undefined
|
|
6
6
|
currentVersion: string | undefined
|
|
7
7
|
hasHydrated: boolean
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
// Actions
|
|
10
10
|
setLocale: (locale: string | undefined) => void
|
|
11
11
|
setVersion: (version: string | undefined) => void
|
|
@@ -22,9 +22,10 @@ export const useBoltdocsStore = create<BoltdocsState>()(
|
|
|
22
22
|
currentLocale: undefined,
|
|
23
23
|
currentVersion: undefined,
|
|
24
24
|
hasHydrated: false,
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
setLocale: (locale: string | undefined) => set({ currentLocale: locale }),
|
|
27
|
-
setVersion: (version: string | undefined) =>
|
|
27
|
+
setVersion: (version: string | undefined) =>
|
|
28
|
+
set({ currentVersion: version }),
|
|
28
29
|
setHasHydrated: (val: boolean) => set({ hasHydrated: val }),
|
|
29
30
|
}),
|
|
30
31
|
{
|
|
@@ -38,6 +39,6 @@ export const useBoltdocsStore = create<BoltdocsState>()(
|
|
|
38
39
|
onRehydrateStorage: () => (state?: BoltdocsState) => {
|
|
39
40
|
state?.setHasHydrated(true)
|
|
40
41
|
},
|
|
41
|
-
}
|
|
42
|
-
)
|
|
42
|
+
},
|
|
43
|
+
),
|
|
43
44
|
)
|
|
@@ -67,6 +67,35 @@
|
|
|
67
67
|
--spacing-sidebar: 16rem;
|
|
68
68
|
--spacing-toc: 14rem;
|
|
69
69
|
--spacing-content-max: 48rem;
|
|
70
|
+
|
|
71
|
+
@keyframes pulse {
|
|
72
|
+
0%,
|
|
73
|
+
100% {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
}
|
|
76
|
+
50% {
|
|
77
|
+
opacity: 0.5;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@keyframes fade-in {
|
|
82
|
+
from {
|
|
83
|
+
opacity: 0;
|
|
84
|
+
transform: translateY(10px);
|
|
85
|
+
}
|
|
86
|
+
to {
|
|
87
|
+
opacity: 1;
|
|
88
|
+
transform: translateY(0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.animate-pulse {
|
|
94
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.animate-fade-in {
|
|
98
|
+
animation: fade-in 0.5s ease-out forwards;
|
|
70
99
|
}
|
|
71
100
|
|
|
72
101
|
:root[data-theme="dark"],
|