boltdocs 1.10.2 → 1.11.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 (225) hide show
  1. package/package.json +29 -7
  2. package/src/client/app/config-context.tsx +18 -0
  3. package/src/client/app/docs-layout.tsx +14 -0
  4. package/src/client/app/index.tsx +132 -260
  5. package/src/client/app/mdx-component.tsx +52 -0
  6. package/src/client/app/mdx-components-context.tsx +23 -0
  7. package/src/client/app/mdx-page.tsx +20 -0
  8. package/src/client/app/preload.tsx +38 -30
  9. package/src/client/app/router.tsx +30 -0
  10. package/src/client/app/scroll-handler.tsx +40 -0
  11. package/src/client/app/theme-context.tsx +75 -0
  12. package/src/client/components/default-layout.tsx +80 -0
  13. package/src/client/components/docs-layout.tsx +105 -0
  14. package/src/client/components/icons-dev.tsx +74 -0
  15. package/src/client/components/mdx/admonition.tsx +107 -0
  16. package/src/client/components/mdx/badge.tsx +41 -0
  17. package/src/client/components/mdx/button.tsx +35 -0
  18. package/src/client/components/mdx/card.tsx +124 -0
  19. package/src/client/components/mdx/code-block.tsx +119 -0
  20. package/src/client/components/mdx/component-preview.tsx +47 -0
  21. package/src/client/components/mdx/component-props.tsx +83 -0
  22. package/src/client/components/mdx/field.tsx +66 -0
  23. package/src/client/components/mdx/file-tree.tsx +287 -0
  24. package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
  25. package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
  26. package/src/client/components/mdx/hooks/useTable.ts +74 -0
  27. package/src/client/components/mdx/hooks/useTabs.ts +68 -0
  28. package/src/client/components/mdx/image.tsx +23 -0
  29. package/src/client/components/mdx/index.ts +53 -0
  30. package/src/client/components/mdx/link.tsx +38 -0
  31. package/src/client/components/mdx/list.tsx +192 -0
  32. package/src/client/components/mdx/table.tsx +156 -0
  33. package/src/client/components/mdx/tabs.tsx +135 -0
  34. package/src/client/components/mdx/video.tsx +68 -0
  35. package/src/client/components/primitives/breadcrumbs.tsx +79 -0
  36. package/src/client/components/primitives/button-group.tsx +54 -0
  37. package/src/client/components/primitives/button.tsx +145 -0
  38. package/src/client/components/primitives/helpers/observer.ts +120 -0
  39. package/src/client/components/primitives/index.ts +17 -0
  40. package/src/client/components/primitives/link.tsx +122 -0
  41. package/src/client/components/primitives/menu.tsx +159 -0
  42. package/src/client/components/primitives/navbar.tsx +359 -0
  43. package/src/client/components/primitives/navigation-menu.tsx +116 -0
  44. package/src/client/components/primitives/on-this-page.tsx +461 -0
  45. package/src/client/components/primitives/page-nav.tsx +87 -0
  46. package/src/client/components/primitives/popover.tsx +47 -0
  47. package/src/client/components/primitives/search-dialog.tsx +183 -0
  48. package/src/client/components/primitives/sidebar.tsx +154 -0
  49. package/src/client/components/primitives/tabs.tsx +90 -0
  50. package/src/client/components/primitives/tooltip.tsx +83 -0
  51. package/src/client/components/primitives/types.ts +11 -0
  52. package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
  53. package/src/client/components/ui-base/copy-markdown.tsx +112 -0
  54. package/src/client/components/ui-base/error-boundary.tsx +52 -0
  55. package/src/client/components/ui-base/github-stars.tsx +27 -0
  56. package/src/client/components/ui-base/head.tsx +69 -0
  57. package/src/client/components/ui-base/loading.tsx +87 -0
  58. package/src/client/components/ui-base/navbar.tsx +138 -0
  59. package/src/client/components/ui-base/not-found.tsx +24 -0
  60. package/src/client/components/ui-base/on-this-page.tsx +152 -0
  61. package/src/client/components/ui-base/page-nav.tsx +39 -0
  62. package/src/client/components/ui-base/powered-by.tsx +19 -0
  63. package/src/client/components/ui-base/progress-bar.tsx +67 -0
  64. package/src/client/components/ui-base/search-dialog.tsx +82 -0
  65. package/src/client/components/ui-base/sidebar.tsx +104 -0
  66. package/src/client/components/ui-base/tabs.tsx +65 -0
  67. package/src/client/components/ui-base/theme-toggle.tsx +32 -0
  68. package/src/client/hooks/index.ts +12 -0
  69. package/src/client/hooks/use-breadcrumbs.ts +22 -0
  70. package/src/client/hooks/use-i18n.ts +84 -0
  71. package/src/client/hooks/use-localized-to.ts +95 -0
  72. package/src/client/hooks/use-location.ts +5 -0
  73. package/src/client/hooks/use-navbar.ts +60 -0
  74. package/src/client/hooks/use-onthispage.ts +23 -0
  75. package/src/client/hooks/use-page-nav.ts +22 -0
  76. package/src/client/hooks/use-routes.ts +72 -0
  77. package/src/client/hooks/use-search.ts +71 -0
  78. package/src/client/hooks/use-sidebar.ts +49 -0
  79. package/src/client/hooks/use-tabs.ts +43 -0
  80. package/src/client/hooks/use-version.ts +78 -0
  81. package/src/client/index.ts +55 -17
  82. package/src/client/integrations/codesandbox.ts +179 -0
  83. package/src/client/ssr.tsx +27 -16
  84. package/src/client/theme/neutral.css +360 -0
  85. package/src/client/types.ts +131 -27
  86. package/src/client/utils/cn.ts +6 -0
  87. package/src/client/utils/copy-clipboard.ts +22 -0
  88. package/src/client/utils/get-base-file-path.ts +21 -0
  89. package/src/client/utils/github.ts +121 -0
  90. package/src/client/utils/use-on-change.ts +15 -0
  91. package/src/client/virtual.d.ts +24 -0
  92. package/src/node/cache.ts +156 -156
  93. package/src/node/config.ts +159 -103
  94. package/src/node/index.ts +13 -13
  95. package/src/node/mdx.ts +213 -61
  96. package/src/node/plugin/entry.ts +29 -18
  97. package/src/node/plugin/html.ts +11 -11
  98. package/src/node/plugin/index.ts +161 -83
  99. package/src/node/plugin/types.ts +2 -4
  100. package/src/node/routes/cache.ts +6 -6
  101. package/src/node/routes/index.ts +206 -113
  102. package/src/node/routes/parser.ts +106 -81
  103. package/src/node/routes/sorter.ts +15 -15
  104. package/src/node/routes/types.ts +24 -24
  105. package/src/node/ssg/index.ts +46 -46
  106. package/src/node/ssg/meta.ts +4 -4
  107. package/src/node/ssg/options.ts +5 -5
  108. package/src/node/ssg/sitemap.ts +14 -14
  109. package/src/node/utils.ts +31 -31
  110. package/tsconfig.json +25 -20
  111. package/tsup.config.ts +23 -14
  112. package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
  113. package/dist/SearchDialog-AGVF6JBO.mjs +0 -194
  114. package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
  115. package/dist/Video-KNTY5BNO.mjs +0 -6
  116. package/dist/cache-KNL5B4EE.mjs +0 -12
  117. package/dist/chunk-7SFUJWTB.mjs +0 -211
  118. package/dist/chunk-FFBNU6IJ.mjs +0 -386
  119. package/dist/chunk-FMTOYQLO.mjs +0 -37
  120. package/dist/chunk-TKLQWU7H.mjs +0 -1920
  121. package/dist/chunk-Z7JHYNAS.mjs +0 -57
  122. package/dist/client/index.css +0 -2847
  123. package/dist/client/index.d.mts +0 -372
  124. package/dist/client/index.d.ts +0 -372
  125. package/dist/client/index.js +0 -3630
  126. package/dist/client/index.mjs +0 -697
  127. package/dist/client/ssr.css +0 -2847
  128. package/dist/client/ssr.d.mts +0 -27
  129. package/dist/client/ssr.d.ts +0 -27
  130. package/dist/client/ssr.js +0 -2928
  131. package/dist/client/ssr.mjs +0 -33
  132. package/dist/config-BsFQ-ErD.d.mts +0 -159
  133. package/dist/config-BsFQ-ErD.d.ts +0 -159
  134. package/dist/node/index.d.mts +0 -91
  135. package/dist/node/index.d.ts +0 -91
  136. package/dist/node/index.js +0 -1187
  137. package/dist/node/index.mjs +0 -762
  138. package/dist/types-Dj-bfnC3.d.mts +0 -74
  139. package/dist/types-Dj-bfnC3.d.ts +0 -74
  140. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -61
  141. package/src/client/theme/components/CodeBlock/index.ts +0 -1
  142. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
  143. package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
  144. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
  145. package/src/client/theme/components/Playground/Playground.tsx +0 -180
  146. package/src/client/theme/components/Playground/index.ts +0 -1
  147. package/src/client/theme/components/Playground/playground.css +0 -238
  148. package/src/client/theme/components/Video/Video.tsx +0 -84
  149. package/src/client/theme/components/Video/index.ts +0 -1
  150. package/src/client/theme/components/Video/video.css +0 -41
  151. package/src/client/theme/components/mdx/Admonition.tsx +0 -80
  152. package/src/client/theme/components/mdx/Badge.tsx +0 -31
  153. package/src/client/theme/components/mdx/Button.tsx +0 -50
  154. package/src/client/theme/components/mdx/Card.tsx +0 -80
  155. package/src/client/theme/components/mdx/Field.tsx +0 -60
  156. package/src/client/theme/components/mdx/FileTree.tsx +0 -229
  157. package/src/client/theme/components/mdx/List.tsx +0 -57
  158. package/src/client/theme/components/mdx/Table.tsx +0 -151
  159. package/src/client/theme/components/mdx/Tabs.tsx +0 -123
  160. package/src/client/theme/components/mdx/index.ts +0 -27
  161. package/src/client/theme/components/mdx/mdx-components.css +0 -764
  162. package/src/client/theme/icons/bun.tsx +0 -62
  163. package/src/client/theme/icons/deno.tsx +0 -20
  164. package/src/client/theme/icons/discord.tsx +0 -12
  165. package/src/client/theme/icons/github.tsx +0 -15
  166. package/src/client/theme/icons/npm.tsx +0 -13
  167. package/src/client/theme/icons/pnpm.tsx +0 -72
  168. package/src/client/theme/icons/twitter.tsx +0 -12
  169. package/src/client/theme/styles/markdown.css +0 -394
  170. package/src/client/theme/styles/variables.css +0 -175
  171. package/src/client/theme/styles.css +0 -39
  172. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
  173. package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
  174. package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
  175. package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
  176. package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
  177. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
  178. package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
  179. package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
  180. package/src/client/theme/ui/Footer/footer.css +0 -32
  181. package/src/client/theme/ui/Head/Head.tsx +0 -69
  182. package/src/client/theme/ui/Head/index.ts +0 -1
  183. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
  184. package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
  185. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
  186. package/src/client/theme/ui/Layout/Layout.tsx +0 -203
  187. package/src/client/theme/ui/Layout/base.css +0 -106
  188. package/src/client/theme/ui/Layout/index.ts +0 -2
  189. package/src/client/theme/ui/Layout/pagination.css +0 -72
  190. package/src/client/theme/ui/Layout/responsive.css +0 -47
  191. package/src/client/theme/ui/Link/Link.tsx +0 -392
  192. package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
  193. package/src/client/theme/ui/Link/index.ts +0 -2
  194. package/src/client/theme/ui/Link/link-preview.css +0 -48
  195. package/src/client/theme/ui/Loading/Loading.tsx +0 -10
  196. package/src/client/theme/ui/Loading/index.ts +0 -1
  197. package/src/client/theme/ui/Loading/loading.css +0 -30
  198. package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
  199. package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
  200. package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
  201. package/src/client/theme/ui/Navbar/index.ts +0 -2
  202. package/src/client/theme/ui/Navbar/navbar.css +0 -347
  203. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
  204. package/src/client/theme/ui/NotFound/index.ts +0 -1
  205. package/src/client/theme/ui/NotFound/not-found.css +0 -64
  206. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
  207. package/src/client/theme/ui/OnThisPage/index.ts +0 -1
  208. package/src/client/theme/ui/OnThisPage/toc.css +0 -152
  209. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
  210. package/src/client/theme/ui/PoweredBy/index.ts +0 -1
  211. package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
  212. package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
  213. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
  214. package/src/client/theme/ui/ProgressBar/index.ts +0 -1
  215. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
  216. package/src/client/theme/ui/SearchDialog/index.ts +0 -1
  217. package/src/client/theme/ui/SearchDialog/search.css +0 -152
  218. package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
  219. package/src/client/theme/ui/Sidebar/index.ts +0 -1
  220. package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
  221. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
  222. package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
  223. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
  224. package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
  225. package/src/client/utils.ts +0 -49
