boltdocs 2.6.2 → 2.7.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 (177) hide show
  1. package/bin/boltdocs.js +0 -1
  2. package/dist/cache-CQKlT4fI.mjs +6 -0
  3. package/dist/cache-DorPMFgW.cjs +6 -0
  4. package/dist/cards-BLoSiRuL.d.ts +30 -0
  5. package/dist/cards-CQn9mXZS.d.cts +30 -0
  6. package/dist/chunk-Ds5LZdWN.cjs +6 -0
  7. package/dist/client/index.cjs +1 -1
  8. package/dist/client/index.d.cts +167 -1338
  9. package/dist/client/index.d.ts +166 -1337
  10. package/dist/client/index.js +1 -1
  11. package/dist/{package-CFP44vfn.cjs → client/mdx.cjs} +1 -1
  12. package/dist/client/mdx.d.cts +128 -0
  13. package/dist/client/mdx.d.ts +129 -0
  14. package/dist/client/mdx.js +6 -0
  15. package/dist/client/primitives.cjs +6 -0
  16. package/dist/client/primitives.d.cts +818 -0
  17. package/dist/client/primitives.d.ts +818 -0
  18. package/dist/client/primitives.js +6 -0
  19. package/dist/client/theme/neutral.css +74 -361
  20. package/dist/client/theme/reset.css +189 -0
  21. package/dist/docs-layout-BlDhcQRv.cjs +6 -0
  22. package/dist/docs-layout-BvAOWEJw.js +6 -0
  23. package/dist/doctor-BQiQhCTl.cjs +6 -0
  24. package/dist/doctor-COpf35L2.cjs +20 -0
  25. package/dist/doctor-Dh1XP7Pz.mjs +20 -0
  26. package/dist/generator-DGW6pkCC.cjs +22 -0
  27. package/dist/generator-Dv3wEmhZ.mjs +22 -0
  28. package/dist/icons-dev-CrQLjoQp.js +6 -0
  29. package/dist/icons-dev-rzdz6Lf3.cjs +6 -0
  30. package/dist/image-BkIfa9oo.js +6 -0
  31. package/dist/image-DIGjCPe6.cjs +6 -0
  32. package/dist/mdx-K0WYBAJ3.js +7 -0
  33. package/dist/mdx-hpErbRUe.cjs +7 -0
  34. package/dist/meta-loader-0gJ4PtBC.cjs +6 -0
  35. package/dist/meta-loader-9IpAHWDS.mjs +6 -0
  36. package/dist/node/cli-entry.cjs +1 -2
  37. package/dist/node/cli-entry.mjs +1 -2
  38. package/dist/node/index.cjs +1 -1
  39. package/dist/node/index.d.cts +55 -11
  40. package/dist/node/index.d.mts +55 -12
  41. package/dist/node/index.mjs +1 -1
  42. package/dist/node/routes/worker.cjs +6 -0
  43. package/dist/node/routes/worker.d.cts +2 -0
  44. package/dist/node/routes/worker.d.mts +2 -0
  45. package/dist/node/routes/worker.mjs +6 -0
  46. package/dist/node-C2nWXElP.mjs +112 -0
  47. package/dist/node-CinkUtxV.cjs +112 -0
  48. package/dist/package-BMYLDBBP.cjs +6 -0
  49. package/dist/{package-Bqbn1AYK.mjs → package-HegMOTL_.mjs} +1 -1
  50. package/dist/parser-Bh11BsdA.cjs +6 -0
  51. package/dist/parser-D8eQvE7N.mjs +6 -0
  52. package/dist/parser-DYRzXWmA.cjs +6 -0
  53. package/dist/routes-CHf76Ye4.cjs +6 -0
  54. package/dist/routes-CMUZGI6T.mjs +6 -0
  55. package/dist/routes-Co1mRM58.cjs +6 -0
  56. package/dist/search-dialog-BACuzoVX.cjs +6 -0
  57. package/dist/search-dialog-BKagVT17.js +6 -0
  58. package/dist/search-dialog-C8w12eUx.js +6 -0
  59. package/dist/search-dialog-CGyrozZE.cjs +6 -0
  60. package/dist/search-dialog-D26rUnJ_.cjs +6 -0
  61. package/dist/sidebar-DKvg6KOc.d.cts +491 -0
  62. package/dist/sidebar-Dr1TiRIy.d.ts +491 -0
  63. package/dist/utils-BxNAXhZZ.mjs +7 -0
  64. package/dist/utils-Clzu7jvb.cjs +7 -0
  65. package/dist/worker-pool-Bd8Y9KDv.mjs +6 -0
  66. package/dist/worker-pool-BwU8ckrg.cjs +6 -0
  67. package/package.json +27 -8
  68. package/src/client/app/doc-page.tsx +9 -5
  69. package/src/client/app/docs-layout.tsx +17 -3
  70. package/src/client/app/head.tsx +122 -0
  71. package/src/client/app/helmet-compat.tsx +36 -0
  72. package/src/client/app/mdx-component.tsx +5 -52
  73. package/src/client/app/mdx-components-context.tsx +32 -8
  74. package/src/client/app/routes-context.tsx +2 -2
  75. package/src/client/app/scroll-handler.tsx +1 -1
  76. package/src/client/app/theme-context.tsx +5 -5
  77. package/src/client/app/ui-context.tsx +42 -0
  78. package/src/client/components/docs-layout-default.tsx +85 -0
  79. package/src/client/components/icons-dev.tsx +38 -15
  80. package/src/client/components/mdx/callout.tsx +97 -0
  81. package/src/client/components/mdx/card.tsx +73 -98
  82. package/src/client/components/mdx/cards.tsx +27 -0
  83. package/src/client/components/mdx/code-block.tsx +37 -17
  84. package/src/client/components/mdx/field.tsx +24 -56
  85. package/src/client/components/mdx/image.tsx +36 -15
  86. package/src/client/components/mdx/index.ts +19 -53
  87. package/src/client/components/mdx/table.tsx +46 -148
  88. package/src/client/components/mdx/typographics.tsx +120 -0
  89. package/src/client/components/mdx/{hooks/use-code-block.ts → use-code-block.ts} +5 -7
  90. package/src/client/components/primitives/breadcrumbs.tsx +5 -24
  91. package/src/client/components/primitives/button.tsx +3 -142
  92. package/src/client/components/primitives/code-block.tsx +104 -97
  93. package/src/client/components/{docs-layout.tsx → primitives/docs-layout.tsx} +15 -24
  94. package/src/client/components/primitives/error-boundary.tsx +107 -0
  95. package/src/client/components/primitives/heading.tsx +128 -0
  96. package/src/client/components/primitives/helpers/observer.ts +62 -32
  97. package/src/client/components/primitives/image.tsx +26 -0
  98. package/src/client/components/primitives/link.tsx +50 -52
  99. package/src/client/components/primitives/menu.tsx +25 -49
  100. package/src/client/components/primitives/navbar.tsx +234 -59
  101. package/src/client/components/primitives/on-this-page.tsx +169 -40
  102. package/src/client/components/primitives/page-nav.tsx +11 -39
  103. package/src/client/components/primitives/popover.tsx +12 -30
  104. package/src/client/components/primitives/search-dialog.tsx +77 -71
  105. package/src/client/components/primitives/sidebar.tsx +312 -119
  106. package/src/client/components/primitives/skeleton.tsx +1 -1
  107. package/src/client/components/primitives/tabs.tsx +5 -16
  108. package/src/client/components/primitives/tooltip.tsx +1 -1
  109. package/src/client/components/ui-base/banner.tsx +66 -0
  110. package/src/client/components/ui-base/breadcrumbs.tsx +26 -20
  111. package/src/client/components/ui-base/copy-markdown.tsx +43 -35
  112. package/src/client/components/ui-base/error-boundary.tsx +9 -46
  113. package/src/client/components/ui-base/github-stars.tsx +5 -3
  114. package/src/client/components/ui-base/index.ts +3 -3
  115. package/src/client/components/ui-base/last-updated.tsx +27 -0
  116. package/src/client/components/ui-base/navbar.tsx +183 -89
  117. package/src/client/components/ui-base/not-found.tsx +11 -9
  118. package/src/client/components/ui-base/on-this-page.tsx +8 -104
  119. package/src/client/components/ui-base/page-nav.tsx +23 -9
  120. package/src/client/components/ui-base/search-dialog.tsx +111 -36
  121. package/src/client/components/ui-base/search-highlight.tsx +10 -0
  122. package/src/client/components/ui-base/sidebar.tsx +77 -154
  123. package/src/client/components/ui-base/tabs.tsx +20 -7
  124. package/src/client/components/ui-base/theme-toggle.tsx +88 -10
  125. package/src/client/components/ui-base/version-i18n.tsx +80 -0
  126. package/src/client/hooks/index.ts +2 -1
  127. package/src/client/hooks/use-analytics.ts +272 -0
  128. package/src/client/hooks/use-i18n.ts +116 -50
  129. package/src/client/hooks/use-localized-to.ts +70 -27
  130. package/src/client/hooks/use-navbar.ts +69 -39
  131. package/src/client/hooks/use-page-nav.ts +28 -25
  132. package/src/client/hooks/use-routes.ts +63 -80
  133. package/src/client/hooks/use-search-highlight.ts +185 -0
  134. package/src/client/hooks/use-search.ts +12 -3
  135. package/src/client/hooks/use-sidebar.ts +183 -80
  136. package/src/client/hooks/use-tabs.ts +3 -4
  137. package/src/client/hooks/use-version.ts +44 -29
  138. package/src/client/index.ts +13 -87
  139. package/src/client/mdx.ts +2 -0
  140. package/src/client/primitives.ts +19 -0
  141. package/src/client/ssg/boltdocs-shell.tsx +68 -79
  142. package/src/client/ssg/create-routes.tsx +268 -72
  143. package/src/client/ssg/mdx-page.tsx +2 -1
  144. package/src/client/store/boltdocs-context.tsx +72 -20
  145. package/src/client/theme/neutral.css +74 -361
  146. package/src/client/theme/reset.css +189 -0
  147. package/src/client/types.ts +10 -2
  148. package/src/client/utils/path.ts +9 -0
  149. package/src/client/utils/react-to-text.ts +24 -24
  150. package/src/client/virtual.d.ts +1 -1
  151. package/src/shared/types.ts +82 -22
  152. package/dist/node-Bogvkxao.mjs +0 -101
  153. package/dist/node-CXaog6St.cjs +0 -101
  154. package/dist/search-dialog-CV3eJzMm.cjs +0 -6
  155. package/dist/search-dialog-DNTomKgu.js +0 -6
  156. package/dist/use-search-CS3gH19M.js +0 -6
  157. package/dist/use-search-DBpJZQuw.cjs +0 -6
  158. package/src/client/components/mdx/admonition.tsx +0 -91
  159. package/src/client/components/mdx/badge.tsx +0 -41
  160. package/src/client/components/mdx/button.tsx +0 -35
  161. package/src/client/components/mdx/component-preview.tsx +0 -37
  162. package/src/client/components/mdx/component-props.tsx +0 -83
  163. package/src/client/components/mdx/file-tree.tsx +0 -325
  164. package/src/client/components/mdx/hooks/use-component-preview.ts +0 -16
  165. package/src/client/components/mdx/hooks/useTable.ts +0 -74
  166. package/src/client/components/mdx/hooks/useTabs.ts +0 -68
  167. package/src/client/components/mdx/link.tsx +0 -38
  168. package/src/client/components/mdx/list.tsx +0 -192
  169. package/src/client/components/mdx/tabs.tsx +0 -135
  170. package/src/client/components/mdx/video.tsx +0 -68
  171. package/src/client/components/primitives/index.ts +0 -19
  172. package/src/client/components/primitives/navigation-menu.tsx +0 -114
  173. package/src/client/components/ui-base/head.tsx +0 -83
  174. package/src/client/components/ui-base/loading.tsx +0 -57
  175. package/src/client/components/ui-base/powered-by.tsx +0 -25
  176. package/src/client/hooks/use-onthispage.ts +0 -23
  177. package/src/client/utils/use-on-change.ts +0 -15
