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,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
+ }