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.
- package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +26 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -7
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +53 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /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
|
+
}
|