@@ -2,37 +2,43 @@ import { useBreadcrumbs } from '../../hooks/use-breadcrumbs'
2
2
  import { Home } from 'lucide-react'
3
3
  import { Breadcrumbs as BreadcrumbsRoot } from '../primitives/breadcrumbs'
4
4
  import { cn } from '../../utils/cn'
5
- import { useConfig } from '../../app/config-context'
6
5
 
7
6
  export function Breadcrumbs() {
8
7
  const { crumbs, activeRoute } = useBreadcrumbs()
9
- const config = useConfig()
10
- const themeConfig = config.theme || {}
11
-
12
8
  if (crumbs.length === 0) return null
13
9
 
14
- if (!themeConfig?.breadcrumbs) return null
15
-
16
10
  return (
17
- <BreadcrumbsRoot.Root>
11
+ <BreadcrumbsRoot.Root className="gap-2 text-xs sm:text-sm font-medium">
18
12
  <BreadcrumbsRoot.Item>
19
- <BreadcrumbsRoot.Link href="/">
13
+ <BreadcrumbsRoot.Link
14
+ href="/"
15
+ className="text-muted hover:text-body transition-colors flex items-center"
16
+ >
20
17
  <Home size={14} />
21
18
  </BreadcrumbsRoot.Link>
22
19
  </BreadcrumbsRoot.Item>
23
- {crumbs.map((crumb, i) => (
24
- <BreadcrumbsRoot.Item key={`crumb-${crumb.href}-${crumb.label}-${i}`}>
25
- <BreadcrumbsRoot.Separator />
26
- <BreadcrumbsRoot.Link
27
- href={crumb.href}
28
- className={cn({
29
- 'font-medium text-text-main': crumb.href === activeRoute?.path,
30
- })}
20
+ {crumbs.map((crumb, i) => {
21
+ const isActive = crumb.href === activeRoute?.path
22
+ return (
23
+ <BreadcrumbsRoot.Item
24
+ key={`crumb-${crumb.href}-${crumb.label}-${i}`}
25
+ className="gap-2"
31
26
  >
32
- {crumb.label}
33
- </BreadcrumbsRoot.Link>
34
- </BreadcrumbsRoot.Item>
35
- ))}
27
+ <BreadcrumbsRoot.Separator className="text-muted/40" />
28
+ <BreadcrumbsRoot.Link
29
+ href={crumb.href}
30
+ className={cn(
31
+ 'transition-colors',
32
+ isActive
33
+ ? 'text-body font-semibold cursor-default pointer-events-none'
34
+ : 'text-muted hover:text-body',
35
+ )}
36
+ >
37
+ {crumb.label}
38
+ </BreadcrumbsRoot.Link>
39
+ </BreadcrumbsRoot.Item>
40
+ )
41
+ })}
36
42
  </BreadcrumbsRoot.Root>
37
43
  )
38
44
  }
