docs-i18n 0.6.3 → 0.7.1

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 (169) hide show
  1. package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
  2. package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
  3. package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
  4. package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
  5. package/admin/app/routeTree.gen.ts +68 -0
  6. package/admin/app/router.tsx +23 -0
  7. package/admin/app/routes/__root.tsx +55 -0
  8. package/admin/app/routes/index.tsx +416 -0
  9. package/{src/admin/ui → admin/app}/styles.css +36 -3
  10. package/admin/package.json +26 -0
  11. package/admin/server/functions/jobs.ts +53 -0
  12. package/admin/server/functions/misc.ts +84 -0
  13. package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
  14. package/admin/server/functions/status.ts +61 -0
  15. package/admin/server/index.ts +35 -0
  16. package/admin/server/init.ts +46 -0
  17. package/{src/admin → admin}/server/services/job-manager.ts +39 -10
  18. package/{src/admin → admin}/server/services/status.ts +6 -6
  19. package/admin/tsconfig.json +19 -0
  20. package/{src/admin → admin}/vite.config.ts +8 -2
  21. package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
  22. package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
  23. package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
  24. package/dist/chunk-L64GJ4OB.js +32 -0
  25. package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
  26. package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
  27. package/dist/chunk-TRURQFP4.js +31 -0
  28. package/dist/cli.js +108 -7
  29. package/dist/index.d.ts +41 -1
  30. package/dist/index.js +92 -3
  31. package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
  32. package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
  33. package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
  34. package/dist/upload-XL6KG6S2.js +132 -0
  35. package/package.json +17 -15
  36. package/template/app/components/BlogArticle.tsx +159 -0
  37. package/template/app/components/BlogList.tsx +88 -0
  38. package/template/app/components/Breadcrumbs.tsx +81 -0
  39. package/template/app/components/Card.tsx +31 -0
  40. package/template/app/components/Doc.tsx +191 -0
  41. package/template/app/components/DocBreadcrumb.tsx +60 -0
  42. package/template/app/components/DocContainer.tsx +13 -0
  43. package/template/app/components/DocTitle.tsx +11 -0
  44. package/template/app/components/DocsLayout.tsx +715 -0
  45. package/template/app/components/Dropdown.tsx +116 -0
  46. package/template/app/components/FallbackBanner.tsx +36 -0
  47. package/template/app/components/Footer.tsx +29 -0
  48. package/template/app/components/FrameworkSelect.tsx +150 -0
  49. package/template/app/components/LibraryCard.tsx +178 -0
  50. package/template/app/components/LocaleSwitcher.tsx +43 -0
  51. package/template/app/components/Navbar.tsx +430 -0
  52. package/template/app/components/PostNotFound.tsx +20 -0
  53. package/template/app/components/SearchButton.tsx +32 -0
  54. package/template/app/components/Select.tsx +103 -0
  55. package/template/app/components/Spinner.tsx +18 -0
  56. package/template/app/components/ThemeProvider.tsx +141 -0
  57. package/template/app/components/ThemeToggle.tsx +31 -0
  58. package/template/app/components/Toc.tsx +86 -0
  59. package/template/app/components/VersionSelect.tsx +118 -0
  60. package/template/app/components/icons/BSkyIcon.tsx +27 -0
  61. package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
  62. package/template/app/components/icons/BrandXIcon.tsx +28 -0
  63. package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
  64. package/template/app/components/icons/CogsIcon.tsx +25 -0
  65. package/template/app/components/icons/DiscordIcon.tsx +24 -0
  66. package/template/app/components/icons/GithubIcon.tsx +24 -0
  67. package/template/app/components/icons/GoogleIcon.tsx +24 -0
  68. package/template/app/components/icons/InstagramIcon.tsx +24 -0
  69. package/template/app/components/icons/NpmIcon.tsx +26 -0
  70. package/template/app/components/icons/YinYangIcon.tsx +26 -0
  71. package/template/app/components/icons/YouTubeIcon.tsx +24 -0
  72. package/template/app/components/markdown/CodeBlock.tsx +254 -0
  73. package/template/app/components/markdown/FileTabs.tsx +58 -0
  74. package/template/app/components/markdown/FrameworkContent.tsx +76 -0
  75. package/template/app/components/markdown/Markdown.tsx +216 -0
  76. package/template/app/components/markdown/MarkdownContent.tsx +89 -0
  77. package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
  78. package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
  79. package/template/app/components/markdown/MarkdownLink.tsx +46 -0
  80. package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
  81. package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
  82. package/template/app/components/markdown/Tabs.tsx +139 -0
  83. package/template/app/components/markdown/index.ts +15 -0
  84. package/template/app/components/ui/Button.tsx +141 -0
  85. package/template/app/components/ui/InlineCode.tsx +16 -0
  86. package/template/app/components/ui/MarkdownImg.tsx +21 -0
  87. package/template/app/config/frameworks.ts +93 -0
  88. package/template/app/contexts/SearchContext.tsx +36 -0
  89. package/template/app/db/index.ts +17 -0
  90. package/template/app/db/schema.ts +74 -0
  91. package/template/app/hooks/useClickOutside.ts +106 -0
  92. package/template/app/routeTree.gen.ts +584 -0
  93. package/template/app/router.tsx +29 -0
  94. package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
  95. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
  96. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
  97. package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
  98. package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
  99. package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
  100. package/template/app/routes/$lang.$project.$version.tsx +69 -0
  101. package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
  102. package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
  103. package/template/app/routes/$lang.$project.docs.tsx +79 -0
  104. package/template/app/routes/$lang.$project.tsx +89 -0
  105. package/template/app/routes/$lang.blog.$.tsx +82 -0
  106. package/template/app/routes/$lang.blog.index.tsx +56 -0
  107. package/template/app/routes/$lang.blog.tsx +26 -0
  108. package/template/app/routes/$lang.docs.$.tsx +100 -0
  109. package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
  110. package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
  111. package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
  112. package/template/app/routes/$lang.docs.index.tsx +20 -0
  113. package/template/app/routes/$lang.docs.tsx +90 -0
  114. package/template/app/routes/$lang.tsx +16 -0
  115. package/template/app/routes/__root.tsx +180 -0
  116. package/template/app/routes/index.tsx +89 -0
  117. package/template/app/site.config.ts +182 -0
  118. package/template/app/styles/app.css +1029 -0
  119. package/template/app/types/index.ts +77 -0
  120. package/template/app/utils/blog.server.ts +193 -0
  121. package/template/app/utils/blog.ts +42 -0
  122. package/template/app/utils/config.ts +120 -0
  123. package/template/app/utils/content-loader.ts +400 -0
  124. package/template/app/utils/dates.ts +29 -0
  125. package/template/app/utils/docs.server.ts +150 -0
  126. package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
  127. package/template/app/utils/markdown/index.ts +2 -0
  128. package/template/app/utils/markdown/installCommand.ts +143 -0
  129. package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
  130. package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
  131. package/template/app/utils/markdown/plugins/helpers.ts +33 -0
  132. package/template/app/utils/markdown/plugins/index.ts +8 -0
  133. package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
  134. package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
  135. package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
  136. package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
  137. package/template/app/utils/markdown/processor.ts +75 -0
  138. package/template/app/utils/site-config.tsx +11 -0
  139. package/template/app/utils/upload.ts +232 -0
  140. package/template/app/utils/useLocalStorage.ts +65 -0
  141. package/template/app/utils/utils.ts +23 -0
  142. package/template/package.json +53 -0
  143. package/template/public/favicon.svg +1 -0
  144. package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
  145. package/template/public/fonts/Inter-latin.woff2 +0 -0
  146. package/template/public/images/frameworks/angular-logo.svg +1 -0
  147. package/template/public/images/frameworks/js-logo.svg +1 -0
  148. package/template/public/images/frameworks/lit-logo.svg +1 -0
  149. package/template/public/images/frameworks/preact-logo.svg +6 -0
  150. package/template/public/images/frameworks/qwik-logo.svg +1 -0
  151. package/template/public/images/frameworks/react-logo.svg +1 -0
  152. package/template/public/images/frameworks/solid-logo.svg +1 -0
  153. package/template/public/images/frameworks/svelte-logo.svg +1 -0
  154. package/template/public/images/frameworks/vue-logo.svg +4 -0
  155. package/template/tsconfig.json +24 -0
  156. package/template/vite.config.ts +43 -0
  157. package/template/wrangler.jsonc +16 -0
  158. package/README.md +0 -161
  159. package/dist/server-73AVSOL5.js +0 -598
  160. package/src/admin/index.html +0 -13
  161. package/src/admin/server/index.ts +0 -138
  162. package/src/admin/server/routes/jobs.ts +0 -113
  163. package/src/admin/server/routes/status.ts +0 -57
  164. package/src/admin/ui/App.tsx +0 -332
  165. package/src/admin/ui/main.tsx +0 -19
  166. /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
  167. /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
  168. /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
  169. /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
