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,430 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
import {
|
|
4
|
+
Link,
|
|
5
|
+
useLocation,
|
|
6
|
+
useMatches,
|
|
7
|
+
} from '@tanstack/react-router'
|
|
8
|
+
import {
|
|
9
|
+
Menu,
|
|
10
|
+
X,
|
|
11
|
+
Grid2X2,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import { ThemeToggle } from './ThemeToggle'
|
|
14
|
+
import { SearchButton } from './SearchButton'
|
|
15
|
+
import { siteConfig, findProject, isSingleProject } from '~/site.config'
|
|
16
|
+
import type { ProjectConfig } from '~/types'
|
|
17
|
+
import { useClickOutside } from '~/hooks/useClickOutside'
|
|
18
|
+
import { GithubIcon } from '~/components/icons/GithubIcon'
|
|
19
|
+
import { DiscordIcon } from '~/components/icons/DiscordIcon'
|
|
20
|
+
import { BrandXIcon } from '~/components/icons/BrandXIcon'
|
|
21
|
+
import { BSkyIcon } from '~/components/icons/BSkyIcon'
|
|
22
|
+
import { YouTubeIcon } from '~/components/icons/YouTubeIcon'
|
|
23
|
+
import { LocaleSwitcher } from './LocaleSwitcher'
|
|
24
|
+
import { Card } from '~/components/Card'
|
|
25
|
+
|
|
26
|
+
type LogoProps = {
|
|
27
|
+
showMenu: boolean
|
|
28
|
+
setShowMenu: React.Dispatch<React.SetStateAction<boolean>>
|
|
29
|
+
menuButtonRef: React.RefObject<HTMLButtonElement | null>
|
|
30
|
+
title?: React.ComponentType<any> | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LogoSection = ({
|
|
34
|
+
showMenu,
|
|
35
|
+
setShowMenu,
|
|
36
|
+
menuButtonRef,
|
|
37
|
+
title,
|
|
38
|
+
}: LogoProps) => {
|
|
39
|
+
const singleProject = isSingleProject()
|
|
40
|
+
const pointerInsideButtonRef = React.useRef(false)
|
|
41
|
+
const toggleMenu = () => {
|
|
42
|
+
setShowMenu((prev) => !prev)
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{/* Hide project menu button for single-project sites */}
|
|
47
|
+
{!singleProject && (
|
|
48
|
+
<button
|
|
49
|
+
aria-label="Open Menu"
|
|
50
|
+
className={twMerge(
|
|
51
|
+
'flex items-center justify-center',
|
|
52
|
+
'transition-all duration-300 h-8 px-2 py-1 lg:px-0',
|
|
53
|
+
// At lg: only visible when Title exists (flyout mode)
|
|
54
|
+
// Below lg: always visible
|
|
55
|
+
title
|
|
56
|
+
? 'lg:w-9 lg:opacity-100 lg:translate-x-0'
|
|
57
|
+
: 'lg:w-0 lg:opacity-0 lg:-translate-x-full',
|
|
58
|
+
)}
|
|
59
|
+
ref={menuButtonRef}
|
|
60
|
+
onClick={toggleMenu}
|
|
61
|
+
onPointerEnter={(e) => {
|
|
62
|
+
// Enable hover to open flyout at md+ (but not touch)
|
|
63
|
+
if (window.innerWidth < 768 || e.pointerType === 'touch') return
|
|
64
|
+
if (pointerInsideButtonRef.current) return
|
|
65
|
+
pointerInsideButtonRef.current = true
|
|
66
|
+
setShowMenu(true)
|
|
67
|
+
}}
|
|
68
|
+
onPointerLeave={() => {
|
|
69
|
+
pointerInsideButtonRef.current = false
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{showMenu ? <X /> : <Menu />}
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
<Link
|
|
76
|
+
to="/"
|
|
77
|
+
className={twMerge(`inline-flex items-center gap-1.5 cursor-pointer`)}
|
|
78
|
+
>
|
|
79
|
+
<div>{siteConfig.name}</div>
|
|
80
|
+
</Link>
|
|
81
|
+
</>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const MobileCard = ({
|
|
86
|
+
children,
|
|
87
|
+
isActive,
|
|
88
|
+
}: {
|
|
89
|
+
children: React.ReactNode
|
|
90
|
+
isActive?: boolean
|
|
91
|
+
}) => (
|
|
92
|
+
<Card
|
|
93
|
+
className={twMerge(
|
|
94
|
+
'md:contents border-gray-200/50 dark:border-gray-700/50 shadow-sm',
|
|
95
|
+
isActive && 'ring-2 ring-gray-400/30 dark:ring-gray-500/30',
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</Card>
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
export function Navbar({ children }: { children: React.ReactNode }) {
|
|
103
|
+
const matches = useMatches()
|
|
104
|
+
|
|
105
|
+
const { Title, project } = React.useMemo(() => {
|
|
106
|
+
const match = [...matches].reverse().find((m) => m.staticData.Title)
|
|
107
|
+
const params = match?.params as { project?: string } | undefined
|
|
108
|
+
const projectId = params?.project
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
Title: match?.staticData.Title ?? null,
|
|
112
|
+
project: projectId ? findProject(projectId) : null,
|
|
113
|
+
}
|
|
114
|
+
}, [matches])
|
|
115
|
+
|
|
116
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
117
|
+
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
const updateContainerHeight = () => {
|
|
120
|
+
if (containerRef.current) {
|
|
121
|
+
const height = containerRef.current.offsetHeight
|
|
122
|
+
document.documentElement.style.setProperty(
|
|
123
|
+
'--navbar-height',
|
|
124
|
+
`${height}px`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
updateContainerHeight() // Initial call to set the height
|
|
130
|
+
|
|
131
|
+
window.addEventListener('resize', updateContainerHeight)
|
|
132
|
+
return () => {
|
|
133
|
+
window.removeEventListener('resize', updateContainerHeight)
|
|
134
|
+
}
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
const [showMenu, setShowMenu] = React.useState(false)
|
|
138
|
+
const largeMenuRef = React.useRef<HTMLDivElement>(null)
|
|
139
|
+
const menuButtonRef = React.useRef<HTMLButtonElement>(null)
|
|
140
|
+
|
|
141
|
+
// Close mobile menu when clicking outside
|
|
142
|
+
const smallMenuRef = useClickOutside<HTMLDivElement>({
|
|
143
|
+
enabled: showMenu,
|
|
144
|
+
onClickOutside: () => setShowMenu(false),
|
|
145
|
+
additionalRefs: [largeMenuRef, menuButtonRef],
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const socialLinks = (
|
|
149
|
+
<div className="flex items-center [&_a]:p-1.5 [&_a]:opacity-50 [&_a:hover]:opacity-100 [&_a]:transition-opacity [&_svg]:text-sm">
|
|
150
|
+
<a
|
|
151
|
+
href={`https://github.com/${project?.repo ?? siteConfig.repo.replace('https://github.com/', '')}`}
|
|
152
|
+
aria-label={`Follow on GitHub`}
|
|
153
|
+
>
|
|
154
|
+
<GithubIcon />
|
|
155
|
+
</a>
|
|
156
|
+
<a href="https://x.com/tan_stack" aria-label="Follow TanStack on X.com">
|
|
157
|
+
<BrandXIcon />
|
|
158
|
+
</a>
|
|
159
|
+
<a
|
|
160
|
+
href="https://bsky.app/profile/tanstack.com"
|
|
161
|
+
aria-label="Follow TanStack on Besky"
|
|
162
|
+
>
|
|
163
|
+
<BSkyIcon />
|
|
164
|
+
</a>
|
|
165
|
+
<a
|
|
166
|
+
href="https://youtube.com/@tan_stack"
|
|
167
|
+
aria-label="Subscribe to TanStack on YouTube"
|
|
168
|
+
>
|
|
169
|
+
<YouTubeIcon />
|
|
170
|
+
</a>
|
|
171
|
+
<a href="https://tlinz.com/discord" aria-label="Join TanStack Discord">
|
|
172
|
+
<DiscordIcon />
|
|
173
|
+
</a>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const navbar = (
|
|
178
|
+
<div
|
|
179
|
+
className={twMerge(
|
|
180
|
+
'w-full p-2 fixed top-0 z-[100] bg-white/90 dark:bg-black/90 backdrop-blur-lg',
|
|
181
|
+
'flex items-center justify-between gap-4',
|
|
182
|
+
'border-b border-gray-500/20',
|
|
183
|
+
)}
|
|
184
|
+
ref={containerRef}
|
|
185
|
+
>
|
|
186
|
+
<div className="flex items-center min-w-0">
|
|
187
|
+
<div className="flex items-center gap-2 font-black text-xl uppercase min-w-0">
|
|
188
|
+
<div className={twMerge(`flex items-center group flex-shrink-0`)}>
|
|
189
|
+
<LogoSection
|
|
190
|
+
menuButtonRef={menuButtonRef}
|
|
191
|
+
setShowMenu={setShowMenu}
|
|
192
|
+
showMenu={showMenu}
|
|
193
|
+
title={Title}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
{Title ? (
|
|
197
|
+
<div className="truncate">
|
|
198
|
+
<Title />
|
|
199
|
+
</div>
|
|
200
|
+
) : null}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
204
|
+
<div className="hidden min-[750px]:block">{socialLinks}</div>
|
|
205
|
+
<div className="hidden sm:block">
|
|
206
|
+
<SearchButton />
|
|
207
|
+
</div>
|
|
208
|
+
<ThemeToggle />
|
|
209
|
+
<LocaleSwitcher />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const activeProject = useLocation({
|
|
215
|
+
select: (location) => {
|
|
216
|
+
return siteConfig.projects.find((p) => {
|
|
217
|
+
const projectPath = `/${siteConfig.defaultLocale}/${p.id}`
|
|
218
|
+
return location.pathname.includes(`/${p.id}/`)
|
|
219
|
+
})
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const linkClasses = `flex items-center justify-between gap-2 group px-3 py-3 md:px-2 md:py-1 rounded-lg hover:bg-gray-500/10 font-bold text-base md:text-sm`
|
|
224
|
+
|
|
225
|
+
const items = (
|
|
226
|
+
<div className="contents md:block">
|
|
227
|
+
<div className="contents md:block">
|
|
228
|
+
{siteConfig.projects.map((proj, i) => {
|
|
229
|
+
const [_, name] = proj.name.includes(' ')
|
|
230
|
+
? proj.name.split(' ')
|
|
231
|
+
: ['', proj.name]
|
|
232
|
+
const isActive = proj.id === activeProject?.id
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div key={i} className="contents md:block">
|
|
236
|
+
<>
|
|
237
|
+
{/* Mobile: Direct link with Card */}
|
|
238
|
+
<MobileCard isActive={isActive}>
|
|
239
|
+
<Link
|
|
240
|
+
to={(siteConfig.hideLatestVersion ? `/${siteConfig.defaultLocale}/${proj.id}/docs` : `/${siteConfig.defaultLocale}/${proj.id}/${proj.latestVersion}`) as any}
|
|
241
|
+
className={twMerge(
|
|
242
|
+
linkClasses,
|
|
243
|
+
'md:hidden',
|
|
244
|
+
isActive ? 'bg-gray-500/5' : '',
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<span
|
|
248
|
+
className={twMerge(
|
|
249
|
+
'w-4 h-4 md:w-3 md:h-3 rounded-sm',
|
|
250
|
+
proj.bgStyle,
|
|
251
|
+
)}
|
|
252
|
+
/>
|
|
253
|
+
<span
|
|
254
|
+
style={{
|
|
255
|
+
viewTransitionName: `library-name-${proj.id}`,
|
|
256
|
+
}}
|
|
257
|
+
className={twMerge(
|
|
258
|
+
'flex-1 text-left',
|
|
259
|
+
isActive ? 'font-bold' : '',
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
{name}
|
|
263
|
+
</span>
|
|
264
|
+
{proj.badge ? (
|
|
265
|
+
<span
|
|
266
|
+
className={twMerge(
|
|
267
|
+
`px-2 py-px uppercase font-black rounded-md text-[.65rem]`,
|
|
268
|
+
'border-2 bg-transparent',
|
|
269
|
+
proj.textStyle,
|
|
270
|
+
proj.borderStyle,
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
{proj.badge}
|
|
274
|
+
</span>
|
|
275
|
+
) : null}
|
|
276
|
+
</Link>
|
|
277
|
+
</MobileCard>
|
|
278
|
+
{/* Desktop: Simple link */}
|
|
279
|
+
<Link
|
|
280
|
+
to={(siteConfig.hideLatestVersion ? `/${siteConfig.defaultLocale}/${proj.id}/docs` : `/${siteConfig.defaultLocale}/${proj.id}/${proj.latestVersion}`) as any}
|
|
281
|
+
className={twMerge(
|
|
282
|
+
linkClasses,
|
|
283
|
+
'hidden md:flex',
|
|
284
|
+
isActive ? 'bg-gray-500/10 dark:bg-gray-500/30' : '',
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
<span
|
|
288
|
+
className={twMerge('w-3 h-3 rounded-sm', proj.bgStyle)}
|
|
289
|
+
/>
|
|
290
|
+
<span
|
|
291
|
+
style={{
|
|
292
|
+
viewTransitionName: `library-name-${proj.id}`,
|
|
293
|
+
}}
|
|
294
|
+
className={twMerge(
|
|
295
|
+
'flex-1 text-left',
|
|
296
|
+
isActive ? 'font-bold' : '',
|
|
297
|
+
)}
|
|
298
|
+
>
|
|
299
|
+
{name}
|
|
300
|
+
</span>
|
|
301
|
+
{proj.badge ? (
|
|
302
|
+
<span
|
|
303
|
+
className={twMerge(
|
|
304
|
+
`px-2 py-px uppercase font-black rounded-md text-[.6rem]`,
|
|
305
|
+
'border bg-transparent',
|
|
306
|
+
'border-current text-current',
|
|
307
|
+
'opacity-90 group-hover:opacity-100 transition-opacity',
|
|
308
|
+
proj.textColor,
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
{proj.badge}
|
|
312
|
+
</span>
|
|
313
|
+
) : null}
|
|
314
|
+
</Link>
|
|
315
|
+
</>
|
|
316
|
+
</div>
|
|
317
|
+
)
|
|
318
|
+
})}
|
|
319
|
+
<div className="py-2 hidden md:block col-span-2">
|
|
320
|
+
<div className="bg-gray-500/10 h-px" />
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
{/* Mobile separator */}
|
|
324
|
+
<div className="col-span-2 sm:col-span-3 py-3 md:hidden">
|
|
325
|
+
<div className="bg-gray-500/10 h-px" />
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const smallMenu = showMenu ? (
|
|
331
|
+
<div
|
|
332
|
+
ref={smallMenuRef}
|
|
333
|
+
className="md:hidden bg-white/50 dark:bg-black/60 backdrop-blur-[20px] z-50
|
|
334
|
+
fixed top-[var(--navbar-height)] left-0 right-0 max-h-[calc(100dvh-var(--navbar-height))] overflow-y-auto
|
|
335
|
+
"
|
|
336
|
+
>
|
|
337
|
+
<div
|
|
338
|
+
className="flex flex-col whitespace-nowrap overflow-y-auto
|
|
339
|
+
border-t border-gray-500/20 text-lg bg-white/80 dark:bg-black/90"
|
|
340
|
+
>
|
|
341
|
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
|
342
|
+
<div
|
|
343
|
+
onClick={(event) => {
|
|
344
|
+
const target = event.target as HTMLElement
|
|
345
|
+
if (target.closest('[data-collapsible-trigger]')) {
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
if (target.closest('a') || target.closest('button')) {
|
|
349
|
+
setShowMenu(false)
|
|
350
|
+
}
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<div className="px-3 pt-3 sm:hidden">
|
|
354
|
+
<SearchButton className="w-full py-3 text-base [&_svg]:w-5 [&_svg]:h-5" />
|
|
355
|
+
</div>
|
|
356
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 text-base p-3 border-b border-gray-500/20">
|
|
357
|
+
{items}
|
|
358
|
+
</div>
|
|
359
|
+
<div className="p-4 sm:hidden">{socialLinks}</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
) : null
|
|
364
|
+
|
|
365
|
+
const inlineMenu = !Title
|
|
366
|
+
|
|
367
|
+
const leaveTimer = React.useRef<NodeJS.Timeout | undefined>(undefined)
|
|
368
|
+
|
|
369
|
+
const largeMenu = (
|
|
370
|
+
<>
|
|
371
|
+
<div
|
|
372
|
+
ref={largeMenuRef}
|
|
373
|
+
className={twMerge(
|
|
374
|
+
`hidden md:flex flex-col
|
|
375
|
+
h-[calc(100dvh-var(--navbar-height))] z-20
|
|
376
|
+
bg-white/50 dark:bg-black/30 border-r border-gray-500/20`,
|
|
377
|
+
'transition-all duration-300',
|
|
378
|
+
'z-50',
|
|
379
|
+
// md breakpoint: always flyout
|
|
380
|
+
'md:fixed md:top-[var(--navbar-height)] md:bg-white md:dark:bg-black/90 md:backdrop-blur-lg md:shadow-xl',
|
|
381
|
+
!showMenu && 'md:-translate-x-full',
|
|
382
|
+
showMenu && 'md:translate-x-0',
|
|
383
|
+
// lg breakpoint: inline when no Title, flyout when Title
|
|
384
|
+
inlineMenu &&
|
|
385
|
+
'lg:sticky lg:top-[var(--navbar-height)] lg:translate-x-0 lg:bg-white/50 lg:dark:bg-black/30 lg:backdrop-blur-none lg:shadow-none',
|
|
386
|
+
!inlineMenu &&
|
|
387
|
+
'lg:fixed lg:top-[var(--navbar-height)] lg:bg-white lg:dark:bg-black/90 lg:backdrop-blur-lg lg:shadow-xl',
|
|
388
|
+
!inlineMenu && !showMenu && 'lg:-translate-x-full',
|
|
389
|
+
!inlineMenu && showMenu && 'lg:translate-x-0',
|
|
390
|
+
)}
|
|
391
|
+
onPointerEnter={(e) => {
|
|
392
|
+
if (e.pointerType === 'touch') return
|
|
393
|
+
clearTimeout(leaveTimer.current)
|
|
394
|
+
}}
|
|
395
|
+
onPointerLeave={(e) => {
|
|
396
|
+
if (e.pointerType === 'touch') return
|
|
397
|
+
leaveTimer.current = setTimeout(() => {
|
|
398
|
+
setShowMenu(false)
|
|
399
|
+
}, 300)
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
<div className="flex-1 flex flex-col gap-4 whitespace-nowrap overflow-y-auto text-base pb-[50px] min-w-[220px]">
|
|
403
|
+
<div className="flex flex-col gap-1 text-sm p-2">{items}</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</>
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
const singleProject = isSingleProject()
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<>
|
|
413
|
+
{navbar}
|
|
414
|
+
<div
|
|
415
|
+
className={twMerge(
|
|
416
|
+
`min-h-[calc(100dvh-var(--navbar-height))] flex flex-col
|
|
417
|
+
min-w-0 md:flex-row w-full transition-all duration-300
|
|
418
|
+
pt-[var(--navbar-height)]`,
|
|
419
|
+
)}
|
|
420
|
+
>
|
|
421
|
+
{/* Hide project switcher menus for single-project sites */}
|
|
422
|
+
{!singleProject && smallMenu}
|
|
423
|
+
{!singleProject && largeMenu}
|
|
424
|
+
<div className="flex-1 min-w-0 flex flex-col w-full min-h-0">
|
|
425
|
+
{children}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
export function PostNotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex-1 p-4 flex flex-col items-center justify-center gap-6">
|
|
6
|
+
<h1 className="opacity-10 flex flex-col text-center font-black">
|
|
7
|
+
<div className="text-7xl leading-none">404</div>
|
|
8
|
+
<div className="text-3xl leading-none">Not Found</div>
|
|
9
|
+
</h1>
|
|
10
|
+
<div className="text-lg">Post not found.</div>
|
|
11
|
+
<Link
|
|
12
|
+
to="/$lang/blog"
|
|
13
|
+
params={(prev: Record<string, string>) => ({ lang: prev.lang || 'en' })}
|
|
14
|
+
className="px-4 py-2 rounded-lg bg-gray-600 text-white hover:bg-gray-700 transition-colors"
|
|
15
|
+
>
|
|
16
|
+
Blog Home
|
|
17
|
+
</Link>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Command, Search } from 'lucide-react'
|
|
3
|
+
import { twMerge } from 'tailwind-merge'
|
|
4
|
+
import { useSearchContext } from '~/contexts/SearchContext'
|
|
5
|
+
|
|
6
|
+
interface SearchButtonProps {
|
|
7
|
+
className?: string
|
|
8
|
+
children?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SearchButton({ className }: SearchButtonProps) {
|
|
12
|
+
const { openSearch } = useSearchContext()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
onClick={openSearch}
|
|
17
|
+
className={twMerge(
|
|
18
|
+
'flex items-center gap-2 px-2 py-1 rounded-md text-sm',
|
|
19
|
+
'bg-gray-500/5 dark:bg-gray-500/30',
|
|
20
|
+
'hover:bg-gray-500/10 dark:hover:bg-gray-500/40',
|
|
21
|
+
'transition-colors duration-200',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Search className="w-3.5 h-3.5" />
|
|
26
|
+
<span>Search...</span>
|
|
27
|
+
<div className="flex items-center bg-gray-500/10 dark:bg-gray-500/30 rounded px-1 py-0.5 gap-0.5 text-[10px] whitespace-nowrap">
|
|
28
|
+
<Command className="w-2.5 h-2.5" /> K
|
|
29
|
+
</div>
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import { Check, ChevronsUpDown } from 'lucide-react'
|
|
3
|
+
import { twMerge } from 'tailwind-merge'
|
|
4
|
+
import {
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropdownTrigger,
|
|
7
|
+
DropdownContent,
|
|
8
|
+
DropdownItem,
|
|
9
|
+
} from './Dropdown'
|
|
10
|
+
|
|
11
|
+
export type SelectOption = {
|
|
12
|
+
label: string
|
|
13
|
+
value: string
|
|
14
|
+
logo?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SelectProps<T extends SelectOption> = {
|
|
18
|
+
className?: string
|
|
19
|
+
icon?: ReactNode
|
|
20
|
+
selected: string
|
|
21
|
+
available: T[]
|
|
22
|
+
onSelect: (selected: T) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Select<T extends SelectOption>({
|
|
26
|
+
className = '',
|
|
27
|
+
icon,
|
|
28
|
+
selected,
|
|
29
|
+
available,
|
|
30
|
+
onSelect,
|
|
31
|
+
}: SelectProps<T>) {
|
|
32
|
+
if (available.length === 0) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectedOption = available.find(({ value }) => selected === value)
|
|
37
|
+
|
|
38
|
+
if (!selectedOption) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={twMerge('w-full', className)}>
|
|
44
|
+
<Dropdown>
|
|
45
|
+
<DropdownTrigger>
|
|
46
|
+
<button className="relative items-center w-full gap-2 flex hover:bg-gray-500/10 cursor-pointer rounded-md py-1.5 px-2 text-left focus:outline-none text-sm">
|
|
47
|
+
{icon ? (
|
|
48
|
+
<span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20">
|
|
49
|
+
{icon}
|
|
50
|
+
</span>
|
|
51
|
+
) : selectedOption.logo ? (
|
|
52
|
+
<span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20">
|
|
53
|
+
<img
|
|
54
|
+
height={16}
|
|
55
|
+
width={16}
|
|
56
|
+
src={selectedOption.logo}
|
|
57
|
+
alt={`${selectedOption.label} logo`}
|
|
58
|
+
/>
|
|
59
|
+
</span>
|
|
60
|
+
) : null}
|
|
61
|
+
<span className="truncate font-medium">{selectedOption.label}</span>
|
|
62
|
+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
63
|
+
<ChevronsUpDown
|
|
64
|
+
className="h-4 w-4 opacity-40"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
/>
|
|
67
|
+
</span>
|
|
68
|
+
</button>
|
|
69
|
+
</DropdownTrigger>
|
|
70
|
+
<DropdownContent align="start" className="max-h-60 overflow-auto">
|
|
71
|
+
{available.map((option) => (
|
|
72
|
+
<DropdownItem
|
|
73
|
+
key={option.value}
|
|
74
|
+
onSelect={() => onSelect(option)}
|
|
75
|
+
className={twMerge(
|
|
76
|
+
'pr-8',
|
|
77
|
+
option.logo ? 'pl-2' : '',
|
|
78
|
+
selected === option.value ? 'font-medium' : 'font-normal',
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{option.logo ? (
|
|
82
|
+
<img
|
|
83
|
+
height={18}
|
|
84
|
+
width={18}
|
|
85
|
+
src={option.logo}
|
|
86
|
+
alt={`${option.label} logo`}
|
|
87
|
+
className="flex-shrink-0"
|
|
88
|
+
/>
|
|
89
|
+
) : null}
|
|
90
|
+
<span className="truncate">{option.label}</span>
|
|
91
|
+
{selected === option.value ? (
|
|
92
|
+
<Check
|
|
93
|
+
className="h-4 w-4 absolute right-2 text-gray-800 dark:text-gray-400"
|
|
94
|
+
aria-hidden="true"
|
|
95
|
+
/>
|
|
96
|
+
) : null}
|
|
97
|
+
</DropdownItem>
|
|
98
|
+
))}
|
|
99
|
+
</DropdownContent>
|
|
100
|
+
</Dropdown>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge'
|
|
2
|
+
import { Loader2 } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface SpinnerProps {
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Spinner({ className }: SpinnerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Loader2
|
|
11
|
+
className={twMerge(
|
|
12
|
+
'animate-spin text-gray-900 dark:text-white text-2xl',
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
aria-label="Loading"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|