boltdocs 2.1.1 → 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 +9 -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
@@ -22,10 +22,10 @@ interface BoltdocsFooterConfig {
22
22
  * Theme-specific configuration options governing the appearance and navigation of the site.
23
23
  */
24
24
  interface BoltdocsThemeConfig {
25
- /** The global title of the documentation site */
26
- title?: string;
27
- /** The global description of the site (used for SEO) */
28
- description?: string;
25
+ /** The global title of the documentation site (can be translated) */
26
+ title?: string | Record<string, string>;
27
+ /** The global description of the site (can be translated) */
28
+ description?: string | Record<string, string>;
29
29
  /** URL path to the site logo or an object for light/dark versions */
30
30
  logo?: string | {
31
31
  dark: string;
@@ -36,13 +36,15 @@ interface BoltdocsThemeConfig {
36
36
  };
37
37
  /** Items to display in the top navigation bar */
38
38
  navbar?: Array<{
39
- /** Text to display */
40
- label: string;
39
+ /** Text to display (can be a string or a map of translations) */
40
+ label: string | Record<string, string>;
41
41
  /** URL path or external link */
42
42
  href: string;
43
43
  /** Nested items for NavigationMenu */
44
44
  items?: Array<{
45
- label: string;
45
+ /** Text to display (can be a string or a map of translations) */
46
+ label: string | Record<string, string>;
47
+ /** URL path or external link */
46
48
  href: string;
47
49
  }>;
48
50
  }>;
@@ -82,7 +84,8 @@ interface BoltdocsThemeConfig {
82
84
  */
83
85
  tabs?: Array<{
84
86
  id: string;
85
- text: string;
87
+ /** Text to display (can be a string or a map of translations) */
88
+ text: string | Record<string, string>;
86
89
  icon?: string;
87
90
  }>;
88
91
  /**
@@ -120,23 +123,52 @@ type BoltdocsRobotsConfig = string | {
120
123
  /** Sitemaps to include in the robots.txt */
121
124
  sitemaps?: string[];
122
125
  };
126
+ /**
127
+ * Configuration for a specific locale.
128
+ */
129
+ interface BoltdocsLocaleConfig {
130
+ /** The display name of the locale */
131
+ label?: string;
132
+ /** The text direction (ltr or rtl) */
133
+ direction?: 'ltr' | 'rtl';
134
+ /** The HTML lang attribute value (e.g., 'en-US') */
135
+ htmlLang?: string;
136
+ /** The calendar system to use (e.g., 'gregory') */
137
+ calendar?: string;
138
+ }
123
139
  /**
124
140
  * Configuration for internationalization (i18n).
125
141
  */
126
142
  interface BoltdocsI18nConfig {
127
143
  /** The default locale (e.g., 'en') */
128
144
  defaultLocale: string;
129
- /** Available locales and their display names (e.g., { en: 'English', es: 'Español' }) */
145
+ /** Available locales and their basic display names (e.g., { en: 'English', es: 'Español' }) */
130
146
  locales: Record<string, string>;
147
+ /** Detailed configuration for each locale */
148
+ localeConfigs?: Record<string, BoltdocsLocaleConfig>;
149
+ }
150
+ /**
151
+ * Configuration for a specific documentation version.
152
+ */
153
+ interface BoltdocsVersionConfig {
154
+ /** The display name of the version (e.g., 'v2.0') */
155
+ label: string;
156
+ /** The URL path prefix for the version (e.g., '2.0') */
157
+ path: string;
131
158
  }
132
159
  /**
133
160
  * Configuration for documentation versioning.
134
161
  */
135
162
  interface BoltdocsVersionsConfig {
136
- /** The default version (e.g., 'v2') */
163
+ /** The default version path (e.g., 'v2') */
137
164
  defaultVersion: string;
138
- /** Available versions and their display names (e.g., { v1: 'Version 1.x', v2: 'Version 2.x' }) */
139
- versions: Record<string, string>;
165
+ /**
166
+ * Optional prefix for all version paths (e.g., 'v').
167
+ * If set to 'v', version '1.1' will be available at '/docs/v1.1'.
168
+ */
169
+ prefix?: string;
170
+ /** Available versions configurations */
171
+ versions: BoltdocsVersionConfig[];
140
172
  }
141
173
  /**
142
174
  * Defines a Boltdocs plugin that can extend the build process and client-side functionality.
@@ -193,8 +225,6 @@ interface BoltdocsConfig {
193
225
  robots?: BoltdocsRobotsConfig;
194
226
  /** Low-level Vite configuration overrides */
195
227
  vite?: vite.InlineConfig;
196
- /** @deprecated Use theme instead */
197
- themeConfig?: BoltdocsThemeConfig;
198
228
  }
199
229
 
200
230
  /**
@@ -317,9 +347,17 @@ interface SandboxEmbedOptions {
317
347
  */
318
348
  interface BoltdocsTab {
319
349
  id: string;
320
- text: string;
350
+ /** Text to display (can be a string or a map of translations) */
351
+ text: string | Record<string, string>;
321
352
  icon?: string;
322
353
  }
354
+ /**
355
+ * Props for the Sidebar component.
356
+ */
357
+ interface SidebarProps {
358
+ routes: ComponentRoute[];
359
+ config: BoltdocsConfig;
360
+ }
323
361
  /**
324
362
  * Props for the OnThisPage (TOC) component.
325
363
  */
@@ -333,6 +371,13 @@ interface OnThisPageProps {
333
371
  communityHelp?: string;
334
372
  filePath?: string;
335
373
  }
374
+ /**
375
+ * Props for the Tabs component.
376
+ */
377
+ interface TabsProps {
378
+ tabs: BoltdocsTab[];
379
+ routes: ComponentRoute[];
380
+ }
336
381
  /**
337
382
  * Props for user-defined layout components (layout.tsx).
338
383
  */
@@ -343,7 +388,8 @@ interface LayoutProps {
343
388
  * Unified type for navbar links.
344
389
  */
345
390
  interface NavbarLink {
346
- label: string;
391
+ /** Label to display (can be a string or a map of translations) */
392
+ label: string | Record<string, string>;
347
393
  href: string;
348
394
  active: boolean;
349
395
  /** Optional icon or string for external link indication */
@@ -352,4 +398,4 @@ interface NavbarLink {
352
398
  items?: NavbarLink[];
353
399
  }
354
400
 
355
- export type { BoltdocsConfig as B, ComponentRoute as C, LayoutProps as L, NavbarLink as N, OnThisPageProps as O, SandboxOptions as S, BoltdocsSocialLink as a, BoltdocsTab as b, CreateBoltdocsAppOptions as c, BoltdocsThemeConfig as d, SandboxEmbedOptions as e, SandboxFile as f, SandboxFiles as g };
401
+ export type { BoltdocsConfig as B, ComponentRoute as C, LayoutProps as L, NavbarLink as N, OnThisPageProps as O, SandboxEmbedOptions as S, TabsProps as T, BoltdocsTab as a, CreateBoltdocsAppOptions as b, BoltdocsThemeConfig as c, SandboxFile as d, SandboxFiles as e, SandboxOptions as f, BoltdocsSocialLink as g, SidebarProps as h, SiteConfig as i };
@@ -1,4 +1,4 @@
1
- import { C as ComponentRoute, B as BoltdocsConfig } from './types-Cp21DHI6.js';
1
+ import { C as ComponentRoute, B as BoltdocsConfig } from './types-j7jvWsJj.js';
2
2
 
3
3
  /**
4
4
  * Hook to access the framework's routing state.
@@ -1,4 +1,4 @@
1
- import { C as ComponentRoute, B as BoltdocsConfig } from './types-Cp21DHI6.mjs';
1
+ import { C as ComponentRoute, B as BoltdocsConfig } from './types-j7jvWsJj.mjs';
2
2
 
3
3
  /**
4
4
  * Hook to access the framework's routing state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boltdocs",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "A lightweight documentation generator for React projects.",
5
5
  "main": "dist/node/index.js",
6
6
  "module": "dist/node/index.mjs",
@@ -22,15 +22,40 @@
22
22
  "import": "./dist/client/index.mjs",
23
23
  "require": "./dist/client/index.js"
24
24
  },
25
+ "./hooks": {
26
+ "types": "./dist/hooks/index.d.ts",
27
+ "import": "./dist/hooks/index.mjs",
28
+ "require": "./dist/hooks/index.js"
29
+ },
30
+ "./primitives": {
31
+ "types": "./dist/primitives/index.d.ts",
32
+ "import": "./dist/primitives/index.mjs",
33
+ "require": "./dist/primitives/index.js"
34
+ },
35
+ "./base-ui": {
36
+ "types": "./dist/base-ui/index.d.ts",
37
+ "import": "./dist/base-ui/index.mjs",
38
+ "require": "./dist/base-ui/index.js"
39
+ },
40
+ "./mdx": {
41
+ "types": "./dist/mdx/index.d.ts",
42
+ "import": "./dist/mdx/index.mjs",
43
+ "require": "./dist/mdx/index.js"
44
+ },
45
+ "./integrations": {
46
+ "types": "./dist/integrations/index.d.ts",
47
+ "import": "./dist/integrations/index.mjs",
48
+ "require": "./dist/integrations/index.js"
49
+ },
25
50
  "./client/hooks": {
26
- "types": "./dist/client/hooks/index.d.ts",
27
- "import": "./dist/client/hooks/index.mjs",
28
- "require": "./dist/client/hooks/index.js"
51
+ "types": "./dist/hooks/index.d.ts",
52
+ "import": "./dist/hooks/index.mjs",
53
+ "require": "./dist/hooks/index.js"
29
54
  },
30
55
  "./client/primitives": {
31
- "types": "./dist/client/components/primitives/index.d.ts",
32
- "import": "./dist/client/components/primitives/index.mjs",
33
- "require": "./dist/client/components/primitives/index.js"
56
+ "types": "./dist/primitives/index.d.ts",
57
+ "import": "./dist/primitives/index.mjs",
58
+ "require": "./dist/primitives/index.js"
34
59
  },
35
60
  "./client/types": {
36
61
  "types": "./dist/client/types.d.ts",
@@ -79,7 +104,8 @@
79
104
  "tailwind-merge": "^3.5.0",
80
105
  "unist-util-visit": "^5.1.0",
81
106
  "vite": "^7.3.1",
82
- "vite-plugin-image-optimizer": "^2.0.3"
107
+ "vite-plugin-image-optimizer": "^2.0.3",
108
+ "zustand": "^5.0.12"
83
109
  },
84
110
  "peerDependencies": {
85
111
  "react": "^19.1.0",
@@ -2,7 +2,6 @@ import React, { useEffect, useState, useMemo } from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
  import { BrowserRouter, Routes, Route } from 'react-router-dom'
4
4
  import { NotFound } from '@components/ui-base/not-found'
5
- import { Loading } from '@components/ui-base/loading'
6
5
  import { ThemeProvider } from './theme-context'
7
6
  import type { ComponentRoute, CreateBoltdocsAppOptions } from '../types'
8
7
  import type { BoltdocsConfig } from '@node/config'
@@ -17,6 +16,84 @@ import { DocsLayout } from './docs-layout'
17
16
  import { MdxPage } from './mdx-page'
18
17
  import { MdxComponentsProvider } from './mdx-components-context'
19
18
  import { mdxComponentsDefault } from './mdx-component'
19
+ import { useRoutes } from '../hooks/use-routes'
20
+ import { useLocation } from 'react-router-dom'
21
+ import { useBoltdocsStore } from '../store/use-boltdocs-store'
22
+
23
+ /**
24
+ * Updates the HTML lang and dir attributes based on the current locale configuration.
25
+ */
26
+ function I18nUpdater() {
27
+ const { currentLocale, config } = useRoutes()
28
+
29
+ useEffect(() => {
30
+ if (!config.i18n) return
31
+ const localeConfig = config.i18n.localeConfigs?.[currentLocale as string]
32
+ document.documentElement.lang =
33
+ localeConfig?.htmlLang || currentLocale || 'en'
34
+ document.documentElement.dir = localeConfig?.direction || 'ltr'
35
+ }, [currentLocale, config.i18n])
36
+
37
+ return null
38
+ }
39
+
40
+ /**
41
+ * Synchronizes the Zustand store with the current URL pathname.
42
+ */
43
+ function StoreSync() {
44
+ const location = useLocation()
45
+ const { config } = useRoutes()
46
+ const setLocale = useBoltdocsStore((s) => s.setLocale)
47
+ const setVersion = useBoltdocsStore((s) => s.setVersion)
48
+ const currentLocaleStore = useBoltdocsStore((s) => s.currentLocale)
49
+ const currentVersionStore = useBoltdocsStore((s) => s.currentVersion)
50
+
51
+ useEffect(() => {
52
+ const parts = location.pathname.split('/').filter(Boolean)
53
+ let cIdx = 0
54
+ let detectedVersion = config.versions?.defaultVersion
55
+ let detectedLocale = config.i18n?.defaultLocale
56
+
57
+ // 0. Skip docs prefix if present
58
+ if (parts[cIdx] === 'docs') cIdx++
59
+
60
+ // 1. Version detection
61
+ if (config.versions && parts.length > cIdx) {
62
+ const versionMatch = config.versions.versions.find(
63
+ (v) => v.path === parts[cIdx],
64
+ )
65
+ if (versionMatch) {
66
+ detectedVersion = versionMatch.path
67
+ cIdx++
68
+ }
69
+ }
70
+
71
+ // 2. Locale detection
72
+ if (
73
+ config.i18n &&
74
+ parts.length > cIdx &&
75
+ config.i18n.locales[parts[cIdx]]
76
+ ) {
77
+ detectedLocale = parts[cIdx]
78
+ } else if (config.i18n && parts.length === 0) {
79
+ // On root, use the stored preference if it exists, otherwise default
80
+ detectedLocale = currentLocaleStore || config.i18n.defaultLocale
81
+ }
82
+
83
+ // Only update if changed to avoid loops
84
+ if (detectedLocale !== currentLocaleStore) setLocale(detectedLocale)
85
+ if (detectedVersion !== currentVersionStore) setVersion(detectedVersion)
86
+ }, [
87
+ location.pathname,
88
+ config,
89
+ setLocale,
90
+ setVersion,
91
+ currentLocaleStore,
92
+ currentVersionStore,
93
+ ])
94
+
95
+ return null
96
+ }
20
97
 
21
98
  export function AppShell({
22
99
  initialRoutes,
@@ -31,14 +108,17 @@ export function AppShell({
31
108
  initialRoutes: ComponentRoute[]
32
109
  initialConfig: BoltdocsConfig
33
110
  docsDirName: string
34
- modules: Record<string, () => Promise<{ default: React.ComponentType<any> }>>
111
+ modules: Record<
112
+ string,
113
+ () => Promise<{ default: React.ComponentType<unknown> }>
114
+ >
35
115
  hot?: CreateBoltdocsAppOptions['hot']
36
116
  homePage?: React.ComponentType
37
117
  externalPages?: Record<string, React.ComponentType>
38
118
  components?: Record<string, React.ComponentType>
39
119
  }) {
40
120
  const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes)
41
- const [config] = useState(initialConfig)
121
+ const [config, setConfig] = useState(initialConfig)
42
122
  const computedExternalPages = externalPages || {}
43
123
 
44
124
  const resolvedRoutes = useMemo(() => {
@@ -53,16 +133,19 @@ export function AppShell({
53
133
  (k) =>
54
134
  k === `/${docsDirName}/${route.filePath}` || // Vite dev/build relative path
55
135
  k.endsWith(`/${docsDirName}/${route.filePath}`) || // SSG absolute path fallback
56
- k.endsWith(`/${docsDirName}\\${route.filePath.replace(/\\/g, '/')}`), // Windows fallback
136
+ k.endsWith(
137
+ `/${docsDirName}\\${route.filePath.replace(/\\/g, '/')}`,
138
+ ), // Windows fallback
57
139
  )
58
140
  const loader = loaderKey ? modules[loaderKey] : null
59
141
 
60
142
  return {
61
143
  ...route,
62
- Component: React.lazy<React.ComponentType<any>>(async () => {
63
- if (!loader) return { default: NotFound as React.ComponentType<any> }
144
+ Component: React.lazy<React.ComponentType<unknown>>(async () => {
145
+ if (!loader)
146
+ return { default: NotFound as React.ComponentType<unknown> }
64
147
  const mod = await loader()
65
- return mod
148
+ return mod as { default: React.ComponentType<unknown> }
66
149
  }),
67
150
  }
68
151
  })
@@ -74,6 +157,9 @@ export function AppShell({
74
157
  hot.on('boltdocs:routes-update', (newRoutes: ComponentRoute[]) => {
75
158
  setRoutesInfo(newRoutes)
76
159
  })
160
+ hot.on('boltdocs:config-update', (newConfig: BoltdocsConfig) => {
161
+ setConfig(newConfig)
162
+ })
77
163
  }
78
164
  }, [hot])
79
165
 
@@ -82,6 +168,8 @@ export function AppShell({
82
168
  [customComponents],
83
169
  )
84
170
 
171
+ const LoadingFallback = allComponents.Loading as React.ComponentType
172
+
85
173
  return (
86
174
  <ThemeProvider>
87
175
  <MdxComponentsProvider components={allComponents}>
@@ -89,41 +177,16 @@ export function AppShell({
89
177
  <BoltdocsRouterProvider>
90
178
  <PreloadProvider routes={routesInfo} modules={modules}>
91
179
  <ScrollHandler />
180
+ <StoreSync />
181
+ <I18nUpdater />
92
182
  <Routes>
93
- {/* Custom home page with user layout */}
94
- {HomePage && (
95
- <Route
96
- path="/"
97
- element={
98
- <UserLayout>
99
- <HomePage />
100
- </UserLayout>
101
- }
102
- />
103
- )}
104
-
105
- {/* Custom External Pages with user layout */}
106
- {Object.entries(computedExternalPages).map(
107
- ([extPath, ExtComponent]) => (
108
- <Route
109
- key={extPath}
110
- path={extPath}
111
- element={
112
- <UserLayout>
113
- <ExtComponent />
114
- </UserLayout>
115
- }
116
- />
117
- ),
118
- )}
119
-
120
183
  <Route key="docs-layout" element={<DocsLayout />}>
121
184
  {resolvedRoutes.map((route) => (
122
185
  <Route
123
186
  key={route.path}
124
187
  path={route.path === '' ? '/' : route.path}
125
188
  element={
126
- <React.Suspense fallback={<Loading />}>
189
+ <React.Suspense fallback={<LoadingFallback />}>
127
190
  <MdxPage Component={route.Component} />
128
191
  </React.Suspense>
129
192
  }
@@ -131,6 +194,63 @@ export function AppShell({
131
194
  ))}
132
195
  </Route>
133
196
 
197
+ {/* Custom home page with user layout */}
198
+ {HomePage && (
199
+ <>
200
+ <Route
201
+ path="/"
202
+ element={
203
+ <UserLayout>
204
+ <HomePage />
205
+ </UserLayout>
206
+ }
207
+ />
208
+ {config.i18n &&
209
+ Object.keys(config.i18n.locales).map((locale) => (
210
+ <Route
211
+ key={`home-${locale}`}
212
+ path={`/${locale}`}
213
+ element={
214
+ <UserLayout>
215
+ <HomePage />
216
+ </UserLayout>
217
+ }
218
+ />
219
+ ))}
220
+ </>
221
+ )}
222
+
223
+ {/* Custom External Pages with user layout */}
224
+ {Object.entries(computedExternalPages).map(
225
+ ([extPath, ExtComponent]) => {
226
+ const cleanPath = extPath === '/' ? '' : extPath
227
+ return (
228
+ <React.Fragment key={extPath}>
229
+ <Route
230
+ path={extPath}
231
+ element={
232
+ <UserLayout>
233
+ <ExtComponent />
234
+ </UserLayout>
235
+ }
236
+ />
237
+ {config.i18n &&
238
+ Object.keys(config.i18n.locales).map((locale) => (
239
+ <Route
240
+ key={`${extPath}-${locale}`}
241
+ path={`/${locale}${cleanPath}`}
242
+ element={
243
+ <UserLayout>
244
+ <ExtComponent />
245
+ </UserLayout>
246
+ }
247
+ />
248
+ ))}
249
+ </React.Fragment>
250
+ )
251
+ },
252
+ )}
253
+
134
254
  <Route
135
255
  path="*"
136
256
  element={
@@ -6,14 +6,15 @@ const Heading = ({
6
6
  level,
7
7
  id,
8
8
  children,
9
+ ...props
9
10
  }: {
10
11
  level: number
11
12
  id?: string
12
13
  children?: React.ReactNode
13
- }) => {
14
- const Tag = `h${level}` as keyof JSX.IntrinsicElements
14
+ } & React.HTMLAttributes<HTMLHeadingElement>) => {
15
+ const Tag = `h${level}` as any
15
16
  return (
16
- <Tag id={id} className="boltdocs-heading">
17
+ <Tag id={id} {...props} className="boltdocs-heading">
17
18
  {children}
18
19
  {id && (
19
20
  <a href={`#${id}`} className="header-anchor" aria-label="Anchor">
@@ -24,8 +25,11 @@ const Heading = ({
24
25
  )
25
26
  }
26
27
 
28
+ import { Loading } from '@components/ui-base/loading'
29
+
27
30
  export const mdxComponentsDefault = {
28
31
  ...MdxComponents,
32
+ Loading,
29
33
  h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
30
34
  <Heading level={1} {...props} />
31
35
  ),
@@ -1,45 +1,68 @@
1
1
  import { createContext, use, useEffect, useState } from 'react'
2
2
 
3
- type Theme = 'light' | 'dark'
3
+ type Theme = 'light' | 'dark' | 'system'
4
4
 
5
5
  interface ThemeContextType {
6
6
  theme: Theme
7
- toggleTheme: () => void
7
+ resolvedTheme: 'light' | 'dark'
8
8
  setTheme: (theme: Theme) => void
9
9
  }
10
10
 
11
11
  const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
12
12
 
13
13
  export function ThemeProvider({ children }: { children: React.ReactNode }) {
14
- const [theme, setThemeState] = useState<Theme>('dark')
14
+ const [theme, setThemeState] = useState<Theme>('system')
15
+ const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark')
15
16
  const [mounted, setMounted] = useState(false)
16
17
 
18
+ // Initialize theme from localStorage and set internal resolved theme
17
19
  useEffect(() => {
18
20
  setMounted(true)
19
- const stored = localStorage.getItem('boltdocs-theme')
20
- if (stored === 'light' || stored === 'dark') {
21
- setThemeState(stored as Theme)
22
- } else {
23
- const prefersDark = window.matchMedia(
24
- '(prefers-color-scheme: dark)',
25
- ).matches
26
- setThemeState(prefersDark ? 'dark' : 'light')
27
- }
21
+ const stored = localStorage.getItem('boltdocs-theme') as Theme | null
22
+ const initialTheme = (stored === 'light' || stored === 'dark' || stored === 'system') ? stored : 'system'
23
+
24
+ setThemeState(initialTheme)
28
25
 
29
26
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
30
- const handleChange = (e: MediaQueryListEvent) => {
31
- if (!localStorage.getItem('boltdocs-theme')) {
32
- setThemeState(e.matches ? 'dark' : 'light')
27
+
28
+ const updateResolved = (currentTheme: Theme, isDark: boolean) => {
29
+ if (currentTheme === 'system') {
30
+ setResolvedTheme(isDark ? 'dark' : 'light')
31
+ } else {
32
+ setResolvedTheme(currentTheme as 'light' | 'dark')
33
33
  }
34
34
  }
35
+
36
+ updateResolved(initialTheme, mediaQuery.matches)
37
+
38
+ const handleChange = (e: MediaQueryListEvent) => {
39
+ // Re-read current theme state from some stable ref would be better, but we can capture it
40
+ // actually, the second useEffect will handle the source of truth,
41
+ // but this listener ensures 'system' updates instantly.
42
+ setResolvedTheme((prevResolved) => {
43
+ const currentTheme = localStorage.getItem('boltdocs-theme') as Theme || 'system'
44
+ if (currentTheme === 'system') {
45
+ return e.matches ? 'dark' : 'light'
46
+ }
47
+ return prevResolved
48
+ })
49
+ }
50
+
35
51
  mediaQuery.addEventListener('change', handleChange)
36
52
  return () => mediaQuery.removeEventListener('change', handleChange)
37
53
  }, [])
38
54
 
55
+ // Sync with DOM and resolved theme when theme preference changes
39
56
  useEffect(() => {
40
57
  if (!mounted) return
58
+
59
+ const isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
60
+ const nextResolved = theme === 'system' ? (isSystemDark ? 'dark' : 'light') : theme
61
+
62
+ setResolvedTheme(nextResolved as 'light' | 'dark')
63
+
41
64
  const root = document.documentElement
42
- if (theme === 'light') {
65
+ if (nextResolved === 'light') {
43
66
  root.classList.add('theme-light')
44
67
  root.dataset.theme = 'light'
45
68
  } else {
@@ -48,19 +71,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
48
71
  }
49
72
  }, [theme, mounted])
50
73
 
51
- const toggleTheme = () => {
52
- const newTheme = theme === 'dark' ? 'light' : 'dark'
53
- setThemeState(newTheme)
54
- localStorage.setItem('boltdocs-theme', newTheme)
55
- }
56
-
57
74
  const setTheme = (newTheme: Theme) => {
58
75
  setThemeState(newTheme)
59
76
  localStorage.setItem('boltdocs-theme', newTheme)
60
77
  }
61
78
 
62
79
  return (
63
- <ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
80
+ <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
64
81
  {children}
65
82
  </ThemeContext.Provider>
66
83
  )