boltdocs 2.1.0 → 2.2.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 (116) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/base-ui/index.d.mts +25 -0
  3. package/dist/base-ui/index.d.ts +25 -0
  4. package/dist/base-ui/index.js +1 -0
  5. package/dist/base-ui/index.mjs +1 -0
  6. package/dist/{cache-Q4T6VAUL.mjs → cache-CRAZ55X7.mjs} +1 -1
  7. package/dist/chunk-5D6XPYQ3.mjs +74 -0
  8. package/dist/chunk-6QXCKZAT.mjs +1 -0
  9. package/dist/chunk-H4M6P3DM.mjs +1 -0
  10. package/dist/chunk-JD3RSDE4.mjs +1 -0
  11. package/dist/chunk-JXHNX2WN.mjs +1 -0
  12. package/dist/chunk-JZXLCA2E.mjs +1 -0
  13. package/dist/chunk-MZBG4N4W.mjs +1 -0
  14. package/dist/chunk-NBCYHLAA.mjs +1 -0
  15. package/dist/chunk-Q3MLYTIQ.mjs +1 -0
  16. package/dist/chunk-RSII2UPE.mjs +1 -0
  17. package/dist/chunk-T3W44KWY.mjs +1 -0
  18. package/dist/chunk-ZK2266IZ.mjs +1 -0
  19. package/dist/chunk-ZRJ55GGF.mjs +1 -0
  20. package/dist/client/index.d.mts +13 -115
  21. package/dist/client/index.d.ts +13 -115
  22. package/dist/client/index.js +1 -1
  23. package/dist/client/index.mjs +1 -1
  24. package/dist/client/ssr.js +1 -1
  25. package/dist/client/ssr.mjs +1 -1
  26. package/dist/client/types.d.mts +3 -0
  27. package/dist/client/types.d.ts +3 -0
  28. package/dist/client/types.js +1 -0
  29. package/dist/client/types.mjs +0 -0
  30. package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
  31. package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
  32. package/dist/{client/hooks → hooks}/index.d.mts +25 -12
  33. package/dist/{client/hooks → hooks}/index.d.ts +25 -12
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/index.mjs +1 -0
  36. package/dist/integrations/index.d.mts +48 -0
  37. package/dist/integrations/index.d.ts +48 -0
  38. package/dist/integrations/index.js +1 -0
  39. package/dist/integrations/index.mjs +1 -0
  40. package/dist/link-DfBwCeZc.d.mts +68 -0
  41. package/dist/link-DfBwCeZc.d.ts +68 -0
  42. package/dist/loading-BqGrFWO5.d.mts +68 -0
  43. package/dist/loading-chS3pm9W.d.ts +68 -0
  44. package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
  45. package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
  46. package/dist/mdx/index.js +1 -0
  47. package/dist/mdx/index.mjs +1 -0
  48. package/dist/node/cli-entry.js +27 -27
  49. package/dist/node/cli-entry.mjs +1 -1
  50. package/dist/node/index.d.mts +44 -14
  51. package/dist/node/index.d.ts +44 -14
  52. package/dist/node/index.js +23 -23
  53. package/dist/node/index.mjs +1 -1
  54. package/dist/primitives/index.d.mts +301 -0
  55. package/dist/primitives/index.d.ts +301 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/index.mjs +1 -0
  58. package/dist/search-dialog-MA5AISC7.mjs +1 -0
  59. package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
  60. package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
  61. package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
  62. package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
  63. package/package.json +34 -8
  64. package/src/client/app/index.tsx +155 -35
  65. package/src/client/app/mdx-component.tsx +7 -3
  66. package/src/client/app/theme-context.tsx +40 -23
  67. package/src/client/components/default-layout.tsx +12 -6
  68. package/src/client/components/primitives/breadcrumbs.tsx +1 -1
  69. package/src/client/components/primitives/navbar.tsx +5 -2
  70. package/src/client/components/primitives/search-dialog.tsx +12 -3
  71. package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
  72. package/src/client/components/ui-base/index.ts +17 -0
  73. package/src/client/components/ui-base/navbar.tsx +66 -33
  74. package/src/client/components/ui-base/powered-by.tsx +8 -5
  75. package/src/client/components/ui-base/sidebar.tsx +31 -22
  76. package/src/client/components/ui-base/tabs.tsx +4 -1
  77. package/src/client/components/ui-base/theme-toggle.tsx +35 -15
  78. package/src/client/hooks/use-i18n.ts +37 -7
  79. package/src/client/hooks/use-localized-to.ts +45 -68
  80. package/src/client/hooks/use-navbar.ts +10 -3
  81. package/src/client/hooks/use-routes.ts +61 -17
  82. package/src/client/hooks/use-search.ts +21 -5
  83. package/src/client/hooks/use-sidebar.ts +5 -2
  84. package/src/client/hooks/use-version.ts +5 -0
  85. package/src/client/integrations/index.ts +1 -0
  86. package/src/client/store/use-boltdocs-store.ts +43 -0
  87. package/src/client/types.ts +4 -2
  88. package/src/client/utils/i18n.ts +23 -0
  89. package/src/node/config.ts +54 -17
  90. package/src/node/index.ts +1 -1
  91. package/src/node/mdx/cache.ts +12 -0
  92. package/src/node/mdx/highlighter.ts +47 -0
  93. package/src/node/mdx/index.ts +114 -0
  94. package/src/node/mdx/rehype-shiki.ts +53 -0
  95. package/src/node/mdx/remark-shiki.ts +61 -0
  96. package/src/node/plugin/html.ts +8 -4
  97. package/src/node/plugin/index.ts +117 -68
  98. package/src/node/routes/index.ts +34 -13
  99. package/src/node/routes/parser.ts +12 -4
  100. package/src/node/ssg/index.ts +3 -3
  101. package/src/node/utils.ts +32 -2
  102. package/tsup.config.ts +7 -2
  103. package/dist/chunk-52MVMZWS.mjs +0 -1
  104. package/dist/chunk-BVWWKXJH.mjs +0 -1
  105. package/dist/chunk-DVY3RDXD.mjs +0 -1
  106. package/dist/chunk-FUVYCYWC.mjs +0 -1
  107. package/dist/chunk-GBLMDJ2B.mjs +0 -1
  108. package/dist/chunk-ISPX45DF.mjs +0 -1
  109. package/dist/chunk-PNXZMUCO.mjs +0 -1
  110. package/dist/chunk-V2ZHKQSP.mjs +0 -74
  111. package/dist/client/components/mdx/index.js +0 -1
  112. package/dist/client/components/mdx/index.mjs +0 -1
  113. package/dist/client/hooks/index.js +0 -1
  114. package/dist/client/hooks/index.mjs +0 -1
  115. package/dist/search-dialog-TWGYKF2D.mjs +0 -1
  116. package/src/node/mdx.ts +0 -279