@@ -1,14 +1,15 @@
1
1
  import { useState } from 'react'
2
2
  import { Copy, Check, ExternalLink, ChevronDown } from 'lucide-react'
3
- import { Button, ButtonGroup, Menu, cn } from '../../components/primitives'
4
-
3
+ import { Button } from '../primitives/button'
4
+ import { ButtonGroup } from '../primitives/button-group'
5
+ import { Menu } from '../primitives/menu'
6
+ import { cn } from '../../utils/cn'
5
7
  import type { ComponentRoute } from '../../types'
6
8
 
7
9
  export interface CopyMarkdownProps {
8
10
  content?: string
9
11
  mdxRaw?: string
10
12
  route?: ComponentRoute
11
- config?: boolean | { text?: string; icon?: string }
12
13
  }
13
14
 
14
15
  const useCopyMarkdown = (content: string) => {
@@ -33,63 +34,70 @@ const useCopyMarkdown = (content: string) => {
33
34
  }
34
35
  }
35
36
 
36
- export function CopyMarkdown({ content, mdxRaw, config }: CopyMarkdownProps) {
37
+ export function CopyMarkdown({ content, mdxRaw }: CopyMarkdownProps) {
37
38
  const displayContent = mdxRaw || content || ''
38
39
  const { copied, handleCopy, handleOpenRaw } = useCopyMarkdown(displayContent)
39
40
 
40
- const isEnabled = config !== false
41
- const buttonText =
42
- typeof config === 'object'
43
- ? config.text || 'Copy Markdown'
44
- : 'Copy Markdown'
45
-
46
- if (!isEnabled || !displayContent) return null
41
+ if (!displayContent) return null
47
42
 
48
43
  return (
49
- <div className="relative inline-flex z-100 flex-shrink-0 w-max translate-y-0 active:translate-y-px transition-transform duration-200">
50
- <ButtonGroup className="rounded-xl border border-border-subtle bg-bg-surface/40 backdrop-blur-md transition-all duration-300 hover:border-primary-500/50 hover:shadow-lg hover:shadow-primary-500/5 group overflow-hidden">
44
+ <div className="relative inline-flex z-100 shrink-0 w-max">
45
+ <ButtonGroup className="rounded-xl border border-subtle bg-surface transition-all duration-300 hover:border-primary-500/50 group overflow-hidden">
46
+ {/* Mobile: icon-only copy button */}
47
+ <Button
48
+ onPress={handleCopy}
49
+ className={cn(
50
+ 'md:hidden flex items-center justify-center w-8 h-8 bg-transparent outline-none select-none cursor-pointer border-none',
51
+ 'text-muted transition-all duration-300 hover:bg-primary-500/5 hover:text-body',
52
+ copied && 'text-emerald-500 hover:bg-emerald-500/5',
53
+ )}
54
+ aria-label={copied ? 'Copied!' : 'Copy Markdown'}
55
+ >
56
+ {copied ? <Check size={14} /> : <Copy size={14} />}
57
+ </Button>
58
+
59
+ {/* Desktop: full copy button with label */}
51
60
  <Button
52
- variant="ghost"
53
61
  onPress={handleCopy}
54
- icon={copied ? <Check size={16} /> : <Copy size={16} />}
55
- iconPosition="left"
56
62
  className={cn(
57
- 'px-5 py-2 bg-transparent text-[0.8125rem] font-semibold h-9 border-none shrink-0',
58
- 'text-text-main transition-all duration-300 hover:bg-primary-500/5',
63
+ 'hidden md:flex items-center gap-2 px-5 py-2 bg-transparent text-[0.8125rem] font-semibold h-9 shrink-0 outline-none select-none cursor-pointer border-none',
64
+ 'text-body transition-all duration-300 hover:bg-primary-500/5',
59
65
  copied && 'text-emerald-500 hover:bg-emerald-500/5',
60
66
  )}
61
67
  >
62
- {copied ? 'Copied!' : buttonText}
68
+ {copied ? <Check size={16} /> : <Copy size={16} />}
69
+ {copied ? 'Copied!' : 'Copy Markdown'}
63
70
  </Button>
64
71
 
65
72
  <Menu.Trigger placement="bottom end">
66
73
  <Button
67
- variant="ghost"
68
- isIconOnly
69
- icon={<ChevronDown size={14} />}
70
74
  className={cn(
71
- 'px-3.5 h-9 border-l border-border-subtle/50 text-text-muted rounded-none bg-transparent shrink-0',
75
+ 'flex items-center justify-center px-2.5 md:px-3.5 h-8 md:h-9 border-none border-l border-subtle/50 text-muted rounded-none bg-transparent shrink-0 outline-none select-none cursor-pointer',
72
76
  'transition-all duration-300 hover:bg-primary-500/5 hover:text-primary-500',
73
77
  )}
74
- />
75
- <Menu.Root className="w-52">
76
- <Menu.Item onAction={handleCopy}>
78
+ >
79
+ <ChevronDown size={14} />
80
+ </Button>
81
+ <Menu.Root className="w-52 bg-main border border-subtle rounded-xl p-1.5 shadow-md outline-none flex flex-col gap-0.5 animate-fade-in z-100">
82
+ <Menu.Item
83
+ onAction={handleCopy}
84
+ className="flex items-center px-3 py-2 rounded-lg text-xs font-medium text-body dark:hover:bg-primary-300/50 hover:bg-primary-200/50 transition-colors duration-100 cursor-pointer select-none outline-none group"
85
+ >
77
86
  <Copy
78
87
  size={16}
79
- className="size-4 mt-0.5 text-text-muted group-hover:text-primary-500"
88
+ className="size-4 text-muted dark:group-hover:text-primary-500 group-hover:text-primary-400"
80
89
  />
81
- <span className="font-medium text-[0.8125rem]">
82
- Copy Markdown
83
- </span>
90
+ <span className="ml-2">Copy Markdown</span>
84
91
  </Menu.Item>
85
- <Menu.Item onAction={handleOpenRaw}>
92
+ <Menu.Item
93
+ onAction={handleOpenRaw}
94
+ className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-body dark:hover:bg-primary-300/50 hover:bg-primary-200/50 transition-colors duration-100 cursor-pointer select-none outline-none group"
95
+ >
86
96
  <ExternalLink
87
97
  size={16}
88
- className="size-4 mt-0.5 text-text-muted group-hover:text-primary-500"
98
+ className="size-4 text-muted dark:group-hover:text-primary-500 group-hover:text-primary-400"
89
99
  />
90
- <span className="font-medium text-[0.8125rem]">
91
- View as Markdown
92
- </span>
100
+ <span className="ml-2">View as Markdown</span>
93
101
  </Menu.Item>
94
102
  </Menu.Root>
95
103
  </Menu.Trigger>
@@ -1,52 +1,15 @@
1
- import type { ErrorInfo, ReactNode } from 'react'
2
- import { Component } from 'react'
1
+ import { ErrorBoundary as PrimitiveErrorBoundary } from '../primitives/error-boundary'
2
+ import type { ReactNode } from 'react'
3
3
 
4
- import { Button } from '../primitives'
5
-
6
- interface Props {
4
+ interface ErrorBoundaryProps {
7
5
  children?: ReactNode
8
6
  fallback?: ReactNode
9
7
  }
10
8
 
11
- interface State {
12
- hasError: boolean
13
- error?: Error
14
- }
15
-
16
- export class ErrorBoundary extends Component<Props, State> {
17
- public state: State = { hasError: false }
18
-
19
- public static getDerivedStateFromError(error: Error): State {
20
- return { hasError: true, error }
21
- }
22
-
23
- public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
24
- console.error('Uncaught error in Boltdocs Layout:', error, errorInfo)
25
- }
26
-
27
- public render() {
28
- if (this.state.hasError) {
29
- return (
30
- this.props.fallback || (
31
- <div className="flex flex-col items-center justify-center min-h-[40vh] text-center gap-4 px-4">
32
- <div className="text-lg font-bold text-red-400">
33
- Something went wrong
34
- </div>
35
- <p className="text-sm text-text-muted max-w-md">
36
- {this.state.error?.message ||
37
- 'An unexpected error occurred while rendering this page.'}
38
- </p>
39
- <Button
40
- className="rounded-lg border border-border-subtle bg-bg-surface px-5 py-2 text-sm font-medium text-text-main transition-colors hover:bg-bg-muted cursor-pointer"
41
- onPress={() => this.setState({ hasError: false })}
42
- >
43
- Try again
44
- </Button>
45
- </div>
46
- )
47
- )
48
- }
49
-
50
- return this.props.children
51
- }
9
+ export function ErrorBoundary({ children, fallback }: ErrorBoundaryProps) {
10
+ return (
11
+ <PrimitiveErrorBoundary fallback={fallback}>
12
+ {children}
13
+ </PrimitiveErrorBoundary>
14
+ )
52
15
  }
@@ -18,10 +18,12 @@ export function GithubStars({ repo }: { repo: string }) {
18
18
  href={`https://github.com/${repo}`}
19
19
  target="_blank"
20
20
  rel="noopener noreferrer"
21
- className="inline-flex items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-muted transition-all hover:bg-bg-main hover:border-border-strong hover:text-text-main"
21
+ className="inline-flex items-center gap-2 rounded-xl border border-subtle bg-surface px-3 py-1.5 text-xs font-semibold text-muted dark:hover:bg-primary-300/50 hover:bg-primary-200/50 transition-colors duration-100 hover:border-primary-500/50 hover:text-body select-none outline-none"
22
22
  >
23
- <Github className="h-4 w-4" />
24
- {stars && <span className="tabular-nums">{stars}</span>}
23
+ <Github className="h-4 w-4 text-body" />
24
+ {stars !== null && (
25
+ <span className="tabular-nums font-medium">{stars} stars</span>
26
+ )}
25
27
  </a>
26
28
  )
27
29
  }
@@ -3,14 +3,14 @@ export { CopyMarkdown } from './copy-markdown'
3
3
  export type { CopyMarkdownProps } from './copy-markdown'
4
4
  export { ErrorBoundary } from './error-boundary'
5
5
  export { GithubStars } from './github-stars'
6
- export { Head } from './head'
7
- export { Loading } from './loading'
8
6
  export { Navbar } from './navbar'
9
7
  export { NotFound } from './not-found'
10
8
  export { OnThisPage } from './on-this-page'
11
9
  export { PageNav } from './page-nav'
12
- export { PoweredBy } from './powered-by'
13
10
  export { SearchDialog } from './search-dialog'
14
11
  export { Sidebar } from './sidebar'
15
12
  export { Tabs } from './tabs'
13
+ export { Banner } from './banner'
16
14
  export { ThemeToggle } from './theme-toggle'
15
+ export { LastUpdated } from './last-updated'
16
+ export { Callout } from '../mdx/callout'
@@ -0,0 +1,27 @@
1
+ interface LastUpdatedProps {
2
+ date?: string | number | Date
3
+ }
4
+
5
+ /**
6
+ * A subtle display for when the page was last updated.
7
+ * Small, opaque, and positioned at the bottom of the content with a thin top border divider.
8
+ */
9
+ export function LastUpdated({ date }: LastUpdatedProps) {
10
+ if (!date) return null
11
+
12
+ const d = new Date(date)
13
+ if (isNaN(d.getTime())) return null
14
+
15
+ const formattedDate = d.toLocaleDateString(undefined, {
16
+ year: 'numeric',
17
+ month: 'long',
18
+ day: 'numeric',
19
+ })
20
+
21
+ return (
22
+ <div className="mt-16 pt-6 border-t border-subtle flex items-center justify-between text-xs text-muted select-none">
23
+ <span></span>
24
+ <span className="italic">Last updated on {formattedDate}</span>
25
+ </div>
26
+ )
27
+ }
@@ -1,7 +1,6 @@
1
- import { Suspense, lazy } from 'react'
1
+ import { Suspense, lazy, useState } from 'react'
2
+ import { cn } from '../../utils/cn'
2
3
  import { useNavbar } from '../../hooks/use-navbar'
3
- import { useVersion } from '../../hooks/use-version'
4
- import { useI18n } from '../../hooks/use-i18n'
5
4
  import { useRoutes } from '../../hooks/use-routes'
6
5
  import NavbarPrimitive from '../primitives/navbar'
7
6
  import { ThemeToggle } from './theme-toggle'
@@ -9,11 +8,12 @@ import { GithubStars } from './github-stars'
9
8
  import { Tabs } from './tabs'
10
9
  import { useLocation } from 'react-router-dom'
11
10
  import type { BoltdocsSocialLink } from '../../../shared/types'
12
- import { Menu } from '../primitives/menu'
13
11
  import { Button } from '../primitives/button'
14
- import { ChevronDown, Languages } from 'lucide-react'
12
+ import { Menu as MenuIcon, X } from 'lucide-react'
15
13
  import { useLocalizedTo } from '../../hooks/use-localized-to'
16
14
  import type { NavbarLink as NavbarLinkType } from '../../types'
15
+ import { useUI } from '../../app/ui-context'
16
+ import { VersionSelector, I18nSelector } from './version-i18n'
17
17
 
18
18
  const SearchDialog = lazy(() =>
19
19
  import('./search-dialog').then((m) => ({
@@ -23,63 +23,146 @@ const SearchDialog = lazy(() =>
23
23
 
24
24
  export function Navbar() {
25
25
  const { links, title, logo, logoProps, github, social, config } = useNavbar()
26
- const { routes, allRoutes, currentVersion, currentLocale } = useRoutes()
26
+ const { routes, allRoutes, currentRoute, currentVersion, currentLocale } = useRoutes()
27
27
  const { pathname } = useLocation()
28
+ const { isSidebarOpen, toggleSidebar } = useUI()
29
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
30
+
28
31
  const themeConfig = config.theme || {}
29
- const isDocs = pathname.startsWith('/docs')
32
+ const isDocs = !!currentRoute?.filePath
30
33
  const hasTabs = themeConfig?.tabs && themeConfig.tabs.length > 0
31
34
 
32
35
  return (
33
- <NavbarPrimitive.Root className={hasTabs ? 'border-b-0' : ''}>
36
+ <NavbarPrimitive.Root
37
+ className={cn(
38
+ 'border-b border-subtle bg-main/80 backdrop-blur-md',
39
+ hasTabs && 'border-b-0',
40
+ )}
41
+ >
34
42
  <NavbarPrimitive.Content>
35
43
  <NavbarPrimitive.Left>
44
+ {isDocs && (
45
+ <Button
46
+ onPress={toggleSidebar}
47
+ className="mr-2 lg:hidden p-1.5 h-8 w-8 flex items-center justify-center bg-transparent border-none outline-none select-none cursor-pointer rounded-xl hover:bg-primary-50/50 transition-colors"
48
+ aria-label={isSidebarOpen ? 'Close sidebar' : 'Open sidebar'}
49
+ >
50
+ {isSidebarOpen ? (
51
+ <X className="w-5 h-5 text-body" />
52
+ ) : (
53
+ <MenuIcon className="w-5 h-5 text-body" />
54
+ )}
55
+ </Button>
56
+ )}
36
57
  {logo && (
37
58
  <NavbarPrimitive.Logo
38
59
  src={logo}
39
60
  alt={logoProps?.alt || title}
40
61
  width={logoProps?.width ?? 24}
41
62
  height={logoProps?.height ?? 24}
63
+ href="site:/"
42
64
  />
43
65
  )}
44
- <NavbarPrimitive.Title>{title}</NavbarPrimitive.Title>
66
+ <NavbarPrimitive.Title href="site:/">{title}</NavbarPrimitive.Title>
45
67
 
46
- {config.versions && currentVersion && <NavbarVersion />}
68
+ <div className="hidden sm:block">
69
+ {config.versions && currentVersion && <VersionSelector />}
70
+ </div>
47
71
  </NavbarPrimitive.Left>
48
72
  <NavbarPrimitive.Center>
49
73
  <Suspense
50
74
  fallback={
51
- <div className="h-9 w-32 animate-pulse rounded-md bg-bg-surface" />
75
+ <div className="h-9 w-32 animate-pulse rounded-md bg-surface" />
52
76
  }
53
77
  >
54
78
  <SearchDialog routes={routes || []} />
55
79
  </Suspense>
56
80
  </NavbarPrimitive.Center>
57
81
  <NavbarPrimitive.Right>
82
+ <Suspense fallback={null}>
83
+ <div className="lg:hidden">
84
+ <SearchDialog routes={routes || []} />
85
+ </div>
86
+ </Suspense>
58
87
  <NavbarPrimitive.Links>
59
88
  {links.map((link) => (
60
89
  <NavbarLinkItem key={link.href} link={link} />
61
90
  ))}
62
91
  </NavbarPrimitive.Links>
63
- {config.i18n && currentLocale && <NavbarI18n />}
64
- <NavbarPrimitive.Split />
65
- <ThemeToggle />
66
- {github && <GithubStars repo={themeConfig?.githubRepo ?? ''} />}
67
- {social.length > 0 && <NavbarPrimitive.Split />}
68
- <div className="flex items-center gap-1">
92
+
93
+ <div className="hidden sm:flex items-center gap-2">
94
+ {config.i18n && currentLocale && <I18nSelector />}
95
+ <NavbarPrimitive.Split className="bg-subtle" />
96
+ </div>
97
+
98
+ <div className="hidden md:block">
99
+ <ThemeToggle />
100
+ </div>
101
+
102
+ {github && (
103
+ <div className="hidden md:block">
104
+ <GithubStars repo={themeConfig?.githubRepo ?? ''} />
105
+ </div>
106
+ )}
107
+ {social.length > 0 && (
108
+ <div className="hidden md:block">
109
+ <NavbarPrimitive.Split className="bg-subtle" />
110
+ </div>
111
+ )}
112
+ <div className="hidden md:flex items-center gap-1">
69
113
  {social.map(({ icon, link }: BoltdocsSocialLink) => (
70
114
  <NavbarPrimitive.Socials
71
115
  key={link}
72
116
  icon={icon}
73
117
  link={link}
74
- className="p-1.5"
118
+ className="p-1.5 text-muted hover:text-body hover:bg-surface rounded-md transition-all focus-visible:ring-2 focus-visible:ring-primary-500/30"
75
119
  />
76
120
  ))}
77
121
  </div>
122
+
123
+ <NavbarPrimitive.More
124
+ onPress={() => setIsMobileMenuOpen(true)}
125
+ className="text-muted hover:text-body active:scale-90 transition-all focus-visible:ring-2 focus-visible:ring-primary-500/30"
126
+ />
78
127
  </NavbarPrimitive.Right>
79
128
  </NavbarPrimitive.Content>
80
129
 
130
+ <NavbarPrimitive.MobileMenu
131
+ isOpen={isMobileMenuOpen}
132
+ onClose={() => setIsMobileMenuOpen(false)}
133
+ className="bg-main/98 backdrop-blur-2xl"
134
+ >
135
+ <div className="flex flex-col gap-1">
136
+ {links.map((link) => (
137
+ <NavbarMobileLinkItem
138
+ key={link.href}
139
+ link={link}
140
+ onClose={() => setIsMobileMenuOpen(false)}
141
+ />
142
+ ))}
143
+ </div>
144
+
145
+ {social.length > 0 && (
146
+ <div className="mt-6">
147
+ <div className="px-4 mb-4 text-xs font-bold uppercase tracking-widest text-muted/50">
148
+ Connect
149
+ </div>
150
+ <div className="flex flex-wrap gap-2 px-2">
151
+ {social.map(({ icon, link }: BoltdocsSocialLink) => (
152
+ <NavbarPrimitive.Socials
153
+ key={link}
154
+ icon={icon}
155
+ link={link}
156
+ className="p-3 bg-surface border border-subtle rounded-xl flex-1 justify-center"
157
+ />
158
+ ))}
159
+ </div>
160
+ </div>
161
+ )}
162
+ </NavbarPrimitive.MobileMenu>
163
+
81
164
  {isDocs && hasTabs && themeConfig?.tabs && (
82
- <div className="w-full border-b border-border-subtle bg-bg-main">
165
+ <div className="w-full border-b border-subtle bg-main">
83
166
  <Tabs tabs={themeConfig.tabs} routes={allRoutes || routes || []} />
84
167
  </div>
85
168
  )}
@@ -89,84 +172,95 @@ export function Navbar() {
89
172
 
90
173
  function NavbarLinkItem({ link }: { link: NavbarLinkType }) {
91
174
  const localizedHref = useLocalizedTo(link.href || '')
92
- return <NavbarPrimitive.Link {...(link as any)} href={localizedHref} />
93
- }
94
-
95
- function NavbarVersion() {
96
- const { currentVersionLabel, availableVersions, handleVersionChange } =
97
- useVersion()
175
+ const { pathname } = useLocation()
176
+ const active =
177
+ pathname === localizedHref || pathname.startsWith(localizedHref + '/')
178
+ const hasItems = link.items && link.items.length > 0
98
179
 
99
- if (availableVersions.length === 0) return null
180
+ if (hasItems) {
181
+ return (
182
+ <NavbarPrimitive.Dropdown
183
+ label={
184
+ <span
185
+ className={cn(
186
+ 'transition-colors outline-none font-medium focus-visible:ring-2 focus-visible:ring-primary-500/30 rounded-sm px-2 py-1',
187
+ active ? 'text-primary-500' : 'text-muted hover:text-body',
188
+ )}
189
+ >
190
+ {link.label as any}
191
+ </span>
192
+ }
193
+ >
194
+ {link.items?.map((item) => (
195
+ <NavbarPrimitive.DropdownItem
196
+ key={item.href}
197
+ href={useLocalizedTo(item.href || '')}
198
+ label={item.label as any}
199
+ />
200
+ ))}
201
+ </NavbarPrimitive.Dropdown>
202
+ )
203
+ }
100
204
 
101
205
  return (
102
- <Menu.Trigger>
103
- <Button
104
- variant={'outline'}
105
- size="sm"
106
- rounded="lg"
107
- iconPosition="right"
108
- icon={<ChevronDown className="w-3.5 h-3.5 text-text-muted/60" />}
109
- className="h-8 border-border-subtle/60 bg-bg-surface/30 backdrop-blur-sm transition-all duration-200 hover:border-primary-500/50 hover:bg-primary-500/5"
110
- >
111
- <span className="font-semibold text-[0.8125rem]">
112
- {currentVersionLabel}
113
- </span>
114
- </Button>
115
- <Menu.Root>
116
- <Menu.Section items={availableVersions}>
117
- {(version) => (
118
- <Menu.Item
119
- key={`${version.value ?? ''}`}
120
- onPress={() => handleVersionChange(version.value)}
121
- >
122
- {version.label as string}
123
- </Menu.Item>
124
- )}
125
- </Menu.Section>
126
- </Menu.Root>
127
- </Menu.Trigger>
206
+ <NavbarPrimitive.Link
207
+ {...(link as any)}
208
+ href={localizedHref}
209
+ active={active}
210
+ className={cn(
211
+ 'transition-colors outline-none font-medium focus-visible:ring-2 focus-visible:ring-primary-500/30 rounded-sm',
212
+ active ? 'text-primary-500' : 'text-muted hover:text-body',
213
+ )}
214
+ />
128
215
  )
129
216
  }
130
217
 
131
- function NavbarI18n() {
132
- const { currentLocale, availableLocales, handleLocaleChange } = useI18n()
218
+ function NavbarMobileLinkItem({
219
+ link,
220
+ onClose,
221
+ }: {
222
+ link: NavbarLinkType
223
+ onClose: () => void
224
+ }) {
225
+ const localizedHref = useLocalizedTo(link.href || '')
226
+ const { pathname } = useLocation()
227
+ const active = pathname === localizedHref
228
+ const hasItems = link.items && link.items.length > 0
133
229
 
134
- if (availableLocales.length === 0) return null
230
+ if (hasItems) {
231
+ return (
232
+ <div className="flex flex-col gap-1">
233
+ <div
234
+ className={cn(
235
+ 'px-3 py-2 text-sm transition-all',
236
+ active ? 'text-body' : 'text-muted/80 hover:text-body',
237
+ )}
238
+ >
239
+ {link.label as any}
240
+ </div>
241
+ <div className="flex flex-col gap-1 pl-4">
242
+ {link.items?.map((item) => (
243
+ <NavbarMobileLinkItem
244
+ key={item.href}
245
+ link={item}
246
+ onClose={onClose}
247
+ />
248
+ ))}
249
+ </div>
250
+ </div>
251
+ )
252
+ }
135
253
 
136
254
  return (
137
- <Menu.Trigger>
138
- <Button
139
- variant={'outline'}
140
- size="sm"
141
- rounded="lg"
142
- iconPosition="right"
143
- icon={<ChevronDown className="w-3.5 h-3.5 text-text-muted/60" />}
144
- className="h-8 border-border-subtle/60 bg-bg-surface/30 backdrop-blur-sm transition-all duration-200 hover:border-primary-500/50 hover:bg-primary-500/5 px-2.5"
145
- >
146
- <div className="flex items-center gap-1.5">
147
- <Languages className="w-3.5 h-3.5 text-primary-500" />
148
- <span className="font-bold text-[0.75rem] uppercase opacity-90">
149
- {currentLocale || 'en'}
150
- </span>
151
- </div>
152
- </Button>
153
- <Menu.Root>
154
- <Menu.Section items={availableLocales}>
155
- {(locale) => (
156
- <Menu.Item
157
- key={`${locale.value ?? ''}`}
158
- onPress={() => handleLocaleChange(locale.value)}
159
- >
160
- <div className="flex items-center justify-between w-full gap-4">
161
- <span>{locale.label as string}</span>
162
- <span className="text-[10px] font-bold opacity-40 uppercase tracking-tighter">
163
- {locale.value}
164
- </span>
165
- </div>
166
- </Menu.Item>
167
- )}
168
- </Menu.Section>
169
- </Menu.Root>
170
- </Menu.Trigger>
255
+ <NavbarPrimitive.MobileLink
256
+ {...(link as any)}
257
+ href={localizedHref}
258
+ active={active}
259
+ onPress={onClose}
260
+ className={cn(
261
+ 'transition-all',
262
+ active ? 'text-body' : 'text-muted/80 hover:text-body',
263
+ )}
264
+ />
171
265
  )
172
266
  }