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
@@ -1,169 +1,92 @@
1
- import { useState, useEffect, useMemo } from 'react'
2
- import { useSidebar } from '../../hooks/use-sidebar'
3
1
  import { Sidebar as SidebarPrimitive } from '../primitives/sidebar'
4
- import { PoweredBy } from './powered-by'
5
2
  import * as LucideIcons from 'lucide-react'
6
- import virtualIcons from 'virtual:boltdocs-icons'
7
3
  import type { ComponentRoute } from '../../types'
8
4
  import type { BoltdocsConfig } from '../../../shared/types'
5
+ import { VersionSelector, I18nSelector } from './version-i18n'
6
+ import { ThemeSwitcher } from './theme-toggle'
7
+ import { useNavbar } from '../../hooks/use-navbar'
8
+ import { useUI } from '../../app/ui-context'
9
+ import { Button } from '../primitives/button'
9
10
 
10
- function getIcon(iconName?: string): React.ElementType | undefined {
11
- if (!iconName) return undefined
12
- const icons = { ...LucideIcons, ...virtualIcons } as unknown as Record<
13
- string,
14
- React.ElementType
15
- >
16
- const IconComponent = icons[iconName] || icons[iconName + 'Icon']
17
- return IconComponent || undefined
11
+ interface SidebarProps {
12
+ routes: ComponentRoute[]
13
+ config: BoltdocsConfig
18
14
  }
19
15
 
20
- function SidebarSubRouteGroup({
21
- route,
22
- activePath,
23
- getIcon,
24
- }: {
25
- route: ComponentRoute
26
- activePath: string
27
- getIcon: (iconName?: string) => React.ElementType | undefined
28
- }) {
29
- const isCurrent =
30
- activePath ===
31
- (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
32
-
33
- const hasActiveSubRoute = useMemo(
34
- () => route.subRoutes?.some((r) => r.path === activePath),
35
- [route.subRoutes, activePath],
36
- )
37
-
38
- const [isOpen, setIsOpen] = useState(hasActiveSubRoute || isCurrent)
16
+ function SidebarMain({ routes, config }: SidebarProps) {
17
+ const { logo, title, logoProps } = useNavbar()
18
+ const { closeSidebar } = useUI()
39
19
 
40
- useEffect(() => {
41
- if (hasActiveSubRoute || isCurrent) {
42
- setIsOpen(true)
43
- }
44
- }, [hasActiveSubRoute, isCurrent])
20
+ const SidebarLogo = logo ? (
21
+ <img
22
+ src={logo}
23
+ alt={logoProps?.alt || title}
24
+ width={24}
25
+ height={24}
26
+ className="rounded-xl"
27
+ />
28
+ ) : null
45
29
 
46
- return (
47
- <SidebarPrimitive.SubGroup
48
- label={route.title}
49
- href={route.path}
50
- active={isCurrent}
51
- icon={getIcon(route.icon)}
52
- badge={route.badge}
53
- isOpen={isOpen}
54
- onToggle={() => setIsOpen(!isOpen)}
55
- >
56
- {route.subRoutes?.map((subRoute: ComponentRoute) => {
57
- const isSubCurrent =
58
- activePath ===
59
- (subRoute.path.endsWith('/') ? subRoute.path.slice(0, -1) : subRoute.path)
60
- return (
61
- <SidebarPrimitive.Link
62
- key={subRoute.path}
63
- label={subRoute.title}
64
- href={subRoute.path}
65
- active={isSubCurrent}
66
- icon={getIcon(subRoute.icon)}
67
- badge={subRoute.badge}
68
- />
69
- )
70
- })}
71
- </SidebarPrimitive.SubGroup>
72
- )
73
- }
30
+ const hasUtilities = config.versions || config.i18n
74
31
 
75
- function SidebarGroupSection({
76
- group,
77
- activePath,
78
- getIcon,
79
- }: {
80
- group: {
81
- slug: string
82
- title: string
83
- routes: ComponentRoute[]
84
- icon?: string
85
- }
86
- activePath: string
87
- getIcon: (iconName?: string) => React.ElementType | undefined
88
- }) {
89
32
  return (
90
- <SidebarPrimitive.Group title={group.title} icon={getIcon(group.icon)}>
91
- {group.routes.map((route: ComponentRoute) => {
92
- if (route.subRoutes && route.subRoutes.length > 0) {
93
- return (
94
- <SidebarSubRouteGroup
95
- key={route.path}
96
- route={route}
97
- activePath={activePath}
98
- getIcon={getIcon}
99
- />
100
- )
101
- }
33
+ <>
34
+ {/* Desktop Version */}
35
+ <SidebarPrimitive.Root>
36
+ <SidebarPrimitive.Content>
37
+ <SidebarPrimitive.Items routes={routes} />
38
+ </SidebarPrimitive.Content>
39
+ </SidebarPrimitive.Root>
102
40
 
103
- const isCurrent =
104
- activePath ===
105
- (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
106
- return (
107
- <SidebarPrimitive.Link
108
- key={route.path}
109
- label={route.title}
110
- href={route.path}
111
- active={isCurrent}
112
- icon={getIcon(route.icon)}
113
- badge={route.badge}
114
- />
115
- )
116
- })}
117
- </SidebarPrimitive.Group>
41
+ {/* Mobile Version */}
42
+ <SidebarPrimitive.Mobile>
43
+ <SidebarPrimitive.Header>
44
+ <div className="flex items-center gap-3">
45
+ {SidebarLogo}
46
+ <span className="font-bold text-lg tracking-tight text-body truncate max-w-[120px]">
47
+ {title}
48
+ </span>
49
+ </div>
50
+ <div className="flex items-center gap-2">
51
+ <ThemeSwitcher className="w-24 h-9 rounded-xl" />
52
+ <Button
53
+ onPress={closeSidebar}
54
+ className="h-9 w-9 flex items-center justify-center bg-transparent border-none outline-none select-none cursor-pointer rounded-xl hover:bg-primary-50/50 text-muted hover:text-body transition-colors"
55
+ aria-label="Close sidebar"
56
+ >
57
+ <LucideIcons.X size={20} />
58
+ </Button>
59
+ </div>
60
+ </SidebarPrimitive.Header>
61
+ <SidebarPrimitive.Content>
62
+ {hasUtilities && (
63
+ <div className="flex flex-col gap-4 mb-10">
64
+ <div className="flex gap-3">
65
+ {config.versions && (
66
+ <VersionSelector className="flex-1 justify-between h-10 bg-surface border-subtle rounded-xl" />
67
+ )}
68
+ {config.i18n && (
69
+ <I18nSelector className="flex-1 justify-between h-10 bg-surface border-subtle rounded-xl" />
70
+ )}
71
+ </div>
72
+ <div className="mt-2 border-b border-subtle" />
73
+ </div>
74
+ )}
75
+ <SidebarPrimitive.Items routes={routes} />
76
+ </SidebarPrimitive.Content>
77
+ </SidebarPrimitive.Mobile>
78
+ </>
118
79
  )
119
80
  }
120
81
 
121
- export function Sidebar({
122
- routes,
123
- config,
124
- }: {
125
- routes: ComponentRoute[]
126
- config: BoltdocsConfig
127
- }) {
128
- const { groups, ungrouped, activePath } = useSidebar(routes)
129
- const themeConfig = config.theme || {}
130
-
131
- return (
132
- <SidebarPrimitive.Root>
133
- {ungrouped.length > 0 && (
134
- <SidebarPrimitive.Group className="mb-6">
135
- {ungrouped.map((route) => {
136
- const isCurrent =
137
- activePath ===
138
- (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
139
- return (
140
- <SidebarPrimitive.Link
141
- key={route.path}
142
- label={route.title}
143
- href={route.path}
144
- active={isCurrent}
145
- icon={getIcon(route.icon)}
146
- badge={route.badge}
147
- />
148
- )
149
- })}
150
- </SidebarPrimitive.Group>
151
- )}
152
-
153
- {groups.map((group) => (
154
- <SidebarGroupSection
155
- key={group.slug}
156
- group={group}
157
- activePath={activePath}
158
- getIcon={getIcon}
159
- />
160
- ))}
161
-
162
- {themeConfig?.poweredBy && (
163
- <div className="mt-auto pt-8">
164
- <PoweredBy />
165
- </div>
166
- )}
167
- </SidebarPrimitive.Root>
168
- )
169
- }
82
+ export const Sidebar = Object.assign(SidebarMain, {
83
+ Root: SidebarPrimitive.Root,
84
+ Mobile: SidebarPrimitive.Mobile,
85
+ Header: SidebarPrimitive.Header,
86
+ Content: SidebarPrimitive.Content,
87
+ Group: SidebarPrimitive.Group,
88
+ Link: SidebarPrimitive.Link,
89
+ SubGroup: SidebarPrimitive.SubGroup,
90
+ Item: SidebarPrimitive.Item,
91
+ Items: SidebarPrimitive.Items,
92
+ })
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'react'
1
2
  import { useTabs as useTabsHook } from '../../hooks/use-tabs'
2
3
  import { Tabs as T } from '../primitives/tabs'
3
4
  import { Link } from '../primitives/link'
@@ -16,6 +17,17 @@ export function Tabs({
16
17
  const { currentLocale } = useRoutes()
17
18
  const { indicatorStyle, tabRefs, activeIndex } = useTabsHook(tabs, routes)
18
19
 
20
+ useEffect(() => {
21
+ const activeTab = tabRefs.current[activeIndex]
22
+ if (activeTab) {
23
+ activeTab.scrollIntoView({
24
+ behavior: 'smooth',
25
+ block: 'nearest',
26
+ inline: 'center',
27
+ })
28
+ }
29
+ }, [activeIndex])
30
+
19
31
  const renderTabIcon = (iconName?: string) => {
20
32
  if (!iconName) return null
21
33
  if (iconName.trim().startsWith('<svg')) {
@@ -34,8 +46,8 @@ export function Tabs({
34
46
  }
35
47
 
36
48
  return (
37
- <div className="mx-auto max-w-(--breakpoint-3xl) px-4 md:px-6">
38
- <T.List className="border-none py-0">
49
+ <div className="mx-auto max-w-(--breakpoint-3xl) px-4 md:px-6 select-none">
50
+ <T.List className="border-none py-0 scrollbar-hide relative flex flex-row items-center">
39
51
  {tabs.map((tab, index) => {
40
52
  const isActive = index === activeIndex
41
53
  const firstRoute = routes.find(
@@ -50,10 +62,8 @@ export function Tabs({
50
62
  ref={(el: HTMLAnchorElement | null) => {
51
63
  tabRefs.current[index] = el
52
64
  }}
53
- className={`relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors outline-none ${
54
- isActive
55
- ? 'text-primary-500'
56
- : 'text-text-muted hover:text-text-main'
65
+ className={`relative flex items-center gap-2 px-4 py-3.5 text-sm font-semibold transition-colors duration-300 outline-none whitespace-nowrap ${
66
+ isActive ? 'text-primary-500' : 'text-muted hover:text-body'
57
67
  }`}
58
68
  >
59
69
  {renderTabIcon(tab.icon)}
@@ -61,7 +71,10 @@ export function Tabs({
61
71
  </Link>
62
72
  )
63
73
  })}
64
- <T.Indicator style={indicatorStyle} />
74
+ <T.Indicator
75
+ style={indicatorStyle}
76
+ className="h-0.5 bg-primary-500 rounded-full transition-all duration-300"
77
+ />
65
78
  </T.List>
66
79
  </div>
67
80
  )
@@ -3,6 +3,7 @@ import { Sun, Moon, Monitor } from 'lucide-react'
3
3
  import { useTheme } from '../../app/theme-context'
4
4
  import { Button } from 'react-aria-components'
5
5
  import { Menu } from '../primitives/menu'
6
+ import { cn } from '../../utils/cn'
6
7
 
7
8
  export function ThemeToggle() {
8
9
  const { theme, setTheme } = useTheme()
@@ -21,7 +22,7 @@ export function ThemeToggle() {
21
22
  return (
22
23
  <Menu.Trigger placement="bottom right">
23
24
  <Button
24
- className="flex h-9 w-9 items-center justify-center rounded-md text-text-muted transition-colors hover:bg-bg-surface hover:text-text-main outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
25
+ className="flex h-9 w-9 items-center justify-center rounded-xl text-muted transition-colors hover:bg-surface hover:text-body outline-none border-none bg-transparent cursor-pointer"
25
26
  aria-label="Selection theme"
26
27
  >
27
28
  <Icon size={20} className="animate-in fade-in zoom-in duration-300" />
@@ -33,20 +34,97 @@ export function ThemeToggle() {
33
34
  const newTheme = Array.from(keys)[0] as 'light' | 'dark' | 'system'
34
35
  setTheme(newTheme)
35
36
  }}
37
+ className="w-36 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"
36
38
  >
37
- <Menu.Item id="light">
38
- <Sun size={16} />
39
- <span>Light</span>
39
+ <Menu.Item
40
+ id="light"
41
+ className="group 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 data-selected:text-primary-500 data-selected:bg-primary-500/5"
42
+ >
43
+ <Sun
44
+ className="group-hover:text-primary-500 dark:group-hover:text-primary-200"
45
+ size={16}
46
+ />
47
+ <span className="ml-2">Light</span>
40
48
  </Menu.Item>
41
- <Menu.Item id="dark">
42
- <Moon size={16} />
43
- <span>Dark</span>
49
+ <Menu.Item
50
+ id="dark"
51
+ 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 data-selected:text-primary-500 data-selected:bg-primary-500/5"
52
+ >
53
+ <Moon
54
+ className="group-hover:text-primary-500 dark:group-hover:text-primary-200"
55
+ size={16}
56
+ />
57
+ <span className="ml-2">Dark</span>
44
58
  </Menu.Item>
45
- <Menu.Item id="system">
46
- <Monitor size={16} />
47
- <span>System</span>
59
+ <Menu.Item
60
+ id="system"
61
+ 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 data-selected:text-primary-500 data-selected:bg-primary-500/5"
62
+ >
63
+ <Monitor
64
+ className="group-hover:text-primary-500 dark:group-hover:text-primary-200"
65
+ size={16}
66
+ />
67
+ <span className="ml-2">System</span>
48
68
  </Menu.Item>
49
69
  </Menu.Root>
50
70
  </Menu.Trigger>
51
71
  )
52
72
  }
73
+
74
+ export function ThemeSwitcher({ className }: { className?: string }) {
75
+ const { theme, setTheme } = useTheme()
76
+ const [mounted, setMounted] = useState(false)
77
+
78
+ useEffect(() => {
79
+ setMounted(true)
80
+ }, [])
81
+
82
+ if (!mounted) {
83
+ return (
84
+ <div
85
+ className={cn(
86
+ 'h-10 w-full bg-surface rounded-xl animate-pulse',
87
+ className,
88
+ )}
89
+ />
90
+ )
91
+ }
92
+
93
+ const isDark = theme === 'dark'
94
+
95
+ return (
96
+ <div
97
+ className={cn(
98
+ 'flex p-1 bg-surface border border-subtle rounded-xl relative w-full h-11',
99
+ className,
100
+ )}
101
+ >
102
+ <div
103
+ className={cn(
104
+ 'absolute inset-y-1 w-[calc(50%-4px)] bg-main border border-subtle rounded-lg transition-all duration-300 ease-out shadow-xs',
105
+ isDark ? 'translate-x-full' : 'translate-x-0',
106
+ )}
107
+ />
108
+ <button
109
+ onClick={() => setTheme('light')}
110
+ className={cn(
111
+ 'flex-1 flex items-center justify-center rounded-lg z-10 transition-colors outline-none cursor-pointer border-none bg-transparent',
112
+ !isDark ? 'text-body font-semibold' : 'text-muted hover:text-body',
113
+ )}
114
+ aria-label="Light mode"
115
+ >
116
+ <Sun size={18} />
117
+ </button>
118
+ <button
119
+ onClick={() => setTheme('dark')}
120
+ className={cn(
121
+ 'flex-1 flex items-center justify-center rounded-lg z-10 transition-colors outline-none cursor-pointer border-none bg-transparent',
122
+ isDark ? 'text-body font-semibold' : 'text-muted hover:text-body',
123
+ )}
124
+ aria-label="Dark mode"
125
+ >
126
+ <Moon size={18} />
127
+ </button>
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,80 @@
1
+ import { useVersion } from '../../hooks/use-version'
2
+ import { useI18n } from '../../hooks/use-i18n'
3
+ import { Menu } from '../primitives/menu'
4
+ import { Button } from '../primitives/button'
5
+ import { ChevronDown, Languages } from 'lucide-react'
6
+ import { cn } from '../../utils/cn'
7
+
8
+ export function VersionSelector({ className }: { className?: string }) {
9
+ const { currentVersionLabel, availableVersions, handleVersionChange } =
10
+ useVersion()
11
+
12
+ if (availableVersions.length === 0) return null
13
+
14
+ return (
15
+ <Menu.Trigger>
16
+ <Button
17
+ className={cn(
18
+ 'flex h-9 items-center justify-between gap-2 border border-subtle bg-surface px-4 py-1.5 rounded-xl text-xs font-semibold text-body hover:bg-primary-50/20 hover:border-primary-500/50 transition-all duration-300 outline-none select-none cursor-pointer',
19
+ className,
20
+ )}
21
+ >
22
+ <span className="font-semibold text-[0.8125rem]">
23
+ {currentVersionLabel}
24
+ </span>
25
+ <ChevronDown className="w-3.5 h-3.5 text-muted/60" />
26
+ </Button>
27
+ <Menu.Root className="w-40 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">
28
+ <Menu.Section items={availableVersions}>
29
+ {(version) => (
30
+ <Menu.Item
31
+ key={`${version.value ?? ''}`}
32
+ onPress={() => handleVersionChange(version.value)}
33
+ className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-body hover:bg-primary-50/50 cursor-pointer select-none outline-none group data-selected:text-primary-500 data-selected:bg-primary-500/5"
34
+ >
35
+ {version.label as string}
36
+ </Menu.Item>
37
+ )}
38
+ </Menu.Section>
39
+ </Menu.Root>
40
+ </Menu.Trigger>
41
+ )
42
+ }
43
+
44
+ export function I18nSelector({ className }: { className?: string }) {
45
+ const { currentLocale, availableLocales, handleLocaleChange } = useI18n()
46
+
47
+ if (availableLocales.length === 0) return null
48
+
49
+ return (
50
+ <Menu.Trigger>
51
+ <Button
52
+ className={cn(
53
+ 'flex h-9 items-center justify-between gap-2 border border-subtle bg-surface px-4 py-1.5 rounded-xl text-xs font-semibold text-body hover:bg-primary-50/20 hover:border-primary-500/50 transition-all duration-300 outline-none select-none cursor-pointer',
54
+ className,
55
+ )}
56
+ >
57
+ <div className="flex items-center gap-1.5">
58
+ <Languages className="w-3.5 h-3.5 text-primary-500" />
59
+ <span className="font-bold text-[0.75rem] uppercase opacity-90">
60
+ {currentLocale || 'en'}
61
+ </span>
62
+ </div>
63
+ <ChevronDown className="w-3.5 h-3.5 text-muted/60" />
64
+ </Button>
65
+ <Menu.Root className="w-40 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">
66
+ <Menu.Section items={availableLocales}>
67
+ {(locale) => (
68
+ <Menu.Item
69
+ key={`${locale.value ?? ''}`}
70
+ onPress={() => handleLocaleChange(locale.value)}
71
+ 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 data-selected:text-primary-500 data-selected:bg-primary-500/5"
72
+ >
73
+ <span>{locale.label as string}</span>
74
+ </Menu.Item>
75
+ )}
76
+ </Menu.Section>
77
+ </Menu.Root>
78
+ </Menu.Trigger>
79
+ )
80
+ }
@@ -1,7 +1,6 @@
1
1
  export { useNavbar } from './use-navbar'
2
2
  export { useSidebar } from './use-sidebar'
3
3
  export { useSearch } from './use-search'
4
- export { useOnThisPage } from './use-onthispage'
5
4
  export { useTabs } from './use-tabs'
6
5
  export { useVersion } from './use-version'
7
6
  export { useI18n } from './use-i18n'
@@ -10,3 +9,5 @@ export { useBreadcrumbs } from './use-breadcrumbs'
10
9
  export { useRoutes } from './use-routes'
11
10
  export { useLocalizedTo } from './use-localized-to'
12
11
  export { useLocation } from './use-location'
12
+ export { useSearchHighlight } from './use-search-highlight'
13
+ export { useAnalytics, useTrackPageView, useTrackEvent } from './use-analytics'