@@ -0,0 +1,82 @@
1
+ import { useEffect, useCallback } from 'react'
2
+ import { useSearch } from '@hooks/use-search'
3
+ import {
4
+ SearchDialogAutocomplete,
5
+ SearchDialogInput,
6
+ SearchDialogItemBio,
7
+ SearchDialogItemIcon,
8
+ SearchDialogItemRoot,
9
+ SearchDialogItemTitle,
10
+ SearchDialogList,
11
+ SearchDialogRoot,
12
+ } from '@components/primitives/search-dialog'
13
+ import Navbar from '@components/primitives/navbar'
14
+ import { useNavigate } from 'react-router-dom'
15
+
16
+ export function SearchDialog({ routes }: { routes: any[] }) {
17
+ const { isOpen, setIsOpen, query, setQuery, list } = useSearch(routes)
18
+ const navigate = useNavigate()
19
+
20
+ useEffect(() => {
21
+ const handleKeyDown = (e: KeyboardEvent) => {
22
+ const isMac = /Mac/.test(navigator.userAgent)
23
+ const isMeta = isMac ? e.metaKey : e.ctrlKey
24
+
25
+ if (isMeta && (e.key === 'k' || e.key === 'j')) {
26
+ e.preventDefault()
27
+ setIsOpen((prev) => !prev)
28
+ }
29
+ }
30
+ window.addEventListener('keydown', handleKeyDown)
31
+ return () => window.removeEventListener('keydown', handleKeyDown)
32
+ }, [setIsOpen])
33
+
34
+ const handleSelect = useCallback(
35
+ (key: React.Key) => {
36
+ const path = String(key)
37
+ setIsOpen(false)
38
+
39
+ if (path.includes('#')) {
40
+ const [p, id] = path.split('#')
41
+ navigate(p)
42
+ setTimeout(() => {
43
+ const el = document.getElementById(id)
44
+ if (el) el.scrollIntoView({ behavior: 'smooth' })
45
+ }, 100)
46
+ } else {
47
+ navigate(path)
48
+ }
49
+ },
50
+ [navigate, setIsOpen],
51
+ )
52
+
53
+ return (
54
+ <>
55
+ <Navbar.SearchTrigger onPress={() => setIsOpen(true)} />
56
+
57
+ <SearchDialogRoot isOpen={isOpen} onOpenChange={setIsOpen}>
58
+ <SearchDialogAutocomplete onSelectionChange={handleSelect}>
59
+ <SearchDialogInput
60
+ value={query}
61
+ onChange={(e: any) => setQuery(e.target.value)}
62
+ />
63
+ <SearchDialogList items={list}>
64
+ {(item: any) => (
65
+ <SearchDialogItemRoot
66
+ key={item.id}
67
+ onPress={() => handleSelect(item.id)}
68
+ textValue={item.title}
69
+ >
70
+ <SearchDialogItemIcon isHeading={item.isHeading} />
71
+ <div className="flex flex-col justify-center gap-0.5">
72
+ <SearchDialogItemTitle>{item.title}</SearchDialogItemTitle>
73
+ <SearchDialogItemBio>{item.groupTitle}</SearchDialogItemBio>
74
+ </div>
75
+ </SearchDialogItemRoot>
76
+ )}
77
+ </SearchDialogList>
78
+ </SearchDialogAutocomplete>
79
+ </SearchDialogRoot>
80
+ </>
81
+ )
82
+ }
@@ -0,0 +1,104 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { useSidebar } from '@hooks/use-sidebar'
3
+ import SidebarPrimitive from '@components/primitives/sidebar'
4
+ import { PoweredBy } from './powered-by'
5
+ import * as LucideIcons from 'lucide-react'
6
+ import type { ComponentRoute } from '@client/types'
7
+ import type { BoltdocsConfig } from '@node/config'
8
+
9
+ function getIcon(iconName?: string): React.ElementType | undefined {
10
+ if (!iconName) return undefined
11
+ const IconComponent = (LucideIcons as Record<string, any>)[iconName]
12
+ return IconComponent || undefined
13
+ }
14
+
15
+ function CollapsibleSidebarGroup({
16
+ group,
17
+ activePath,
18
+ getIcon,
19
+ }: {
20
+ group: {
21
+ slug: string
22
+ title: string
23
+ routes: ComponentRoute[]
24
+ icon?: string
25
+ }
26
+ activePath: string
27
+ getIcon: (iconName?: string) => React.ElementType | undefined
28
+ }) {
29
+ const hasActiveRoute = useMemo(
30
+ () => group.routes.some((r) => r.path === activePath),
31
+ [group.routes, activePath],
32
+ )
33
+
34
+ const [isOpen, setIsOpen] = useState(true)
35
+
36
+ useEffect(() => {
37
+ if (hasActiveRoute) {
38
+ setIsOpen(true)
39
+ }
40
+ }, [hasActiveRoute])
41
+
42
+ return (
43
+ <SidebarPrimitive.SidebarGroup
44
+ title={group.title}
45
+ isOpen={isOpen}
46
+ onToggle={() => setIsOpen(!isOpen)}
47
+ >
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
+ ))}
58
+ </SidebarPrimitive.SidebarGroup>
59
+ )
60
+ }
61
+
62
+ export function Sidebar({
63
+ routes,
64
+ config,
65
+ }: {
66
+ routes: ComponentRoute[]
67
+ config: BoltdocsConfig
68
+ }) {
69
+ const { groups, ungrouped, activePath } = useSidebar(routes)
70
+
71
+ return (
72
+ <SidebarPrimitive.SidebarRoot>
73
+ {ungrouped.length > 0 && (
74
+ <SidebarPrimitive.SidebarGroup className="mb-6">
75
+ {ungrouped.map((route) => (
76
+ <SidebarPrimitive.SidebarLink
77
+ key={route.path}
78
+ label={route.title}
79
+ href={route.path}
80
+ active={activePath === route.path}
81
+ icon={getIcon(route.icon)}
82
+ badge={route.badge}
83
+ />
84
+ ))}
85
+ </SidebarPrimitive.SidebarGroup>
86
+ )}
87
+
88
+ {groups.map((group) => (
89
+ <CollapsibleSidebarGroup
90
+ key={group.slug}
91
+ group={group}
92
+ activePath={activePath}
93
+ getIcon={getIcon}
94
+ />
95
+ ))}
96
+
97
+ {config.themeConfig?.poweredBy && (
98
+ <div className="mt-auto pt-8">
99
+ <PoweredBy />
100
+ </div>
101
+ )}
102
+ </SidebarPrimitive.SidebarRoot>
103
+ )
104
+ }
@@ -0,0 +1,65 @@
1
+ import { useTabs as useTabsHook } from '@hooks/use-tabs'
2
+ import T from '@components/primitives/tabs'
3
+ import { Link } from '@components/primitives/link'
4
+ import type { BoltdocsTab, ComponentRoute } from '@client/types'
5
+ import * as Icons from 'lucide-react'
6
+
7
+ export function Tabs({
8
+ tabs,
9
+ routes,
10
+ }: {
11
+ tabs: BoltdocsTab[]
12
+ routes: ComponentRoute[]
13
+ }) {
14
+ const { indicatorStyle, tabRefs, activeIndex } = useTabsHook(tabs, routes)
15
+
16
+ const renderTabIcon = (iconName?: string) => {
17
+ if (!iconName) return null
18
+ if (iconName.trim().startsWith('<svg')) {
19
+ return (
20
+ <span
21
+ className="h-4 w-4"
22
+ dangerouslySetInnerHTML={{ __html: iconName }}
23
+ />
24
+ )
25
+ }
26
+ const LucideIcon = (Icons as Record<string, any>)[iconName]
27
+ if (LucideIcon) {
28
+ return <LucideIcon size={16} />
29
+ }
30
+ return <img src={iconName} alt="" className="h-4 w-4 object-contain" />
31
+ }
32
+
33
+ return (
34
+ <div className="mx-auto max-w-(--breakpoint-3xl) px-4 md:px-6">
35
+ <T.TabsList className="border-none py-0">
36
+ {tabs.map((tab, index) => {
37
+ const isActive = index === activeIndex
38
+ const firstRoute = routes.find(
39
+ (r) => r.tab && r.tab.toLowerCase() === tab.id.toLowerCase(),
40
+ )
41
+ const linkTo = firstRoute ? firstRoute.path : '#'
42
+
43
+ return (
44
+ <Link
45
+ key={tab.id}
46
+ href={linkTo}
47
+ ref={(el: HTMLAnchorElement | null) => {
48
+ tabRefs.current[index] = el
49
+ }}
50
+ className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors outline-none ${
51
+ isActive
52
+ ? 'text-primary-500'
53
+ : 'text-text-muted hover:text-text-main'
54
+ }`}
55
+ >
56
+ {renderTabIcon(tab.icon)}
57
+ <span>{tab.text}</span>
58
+ </Link>
59
+ )
60
+ })}
61
+ <T.TabsIndicator style={indicatorStyle} />
62
+ </T.TabsList>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,32 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Sun, Moon } from 'lucide-react'
3
+ import { useTheme } from '@client/app/theme-context'
4
+ import { ToggleButton } from 'react-aria-components'
5
+
6
+ export function ThemeToggle() {
7
+ const { theme, toggleTheme } = useTheme()
8
+ const [mounted, setMounted] = useState(false)
9
+
10
+ useEffect(() => {
11
+ setMounted(true)
12
+ }, [])
13
+
14
+ if (!mounted) {
15
+ return <div className="h-9 w-9" />
16
+ }
17
+
18
+ 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>
31
+ )
32
+ }
@@ -0,0 +1,12 @@
1
+ export * from './use-navbar'
2
+ export * from './use-sidebar'
3
+ export * from './use-search'
4
+ export * from './use-onthispage'
5
+ export * from './use-tabs'
6
+ export * from './use-version'
7
+ export * from './use-i18n'
8
+ export * from './use-page-nav'
9
+ export * from './use-breadcrumbs'
10
+ export * from './use-routes'
11
+ export * from './use-localized-to'
12
+ export * from './use-location'
@@ -0,0 +1,22 @@
1
+ import { useRoutes } from './use-routes'
2
+
3
+ /**
4
+ * Hook to generate breadcrumbs based on the current active route.
5
+ */
6
+ export function useBreadcrumbs() {
7
+ const { currentRoute: activeRoute } = useRoutes()
8
+
9
+ const crumbs: Array<{ label: string; href?: string }> = []
10
+
11
+ if (activeRoute) {
12
+ if (activeRoute.groupTitle) {
13
+ crumbs.push({ label: activeRoute.groupTitle })
14
+ }
15
+ crumbs.push({ label: activeRoute.title, href: activeRoute.path })
16
+ }
17
+
18
+ return {
19
+ crumbs,
20
+ activeRoute,
21
+ }
22
+ }
@@ -0,0 +1,84 @@
1
+ import { useNavigate } from 'react-router-dom'
2
+ import { getBaseFilePath } from '@client/utils/get-base-file-path'
3
+ import { useRoutes } from './use-routes'
4
+
5
+ export interface LocaleOption {
6
+ key: string
7
+ label: string
8
+ value: string
9
+ isCurrent: boolean
10
+ }
11
+
12
+ export interface UseI18nReturn {
13
+ currentLocale: string | undefined
14
+ currentLocaleLabel: string | undefined
15
+ availableLocales: LocaleOption[]
16
+ handleLocaleChange: (locale: string) => void
17
+ }
18
+
19
+ /**
20
+ * Hook to manage and switch between different locales (languages) of the documentation.
21
+ */
22
+ export function useI18n(): UseI18nReturn {
23
+ const navigate = useNavigate()
24
+ const routeContext = useRoutes()
25
+ const { allRoutes, currentRoute, currentLocale, config } = routeContext
26
+ const i18n = config.i18n
27
+
28
+ const handleLocaleChange = (locale: string) => {
29
+ if (!i18n || locale === currentLocale) return
30
+
31
+ let targetPath = '/'
32
+
33
+ if (currentRoute) {
34
+ const baseFile = getBaseFilePath(
35
+ currentRoute.filePath,
36
+ currentRoute.version,
37
+ currentRoute.locale,
38
+ )
39
+ const targetRoute = allRoutes.find(
40
+ (r) =>
41
+ getBaseFilePath(r.filePath, r.version, r.locale) === baseFile &&
42
+ (r.locale || i18n.defaultLocale) === locale &&
43
+ r.version === currentRoute.version,
44
+ )
45
+
46
+ if (targetRoute) {
47
+ targetPath = targetRoute.path
48
+ } else {
49
+ const defaultIndexRoute = allRoutes.find(
50
+ (r) =>
51
+ getBaseFilePath(r.filePath, r.version, r.locale) === 'index.md' &&
52
+ (r.locale || i18n.defaultLocale) === locale &&
53
+ r.version === currentRoute.version,
54
+ )
55
+ targetPath = defaultIndexRoute
56
+ ? defaultIndexRoute.path
57
+ : locale === i18n.defaultLocale
58
+ ? currentRoute.version
59
+ ? `/${currentRoute.version}`
60
+ : '/'
61
+ : currentRoute.version
62
+ ? `/${currentRoute.version}/${locale}`
63
+ : `/${locale}`
64
+ }
65
+ } else {
66
+ targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
67
+ }
68
+
69
+ navigate(targetPath)
70
+ }
71
+
72
+ const availableLocales = routeContext.availableLocales.map((l) => ({
73
+ ...l,
74
+ label: l.label as string,
75
+ value: l.key,
76
+ }))
77
+
78
+ return {
79
+ currentLocale,
80
+ currentLocaleLabel: routeContext.currentLocaleLabel,
81
+ availableLocales,
82
+ handleLocaleChange,
83
+ }
84
+ }
@@ -0,0 +1,95 @@
1
+ import { useLocation } from 'react-router-dom'
2
+ import { useConfig } from '@client/app/config-context'
3
+ import type { LinkProps as RouterLinkProps } from 'react-router-dom'
4
+
5
+ /**
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.
8
+ */
9
+ export function useLocalizedTo(to: RouterLinkProps['to']) {
10
+ const location = useLocation()
11
+ const config = useConfig()
12
+
13
+ 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
+
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++
66
+ }
67
+
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)
78
+ }
79
+ }
80
+ if (config.i18n) {
81
+ if (hasLocale) {
82
+ finalParts.push(toParts[hasVersion ? 1 : 0])
83
+ } else if (currentLocale) {
84
+ finalParts.push(currentLocale)
85
+ }
86
+ }
87
+
88
+ finalParts.push(...routeParts)
89
+
90
+ let finalPath = `${basePath}/${finalParts.join('/')}`
91
+ if (finalPath.endsWith('/')) {
92
+ finalPath = finalPath.slice(0, -1)
93
+ }
94
+ return finalPath === basePath ? basePath : finalPath
95
+ }
@@ -0,0 +1,5 @@
1
+ import { useLocation as useReactLocation } from 'react-router-dom'
2
+
3
+ export const useLocation = () => {
4
+ return useReactLocation()
5
+ }
@@ -0,0 +1,60 @@
1
+ import { useLocation } from 'react-router-dom'
2
+ import { useConfig } from '@client/app/config-context'
3
+ import { useTheme } from '@client/app/theme-context'
4
+ import type { NavbarLink } from '@client/types'
5
+
6
+ export function useNavbar() {
7
+ const config = useConfig()
8
+ const { theme } = useTheme()
9
+ const location = useLocation()
10
+
11
+ const themeConfig = config.themeConfig || {}
12
+ const title = themeConfig.title || 'Boltdocs'
13
+ const rawLinks = themeConfig.navbar || []
14
+ const socialLinks = themeConfig.socialLinks || []
15
+ const githubRepo = themeConfig.githubRepo
16
+
17
+ // Transform links to the new NavbarLink structure
18
+ const links: NavbarLink[] = rawLinks.map((item: any) => {
19
+ const href = item.href || item.to || item.link || ''
20
+ return {
21
+ label: item.label || item.text || '',
22
+ href,
23
+ active: location.pathname === href,
24
+ to:
25
+ href.startsWith('http') || href.startsWith('//')
26
+ ? 'external'
27
+ : undefined,
28
+ }
29
+ })
30
+
31
+ const logo = themeConfig.logo
32
+ const logoSrc = !logo
33
+ ? null
34
+ : typeof logo === 'string'
35
+ ? logo
36
+ : theme === 'dark'
37
+ ? (logo as any).dark
38
+ : (logo as any).light
39
+
40
+ const logoProps = {
41
+ alt:
42
+ (logo && typeof logo === 'object' ? (logo as any).alt : undefined) ||
43
+ title,
44
+ width: logo && typeof logo === 'object' ? (logo as any).width : undefined,
45
+ height: logo && typeof logo === 'object' ? (logo as any).height : undefined,
46
+ }
47
+
48
+ const github = githubRepo ? `https://github.com/${githubRepo}` : null
49
+
50
+ return {
51
+ links,
52
+ title,
53
+ logo: logoSrc,
54
+ logoProps,
55
+ github,
56
+ social: socialLinks,
57
+ config,
58
+ theme,
59
+ }
60
+ }
@@ -0,0 +1,23 @@
1
+ import { useState } from 'react'
2
+
3
+ interface Heading {
4
+ id: string
5
+ text: string
6
+ level: number
7
+ }
8
+
9
+ /**
10
+ * Hook to manage and provide current page headings for the OnThisPage component.
11
+ */
12
+ export function useOnThisPage(headings: Heading[] = []) {
13
+ const [activeId, setActiveId] = useState<string | null>(null)
14
+
15
+ // We keep the signature the same for backward compatibility,
16
+ // but the activeId tracking is now handled by AnchorProvider in the primitives.
17
+
18
+ return {
19
+ headings,
20
+ activeId,
21
+ setActiveId,
22
+ }
23
+ }
@@ -0,0 +1,22 @@
1
+ import { useRoutes } from './use-routes'
2
+
3
+ /**
4
+ * Hook to manage the previous and next button functionality for documentation pages.
5
+ */
6
+ export function usePageNav() {
7
+ const { routes } = useRoutes()
8
+ const currentPath = window.location.pathname
9
+
10
+ const currentIndex = routes.findIndex((r) => r.path === currentPath)
11
+ const currentRoute = routes[currentIndex]
12
+
13
+ const prevPage = currentIndex > 0 ? routes[currentIndex - 1] : null
14
+ const nextPage =
15
+ currentIndex < routes.length - 1 ? routes[currentIndex + 1] : null
16
+
17
+ return {
18
+ prevPage,
19
+ nextPage,
20
+ currentRoute,
21
+ }
22
+ }
@@ -0,0 +1,72 @@
1
+ import { useLocation } from 'react-router-dom'
2
+ import { useConfig } from '@client/app/config-context'
3
+ import { usePreload } from '@client/app/preload'
4
+
5
+ /**
6
+ * Hook to access the framework's routing state.
7
+ * Returns both the complete set of routes and a filtered list based on the current
8
+ * version and locale.
9
+ */
10
+ export function useRoutes() {
11
+ const { routes: allRoutes } = usePreload()
12
+ const config = useConfig()
13
+ const location = useLocation()
14
+
15
+ // Find the current route exactly matching the pathname
16
+ const currentRoute = allRoutes.find((r) => r.path === location.pathname)
17
+
18
+ // Derive current locale and version from the route or defaults
19
+ const currentLocale = config.i18n
20
+ ? currentRoute?.locale || config.i18n.defaultLocale
21
+ : undefined
22
+
23
+ const currentVersion = config.versions
24
+ ? currentRoute?.version || config.versions.defaultVersion
25
+ : undefined
26
+
27
+ // Filter routes to those matching the current version and locale
28
+ const routes = allRoutes.filter((r) => {
29
+ const localeMatch = config.i18n
30
+ ? (r.locale || config.i18n.defaultLocale) === currentLocale
31
+ : true
32
+ const versionMatch = config.versions
33
+ ? (r.version || config.versions.defaultVersion) === currentVersion
34
+ : true
35
+ return localeMatch && versionMatch
36
+ })
37
+
38
+ // Labels and lists for UI convenience
39
+ const currentLocaleLabel =
40
+ config.i18n?.locales[currentLocale as string] || currentLocale
41
+ const currentVersionLabel =
42
+ config.versions?.versions[currentVersion as string] || currentVersion
43
+
44
+ const availableLocales = config.i18n
45
+ ? Object.entries(config.i18n.locales).map(([key, label]) => ({
46
+ key,
47
+ label,
48
+ isCurrent: key === currentLocale,
49
+ }))
50
+ : []
51
+
52
+ const availableVersions = config.versions
53
+ ? Object.entries(config.versions.versions).map(([key, label]) => ({
54
+ key,
55
+ label,
56
+ isCurrent: key === currentVersion,
57
+ }))
58
+ : []
59
+
60
+ return {
61
+ routes,
62
+ allRoutes,
63
+ currentRoute,
64
+ currentLocale,
65
+ currentLocaleLabel,
66
+ availableLocales,
67
+ currentVersion,
68
+ currentVersionLabel,
69
+ availableVersions,
70
+ config,
71
+ }
72
+ }