docs-i18n 0.6.2 → 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 -23
  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,24 @@
1
+ import * as React from 'react'
2
+
3
+ export function YouTubeIcon({
4
+ className,
5
+ width = '1em',
6
+ height = '1em',
7
+ ...props
8
+ }: React.SVGProps<SVGSVGElement>) {
9
+ return (
10
+ <svg
11
+ viewBox="0 0 461 461"
12
+ fill="currentColor"
13
+ width={width}
14
+ height={height}
15
+ role="img"
16
+ aria-hidden={props['aria-label'] ? undefined : true}
17
+ className={className}
18
+ {...props}
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ >
21
+ <path d="M365.257,67.393H95.744C42.866,67.393,0,110.259,0,163.137v134.728 c0,52.878,42.866,95.744,95.744,95.744h269.513c52.878,0,95.744-42.866,95.744-95.744V163.137 C461.001,110.259,418.135,67.393,365.257,67.393z M300.506,237.056l-126.06,60.123c-3.359,1.602-7.239-0.847-7.239-4.568V168.607 c0-3.774,3.982-6.22,7.348-4.514l126.06,63.881C304.363,229.873,304.298,235.248,300.506,237.056z" />
22
+ </svg>
23
+ )
24
+ }
@@ -0,0 +1,254 @@
1
+ import * as React from 'react'
2
+ import { twMerge } from 'tailwind-merge'
3
+ import { Copy } from 'lucide-react'
4
+ import type { Mermaid } from 'mermaid'
5
+ import { transformerNotationDiff } from '@shikijs/transformers'
6
+ import type { HighlighterGeneric } from 'shiki'
7
+ import { createHighlighter } from 'shiki'
8
+ import { Button } from '../ui/Button'
9
+
10
+ // Language aliases mapping
11
+ const LANG_ALIASES: Record<string, string> = {
12
+ ts: 'typescript',
13
+ js: 'javascript',
14
+ sh: 'bash',
15
+ shell: 'bash',
16
+ console: 'bash',
17
+ zsh: 'bash',
18
+ cmd: 'bash',
19
+ md: 'markdown',
20
+ txt: 'plaintext',
21
+ text: 'plaintext',
22
+ yml: 'yaml',
23
+ json5: 'jsonc',
24
+ eslintrc: 'jsonc',
25
+ }
26
+
27
+ // Lazy highlighter singleton
28
+ let highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null
29
+ let mermaidInstance: Mermaid | null = null
30
+ const genSvgMap = new Map<string, string>()
31
+ const failedLanguages = new Set<string>()
32
+
33
+ async function getHighlighter(language: string): Promise<{
34
+ highlighter: HighlighterGeneric<any, any>
35
+ effectiveLang: string
36
+ }> {
37
+ if (!highlighterPromise) {
38
+ highlighterPromise = createHighlighter({
39
+ themes: ['github-light', 'aurora-x'],
40
+ langs: [
41
+ 'typescript',
42
+ 'javascript',
43
+ 'tsx',
44
+ 'jsx',
45
+ 'bash',
46
+ 'json',
47
+ 'html',
48
+ 'css',
49
+ 'markdown',
50
+ 'plaintext',
51
+ 'toml',
52
+ 'yaml',
53
+ 'sql',
54
+ 'diff',
55
+ 'vue',
56
+ 'svelte',
57
+ 'scss',
58
+ 'jsonc',
59
+ 'vue-html',
60
+ 'angular-html',
61
+ 'angular-ts',
62
+ ],
63
+ })
64
+ }
65
+
66
+ const highlighter = await highlighterPromise
67
+ const normalizedLang = LANG_ALIASES[language] || language
68
+ const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
69
+
70
+ // Return plaintext for known failed languages
71
+ if (failedLanguages.has(langToLoad)) {
72
+ return { highlighter, effectiveLang: 'plaintext' }
73
+ }
74
+
75
+ // Load language if not already loaded
76
+ if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
77
+ try {
78
+ await highlighter.loadLanguage(langToLoad as any)
79
+ } catch {
80
+ console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
81
+ failedLanguages.add(langToLoad)
82
+ return { highlighter, effectiveLang: 'plaintext' }
83
+ }
84
+ }
85
+
86
+ return { highlighter, effectiveLang: langToLoad }
87
+ }
88
+
89
+ // Lazy load mermaid only when needed
90
+ async function getMermaid(): Promise<Mermaid> {
91
+ if (!mermaidInstance) {
92
+ const { default: mermaid } = await import('mermaid')
93
+ mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' })
94
+ mermaidInstance = mermaid
95
+ }
96
+ return mermaidInstance
97
+ }
98
+
99
+ function extractPreAttributes(html: string): {
100
+ class: string | null
101
+ style: string | null
102
+ } {
103
+ const match = html.match(/<pre\b([^>]*)>/i)
104
+ if (!match) {
105
+ return { class: null, style: null }
106
+ }
107
+
108
+ const attributes = match[1]
109
+
110
+ const classMatch = attributes.match(/\bclass\s*=\s*["']([^"']*)["']/i)
111
+ const styleMatch = attributes.match(/\bstyle\s*=\s*["']([^"']*)["']/i)
112
+
113
+ return {
114
+ class: classMatch ? classMatch[1] : null,
115
+ style: styleMatch ? styleMatch[1] : null,
116
+ }
117
+ }
118
+
119
+ export function CodeBlock({
120
+ isEmbedded,
121
+ showTypeCopyButton = true,
122
+ ...props
123
+ }: React.HTMLProps<HTMLPreElement> & {
124
+ isEmbedded?: boolean
125
+ showTypeCopyButton?: boolean
126
+ dataCodeTitle?: string
127
+ }) {
128
+ // Extract title from data-code-title attribute, handling both camelCase and kebab-case
129
+ const rawTitle = ((props as any)?.dataCodeTitle ||
130
+ (props as any)?.['data-code-title']) as string | undefined
131
+
132
+ // Filter out "undefined" strings, null, and empty strings
133
+ const title =
134
+ rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
135
+ ? rawTitle.trim()
136
+ : undefined
137
+
138
+ const childElement = props.children as
139
+ | undefined
140
+ | { props?: { className?: string; children?: string } }
141
+ const lang = childElement?.props?.className?.replace('language-', '')
142
+
143
+ const children = props.children as
144
+ | undefined
145
+ | {
146
+ props: {
147
+ children: string
148
+ }
149
+ }
150
+
151
+ const [copied, setCopied] = React.useState(false)
152
+ const ref = React.useRef<any>(null)
153
+
154
+ const code = children?.props.children
155
+
156
+ const [codeElement, setCodeElement] = React.useState(
157
+ <pre ref={ref} className={`shiki h-full github-light dark:aurora-x`}>
158
+ <code>{lang === 'mermaid' ? <svg /> : code}</code>
159
+ </pre>,
160
+ )
161
+
162
+ React[
163
+ typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect'
164
+ ](() => {
165
+ ;(async () => {
166
+ const themes = ['github-light', 'aurora-x']
167
+ const langStr = lang || 'plaintext'
168
+
169
+ const { highlighter, effectiveLang } = await getHighlighter(langStr)
170
+ // Trim trailing newlines to prevent empty lines at end of code block
171
+ const trimmedCode = (code || '').trimEnd()
172
+
173
+ const htmls = await Promise.all(
174
+ themes.map(async (theme) => {
175
+ const output = highlighter.codeToHtml(trimmedCode, {
176
+ lang: effectiveLang,
177
+ theme,
178
+ transformers: [transformerNotationDiff()],
179
+ })
180
+
181
+ if (lang === 'mermaid') {
182
+ const preAttributes = extractPreAttributes(output)
183
+ let svgHtml = genSvgMap.get(trimmedCode)
184
+ if (!svgHtml) {
185
+ const mermaid = await getMermaid()
186
+ const { svg } = await mermaid.render('foo', trimmedCode)
187
+ genSvgMap.set(trimmedCode, svg)
188
+ svgHtml = svg
189
+ }
190
+ return `<div class='${preAttributes.class} py-4 bg-neutral-50'>${svgHtml}</div>`
191
+ }
192
+
193
+ return output
194
+ }),
195
+ )
196
+
197
+ setCodeElement(
198
+ <div
199
+ className={twMerge(
200
+ isEmbedded ? 'h-full [&>pre]:h-full [&>pre]:rounded-none' : '',
201
+ )}
202
+ dangerouslySetInnerHTML={{ __html: htmls.join('') }}
203
+ ref={ref}
204
+ />,
205
+ )
206
+ })()
207
+ }, [code, lang])
208
+
209
+ return (
210
+ <div
211
+ className={twMerge(
212
+ 'codeblock w-full max-w-full relative not-prose border border-gray-500/20 rounded-md [&_pre]:rounded-md',
213
+ props.className,
214
+ )}
215
+ style={props.style}
216
+ >
217
+ {(title || showTypeCopyButton) && (
218
+ <div className="flex items-center justify-between px-4 py-2 bg-gray-50 dark:bg-gray-900">
219
+ <div className="text-xs text-gray-700 dark:text-gray-300">
220
+ {title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
221
+ </div>
222
+
223
+ <Button
224
+ variant="ghost"
225
+ size="xs"
226
+ className={twMerge('border-0 rounded-md transition-opacity')}
227
+ onClick={() => {
228
+ let copyContent =
229
+ typeof ref.current?.innerText === 'string'
230
+ ? ref.current.innerText
231
+ : ''
232
+
233
+ if (copyContent.endsWith('\n')) {
234
+ copyContent = copyContent.slice(0, -1)
235
+ }
236
+
237
+ navigator.clipboard.writeText(copyContent)
238
+ setCopied(true)
239
+ setTimeout(() => setCopied(false), 2000)
240
+ }}
241
+ aria-label="Copy code to clipboard"
242
+ >
243
+ {copied ? (
244
+ <span className="text-xs">Copied!</span>
245
+ ) : (
246
+ <Copy className="w-4 h-4" />
247
+ )}
248
+ </Button>
249
+ </div>
250
+ )}
251
+ {codeElement}
252
+ </div>
253
+ )
254
+ }
@@ -0,0 +1,58 @@
1
+ import * as React from 'react'
2
+
3
+ export type FileTabDefinition = {
4
+ slug: string
5
+ name: string
6
+ }
7
+
8
+ export type FileTabsProps = {
9
+ tabs: Array<FileTabDefinition>
10
+ children: Array<React.ReactNode> | React.ReactNode
11
+ }
12
+
13
+ export function FileTabs({ tabs, children }: FileTabsProps) {
14
+ const id = React.useId()
15
+ const childrenArray = React.Children.toArray(children)
16
+ const [activeSlug, setActiveSlug] = React.useState(tabs[0]?.slug ?? '')
17
+
18
+ if (tabs.length === 0) return null
19
+
20
+ return (
21
+ <div className="not-prose my-4">
22
+ <div className="flex items-center justify-start gap-0 overflow-x-auto overflow-y-hidden bg-gray-100 dark:bg-gray-900 border border-b-0 border-gray-500/20 rounded-t-md">
23
+ {tabs.map((tab) => (
24
+ <button
25
+ key={`${id}-${tab.slug}`}
26
+ type="button"
27
+ onClick={() => setActiveSlug(tab.slug)}
28
+ aria-label={tab.name}
29
+ title={tab.name}
30
+ className={`px-3 py-1.5 text-sm font-medium transition-colors border-b-2 -mb-[1px] ${
31
+ activeSlug === tab.slug
32
+ ? 'border-current text-current bg-white dark:bg-gray-950'
33
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-800'
34
+ }`}
35
+ >
36
+ {tab.name}
37
+ </button>
38
+ ))}
39
+ </div>
40
+ <div>
41
+ {childrenArray.map((child, index) => {
42
+ const tab = tabs[index]
43
+ if (!tab) return null
44
+ return (
45
+ <div
46
+ key={`${id}-${tab.slug}-panel`}
47
+ data-tab={tab.slug}
48
+ hidden={tab.slug !== activeSlug}
49
+ className="file-tabs-panel"
50
+ >
51
+ {child}
52
+ </div>
53
+ )
54
+ })}
55
+ </div>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,76 @@
1
+ import { useParams } from '@tanstack/react-router'
2
+ import { Tabs } from './Tabs'
3
+ import { Children, ReactNode } from 'react'
4
+
5
+ type CodeBlockMeta = {
6
+ title: string
7
+ code: string
8
+ language: string
9
+ }
10
+
11
+ type FrameworkContentProps = {
12
+ codeBlocksByFramework: Record<string, CodeBlockMeta[]>
13
+ availableFrameworks: string[]
14
+ /** Pre-rendered React children for each framework (from domToReact) */
15
+ panelsByFramework: Record<string, ReactNode>
16
+ }
17
+
18
+ // Simple localStorage-based framework preference (replaces useCurrentUserQuery auth dependency)
19
+ function useLocalFrameworkPreference(): string | undefined {
20
+ if (typeof document === 'undefined') return undefined
21
+ return localStorage.getItem('framework') || undefined
22
+ }
23
+
24
+ /**
25
+ * Renders content for the currently selected framework.
26
+ * - If no content for framework: shows nothing
27
+ * - If 1 code block: shows just the code block (minimal style)
28
+ * - If multiple code blocks: shows as file tabs
29
+ * - If no code blocks but has content: shows the content directly
30
+ */
31
+ export function FrameworkContent({
32
+ codeBlocksByFramework,
33
+ panelsByFramework,
34
+ }: FrameworkContentProps) {
35
+ const { framework: paramsFramework } = useParams({ strict: false })
36
+ const localFramework = useLocalFrameworkPreference()
37
+
38
+ const actualFramework = (paramsFramework ||
39
+ localFramework ||
40
+ 'react') as string
41
+
42
+ const normalizedFramework = actualFramework.toLowerCase()
43
+
44
+ // Find the framework's code blocks
45
+ const frameworkBlocks = codeBlocksByFramework[normalizedFramework] || []
46
+ const frameworkPanel = panelsByFramework[normalizedFramework]
47
+
48
+ // If no panel content at all for this framework, show nothing
49
+ if (!frameworkPanel) {
50
+ return null
51
+ }
52
+
53
+ // If no code blocks, just render the content directly
54
+ if (frameworkBlocks.length === 0) {
55
+ return <div className="framework-content">{frameworkPanel}</div>
56
+ }
57
+
58
+ // If 1 code block, render minimal style
59
+ if (frameworkBlocks.length === 1) {
60
+ return <div className="framework-content">{frameworkPanel}</div>
61
+ }
62
+
63
+ // Multiple code blocks - show as file tabs
64
+ const tabs = frameworkBlocks.map((block, index) => ({
65
+ slug: `file-${index}`,
66
+ name: block.title || 'Untitled',
67
+ }))
68
+
69
+ const childrenArray = Children.toArray(frameworkPanel)
70
+
71
+ return (
72
+ <div className="framework-content">
73
+ <Tabs tabs={tabs}>{childrenArray}</Tabs>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,216 @@
1
+ import type { HTMLProps } from 'react'
2
+ import * as React from 'react'
3
+ import { MarkdownLink } from './MarkdownLink'
4
+
5
+ import parse, {
6
+ attributesToProps,
7
+ domToReact,
8
+ Element,
9
+ HTMLReactParserOptions,
10
+ } from 'html-react-parser'
11
+
12
+ import { CodeBlock } from './CodeBlock'
13
+ import { handleTabsComponent } from './MarkdownTabsHandler'
14
+ import { handleFrameworkComponent } from './MarkdownFrameworkHandler'
15
+ import { InlineCode } from '../ui/InlineCode'
16
+ import { MarkdownImg } from '../ui/MarkdownImg'
17
+ import type { SiteConfig } from '~/types'
18
+
19
+ type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
20
+
21
+ const CustomHeading = ({
22
+ Comp,
23
+ id,
24
+ children,
25
+ ...props
26
+ }: HTMLProps<HTMLHeadingElement> & {
27
+ Comp: HeadingLevel
28
+ }) => {
29
+ // Convert children to array and strip any inner anchor (native 'a' or MarkdownLink)
30
+ const childrenArray = React.Children.toArray(children)
31
+ const sanitizedChildren = childrenArray.map((child) => {
32
+ if (
33
+ React.isValidElement(child) &&
34
+ (child.type === 'a' || child.type === MarkdownLink)
35
+ ) {
36
+ // replace anchor child with its own children so outer anchor remains the only link
37
+ return (child.props as { children?: React.ReactNode }).children ?? null
38
+ }
39
+ return child
40
+ })
41
+
42
+ const heading = (
43
+ <Comp id={id} {...props}>
44
+ {sanitizedChildren}
45
+ </Comp>
46
+ )
47
+
48
+ if (id) {
49
+ return (
50
+ <a
51
+ href={`#${id}`}
52
+ className={`anchor-heading *:scroll-my-20 *:lg:scroll-my-4`}
53
+ >
54
+ {heading}
55
+ </a>
56
+ )
57
+ }
58
+
59
+ return heading
60
+ }
61
+
62
+ const makeHeading =
63
+ (type: HeadingLevel) => (props: HTMLProps<HTMLHeadingElement>) => (
64
+ <CustomHeading
65
+ Comp={type}
66
+ {...props}
67
+ className={`${props.className ?? ''} block`}
68
+ />
69
+ )
70
+
71
+ const MarkdownIframe = React.memo(function MarkdownIframe(
72
+ props: HTMLProps<HTMLIFrameElement>,
73
+ ) {
74
+ return <iframe {...props} className="w-full" title="Embedded Content" />
75
+ })
76
+
77
+ const markdownComponents: Record<string, React.FC> = {
78
+ a: MarkdownLink,
79
+ pre: CodeBlock,
80
+ h1: makeHeading('h1'),
81
+ h2: makeHeading('h2'),
82
+ h3: makeHeading('h3'),
83
+ h4: makeHeading('h4'),
84
+ h5: makeHeading('h5'),
85
+ h6: makeHeading('h6'),
86
+ code: InlineCode,
87
+ iframe: MarkdownIframe,
88
+ img: MarkdownImg,
89
+ }
90
+
91
+ // Cache for lazily-loaded custom components keyed by component name
92
+ const lazyComponentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>()
93
+
94
+ function getLazyComponent(
95
+ name: string,
96
+ loader: () => Promise<{ default: React.ComponentType<any> }>,
97
+ ): React.LazyExoticComponent<React.ComponentType<any>> {
98
+ let cached = lazyComponentCache.get(name)
99
+ if (!cached) {
100
+ cached = React.lazy(loader)
101
+ lazyComponentCache.set(name, cached)
102
+ }
103
+ return cached
104
+ }
105
+
106
+ function createParserOptions(
107
+ customComponents?: SiteConfig['components'],
108
+ ): HTMLReactParserOptions {
109
+ const options: HTMLReactParserOptions = {
110
+ replace: (domNode) => {
111
+ if (domNode instanceof Element && domNode.attribs) {
112
+ if (domNode.name === 'md-comment-component') {
113
+ const componentName = domNode.attribs['data-component']
114
+ const rawAttributes = domNode.attribs['data-attributes']
115
+ const attributes: Record<string, any> = {}
116
+ try {
117
+ Object.assign(attributes, JSON.parse(rawAttributes))
118
+ } catch {
119
+ // ignore JSON parse errors and fall back to empty props
120
+ }
121
+
122
+ switch (componentName?.toLowerCase()) {
123
+ case 'tabs':
124
+ return handleTabsComponent(domNode, attributes, options)
125
+ case 'framework':
126
+ return handleFrameworkComponent(domNode, attributes, options)
127
+ default: {
128
+ // Check custom components for md-comment-component with unknown data-component
129
+ if (componentName && customComponents?.[componentName]) {
130
+ const LazyComp = getLazyComponent(componentName, customComponents[componentName])
131
+ return (
132
+ <React.Suspense fallback={<div />}>
133
+ <LazyComp {...attributes}>
134
+ {domToReact(domNode.children as any, options)}
135
+ </LazyComp>
136
+ </React.Suspense>
137
+ )
138
+ }
139
+ return <div>{domToReact(domNode.children as any, options)}</div>
140
+ }
141
+ }
142
+ }
143
+
144
+ // Check if an unknown HTML element matches a custom component
145
+ if (customComponents?.[domNode.name]) {
146
+ const LazyComp = getLazyComponent(domNode.name, customComponents[domNode.name])
147
+ return (
148
+ <React.Suspense fallback={<div />}>
149
+ <LazyComp {...attributesToProps(domNode.attribs)}>
150
+ {domToReact(domNode.children as any, options)}
151
+ </LazyComp>
152
+ </React.Suspense>
153
+ )
154
+ }
155
+
156
+ const replacer = markdownComponents[domNode.name]
157
+ if (replacer) {
158
+ return React.createElement(
159
+ replacer,
160
+ attributesToProps(domNode.attribs),
161
+ domToReact(domNode.children as any, options),
162
+ )
163
+ }
164
+ }
165
+
166
+ return
167
+ },
168
+ }
169
+ return options
170
+ }
171
+
172
+ // Default options (no custom components) for backward compatibility
173
+ const defaultOptions = createParserOptions()
174
+
175
+ type MarkdownProps = {
176
+ /** Raw markdown content -- will be rendered to HTML if provided */
177
+ rawContent?: string
178
+ /** Pre-rendered HTML markup. If not provided, rawContent will be used. */
179
+ htmlMarkup?: string
180
+ /** Optional render function for raw markdown to HTML. Must be provided if rawContent is used. */
181
+ renderMarkdown?: (raw: string) => { markup: string; headings: any[] }
182
+ /** Custom components from SiteConfig for lazy-loading unknown elements */
183
+ customComponents?: SiteConfig['components']
184
+ }
185
+
186
+ export const Markdown = React.memo(function Markdown({
187
+ rawContent,
188
+ htmlMarkup,
189
+ renderMarkdown,
190
+ customComponents,
191
+ }: MarkdownProps) {
192
+ const rendered = React.useMemo(() => {
193
+ if (rawContent && renderMarkdown) {
194
+ return renderMarkdown(rawContent)
195
+ }
196
+
197
+ if (htmlMarkup) {
198
+ return { markup: htmlMarkup, headings: [] }
199
+ }
200
+
201
+ return { markup: '', headings: [] }
202
+ }, [rawContent, htmlMarkup, renderMarkdown])
203
+
204
+ const options = React.useMemo(
205
+ () => (customComponents ? createParserOptions(customComponents) : defaultOptions),
206
+ [customComponents],
207
+ )
208
+
209
+ return React.useMemo(() => {
210
+ if (!rendered.markup) {
211
+ return null
212
+ }
213
+
214
+ return parse(rendered.markup, options)
215
+ }, [rendered.markup, options])
216
+ })