@@ -0,0 +1,715 @@
1
+ import * as React from 'react'
2
+ import { ChevronLeft, ChevronRight, Menu } from 'lucide-react'
3
+ import { GithubIcon } from '~/components/icons/GithubIcon'
4
+ import { Link, useMatches, useParams } from '@tanstack/react-router'
5
+ import { useLocalStorage } from '~/utils/useLocalStorage'
6
+ import { useClickOutside } from '~/hooks/useClickOutside'
7
+ import { last } from '~/utils/utils'
8
+ import type { DocsConfig, DocsConfigSection, DocsConfigItem } from '~/types'
9
+ import type { Framework } from '~/config/frameworks'
10
+ import { frameworkOptions } from '~/config/frameworks'
11
+ import { twMerge } from 'tailwind-merge'
12
+ import { Footer } from './Footer'
13
+ import { FrameworkSelect, useCurrentFramework } from './FrameworkSelect'
14
+ import { VersionSelect } from './VersionSelect'
15
+ import { Card } from './Card'
16
+ import { LocaleSwitcher } from './LocaleSwitcher'
17
+
18
+ // MenuItem type for internal menu structure
19
+ type MenuItem = {
20
+ label: string | React.ReactNode
21
+ children: {
22
+ label: string | React.ReactNode
23
+ to: string
24
+ badge?: string
25
+ }[]
26
+ collapsible?: boolean
27
+ defaultCollapsed?: boolean
28
+ }
29
+
30
+ // Component for the collapsed menu strip showing box indicators
31
+ // Minimap style: boxes with flex height filling available vertical space
32
+ function DocsMenuStrip({
33
+ menuConfig,
34
+ activeItem,
35
+ fullPathname,
36
+ colorFrom,
37
+ colorTo,
38
+ frameworkLogo,
39
+ version,
40
+ onHover,
41
+ onClick,
42
+ }: {
43
+ menuConfig: MenuItem[]
44
+ activeItem: string | undefined
45
+ fullPathname: string
46
+ colorFrom: string
47
+ colorTo: string
48
+ frameworkLogo: string | undefined
49
+ version: string
50
+ onHover: () => void
51
+ onClick: () => void
52
+ }) {
53
+ // Flatten all menu items with section markers
54
+ const itemsWithSections: Array<{
55
+ to?: string
56
+ label: React.ReactNode
57
+ isSection: boolean
58
+ }> = []
59
+ menuConfig.forEach((group) => {
60
+ itemsWithSections.push({ label: group.label, isSection: true })
61
+ group.children?.forEach((child) => {
62
+ itemsWithSections.push({
63
+ to: child.to,
64
+ label: child.label,
65
+ isSection: false,
66
+ })
67
+ })
68
+ })
69
+
70
+ // Check if a menu item path matches the current location
71
+ const isItemActive = (itemTo: string | undefined): boolean => {
72
+ if (!itemTo) return false
73
+
74
+ // External links are never active
75
+ if (itemTo.startsWith('http')) return false
76
+
77
+ // Standard relative path comparison
78
+ if (itemTo === activeItem) return true
79
+
80
+ // Handle special menu items with different path formats
81
+ // ".." means we're on the library home page (no /docs suffix in pathname)
82
+ if (itemTo === '..') {
83
+ // Active when on the library version index (e.g., /query/latest but not /query/latest/docs/...)
84
+ return fullPathname.match(/^\/[^/]+\/[^/]+\/?$/) !== null
85
+ }
86
+
87
+ // "./framework" means we're on the frameworks index page
88
+ if (itemTo === './framework') {
89
+ return (
90
+ fullPathname.includes('/docs/framework') &&
91
+ !fullPathname.match(/\/docs\/framework\/[^/]+/)
92
+ )
93
+ }
94
+
95
+ // Handle absolute paths like "/$libraryId/$version/docs/contributors"
96
+ if (itemTo.includes('/$libraryId')) {
97
+ const pathSuffix = itemTo.split('/docs/')[1]
98
+ if (pathSuffix && fullPathname.includes(`/docs/${pathSuffix}`)) {
99
+ return true
100
+ }
101
+ }
102
+
103
+ return false
104
+ }
105
+
106
+ return (
107
+ <button
108
+ type="button"
109
+ className="flex flex-col gap-2 py-2 px-2 cursor-pointer h-full w-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-400/50"
110
+ onPointerEnter={onHover}
111
+ onFocus={onHover}
112
+ onClick={onClick}
113
+ aria-label="Open documentation menu"
114
+ >
115
+ {/* FrameworkSelect + VersionSelect icons */}
116
+ <div className="flex flex-col gap-2 shrink-0">
117
+ <div className="flex items-center justify-center">
118
+ <span className="flex items-center justify-center w-6 h-6">
119
+ {frameworkLogo ? (
120
+ <img src={frameworkLogo} alt="" className="w-4 h-4" />
121
+ ) : (
122
+ <Menu className="w-3.5 h-3.5 opacity-60" />
123
+ )}
124
+ </span>
125
+ </div>
126
+ <div className="flex items-center justify-center">
127
+ <span className="flex items-center justify-center px-1 py-0.5 text-[9px] font-medium opacity-60 border border-gray-500/30 rounded">
128
+ {version}
129
+ </span>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Minimap: flex-height boxes filling remaining space */}
134
+ <div className="flex-1 flex flex-col gap-1 min-h-0">
135
+ {itemsWithSections.map((item, index) => {
136
+ const isActive = !item.isSection && isItemActive(item.to)
137
+
138
+ return (
139
+ <div
140
+ key={index}
141
+ className={twMerge(
142
+ 'flex-1 min-h-[4px] max-h-[9px] min-w-[20px] rounded-sm',
143
+ item.isSection
144
+ ? 'w-full bg-current opacity-15'
145
+ : isActive
146
+ ? `ml-2 w-[calc(100%-0.5rem)] bg-linear-to-r ${colorFrom} ${colorTo}`
147
+ : 'ml-2 w-[calc(100%-0.5rem)] bg-current opacity-[0.06]',
148
+ )}
149
+ title={
150
+ typeof item.label === 'string'
151
+ ? item.label
152
+ : `Item ${index + 1}`
153
+ }
154
+ />
155
+ )
156
+ })}
157
+ </div>
158
+ </button>
159
+ )
160
+ }
161
+
162
+ // Create context for width toggle state
163
+ const WidthToggleContext = React.createContext<{
164
+ isFullWidth: boolean
165
+ setIsFullWidth: (isFullWidth: boolean) => void
166
+ } | null>(null)
167
+
168
+ export const useWidthToggle = () => {
169
+ const context = React.useContext(WidthToggleContext)
170
+ if (!context) {
171
+ throw new Error('useWidthToggle must be used within a WidthToggleProvider')
172
+ }
173
+ return context
174
+ }
175
+
176
+ // Create context for doc navigation (prev/next)
177
+ type DocNavItem = { label: React.ReactNode; to: string }
178
+ const DocNavigationContext = React.createContext<{
179
+ prevItem?: DocNavItem
180
+ nextItem?: DocNavItem
181
+ colorFrom: string
182
+ colorTo: string
183
+ textColor: string
184
+ docsBasePath: string
185
+ } | null>(null)
186
+
187
+ export const useDocNavigation = () => {
188
+ return React.useContext(DocNavigationContext)
189
+ }
190
+
191
+ export function DocNavigation() {
192
+ const context = useDocNavigation()
193
+ if (!context) return null
194
+
195
+ const { prevItem, nextItem, colorFrom, colorTo, textColor, docsBasePath } = context
196
+
197
+ if (!prevItem && !nextItem) return null
198
+
199
+ return (
200
+ <div className="sticky flex items-stretch bottom-2 z-10 right-0 text-[10px] sm:text-xs md:text-sm print:hidden">
201
+ <div className="flex-1 flex justify-start">
202
+ {prevItem ? (
203
+ <Card
204
+ as={Link}
205
+ from={docsBasePath as any}
206
+ to={prevItem.to}
207
+ params
208
+ className="py-1 px-2 sm:py-2 sm:px-3 flex items-center gap-1 sm:gap-2"
209
+ >
210
+ <ChevronLeft className="w-3 h-3 sm:w-4 sm:h-4" />
211
+ <div className="flex flex-col">
212
+ <span className="hidden sm:block text-[10px] uppercase tracking-wider opacity-60 mb-0.5">
213
+ Previous
214
+ </span>
215
+ <span className="font-bold">{prevItem.label}</span>
216
+ </div>
217
+ </Card>
218
+ ) : null}
219
+ </div>
220
+ <div className="flex-1 flex justify-end">
221
+ {nextItem ? (
222
+ <Card
223
+ as={Link}
224
+ from={docsBasePath as any}
225
+ to={nextItem.to}
226
+ params
227
+ className="py-1 px-2 sm:py-2 sm:px-3 flex items-center gap-1 sm:gap-2"
228
+ >
229
+ <div className="flex flex-col items-end">
230
+ <span className="hidden sm:block text-[10px] uppercase tracking-wider opacity-60 mb-0.5">
231
+ Next
232
+ </span>
233
+ <span
234
+ className={`font-bold text-right bg-linear-to-r ${colorFrom} ${colorTo} bg-clip-text text-transparent`}
235
+ >
236
+ {nextItem.label}
237
+ </span>
238
+ </div>
239
+ <ChevronRight
240
+ className={twMerge('w-3 h-3 sm:w-4 sm:h-4', textColor)}
241
+ />
242
+ </Card>
243
+ ) : null}
244
+ </div>
245
+ </div>
246
+ )
247
+ }
248
+
249
+ const useMenuConfig = ({
250
+ config,
251
+ repo,
252
+ frameworks,
253
+ libraryId,
254
+ }: {
255
+ config: DocsConfig
256
+ repo: string
257
+ frameworks: Framework[]
258
+ libraryId: string
259
+ }): MenuItem[] => {
260
+ const currentFramework = useCurrentFramework(frameworks)
261
+
262
+ const localMenu: MenuItem = {
263
+ label: 'Menu',
264
+ children: [
265
+ {
266
+ label: 'Home',
267
+ to: '..',
268
+ },
269
+ ...(frameworks.length > 1
270
+ ? [
271
+ {
272
+ label: 'Frameworks',
273
+ to: './framework',
274
+ },
275
+ ]
276
+ : []),
277
+ {
278
+ label: (
279
+ <div className="flex items-center gap-2">
280
+ GitHub <GithubIcon className="opacity-20" />
281
+ </div>
282
+ ),
283
+ to: `https://github.com/${repo}`,
284
+ },
285
+ ],
286
+ }
287
+
288
+ return [
289
+ localMenu,
290
+ // Merge the two menus together based on their group labels
291
+ ...config.sections.map((section): MenuItem | undefined => {
292
+ const frameworkDocs = section.frameworks?.find(
293
+ (f) => f.label === currentFramework.framework,
294
+ )
295
+ const frameworkItems = frameworkDocs?.children ?? []
296
+
297
+ const children = [
298
+ ...(section.children ?? []).map((d) => ({ ...d, badge: 'core' })),
299
+ ...frameworkItems.map((d) => ({
300
+ ...d,
301
+ badge: currentFramework.framework,
302
+ })),
303
+ ]
304
+
305
+ if (children.length === 0) {
306
+ return undefined
307
+ }
308
+
309
+ return {
310
+ label: section.label,
311
+ children,
312
+ collapsible: section.collapsible ?? false,
313
+ defaultCollapsed: section.defaultCollapsed ?? false,
314
+ }
315
+ }),
316
+ ].filter((item) => item !== undefined)
317
+ }
318
+
319
+ type DocsLayoutProps = {
320
+ name: string
321
+ version: string
322
+ colorFrom: string
323
+ colorTo: string
324
+ textColor: string
325
+ config: DocsConfig
326
+ frameworks: Framework[]
327
+ versions: string[]
328
+ repo: string
329
+ children: React.ReactNode
330
+ isLandingPage?: boolean
331
+ /** Base route path for link resolution. Defaults to '/$lang/$project/$version/docs' */
332
+ docsBasePath?: string
333
+ }
334
+
335
+ export function DocsLayout({
336
+ colorFrom,
337
+ colorTo,
338
+ textColor,
339
+ config,
340
+ frameworks,
341
+ versions,
342
+ repo,
343
+ children,
344
+ version,
345
+ isLandingPage = false,
346
+ docsBasePath = '/$lang/$project/$version/docs',
347
+ }: DocsLayoutProps) {
348
+ const { project: libraryId } = useParams({
349
+ strict: false,
350
+ }) as { project: string }
351
+ const { _splat } = useParams({ strict: false })
352
+ const menuConfig = useMenuConfig({ config, frameworks, repo, libraryId })
353
+
354
+ const matches = useMatches()
355
+ const lastMatch = last(matches)
356
+
357
+ const isExample = matches.some((d) => d.pathname.includes('/examples/'))
358
+
359
+ const isNpmStats = matches.some((d) =>
360
+ d.pathname.includes('/docs/npm-stats'),
361
+ )
362
+
363
+ const detailsRef = React.useRef<HTMLElement>(null!)
364
+
365
+ const flatMenu = React.useMemo(
366
+ () => menuConfig.flatMap((d) => d?.children),
367
+ [menuConfig],
368
+ )
369
+
370
+ // Filter out external links for prev/next navigation
371
+ const internalFlatMenu = React.useMemo(
372
+ () => flatMenu.filter((d) => d && !d.to.startsWith('http')),
373
+ [flatMenu],
374
+ )
375
+
376
+ const docsMatch = matches.find((d) => d.pathname.includes('/docs'))
377
+ const docsPathname = docsMatch?.pathname ?? ''
378
+
379
+ const relativePathname = lastMatch.pathname.replace(docsPathname + '/', '')
380
+
381
+ const index = internalFlatMenu.findIndex((d) => d?.to === relativePathname)
382
+ const prevItem = internalFlatMenu[index - 1]
383
+ const nextItem = internalFlatMenu[index + 1]
384
+
385
+ // Get current framework's logo for the preview strip
386
+ const currentFramework = useCurrentFramework(frameworks)
387
+ const currentFrameworkOption = frameworkOptions.find(
388
+ (f) => f.value === currentFramework.framework,
389
+ )
390
+
391
+ const [isFullWidth, setIsFullWidth] = useLocalStorage('docsFullWidth', false)
392
+
393
+ const groupInitialOpenState = React.useMemo(() => {
394
+ return menuConfig.reduce<Record<string, boolean>>((acc, group, index) => {
395
+ const isChildActive = group.children.some((child) => child.to === _splat)
396
+ const key = `${index}:${String(group.label)}`
397
+
398
+ acc[key] = isChildActive
399
+ ? true
400
+ : typeof group.defaultCollapsed !== 'undefined'
401
+ ? !group.defaultCollapsed
402
+ : false
403
+
404
+ return acc
405
+ }, {})
406
+ }, [menuConfig, _splat])
407
+
408
+ const [openGroups, setOpenGroups] = React.useState(groupInitialOpenState)
409
+
410
+ React.useEffect(() => {
411
+ setOpenGroups((prev) => {
412
+ let hasChanged = false
413
+ const next = { ...prev }
414
+
415
+ Object.entries(groupInitialOpenState).forEach(([key, isOpen]) => {
416
+ if (!(key in next)) {
417
+ next[key] = isOpen
418
+ hasChanged = true
419
+ return
420
+ }
421
+
422
+ if (isOpen && !next[key]) {
423
+ next[key] = true
424
+ hasChanged = true
425
+ }
426
+ })
427
+
428
+ return hasChanged ? next : prev
429
+ })
430
+ }, [groupInitialOpenState])
431
+
432
+ const menuItems = menuConfig.map((group, i) => {
433
+ const groupKey = `${i}:${String(group.label)}`
434
+
435
+ const groupContent = (
436
+ <>
437
+ {group.collapsible ? (
438
+ <summary className="text-[.8em] font-bold leading-4 px-2 ts-sidebar-label">
439
+ {group.label}
440
+ </summary>
441
+ ) : (
442
+ <div className="text-[.8em] font-bold leading-4 px-2 ts-sidebar-label">
443
+ {group.label}
444
+ </div>
445
+ )}
446
+ <div className="h-2" />
447
+ <ul className="text-[.85em] leading-snug list-none">
448
+ {group?.children?.map((child, i) => {
449
+ const linkClasses = `flex gap-2 items-center justify-between group px-2 py-1.5 rounded-lg hover:bg-gray-500/10 opacity-60 hover:opacity-100`
450
+
451
+ return (
452
+ <li key={i}>
453
+ {child.to.startsWith('http') ? (
454
+ <a
455
+ href={child.to}
456
+ className={linkClasses}
457
+ target="_blank"
458
+ rel="noopener noreferrer"
459
+ >
460
+ {child.label}
461
+ </a>
462
+ ) : (
463
+ <Link
464
+ from={docsBasePath as any}
465
+ to={child.to}
466
+ params
467
+ onClick={() => {
468
+ detailsRef.current.removeAttribute('open')
469
+ }}
470
+ preload={false}
471
+ activeOptions={{
472
+ exact: true,
473
+ includeHash: false,
474
+ includeSearch: false,
475
+ }}
476
+ className="relative"
477
+ >
478
+ {(props) => {
479
+ return (
480
+ <div
481
+ className={twMerge(
482
+ linkClasses,
483
+ props.isActive && 'opacity-100',
484
+ )}
485
+ >
486
+ <div
487
+ className={twMerge(
488
+ 'w-full',
489
+ props.isActive
490
+ ? `font-bold text-transparent bg-clip-text bg-linear-to-r ${colorFrom} ${colorTo}`
491
+ : '',
492
+ )}
493
+ >
494
+ {child.label}
495
+ </div>
496
+ </div>
497
+ )
498
+ }}
499
+ </Link>
500
+ )}
501
+ </li>
502
+ )
503
+ })}
504
+ </ul>
505
+ </>
506
+ )
507
+
508
+ return group.collapsible ? (
509
+ <details
510
+ key={`group-${i}`}
511
+ className="[&>summary]:before:mr-1 [&>summary]:marker:text-[0.8em] [&>summary]:marker:leading-4 relative select-none"
512
+ open={openGroups[groupKey] ?? false}
513
+ onToggle={(event) => {
514
+ const nextOpen = event.currentTarget.open
515
+ setOpenGroups((prev) =>
516
+ prev[groupKey] === nextOpen
517
+ ? prev
518
+ : { ...prev, [groupKey]: nextOpen },
519
+ )
520
+ }}
521
+ >
522
+ {groupContent}
523
+ </details>
524
+ ) : (
525
+ <div
526
+ key={`group-${i}`}
527
+ className="[&>summary]:before:mr-1 [&>summary]:marker:text-[0.8em] [&>summary]:marker:leading-4 relative select-none"
528
+ >
529
+ {groupContent}
530
+ </div>
531
+ )
532
+ })
533
+
534
+ const smallMenu = (
535
+ <div
536
+ className="md:hidden bg-white/50 sticky top-[var(--navbar-height)]
537
+ max-h-[calc(100dvh-var(--navbar-height))] overflow-y-auto z-20 dark:bg-black/60 backdrop-blur-lg"
538
+ >
539
+ <details
540
+ ref={detailsRef as any}
541
+ id="docs-details"
542
+ className="border-b border-gray-500/20"
543
+ >
544
+ <summary className="py-2 px-4 flex gap-2 items-center">
545
+ <div className="flex gap-2 items-center shrink-0 pr-2">
546
+ <Menu className="cursor-pointer" />
547
+ Docs
548
+ </div>
549
+ <div className="w-px h-6 bg-gray-300 dark:bg-gray-600 shrink-0" />
550
+ {/* Mobile partners strip placeholder */}
551
+ <div className="flex-1 flex items-center gap-2 min-w-0" />
552
+ </summary>
553
+ <div className="flex flex-col gap-4 p-4 overflow-y-auto border-t border-gray-500/20 bg-white/20 text-lg dark:bg-black/20">
554
+ <div className="flex flex-col gap-1">
555
+ <FrameworkSelect frameworks={frameworks} />
556
+ <VersionSelect versions={versions} />
557
+ <LocaleSwitcher />
558
+ </div>
559
+ {menuItems}
560
+ </div>
561
+ </details>
562
+ </div>
563
+ )
564
+
565
+ // State and timer for auto-hide behavior (similar to Navbar)
566
+ const [showLargeMenu, setShowLargeMenu] = React.useState(false)
567
+ const leaveTimer = React.useRef<NodeJS.Timeout | undefined>(undefined)
568
+
569
+ // Close menu when clicking outside (only on sm-xl screens where it's an overlay)
570
+ const expandedMenuRef = useClickOutside<HTMLDivElement>({
571
+ enabled:
572
+ showLargeMenu &&
573
+ typeof window !== 'undefined' &&
574
+ window.innerWidth < 1280,
575
+ onClickOutside: () => setShowLargeMenu(false),
576
+ })
577
+
578
+ const largeMenu = (
579
+ <>
580
+ {/* Collapsed strip - visible on md to xl, hidden on xl+. Lower z-index so expanded menu covers it */}
581
+ <div
582
+ className={twMerge(
583
+ 'hidden md:flex xl:hidden flex-col overflow-hidden',
584
+ 'sticky top-[var(--navbar-height)] h-[calc(100dvh-var(--navbar-height))]',
585
+ 'z-10 border-r border-gray-500/20',
586
+ 'bg-white/50 dark:bg-black/30',
587
+ 'w-10',
588
+ )}
589
+ >
590
+ <DocsMenuStrip
591
+ menuConfig={menuConfig}
592
+ activeItem={relativePathname}
593
+ fullPathname={lastMatch.pathname}
594
+ colorFrom={colorFrom}
595
+ colorTo={colorTo}
596
+ frameworkLogo={currentFrameworkOption?.logo}
597
+ version={version}
598
+ onHover={() => {
599
+ if (window.innerWidth < 1280) {
600
+ // Only auto-show on lg screens, not xl+
601
+ clearTimeout(leaveTimer.current)
602
+ setShowLargeMenu(true)
603
+ }
604
+ }}
605
+ onClick={() => {
606
+ if (window.innerWidth < 1280) {
607
+ clearTimeout(leaveTimer.current)
608
+ setShowLargeMenu(true)
609
+ }
610
+ }}
611
+ />
612
+ </div>
613
+
614
+ {/* Expanded menu - always visible on xl+, toggleable overlay on md-xl */}
615
+ <div
616
+ ref={expandedMenuRef}
617
+ className={twMerge(
618
+ 'max-w-[250px] xl:max-w-[300px] 2xl:max-w-[400px]',
619
+ 'flex-col overflow-hidden',
620
+ 'h-[calc(100dvh-var(--navbar-height))] top-[var(--navbar-height)]',
621
+ 'z-20 border-r border-gray-500/20',
622
+ 'transition-all duration-300',
623
+ // Hidden on smallest screens, flex on md+
624
+ 'hidden md:flex',
625
+ // On md to xl: fixed overlay that slides in from left-0 (covers the strip)
626
+ 'md:fixed md:left-0 md:bg-white md:dark:bg-black/95 md:backdrop-blur-lg md:shadow-xl',
627
+ // On xl+: sticky positioning, no overlay styling
628
+ 'xl:sticky xl:bg-transparent xl:dark:bg-transparent xl:backdrop-blur-none xl:shadow-none',
629
+ // Slide animation for md-xl screens (off-screen by default, slides in when shown)
630
+ // On xl+: always visible (no translate)
631
+ !showLargeMenu && 'md:-translate-x-full xl:translate-x-0',
632
+ showLargeMenu && 'md:translate-x-0',
633
+ )}
634
+ onPointerEnter={(e) => {
635
+ if (e.pointerType === 'touch') return
636
+ if (window.innerWidth < 1280) {
637
+ clearTimeout(leaveTimer.current)
638
+ }
639
+ }}
640
+ onPointerLeave={(e) => {
641
+ if (e.pointerType === 'touch') return
642
+ if (window.innerWidth < 1280) {
643
+ leaveTimer.current = setTimeout(() => {
644
+ setShowLargeMenu(false)
645
+ }, 300)
646
+ }
647
+ }}
648
+ >
649
+ <div className="flex-1 flex flex-col overflow-y-auto">
650
+ <div className="flex flex-col gap-1 p-4">
651
+ <FrameworkSelect frameworks={frameworks} />
652
+ <VersionSelect versions={versions} />
653
+ <LocaleSwitcher />
654
+ </div>
655
+ <div className="flex-1 flex flex-col gap-4 text-base px-4 pt-0 pb-4">
656
+ {menuItems}
657
+ </div>
658
+ </div>
659
+ </div>
660
+ </>
661
+ )
662
+
663
+ return (
664
+ <WidthToggleContext.Provider value={{ isFullWidth, setIsFullWidth }}>
665
+ <DocNavigationContext.Provider
666
+ value={{ prevItem, nextItem, colorFrom, colorTo, textColor, docsBasePath }}
667
+ >
668
+ <div
669
+ className={`
670
+ md:min-h-[calc(100dvh-var(--navbar-height))]
671
+ flex flex-col md:flex-row
672
+ w-full transition-all duration-300
673
+ [overflow-x:clip]`}
674
+ >
675
+ {smallMenu}
676
+ {largeMenu}
677
+ <div
678
+ className={twMerge(
679
+ 'flex flex-col max-w-full min-w-0 flex-1 min-h-0 relative',
680
+ !isLandingPage && 'px-4 md:px-8',
681
+ )}
682
+ >
683
+ <div
684
+ className={twMerge(
685
+ `max-w-full min-w-0 flex flex-col justify-center w-full`,
686
+
687
+ !isLandingPage &&
688
+ !isExample &&
689
+ !isNpmStats &&
690
+ !isFullWidth &&
691
+ 'mx-auto w-[900px]',
692
+ )}
693
+ >
694
+ {children}
695
+ </div>
696
+ {!isLandingPage && <Footer />}
697
+ </div>
698
+ {/* Right rail placeholder - maintains layout grid position */}
699
+ {!isLandingPage && (
700
+ <div className="w-full md:w-[300px] shrink-0 md:sticky md:top-[var(--navbar-height)] hidden md:block">
701
+ <div className="md:sticky md:top-[var(--navbar-height)] ml-auto flex flex-col gap-4 pb-4 max-w-full overflow-hidden">
702
+ {/* Partners rail placeholder */}
703
+ <div className="flex flex-wrap items-stretch border-l border-gray-500/20 rounded-bl-lg overflow-hidden w-full" />
704
+ {/* Recent posts placeholder */}
705
+ <div className="hidden md:block border border-gray-500/20 rounded-l-lg overflow-hidden w-full" />
706
+ {/* Ad placeholder */}
707
+ <div />
708
+ </div>
709
+ </div>
710
+ )}
711
+ </div>
712
+ </DocNavigationContext.Provider>
713
+ </WidthToggleContext.Provider>
714
+ )
715
+ }