@@ -13,6 +13,7 @@ import { useConfig } from '@client/app/config-context'
13
13
  import { useMdxComponents } from '@client/app/mdx-components-context'
14
14
 
15
15
  import { useLocation } from 'react-router-dom'
16
+ import { getTranslated } from '@client/utils/i18n'
16
17
 
17
18
  export interface LayoutProps {
18
19
  children: React.ReactNode
@@ -24,7 +25,12 @@ export interface LayoutProps {
24
25
  * and rearrange, wrap, or replace any section.
25
26
  */
26
27
  export function DefaultLayout({ children }: LayoutProps) {
27
- const { routes: filteredRoutes, allRoutes, currentRoute } = useRoutes()
28
+ const {
29
+ routes: filteredRoutes,
30
+ allRoutes,
31
+ currentRoute,
32
+ currentLocale,
33
+ } = useRoutes()
28
34
  const { pathname } = useLocation()
29
35
  const config = useConfig()
30
36
  const mdxComponents = useMdxComponents()
@@ -36,8 +42,8 @@ export function DefaultLayout({ children }: LayoutProps) {
36
42
  <DocsLayout>
37
43
  <ProgressBar />
38
44
  <Head
39
- siteTitle={config.themeConfig?.title || 'Boltdocs'}
40
- siteDescription={config.themeConfig?.description || ''}
45
+ siteTitle={getTranslated(config.theme?.title, currentLocale) || 'Boltdocs'}
46
+ siteDescription={getTranslated(config.theme?.description, currentLocale) || ''}
41
47
  routes={allRoutes}
42
48
  />
43
49
  <Navbar />
@@ -52,7 +58,7 @@ export function DefaultLayout({ children }: LayoutProps) {
52
58
  <CopyMarkdownComp
53
59
  mdxRaw={currentRoute?._rawContent}
54
60
  route={currentRoute}
55
- config={config.themeConfig?.copyMarkdown}
61
+ config={config.theme?.copyMarkdown}
56
62
  />
57
63
  </DocsLayout.ContentHeader>
58
64
  )}
@@ -69,8 +75,8 @@ export function DefaultLayout({ children }: LayoutProps) {
69
75
  {!isHome && (
70
76
  <OnThisPage
71
77
  headings={currentRoute?.headings}
72
- editLink={config.themeConfig?.editLink}
73
- communityHelp={config.themeConfig?.communityHelp}
78
+ editLink={config.theme?.editLink}
79
+ communityHelp={config.theme?.communityHelp}
74
80
  filePath={currentRoute?.filePath}
75
81
  />
76
82
  )}
@@ -16,7 +16,7 @@ export const BreadcrumbsRoot = ({
16
16
  return (
17
17
  <BreadcrumbsRAC
18
18
  className={cn(
19
- 'flex items-center gap-1.5 mb-0 text-sm text-text-muted',
19
+ 'flex items-center gap-1.5 pl-0! mb-0 text-sm text-text-muted',
20
20
  className,
21
21
  )}
22
22
  {...props}
@@ -180,8 +180,11 @@ export const NavbarLink = ({
180
180
  href={href}
181
181
  target={to === 'external' ? '_blank' : undefined}
182
182
  className={cn(
183
- 'transition-colors outline-none hover:text-text-main focus-visible:ring-2 focus-visible:ring-primary-500/30 rounded-sm',
184
- active ? 'text-primary-500' : 'text-text-muted',
183
+ 'transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30 rounded-sm',
184
+ {
185
+ 'text-primary-500 font-bold': active,
186
+ 'text-text-muted hover:text-text-main font-medium': !active,
187
+ },
185
188
  className,
186
189
  )}
187
190
  >
@@ -53,13 +53,22 @@ export const SearchDialogRoot = ({
53
53
  export const SearchDialogAutocomplete = ({
54
54
  children,
55
55
  className,
56
+ onSelectionChange,
56
57
  ...props
57
- }: RAC.AutocompleteProps<object> & { className?: string }) => {
58
+ }: RAC.AutocompleteProps<object> & {
59
+ className?: string
60
+ onSelectionChange?: (key: RAC.Key) => void
61
+ }) => {
62
+ const Autocomplete = RAC.Autocomplete as any
58
63
  return (
59
64
  <div className={className}>
60
- <RAC.Autocomplete {...props} className="flex flex-col min-h-0">
65
+ <Autocomplete
66
+ {...props}
67
+ onSelectionChange={onSelectionChange}
68
+ className="flex flex-col min-h-0"
69
+ >
61
70
  {children}
62
- </RAC.Autocomplete>
71
+ </Autocomplete>
63
72
  </div>
64
73
  )
65
74
  }
@@ -12,7 +12,7 @@ import { useConfig } from '@client/app/config-context'
12
12
  export function Breadcrumbs() {
13
13
  const { crumbs, activeRoute } = useBreadcrumbs()
14
14
  const config = useConfig()
15
- const themeConfig = config.theme || config.themeConfig || {}
15
+ const themeConfig = config.theme || {}
16
16
 
17
17
  if (crumbs.length === 0) return null
18
18
 
@@ -0,0 +1,17 @@
1
+ export { Breadcrumbs } from './breadcrumbs'
2
+ export { CopyMarkdown } from './copy-markdown'
3
+ export type { CopyMarkdownProps } from './copy-markdown'
4
+ export { ErrorBoundary } from './error-boundary'
5
+ export { GithubStars } from './github-stars'
6
+ export { Head } from './head'
7
+ export { Loading } from './loading'
8
+ export { Navbar } from './navbar'
9
+ export { NotFound } from './not-found'
10
+ export { OnThisPage } from './on-this-page'
11
+ export { PageNav } from './page-nav'
12
+ export { PoweredBy } from './powered-by'
13
+ export { ProgressBar } from './progress-bar'
14
+ export { SearchDialog } from './search-dialog'
15
+ export { Sidebar } from './sidebar'
16
+ export { Tabs } from './tabs'
17
+ export { ThemeToggle } from './theme-toggle'
@@ -11,8 +11,9 @@ import { useLocation } from 'react-router-dom'
11
11
  import type { BoltdocsSocialLink } from '@node/config'
12
12
  import Menu from '@components/primitives/menu'
13
13
  import { Button } from '@components/primitives/button'
14
- import { ChevronDown } from 'lucide-react'
15
- import { useConfig } from '@client/app/config-context'
14
+ import { ChevronDown, Languages } from 'lucide-react'
15
+ import { useLocalizedTo } from '@hooks/use-localized-to'
16
+ import type { NavbarLink as NavbarLinkType } from '@client/types'
16
17
 
17
18
  const SearchDialog = lazy(() =>
18
19
  import('./search-dialog').then((m) => ({
@@ -24,7 +25,7 @@ export function Navbar() {
24
25
  const { links, title, logo, logoProps, github, social, config } = useNavbar()
25
26
  const { routes, allRoutes, currentVersion, currentLocale } = useRoutes()
26
27
  const { pathname } = useLocation()
27
- const themeConfig = config.theme || config.themeConfig || {}
28
+ const themeConfig = config.theme || {}
28
29
 
29
30
  const hasTabs = themeConfig?.tabs && themeConfig.tabs.length > 0
30
31
 
@@ -44,7 +45,7 @@ export function Navbar() {
44
45
 
45
46
  <NavbarPrimitive.Links>
46
47
  {links.map((link) => (
47
- <NavbarPrimitive.Link key={link.href} {...(link as any)} />
48
+ <NavbarLinkItem key={link.href} link={link} />
48
49
  ))}
49
50
  </NavbarPrimitive.Links>
50
51
  </NavbarPrimitive.NavbarLeft>
@@ -78,16 +79,18 @@ export function Navbar() {
78
79
 
79
80
  {pathname !== '/' && hasTabs && themeConfig?.tabs && (
80
81
  <div className="w-full border-b border-border-subtle bg-bg-main">
81
- <Tabs
82
- tabs={themeConfig.tabs}
83
- routes={allRoutes || routes || []}
84
- />
82
+ <Tabs tabs={themeConfig.tabs} routes={allRoutes || routes || []} />
85
83
  </div>
86
84
  )}
87
85
  </NavbarPrimitive.NavbarRoot>
88
86
  )
89
87
  }
90
88
 
89
+ function NavbarLinkItem({ link }: { link: NavbarLinkType }) {
90
+ const localizedHref = useLocalizedTo(link.href)
91
+ return <NavbarPrimitive.Link {...(link as any)} href={localizedHref} />
92
+ }
93
+
91
94
  function NavbarVersion() {
92
95
  const { currentVersionLabel, availableVersions, handleVersionChange } =
93
96
  useVersion()
@@ -96,43 +99,73 @@ function NavbarVersion() {
96
99
 
97
100
  return (
98
101
  <Menu.Trigger>
99
- <Button variant={'outline'} iconPosition="right" icon={<ChevronDown />}>
100
- {currentVersionLabel}
102
+ <Button
103
+ variant={'outline'}
104
+ size="sm"
105
+ rounded="lg"
106
+ iconPosition="right"
107
+ icon={<ChevronDown className="w-3.5 h-3.5 text-text-muted/60" />}
108
+ 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"
109
+ >
110
+ <span className="font-semibold text-[0.8125rem]">
111
+ {currentVersionLabel}
112
+ </span>
101
113
  </Button>
102
- <Menu.Section items={availableVersions}>
103
- {(version) => (
104
- <Menu.Item
105
- key={`${version.value ?? ''}`}
106
- onPress={() => handleVersionChange(version.value)}
107
- >
108
- {version.label as string}
109
- </Menu.Item>
110
- )}
111
- </Menu.Section>
114
+ <Menu.Root>
115
+ <Menu.Section items={availableVersions}>
116
+ {(version) => (
117
+ <Menu.Item
118
+ key={`${version.value ?? ''}`}
119
+ onPress={() => handleVersionChange(version.value)}
120
+ >
121
+ {version.label as string}
122
+ </Menu.Item>
123
+ )}
124
+ </Menu.Section>
125
+ </Menu.Root>
112
126
  </Menu.Trigger>
113
127
  )
114
128
  }
115
129
 
116
130
  function NavbarI18n() {
117
- const { currentLocaleLabel, availableLocales, handleLocaleChange } = useI18n()
131
+ const { currentLocale, availableLocales, handleLocaleChange } = useI18n()
118
132
 
119
133
  if (availableLocales.length === 0) return null
120
134
 
121
135
  return (
122
136
  <Menu.Trigger>
123
- <Button variant={'outline'} iconPosition="right" icon={<ChevronDown />}>
124
- {currentLocaleLabel}
137
+ <Button
138
+ variant={'outline'}
139
+ size="sm"
140
+ rounded="lg"
141
+ iconPosition="right"
142
+ icon={<ChevronDown className="w-3.5 h-3.5 text-text-muted/60" />}
143
+ 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"
144
+ >
145
+ <div className="flex items-center gap-1.5">
146
+ <Languages className="w-3.5 h-3.5 text-primary-500" />
147
+ <span className="font-bold text-[0.75rem] tracking-wider uppercase opacity-90">
148
+ {currentLocale || 'en'}
149
+ </span>
150
+ </div>
125
151
  </Button>
126
- <Menu.Section items={availableLocales}>
127
- {(locale) => (
128
- <Menu.Item
129
- key={`${locale.value ?? ''}`}
130
- onPress={() => handleLocaleChange(locale.value)}
131
- >
132
- {locale.label as string}
133
- </Menu.Item>
134
- )}
135
- </Menu.Section>
152
+ <Menu.Root>
153
+ <Menu.Section items={availableLocales}>
154
+ {(locale) => (
155
+ <Menu.Item
156
+ key={`${locale.value ?? ''}`}
157
+ onPress={() => handleLocaleChange(locale.value)}
158
+ >
159
+ <div className="flex items-center justify-between w-full gap-4">
160
+ <span>{locale.label as string}</span>
161
+ <span className="text-[10px] font-bold opacity-40 uppercase tracking-tighter">
162
+ {locale.value}
163
+ </span>
164
+ </div>
165
+ </Menu.Item>
166
+ )}
167
+ </Menu.Section>
168
+ </Menu.Root>
136
169
  </Menu.Trigger>
137
170
  )
138
171
  }
@@ -2,16 +2,19 @@ import { Zap } from 'lucide-react'
2
2
 
3
3
  export function PoweredBy() {
4
4
  return (
5
- <div className="rounded-full px-4 py-2 bg-gray-100 text-xs text-gray-500 flex items-center gap-1 mt-6 justify-center">
5
+ <div className="flex items-center justify-center mt-10 mb-4 px-4 w-full">
6
6
  <a
7
7
  href="https://github.com/jesusalcaladev/boltdocs"
8
8
  target="_blank"
9
9
  rel="noopener noreferrer"
10
- className="flex items-center gap-1"
10
+ className="group relative flex items-center gap-2 px-4 py-2 rounded-full border border-border-subtle bg-bg-surface/50 backdrop-blur-md transition-all duration-300 hover:border-primary-500/50 hover:bg-bg-surface hover:shadow-xl hover:shadow-primary-500/5 select-none"
11
11
  >
12
- <Zap className="powered-by-icon" size={12} fill="currentColor" />
13
- <span>
14
- Powered by <strong>Boltdocs</strong>
12
+ <Zap
13
+ className="w-3.5 h-3.5 text-text-muted group-hover:text-primary-500 transition-colors duration-300"
14
+ fill="currentColor"
15
+ />
16
+ <span className="text-[11px] font-medium text-text-muted group-hover:text-text-main transition-colors duration-300 tracking-wide">
17
+ Powered by <strong className="font-bold text-text-main/80 group-hover:text-text-main">Boltdocs</strong>
15
18
  </span>
16
19
  </a>
17
20
  </div>
@@ -8,7 +8,8 @@ import type { BoltdocsConfig } from '@node/config'
8
8
 
9
9
  function getIcon(iconName?: string): React.ElementType | undefined {
10
10
  if (!iconName) return undefined
11
- const IconComponent = (LucideIcons as Record<string, any>)[iconName]
11
+ const icons = LucideIcons as unknown as Record<string, React.ElementType>
12
+ const IconComponent = icons[iconName]
12
13
  return IconComponent || undefined
13
14
  }
14
15
 
@@ -45,16 +46,20 @@ function CollapsibleSidebarGroup({
45
46
  isOpen={isOpen}
46
47
  onToggle={() => setIsOpen(!isOpen)}
47
48
  >
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
- ))}
49
+ {group.routes.map((route: ComponentRoute) => {
50
+ const isCurrent =
51
+ activePath === (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
52
+ return (
53
+ <SidebarPrimitive.SidebarLink
54
+ key={route.path}
55
+ label={route.title}
56
+ href={route.path}
57
+ active={isCurrent}
58
+ icon={getIcon(route.icon)}
59
+ badge={route.badge}
60
+ />
61
+ )
62
+ })}
58
63
  </SidebarPrimitive.SidebarGroup>
59
64
  )
60
65
  }
@@ -67,22 +72,26 @@ export function Sidebar({
67
72
  config: BoltdocsConfig
68
73
  }) {
69
74
  const { groups, ungrouped, activePath } = useSidebar(routes)
70
- const themeConfig = config.theme || config.themeConfig || {}
75
+ const themeConfig = config.theme || {}
71
76
 
72
77
  return (
73
78
  <SidebarPrimitive.SidebarRoot>
74
79
  {ungrouped.length > 0 && (
75
80
  <SidebarPrimitive.SidebarGroup className="mb-6">
76
- {ungrouped.map((route) => (
77
- <SidebarPrimitive.SidebarLink
78
- key={route.path}
79
- label={route.title}
80
- href={route.path}
81
- active={activePath === route.path}
82
- icon={getIcon(route.icon)}
83
- badge={route.badge}
84
- />
85
- ))}
81
+ {ungrouped.map((route) => {
82
+ const isCurrent =
83
+ activePath === (route.path.endsWith('/') ? route.path.slice(0, -1) : route.path)
84
+ return (
85
+ <SidebarPrimitive.SidebarLink
86
+ key={route.path}
87
+ label={route.title}
88
+ href={route.path}
89
+ active={isCurrent}
90
+ icon={getIcon(route.icon)}
91
+ badge={route.badge}
92
+ />
93
+ )
94
+ })}
86
95
  </SidebarPrimitive.SidebarGroup>
87
96
  )}
88
97
 
@@ -3,6 +3,8 @@ import T from '@components/primitives/tabs'
3
3
  import { Link } from '@components/primitives/link'
4
4
  import type { BoltdocsTab, ComponentRoute } from '@client/types'
5
5
  import * as Icons from 'lucide-react'
6
+ import { getTranslated } from '@client/utils/i18n'
7
+ import { useRoutes } from '@hooks/use-routes'
6
8
 
7
9
  export function Tabs({
8
10
  tabs,
@@ -11,6 +13,7 @@ export function Tabs({
11
13
  tabs: BoltdocsTab[]
12
14
  routes: ComponentRoute[]
13
15
  }) {
16
+ const { currentLocale } = useRoutes()
14
17
  const { indicatorStyle, tabRefs, activeIndex } = useTabsHook(tabs, routes)
15
18
 
16
19
  const renderTabIcon = (iconName?: string) => {
@@ -54,7 +57,7 @@ export function Tabs({
54
57
  }`}
55
58
  >
56
59
  {renderTabIcon(tab.icon)}
57
- <span>{tab.text}</span>
60
+ <span>{getTranslated(tab.text, currentLocale)}</span>
58
61
  </Link>
59
62
  )
60
63
  })}
@@ -1,10 +1,11 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { Sun, Moon } from 'lucide-react'
2
+ import { Sun, Moon, Monitor } from 'lucide-react'
3
3
  import { useTheme } from '@client/app/theme-context'
4
- import { ToggleButton } from 'react-aria-components'
4
+ import { Button } from 'react-aria-components'
5
+ import { Menu, MenuItem, MenuTrigger } from '@components/primitives/menu'
5
6
 
6
7
  export function ThemeToggle() {
7
- const { theme, toggleTheme } = useTheme()
8
+ const { theme, setTheme } = useTheme()
8
9
  const [mounted, setMounted] = useState(false)
9
10
 
10
11
  useEffect(() => {
@@ -15,18 +16,37 @@ export function ThemeToggle() {
15
16
  return <div className="h-9 w-9" />
16
17
  }
17
18
 
19
+ const Icon = theme === 'system' ? Monitor : theme === 'dark' ? Moon : Sun
20
+
18
21
  return (
19
- <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>
22
+ <MenuTrigger placement="bottom right">
23
+ <Button
24
+ className="flex h-9 w-9 items-center justify-center rounded-md text-text-muted transition-colors hover:bg-bg-surface hover:text-text-main outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
25
+ aria-label="Selection theme"
26
+ >
27
+ <Icon size={20} className="animate-in fade-in zoom-in duration-300" />
28
+ </Button>
29
+ <Menu
30
+ selectionMode="single"
31
+ selectedKeys={[theme]}
32
+ onSelectionChange={(keys) => {
33
+ const newTheme = Array.from(keys)[0] as 'light' | 'dark' | 'system'
34
+ setTheme(newTheme)
35
+ }}
36
+ >
37
+ <MenuItem id="light">
38
+ <Sun size={16} />
39
+ <span>Light</span>
40
+ </MenuItem>
41
+ <MenuItem id="dark">
42
+ <Moon size={16} />
43
+ <span>Dark</span>
44
+ </MenuItem>
45
+ <MenuItem id="system">
46
+ <Monitor size={16} />
47
+ <span>System</span>
48
+ </MenuItem>
49
+ </Menu>
50
+ </MenuTrigger>
31
51
  )
32
52
  }
@@ -1,6 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom'
2
2
  import { getBaseFilePath } from '@client/utils/get-base-file-path'
3
3
  import { useRoutes } from './use-routes'
4
+ import { useBoltdocsStore } from '../store/use-boltdocs-store'
4
5
 
5
6
  export interface LocaleOption {
6
7
  key: string
@@ -24,9 +25,13 @@ export function useI18n(): UseI18nReturn {
24
25
  const routeContext = useRoutes()
25
26
  const { allRoutes, currentRoute, currentLocale, config } = routeContext
26
27
  const i18n = config.i18n
28
+ const setLocale = useBoltdocsStore((s) => s.setLocale)
27
29
 
28
30
  const handleLocaleChange = (locale: string) => {
29
31
  if (!i18n || locale === currentLocale) return
32
+
33
+ // Update store
34
+ setLocale(locale)
30
35
 
31
36
  let targetPath = '/'
32
37
 
@@ -63,21 +68,46 @@ export function useI18n(): UseI18nReturn {
63
68
  : `/${locale}`
64
69
  }
65
70
  } else {
66
- targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
71
+ // Fallback for when we don't have a current route (e.g. 404 page)
72
+ // Try to find the root documentation page for the target locale
73
+ const targetRoute = allRoutes.find(
74
+ (r) =>
75
+ (r.filePath === 'index.mdx' || r.filePath === 'index.md') &&
76
+ (r.locale || i18n.defaultLocale) === locale &&
77
+ !r.version, // Prefer non-versioned root
78
+ )
79
+
80
+ if (targetRoute) {
81
+ targetPath = targetRoute.path
82
+ } else {
83
+ targetPath = locale === i18n.defaultLocale ? '/' : `/${locale}`
84
+ }
67
85
  }
68
86
 
69
87
  navigate(targetPath)
70
88
  }
71
89
 
72
- const availableLocales = routeContext.availableLocales.map((l) => ({
73
- ...l,
74
- label: l.label as string,
75
- value: l.key,
76
- }))
90
+ const currentLocaleConfig = config.i18n?.localeConfigs?.[currentLocale as string]
91
+ const currentLocaleLabel =
92
+ currentLocaleConfig?.label ||
93
+ config.i18n?.locales[currentLocale as string] ||
94
+ currentLocale
95
+
96
+ const availableLocales = config.i18n
97
+ ? Object.entries(config.i18n.locales).map(([key, defaultLabel]) => {
98
+ const localeConfig = config.i18n?.localeConfigs?.[key]
99
+ return {
100
+ key,
101
+ label: localeConfig?.label || defaultLabel,
102
+ value: key,
103
+ isCurrent: key === currentLocale,
104
+ }
105
+ })
106
+ : []
77
107
 
78
108
  return {
79
109
  currentLocale,
80
- currentLocaleLabel: routeContext.currentLocaleLabel,
110
+ currentLocaleLabel,
81
111
  availableLocales,
82
112
  handleLocaleChange,
83
113
  }