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,116 @@
1
+ import * as React from 'react'
2
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
+ import { twMerge } from 'tailwind-merge'
4
+
5
+ type DropdownProps = {
6
+ children: React.ReactNode
7
+ open?: boolean
8
+ onOpenChange?: (open: boolean) => void
9
+ modal?: boolean
10
+ }
11
+
12
+ type DropdownTriggerProps = {
13
+ children: React.ReactNode
14
+ className?: string
15
+ asChild?: boolean
16
+ }
17
+
18
+ type DropdownContentProps = {
19
+ children: React.ReactNode
20
+ className?: string
21
+ align?: 'start' | 'center' | 'end'
22
+ sideOffset?: number
23
+ }
24
+
25
+ type DropdownItemProps = {
26
+ children: React.ReactNode
27
+ className?: string
28
+ onSelect?: () => void
29
+ asChild?: boolean
30
+ }
31
+
32
+ type DropdownSeparatorProps = {
33
+ className?: string
34
+ }
35
+
36
+ export function Dropdown({
37
+ children,
38
+ open,
39
+ onOpenChange,
40
+ modal = false,
41
+ }: DropdownProps) {
42
+ return (
43
+ <DropdownMenu.Root open={open} onOpenChange={onOpenChange} modal={modal}>
44
+ {children}
45
+ </DropdownMenu.Root>
46
+ )
47
+ }
48
+
49
+ export function DropdownTrigger({
50
+ children,
51
+ className,
52
+ asChild = true,
53
+ }: DropdownTriggerProps) {
54
+ return (
55
+ <DropdownMenu.Trigger asChild={asChild} className={className}>
56
+ {children}
57
+ </DropdownMenu.Trigger>
58
+ )
59
+ }
60
+
61
+ export function DropdownContent({
62
+ children,
63
+ className,
64
+ align = 'end',
65
+ sideOffset = 6,
66
+ }: DropdownContentProps) {
67
+ return (
68
+ <DropdownMenu.Portal>
69
+ <DropdownMenu.Content
70
+ align={align}
71
+ sideOffset={sideOffset}
72
+ className={twMerge(
73
+ 'dropdown-content z-[1000] min-w-48 rounded-lg p-1.5',
74
+ 'border border-gray-200 dark:border-gray-700',
75
+ 'bg-white dark:bg-gray-800',
76
+ 'shadow-lg',
77
+ className,
78
+ )}
79
+ >
80
+ {children}
81
+ </DropdownMenu.Content>
82
+ </DropdownMenu.Portal>
83
+ )
84
+ }
85
+
86
+ export function DropdownItem({
87
+ children,
88
+ className,
89
+ onSelect,
90
+ asChild,
91
+ }: DropdownItemProps) {
92
+ return (
93
+ <DropdownMenu.Item
94
+ asChild={asChild}
95
+ onSelect={onSelect}
96
+ className={twMerge(
97
+ 'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 outline-none',
98
+ 'text-sm text-gray-700 dark:text-gray-300',
99
+ 'hover:bg-gray-100 dark:hover:bg-gray-700/50',
100
+ 'focus:bg-gray-100 dark:focus:bg-gray-700/50',
101
+ 'transition-colors duration-150',
102
+ className,
103
+ )}
104
+ >
105
+ {children}
106
+ </DropdownMenu.Item>
107
+ )
108
+ }
109
+
110
+ export function DropdownSeparator({ className }: DropdownSeparatorProps) {
111
+ return (
112
+ <DropdownMenu.Separator
113
+ className={twMerge('my-1 h-px bg-gray-200 dark:bg-gray-700', className)}
114
+ />
115
+ )
116
+ }
@@ -0,0 +1,36 @@
1
+ type FallbackBannerProps = {
2
+ /** The display name of the requested locale (e.g., "Japanese", "Spanish") */
3
+ locale: string
4
+ }
5
+
6
+ /**
7
+ * Warning banner displayed when a page has not been translated to the requested locale.
8
+ * Uses the same styling as GitHub-style markdown alerts for visual consistency.
9
+ */
10
+ export function FallbackBanner({ locale }: FallbackBannerProps) {
11
+ return (
12
+ <div className="markdown-alert markdown-alert-warning mb-4">
13
+ <div className="markdown-alert-title">
14
+ <svg
15
+ className="octicon octicon-info mr-2"
16
+ viewBox="0 0 16 16"
17
+ width="16"
18
+ height="16"
19
+ aria-hidden="true"
20
+ >
21
+ <path
22
+ fillRule="evenodd"
23
+ d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"
24
+ ></path>
25
+ </svg>
26
+ Translation Unavailable
27
+ </div>
28
+ <div className="markdown-alert-content">
29
+ <p>
30
+ This page has not been translated to {locale} yet. You are viewing the
31
+ English version.
32
+ </p>
33
+ </div>
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,29 @@
1
+ import { Link } from '@tanstack/react-router'
2
+ import { Card } from './Card'
3
+
4
+ export function Footer() {
5
+ return (
6
+ <Card
7
+ className={`relative flex flex-col items-start justify-center gap-4 p-8
8
+ max-w-(--breakpoint-lg) mx-auto text-sm`}
9
+ >
10
+ <div className={`grid gap-1 sm:grid-cols-2 md:grid-cols-3`}>
11
+ <div>
12
+ <Link to="/">Home</Link>
13
+ </div>
14
+ <div>
15
+ <a
16
+ href="https://github.com/tanstack"
17
+ target="_blank"
18
+ rel="noreferrer"
19
+ >
20
+ GitHub
21
+ </a>
22
+ </div>
23
+ </div>
24
+ <div className={`text-center opacity-60`}>
25
+ &copy; {new Date().getFullYear()} TanStack
26
+ </div>
27
+ </Card>
28
+ )
29
+ }
@@ -0,0 +1,150 @@
1
+ import * as React from 'react'
2
+ import { create } from 'zustand'
3
+ import { useNavigate, useParams } from '@tanstack/react-router'
4
+ import { Select } from './Select'
5
+ import {
6
+ type Framework,
7
+ type FrameworkOption,
8
+ getFrameworkOptions,
9
+ } from '~/config/frameworks'
10
+
11
+ export function FrameworkSelect({
12
+ frameworks,
13
+ }: {
14
+ frameworks: Framework[]
15
+ }) {
16
+ const frameworkConfig = useFrameworkConfig({
17
+ frameworks,
18
+ })
19
+ const selectedFramework = frameworkConfig.available.find(
20
+ (f) => f.value === frameworkConfig.selected,
21
+ )
22
+ return (
23
+ <Select
24
+ className="w-full"
25
+ icon={
26
+ selectedFramework?.logo ? (
27
+ <img
28
+ src={selectedFramework.logo}
29
+ alt={selectedFramework.label}
30
+ className="w-4 h-4"
31
+ />
32
+ ) : undefined
33
+ }
34
+ selected={frameworkConfig.selected}
35
+ available={frameworkConfig.available}
36
+ onSelect={frameworkConfig.onSelect}
37
+ />
38
+ )
39
+ }
40
+
41
+ // Let's use zustand to wrap the local storage logic. This way
42
+ // we'll get subscriptions for free and we can use it in other
43
+ // components if we need to.
44
+ export const useLocalCurrentFramework = create<{
45
+ currentFramework?: string
46
+ setCurrentFramework: (framework: string) => void
47
+ }>((set) => ({
48
+ currentFramework:
49
+ typeof document !== 'undefined'
50
+ ? localStorage.getItem('framework') || undefined
51
+ : undefined,
52
+ setCurrentFramework: (framework: string) => {
53
+ localStorage.setItem('framework', framework)
54
+ set({ currentFramework: framework })
55
+ },
56
+ }))
57
+
58
+ /**
59
+ * Get the stored framework preference from localStorage.
60
+ * Safe to call during SSR (returns undefined).
61
+ */
62
+ export function getStoredFrameworkPreference(): string | undefined {
63
+ if (typeof window === 'undefined') return undefined
64
+ return localStorage.getItem('framework') || undefined
65
+ }
66
+
67
+ /**
68
+ * Hook to persist framework preference (localStorage only, no auth).
69
+ */
70
+ export function usePersistFrameworkPreference() {
71
+ const localCurrentFramework = useLocalCurrentFramework()
72
+
73
+ return React.useCallback(
74
+ (framework: string) => {
75
+ localCurrentFramework.setCurrentFramework(framework)
76
+ },
77
+ [localCurrentFramework],
78
+ )
79
+ }
80
+
81
+ function useFrameworkConfig({ frameworks }: { frameworks: Framework[] }) {
82
+ const currentFramework = useCurrentFramework(frameworks)
83
+
84
+ const frameworkConfig = React.useMemo(() => {
85
+ return {
86
+ label: 'Framework',
87
+ selected: frameworks.includes(currentFramework.framework as Framework)
88
+ ? currentFramework.framework
89
+ : 'react',
90
+ available: getFrameworkOptions(frameworks),
91
+ onSelect: (option: { label: string; value: string }) => {
92
+ currentFramework.setFramework(option.value)
93
+ },
94
+ }
95
+ }, [frameworks, currentFramework])
96
+
97
+ return frameworkConfig
98
+ }
99
+
100
+ /**
101
+ * Use framework in URL path
102
+ * Otherwise use framework in localStorage if it exists
103
+ * Otherwise fallback to react
104
+ */
105
+ export function useCurrentFramework(frameworks: Framework[]) {
106
+ const navigate = useNavigate()
107
+
108
+ const { framework: paramsFramework } = useParams({
109
+ strict: false,
110
+ })
111
+
112
+ const localCurrentFramework = useLocalCurrentFramework()
113
+
114
+ // Priority: URL params > localStorage > 'react'
115
+ let framework = (paramsFramework ||
116
+ localCurrentFramework.currentFramework ||
117
+ 'react') as Framework
118
+
119
+ framework = frameworks.includes(framework) ? framework : 'react'
120
+
121
+ const setFramework = React.useCallback(
122
+ (framework: string) => {
123
+ navigate({
124
+ params: { framework } as any,
125
+ })
126
+ localCurrentFramework.setCurrentFramework(framework)
127
+ },
128
+ [localCurrentFramework, navigate],
129
+ )
130
+
131
+ React.useEffect(() => {
132
+ // Set the framework in localStorage if it doesn't exist
133
+ if (!localCurrentFramework.currentFramework) {
134
+ localCurrentFramework.setCurrentFramework(framework)
135
+ }
136
+
137
+ // Set the framework in localStorage if it doesn't match the URL
138
+ if (
139
+ paramsFramework &&
140
+ paramsFramework !== localCurrentFramework.currentFramework
141
+ ) {
142
+ localCurrentFramework.setCurrentFramework(paramsFramework)
143
+ }
144
+ })
145
+
146
+ return {
147
+ framework,
148
+ setFramework,
149
+ }
150
+ }
@@ -0,0 +1,178 @@
1
+ import { Link } from '@tanstack/react-router'
2
+ import { twMerge } from 'tailwind-merge'
3
+ import type { ProjectConfig } from '~/types'
4
+ import { siteConfig, isSingleProject } from '~/site.config'
5
+
6
+ export default function LibraryCard({
7
+ project,
8
+ index = 0,
9
+ }: {
10
+ project: ProjectConfig
11
+ index?: number
12
+ }) {
13
+ // Use simplified URL for single-project sites
14
+ const singleProject = isSingleProject()
15
+ const to = singleProject
16
+ ? '/$lang/docs/$'
17
+ : siteConfig.hideLatestVersion
18
+ ? '/$lang/$project/docs/$'
19
+ : '/$lang/$project/$version/docs/$'
20
+
21
+ const params = singleProject
22
+ ? {
23
+ lang: siteConfig.defaultLocale,
24
+ _splat: project.defaultDocs || 'overview',
25
+ }
26
+ : siteConfig.hideLatestVersion
27
+ ? {
28
+ lang: siteConfig.defaultLocale,
29
+ project: project.id,
30
+ _splat: project.defaultDocs || 'overview',
31
+ }
32
+ : {
33
+ lang: siteConfig.defaultLocale,
34
+ project: project.id,
35
+ version: project.latestVersion,
36
+ _splat: project.defaultDocs || 'overview',
37
+ }
38
+
39
+ return (
40
+ <Link
41
+ to={to}
42
+ params={params as any}
43
+ className={twMerge(
44
+ // General
45
+ 'p-8 relative group z-0 min-h-[250px] xl:min-h-[220px] shadow-sm hover:shadow-none bg-white dark:bg-gray-900',
46
+
47
+ // Transition
48
+ 'transition-all duration-300 ease-out',
49
+
50
+ // Border
51
+ 'rounded-xl border border-gray-200 dark:border-gray-800 hover:border-current/50',
52
+
53
+ // Shadow / Glow (behind everything)
54
+ 'before:bg-current',
55
+ 'before:content-[""] before:absolute before:inset-0 before:blur-xl before:opacity-0 hover:before:opacity-20 before:transition-all before:duration-300 before:ease-out',
56
+
57
+ // Card Background (behind content, front of shadow)
58
+ 'after:absolute after:inset-0 after:-z-10 after:bg-white dark:after:bg-gray-900 after:backdrop-blur-sm after:rounded-xl',
59
+
60
+ // Transform
61
+ 'hover:-translate-y-1',
62
+ project.textColor,
63
+ )}
64
+ style={{
65
+ zIndex: index,
66
+ willChange: 'transform',
67
+ }}
68
+ >
69
+ {/* Background content that will blur on hover */}
70
+ <div className="z-0 relative group-hover:blur-[0.5px] transition-[filter] duration-300 ease-out">
71
+ <div className="flex gap-2 justify-between items-center">
72
+ <div
73
+ className={twMerge(
74
+ `flex items-center gap-2 text-[1.2rem] font-extrabold uppercase [letter-spacing:-.04em]`,
75
+ )}
76
+ >
77
+ <span
78
+ className={twMerge(
79
+ 'rounded-lg leading-none flex items-center',
80
+ 'bg-current',
81
+ )}
82
+ >
83
+ <span className="text-white dark:text-black text-xs leading-none p-1.5 px-2 uppercase">
84
+ {siteConfig.name}
85
+ </span>
86
+ </span>
87
+ <span className="text-current">{project.name}</span>
88
+ </div>
89
+ </div>
90
+ {project.tagline && (
91
+ <div
92
+ className={twMerge(
93
+ `text-sm italic font-medium mt-3`,
94
+ 'text-current',
95
+ )}
96
+ >
97
+ {project.tagline}
98
+ </div>
99
+ )}
100
+
101
+ {/* Description preview with ellipsis */}
102
+ {project.description && (
103
+ <div
104
+ className={`text-sm mt-3 text-gray-600 dark:text-gray-400 line-clamp-3 leading-relaxed`}
105
+ >
106
+ {project.description}
107
+ </div>
108
+ )}
109
+ </div>
110
+
111
+ {/* Foreground content that appears on hover */}
112
+ <div
113
+ className="absolute inset-0 z-30 bg-white/95 dark:bg-black/95 p-6 rounded-xl
114
+ backdrop-blur-sm flex flex-col justify-center opacity-0 group-hover:opacity-100
115
+ transition-opacity duration-300 ease-out pointer-events-none group-hover:pointer-events-auto"
116
+ >
117
+ <div
118
+ className={`text-sm text-gray-800 dark:text-gray-200 leading-relaxed`}
119
+ >
120
+ {project.description}
121
+ </div>
122
+ {project.frameworks && project.frameworks.length > 0 && (
123
+ <div className="flex flex-wrap gap-2 mt-4">
124
+ {project.frameworks.map((fw) => (
125
+ <span
126
+ key={fw}
127
+ className="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded-full text-gray-600 dark:text-gray-400"
128
+ >
129
+ {fw.charAt(0).toUpperCase() + fw.slice(1)}
130
+ </span>
131
+ ))}
132
+ </div>
133
+ )}
134
+ <div className="mt-6 text-center">
135
+ <span
136
+ className="inline-flex items-center gap-2 px-4 py-2 bg-black/5 dark:bg-white/10
137
+ rounded-full text-sm font-medium text-gray-900 dark:text-white"
138
+ >
139
+ Click to learn more
140
+ <svg
141
+ className="w-4 h-4 transform transition-transform duration-200 group-hover:translate-x-0.5"
142
+ fill="none"
143
+ viewBox="0 0 24 24"
144
+ stroke="currentColor"
145
+ >
146
+ <path
147
+ strokeLinecap="round"
148
+ strokeLinejoin="round"
149
+ strokeWidth={2}
150
+ d="M9 5l7 7-7 7"
151
+ />
152
+ </svg>
153
+ </span>
154
+ </div>
155
+ </div>
156
+ {/* Badge */}
157
+ {project.badge ? (
158
+ <div
159
+ className={twMerge(
160
+ `absolute -top-2 -right-2 z-40 px-2 py-1 rounded-md`,
161
+ 'bg-gradient-to-r',
162
+ project.colorFrom,
163
+ project.colorTo,
164
+ 'uppercase font-black italic text-xs',
165
+ 'text-white',
166
+ )}
167
+ style={{
168
+ animation: 'pulseScale 3s infinite',
169
+ animationTimingFunction: 'ease-in-out',
170
+ animationDelay: `${index * 0.5}s`,
171
+ }}
172
+ >
173
+ <span>{project.badge}</span>
174
+ </div>
175
+ ) : null}
176
+ </Link>
177
+ )
178
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react'
2
+ import { useNavigate, useParams } from '@tanstack/react-router'
3
+ import { Globe } from 'lucide-react'
4
+ import { siteConfig } from '~/site.config'
5
+ import { Select, type SelectOption } from './Select'
6
+
7
+ export function LocaleSwitcher() {
8
+ const params = useParams({ strict: false }) as {
9
+ lang?: string
10
+ _splat?: string
11
+ }
12
+ const navigate = useNavigate()
13
+ const currentLang = params.lang || siteConfig.defaultLocale
14
+
15
+ const localeOptions: SelectOption[] = React.useMemo(
16
+ () =>
17
+ Object.entries(siteConfig.supportedLocales).map(([code, name]) => ({
18
+ label: name,
19
+ value: code,
20
+ })),
21
+ [],
22
+ )
23
+
24
+ const handleSelect = React.useCallback(
25
+ (option: SelectOption) => {
26
+ const newLang = option.value
27
+ const pathname = window.location.pathname
28
+ const newPath = pathname.replace(/^\/[^/]+/, `/${newLang}`)
29
+ navigate({ to: newPath })
30
+ },
31
+ [navigate],
32
+ )
33
+
34
+ return (
35
+ <Select
36
+ className="w-full"
37
+ icon={<Globe className="w-3.5 h-3.5 opacity-60" />}
38
+ selected={currentLang}
39
+ available={localeOptions}
40
+ onSelect={handleSelect}
41
+ />
42
+ )
43
+ }