docs-i18n 0.6.3 → 0.7.0

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 +27 -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 +54 -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,141 @@
1
+ import * as React from 'react'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'icon'
5
+
6
+ type ButtonColor =
7
+ | 'blue'
8
+ | 'green'
9
+ | 'red'
10
+ | 'orange'
11
+ | 'purple'
12
+ | 'gray'
13
+ | 'emerald'
14
+ | 'cyan'
15
+ | 'yellow'
16
+
17
+ type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon-md'
18
+
19
+ type ButtonRounded = 'none' | 'md' | 'lg' | 'full'
20
+
21
+ type ButtonOwnProps<TElement extends React.ElementType = 'button'> = {
22
+ as?: TElement
23
+ children: React.ReactNode
24
+ variant?: ButtonVariant
25
+ color?: ButtonColor
26
+ size?: ButtonSize
27
+ rounded?: ButtonRounded
28
+ className?: string
29
+ }
30
+
31
+ type ButtonProps<TElement extends React.ElementType = 'button'> =
32
+ ButtonOwnProps<TElement> &
33
+ Omit<
34
+ React.ComponentPropsWithoutRef<TElement>,
35
+ keyof ButtonOwnProps<TElement>
36
+ >
37
+
38
+ type ButtonComponent = <TElement extends React.ElementType = 'button'>(
39
+ props: ButtonProps<TElement>,
40
+ ) => React.ReactNode
41
+
42
+ const primaryColorStyles: Record<ButtonColor, string> = {
43
+ blue: 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700',
44
+ green: 'bg-green-600 text-white border-green-600 hover:bg-green-700',
45
+ red: 'bg-red-600 text-white border-red-600 hover:bg-red-700',
46
+ orange: 'bg-orange-600 text-white border-orange-600 hover:bg-orange-700',
47
+ purple: 'bg-purple-600 text-white border-purple-600 hover:bg-purple-700',
48
+ gray: 'bg-gray-600 text-white border-gray-600 hover:bg-gray-700',
49
+ emerald: 'bg-emerald-500 text-white border-emerald-500 hover:bg-emerald-600',
50
+ cyan: 'bg-cyan-500 text-white border-cyan-500 hover:bg-cyan-600',
51
+ yellow: 'bg-yellow-400 text-black border-yellow-400 hover:bg-yellow-500',
52
+ }
53
+
54
+ const iconColorStyles: Record<ButtonColor, string> = {
55
+ blue: 'text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/30',
56
+ green: 'text-green-600 hover:bg-green-100 dark:hover:bg-green-900/30',
57
+ red: 'text-red-600 hover:bg-red-100 dark:hover:bg-red-900/30',
58
+ orange: 'text-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/30',
59
+ purple: 'text-purple-600 hover:bg-purple-100 dark:hover:bg-purple-900/30',
60
+ gray: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700',
61
+ emerald: 'text-emerald-600 hover:bg-emerald-100 dark:hover:bg-emerald-900/30',
62
+ cyan: 'text-cyan-600 hover:bg-cyan-100 dark:hover:bg-cyan-900/30',
63
+ yellow: 'text-yellow-600 hover:bg-yellow-100 dark:hover:bg-yellow-900/30',
64
+ }
65
+
66
+ const variantStyles: Record<ButtonVariant, string> = {
67
+ primary: 'border font-medium',
68
+ secondary:
69
+ 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent font-medium',
70
+ ghost:
71
+ 'border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 font-medium',
72
+ icon: 'border-transparent',
73
+ }
74
+
75
+ const sizeStyles: Record<ButtonSize, string> = {
76
+ xs: 'px-2 py-1.5 text-xs',
77
+ sm: 'px-3 py-1 text-sm',
78
+ md: 'px-4 py-2 text-sm',
79
+ lg: 'px-6 py-3 text-base',
80
+ 'icon-sm': 'p-1.5',
81
+ 'icon-md': 'p-2',
82
+ }
83
+
84
+ const roundedStyles: Record<ButtonRounded, string> = {
85
+ none: 'rounded-none',
86
+ md: 'rounded-md',
87
+ lg: 'rounded-lg',
88
+ full: 'rounded-full',
89
+ }
90
+
91
+ const baseStyles =
92
+ 'inline-flex items-center justify-center gap-2 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
93
+
94
+ function getDefaultSize(variant: ButtonVariant): ButtonSize {
95
+ if (variant === 'icon') return 'icon-md'
96
+ return 'md'
97
+ }
98
+
99
+ function getDefaultRounded(size: ButtonSize): ButtonRounded {
100
+ if (size === 'xs' || size === 'sm') return 'md'
101
+ if (size === 'icon-sm' || size === 'icon-md') return 'lg'
102
+ return 'lg'
103
+ }
104
+
105
+ export const Button: ButtonComponent = ({
106
+ as,
107
+ children,
108
+ variant = 'primary',
109
+ color = 'blue',
110
+ size,
111
+ rounded,
112
+ className,
113
+ ...props
114
+ }) => {
115
+ const Component = as || 'button'
116
+ const resolvedSize = size ?? getDefaultSize(variant)
117
+ const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)
118
+
119
+ const colorStyles =
120
+ variant === 'primary'
121
+ ? primaryColorStyles[color]
122
+ : variant === 'icon'
123
+ ? iconColorStyles[color]
124
+ : ''
125
+
126
+ return React.createElement(
127
+ Component,
128
+ {
129
+ className: twMerge(
130
+ baseStyles,
131
+ variantStyles[variant],
132
+ sizeStyles[resolvedSize],
133
+ roundedStyles[resolvedRounded],
134
+ colorStyles,
135
+ className,
136
+ ),
137
+ ...props,
138
+ },
139
+ children,
140
+ )
141
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from 'react'
2
+ import type { HTMLProps } from 'react'
3
+
4
+ export const InlineCode = React.memo(function InlineCode({
5
+ className,
6
+ ...rest
7
+ }: HTMLProps<HTMLElement>) {
8
+ return (
9
+ <span
10
+ className={`border border-gray-500/20 bg-gray-500/10 rounded px-1 py-0.5${
11
+ className ? ` ${className}` : ''
12
+ }`}
13
+ {...rest}
14
+ />
15
+ )
16
+ })
@@ -0,0 +1,21 @@
1
+ import * as React from 'react'
2
+ import type { HTMLProps } from 'react'
3
+
4
+ export const MarkdownImg = React.memo(function MarkdownImg({
5
+ alt,
6
+ src,
7
+ className,
8
+ children: _,
9
+ ...props
10
+ }: HTMLProps<HTMLImageElement>) {
11
+ return (
12
+ <img
13
+ {...props}
14
+ src={src}
15
+ alt={alt ?? ''}
16
+ className={`max-w-full h-auto rounded-lg shadow-md ${className ?? ''}`}
17
+ loading="lazy"
18
+ decoding="async"
19
+ />
20
+ )
21
+ })
@@ -0,0 +1,93 @@
1
+ export type Framework =
2
+ | 'angular'
3
+ | 'lit'
4
+ | 'preact'
5
+ | 'qwik'
6
+ | 'react'
7
+ | 'solid'
8
+ | 'svelte'
9
+ | 'vanilla'
10
+ | 'vue'
11
+
12
+ export type FrameworkOption = {
13
+ label: string
14
+ value: string
15
+ logo: string
16
+ color: string
17
+ fontColor: string
18
+ }
19
+
20
+ // Framework logo URLs — these are imported from the tanstack.com image assets.
21
+ // In production, replace these with actual SVG imports or CDN URLs.
22
+ // For now, we use placeholder paths that match the tanstack.com convention.
23
+ export const frameworkOptions: FrameworkOption[] = [
24
+ {
25
+ label: 'React',
26
+ value: 'react',
27
+ logo: '/images/frameworks/react-logo.svg',
28
+ color: 'bg-blue-500',
29
+ fontColor: 'text-sky-500',
30
+ },
31
+ {
32
+ label: 'Preact',
33
+ value: 'preact',
34
+ logo: '/images/frameworks/preact-logo.svg',
35
+ color: 'bg-purple-500',
36
+ fontColor: 'text-purple-500',
37
+ },
38
+ {
39
+ label: 'Vue',
40
+ value: 'vue',
41
+ logo: '/images/frameworks/vue-logo.svg',
42
+ color: 'bg-green-500',
43
+ fontColor: 'text-green-500',
44
+ },
45
+ {
46
+ label: 'Angular',
47
+ value: 'angular',
48
+ logo: '/images/frameworks/angular-logo.svg',
49
+ color: 'bg-red-500',
50
+ fontColor: 'text-fuchsia-500',
51
+ },
52
+ {
53
+ label: 'Solid',
54
+ value: 'solid',
55
+ logo: '/images/frameworks/solid-logo.svg',
56
+ color: 'bg-blue-600',
57
+ fontColor: 'text-blue-600',
58
+ },
59
+ {
60
+ label: 'Lit',
61
+ value: 'lit',
62
+ logo: '/images/frameworks/lit-logo.svg',
63
+ color: 'bg-emerald-500',
64
+ fontColor: 'text-emerald-500',
65
+ },
66
+ {
67
+ label: 'Svelte',
68
+ value: 'svelte',
69
+ logo: '/images/frameworks/svelte-logo.svg',
70
+ color: 'bg-orange-600',
71
+ fontColor: 'text-orange-600',
72
+ },
73
+ {
74
+ label: 'Qwik',
75
+ value: 'qwik',
76
+ logo: '/images/frameworks/qwik-logo.svg',
77
+ color: 'bg-indigo-500',
78
+ fontColor: 'text-indigo-500',
79
+ },
80
+ {
81
+ label: 'Vanilla',
82
+ value: 'vanilla',
83
+ logo: '/images/frameworks/js-logo.svg',
84
+ color: 'bg-yellow-500',
85
+ fontColor: 'text-yellow-500',
86
+ },
87
+ ]
88
+
89
+ export function getFrameworkOptions(frameworkStrs: Framework[]) {
90
+ return frameworkOptions.filter((d) =>
91
+ frameworkStrs.includes(d.value as Framework),
92
+ )
93
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react'
2
+
3
+ interface SearchContextType {
4
+ isOpen: boolean
5
+ openSearch: () => void
6
+ closeSearch: () => void
7
+ }
8
+
9
+ const SearchContext = React.createContext<SearchContextType | undefined>(
10
+ undefined,
11
+ )
12
+
13
+ export function SearchProvider({ children }: { children: React.ReactNode }) {
14
+ const [isOpen, setIsOpen] = React.useState(false)
15
+
16
+ const value = React.useMemo(
17
+ () => ({
18
+ isOpen,
19
+ openSearch: () => setIsOpen(true),
20
+ closeSearch: () => setIsOpen(false),
21
+ }),
22
+ [isOpen],
23
+ )
24
+
25
+ return (
26
+ <SearchContext.Provider value={value}>{children}</SearchContext.Provider>
27
+ )
28
+ }
29
+
30
+ export function useSearchContext() {
31
+ const context = React.useContext(SearchContext)
32
+ if (context === undefined) {
33
+ throw new Error('useSearch must be used within a SearchProvider')
34
+ }
35
+ return context
36
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Database connection factory for D1.
3
+ *
4
+ * Usage:
5
+ * import { createDb } from '~/db'
6
+ * const db = createDb(env.DB)
7
+ */
8
+
9
+ import { drizzle } from 'drizzle-orm/d1'
10
+ import * as schema from './schema'
11
+
12
+ export function createDb(d1: D1Database) {
13
+ return drizzle(d1, { schema })
14
+ }
15
+
16
+ export type Db = ReturnType<typeof createDb>
17
+ export { schema }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Drizzle ORM schema for D1 database.
3
+ *
4
+ * Matches the existing SQLite schema from src/core/cache.ts:
5
+ * - sources: EN source texts keyed by MD5 hash
6
+ * - source_files: maps sources to file locations per version
7
+ * - translations: translated texts per language and key
8
+ *
9
+ * Plus a new table for production content serving:
10
+ * - content: full markdown documents for D1-based serving
11
+ */
12
+
13
+ import { sqliteTable, text, integer, primaryKey, index } from 'drizzle-orm/sqlite-core'
14
+
15
+ /**
16
+ * EN source texts keyed by MD5 hash.
17
+ * Matches: CREATE TABLE sources (key TEXT PRIMARY KEY, text TEXT, type TEXT)
18
+ */
19
+ export const sources = sqliteTable('sources', {
20
+ key: text('key').primaryKey(),
21
+ text: text('text').notNull(),
22
+ type: text('type').notNull().default('paragraph'),
23
+ })
24
+
25
+ /**
26
+ * Maps source keys to file locations per version.
27
+ * Matches: CREATE TABLE source_files (key TEXT, file TEXT, line INTEGER, version TEXT)
28
+ */
29
+ export const sourceFiles = sqliteTable(
30
+ 'source_files',
31
+ {
32
+ key: text('key').notNull(),
33
+ file: text('file').notNull(),
34
+ line: integer('line').notNull(),
35
+ version: text('version').notNull().default('latest'),
36
+ },
37
+ (table) => [
38
+ primaryKey({ columns: [table.version, table.key, table.file, table.line] }),
39
+ index('idx_source_files_version').on(table.version),
40
+ index('idx_source_files_file').on(table.version, table.file),
41
+ ],
42
+ )
43
+
44
+ /**
45
+ * Translated texts per language and key.
46
+ * Matches: CREATE TABLE translations (lang TEXT, key TEXT, value TEXT, created_at INTEGER, updated_at INTEGER)
47
+ */
48
+ export const translations = sqliteTable(
49
+ 'translations',
50
+ {
51
+ lang: text('lang').notNull(),
52
+ key: text('key').notNull(),
53
+ value: text('value').notNull(),
54
+ createdAt: integer('created_at'),
55
+ updatedAt: integer('updated_at'),
56
+ },
57
+ (table) => [
58
+ primaryKey({ columns: [table.lang, table.key] }),
59
+ index('idx_translations_lang').on(table.lang),
60
+ ],
61
+ )
62
+
63
+ /**
64
+ * Full markdown documents for production content serving via D1.
65
+ * This is new — not in the original cache.ts schema.
66
+ */
67
+ export const content = sqliteTable('content', {
68
+ path: text('path').primaryKey(),
69
+ body: text('body').notNull(),
70
+ project: text('project').notNull(),
71
+ version: text('version').notNull(),
72
+ lang: text('lang').notNull(),
73
+ updatedAt: integer('updated_at'),
74
+ })
@@ -0,0 +1,106 @@
1
+ import * as React from 'react'
2
+
3
+ type UseClickOutsideOptions = {
4
+ /** Whether the click-outside detection is active */
5
+ enabled: boolean
6
+ /** Callback when a click outside is detected */
7
+ onClickOutside: () => void
8
+ /** Whether to also close on Escape key press (default: true) */
9
+ closeOnEscape?: boolean
10
+ /** Additional refs to consider as "inside" */
11
+ additionalRefs?: React.RefObject<HTMLElement | null>[]
12
+ }
13
+
14
+ /**
15
+ * Hook to detect clicks outside of a referenced element.
16
+ * Returns a ref to attach to the element you want to detect clicks outside of.
17
+ */
18
+ export function useClickOutside<T extends HTMLElement = HTMLElement>({
19
+ enabled,
20
+ onClickOutside,
21
+ closeOnEscape = true,
22
+ additionalRefs = [],
23
+ }: UseClickOutsideOptions): React.RefObject<T | null> {
24
+ const ref = React.useRef<T>(null)
25
+ const touchStartRef = React.useRef<{
26
+ x: number
27
+ y: number
28
+ outside: boolean
29
+ } | null>(null)
30
+
31
+ React.useEffect(() => {
32
+ if (!enabled) return
33
+
34
+ const isInsideRefs = (target: Node) => {
35
+ if (ref.current?.contains(target)) return true
36
+ for (const additionalRef of additionalRefs) {
37
+ if (additionalRef.current?.contains(target)) return true
38
+ }
39
+ return false
40
+ }
41
+
42
+ // Mouse: handle on mousedown for immediate response
43
+ const handleMouseDown = (event: MouseEvent) => {
44
+ if ((event as any).sourceCapabilities?.firesTouchEvents) return
45
+
46
+ if (!isInsideRefs(event.target as Node)) {
47
+ onClickOutside()
48
+ }
49
+ }
50
+
51
+ // Touch: only close if tap started AND ended outside
52
+ const handleTouchStart = (event: TouchEvent) => {
53
+ const touch = event.touches[0]
54
+ if (touch) {
55
+ const target = event.target as Node
56
+ const startedOutside = !isInsideRefs(target)
57
+ touchStartRef.current = {
58
+ x: touch.clientX,
59
+ y: touch.clientY,
60
+ outside: startedOutside,
61
+ }
62
+ }
63
+ }
64
+
65
+ const handleTouchEnd = (event: TouchEvent) => {
66
+ const start = touchStartRef.current
67
+ touchStartRef.current = null
68
+
69
+ if (!start) return
70
+ if (!start.outside) return
71
+
72
+ const touch = event.changedTouches[0]
73
+ if (!touch) return
74
+
75
+ const dx = Math.abs(touch.clientX - start.x)
76
+ const dy = Math.abs(touch.clientY - start.y)
77
+ if (dx > 10 || dy > 10) return
78
+
79
+ onClickOutside()
80
+ }
81
+
82
+ const handleEscape = (event: KeyboardEvent) => {
83
+ if (event.key === 'Escape') {
84
+ onClickOutside()
85
+ }
86
+ }
87
+
88
+ document.addEventListener('mousedown', handleMouseDown)
89
+ document.addEventListener('touchstart', handleTouchStart, { passive: true })
90
+ document.addEventListener('touchend', handleTouchEnd)
91
+ if (closeOnEscape) {
92
+ document.addEventListener('keydown', handleEscape)
93
+ }
94
+
95
+ return () => {
96
+ document.removeEventListener('mousedown', handleMouseDown)
97
+ document.removeEventListener('touchstart', handleTouchStart)
98
+ document.removeEventListener('touchend', handleTouchEnd)
99
+ if (closeOnEscape) {
100
+ document.removeEventListener('keydown', handleEscape)
101
+ }
102
+ }
103
+ }, [enabled, onClickOutside, closeOnEscape, additionalRefs])
104
+
105
+ return ref
106
+ }