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,77 @@
1
+ export interface SiteConfig {
2
+ name: string
3
+ description: string
4
+ repo: string
5
+ defaultLocale: string
6
+ supportedLocales: Record<string, string>
7
+ defaultProject?: string
8
+ projects: ProjectConfig[]
9
+ features: {
10
+ frameworkSelector: boolean
11
+ versionSelector: boolean
12
+ editOnGithub: boolean
13
+ }
14
+ /** When true and viewing the latest version, omit the version segment from URLs */
15
+ hideLatestVersion?: boolean
16
+ components?: Record<string, () => Promise<{ default: React.ComponentType<any> }>>
17
+ }
18
+
19
+ export interface ProjectConfig {
20
+ id: string
21
+ name: string
22
+ type: 'doc' | 'blog'
23
+ repo: string
24
+ latestVersion: string
25
+ latestBranch: string
26
+ availableVersions: string[]
27
+ docsRoot?: string
28
+ defaultDocs?: string
29
+ frameworks?: string[]
30
+ colorFrom: string
31
+ colorTo: string
32
+ textColor?: string
33
+ bgStyle?: string
34
+ borderStyle?: string
35
+ textStyle?: string
36
+ badge?: string
37
+ tagline?: string
38
+ description?: string
39
+ }
40
+
41
+ export interface MarkdownHeading {
42
+ id: string
43
+ level: number
44
+ text: string
45
+ framework?: string
46
+ }
47
+
48
+ export interface DocsConfig {
49
+ sections: DocsConfigSection[]
50
+ }
51
+
52
+ export interface DocsConfigSection {
53
+ label: string
54
+ children: DocsConfigItem[]
55
+ collapsible?: boolean
56
+ defaultCollapsed?: boolean
57
+ frameworks?: DocsConfigFramework[]
58
+ }
59
+
60
+ export interface DocsConfigItem {
61
+ label: string
62
+ to: string
63
+ badge?: string
64
+ }
65
+
66
+ export interface DocsConfigFramework {
67
+ label: string
68
+ children: DocsConfigItem[]
69
+ }
70
+
71
+ export interface LoadedDoc {
72
+ content: string
73
+ meta: Record<string, any>
74
+ locale: string
75
+ isFallback: boolean
76
+ filePath: string
77
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Server-side blog content loading functions.
3
+ *
4
+ * Blog posts are stored as markdown files in content/blog/{lang}/{slug}.md
5
+ * with frontmatter: title, published, excerpt, authors, draft
6
+ *
7
+ * Uses the same filesystem loading pattern as docs.server.ts,
8
+ * with i18n fallback to English when a translation is missing.
9
+ */
10
+
11
+ import { readFileSync, existsSync, readdirSync } from 'node:fs'
12
+ import { resolve, join } from 'node:path'
13
+ import matter from 'gray-matter'
14
+
15
+ // Re-export shared types and utilities from the non-server module
16
+ export type { BlogPostMeta, LoadedBlogPost } from './blog'
17
+ export { formatAuthors } from './blog'
18
+
19
+ import type { BlogPostMeta, LoadedBlogPost } from './blog'
20
+
21
+ /**
22
+ * Get the project root directory.
23
+ * Same resolution as docs.server.ts.
24
+ */
25
+ function getProjectRoot(): string {
26
+ if (process.env.DOCS_I18N_PROJECT_ROOT) {
27
+ return process.env.DOCS_I18N_PROJECT_ROOT
28
+ }
29
+ // Prefer cwd if it has a content directory (avoids stale temp file from another project)
30
+ const cwd = process.cwd()
31
+ if (existsSync(resolve(cwd, 'content'))) {
32
+ return cwd
33
+ }
34
+ try {
35
+ const tmpDir = process.env.TMPDIR || process.env.TEMP || '/tmp'
36
+ return readFileSync(resolve(tmpDir, 'docs-i18n-project-root'), 'utf-8').trim()
37
+ } catch {
38
+ // ignore
39
+ }
40
+ return cwd
41
+ }
42
+
43
+ // formatAuthors is re-exported from ./blog above
44
+
45
+ /**
46
+ * Load all published blog posts for a given locale, sorted newest first.
47
+ *
48
+ * Resolution:
49
+ * 1. Scan content/blog/en/ for all .md files (source of truth for available posts)
50
+ * 2. For each post, try content/blog/{lang}/{slug}.md first, fallback to en
51
+ * 3. Filter out drafts and future-dated posts
52
+ * 4. Sort by published date descending
53
+ */
54
+ export function loadBlogPosts(lang: string): BlogPostMeta[] {
55
+ const root = getProjectRoot()
56
+ const enDir = resolve(root, 'content', 'blog', 'en')
57
+
58
+ if (!existsSync(enDir)) {
59
+ return []
60
+ }
61
+
62
+ const now = new Date()
63
+ const posts: BlogPostMeta[] = []
64
+
65
+ try {
66
+ const files = readdirSync(enDir).filter((f) => f.endsWith('.md'))
67
+
68
+ for (const file of files) {
69
+ const slug = file.replace(/\.md$/, '')
70
+ const meta = loadPostMeta(root, lang, slug)
71
+
72
+ if (!meta) continue
73
+ // Filter drafts and future posts
74
+ if (meta.draft) continue
75
+ if (new Date(meta.published) > now) continue
76
+
77
+ posts.push(meta)
78
+ }
79
+ } catch {
80
+ // ignore read errors
81
+ }
82
+
83
+ // Sort newest first
84
+ posts.sort(
85
+ (a, b) =>
86
+ new Date(b.published).getTime() - new Date(a.published).getTime(),
87
+ )
88
+
89
+ return posts
90
+ }
91
+
92
+ /**
93
+ * Load a single blog post by slug with i18n fallback.
94
+ *
95
+ * Resolution:
96
+ * 1. content/blog/{lang}/{slug}.md
97
+ * 2. content/blog/en/{slug}.md (fallback, isFallback = true)
98
+ */
99
+ export function loadBlogPost(lang: string, slug: string): LoadedBlogPost | null {
100
+ const root = getProjectRoot()
101
+
102
+ // Try requested language
103
+ const langPath = resolve(root, 'content', 'blog', lang, `${slug}.md`)
104
+ if (existsSync(langPath)) {
105
+ return parsePostFile(langPath, slug, lang, false)
106
+ }
107
+
108
+ // Fallback to English
109
+ if (lang !== 'en') {
110
+ const enPath = resolve(root, 'content', 'blog', 'en', `${slug}.md`)
111
+ if (existsSync(enPath)) {
112
+ return parsePostFile(enPath, slug, 'en', true)
113
+ }
114
+ }
115
+
116
+ return null
117
+ }
118
+
119
+ /**
120
+ * Load just the metadata for a blog post (used for listing).
121
+ */
122
+ function loadPostMeta(
123
+ root: string,
124
+ lang: string,
125
+ slug: string,
126
+ ): BlogPostMeta | null {
127
+ // Try requested language
128
+ const langPath = resolve(root, 'content', 'blog', lang, `${slug}.md`)
129
+ if (existsSync(langPath)) {
130
+ return parsePostMeta(langPath, slug)
131
+ }
132
+
133
+ // Fallback to English
134
+ if (lang !== 'en') {
135
+ const enPath = resolve(root, 'content', 'blog', 'en', `${slug}.md`)
136
+ if (existsSync(enPath)) {
137
+ return parsePostMeta(enPath, slug)
138
+ }
139
+ }
140
+
141
+ return null
142
+ }
143
+
144
+ function parsePostMeta(filePath: string, slug: string): BlogPostMeta | null {
145
+ try {
146
+ const raw = readFileSync(filePath, 'utf-8')
147
+ const { data, content } = matter(raw)
148
+
149
+ // Extract header image from first markdown image
150
+ const headerImageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/)
151
+ const headerImage = headerImageMatch ? headerImageMatch[2] : undefined
152
+
153
+ return {
154
+ slug,
155
+ title: (data.title as string) || slug,
156
+ published: (data.published as string) || '',
157
+ excerpt: (data.excerpt as string) || '',
158
+ authors: Array.isArray(data.authors) ? data.authors : [],
159
+ draft: data.draft === true,
160
+ headerImage,
161
+ }
162
+ } catch {
163
+ return null
164
+ }
165
+ }
166
+
167
+ function parsePostFile(
168
+ filePath: string,
169
+ slug: string,
170
+ locale: string,
171
+ isFallback: boolean,
172
+ ): LoadedBlogPost {
173
+ const raw = readFileSync(filePath, 'utf-8')
174
+ const { data, content } = matter(raw)
175
+
176
+ // Extract header image from first markdown image
177
+ const headerImageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/)
178
+ const headerImage = headerImageMatch ? headerImageMatch[2] : undefined
179
+
180
+ return {
181
+ slug,
182
+ title: (data.title as string) || slug,
183
+ published: (data.published as string) || '',
184
+ excerpt: (data.excerpt as string) || '',
185
+ authors: Array.isArray(data.authors) ? data.authors : [],
186
+ draft: data.draft === true,
187
+ headerImage,
188
+ content,
189
+ locale,
190
+ isFallback,
191
+ filePath,
192
+ }
193
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared blog utilities — safe for both client and server.
3
+ */
4
+
5
+ /**
6
+ * Intl-based author list formatter.
7
+ */
8
+ const listJoiner = new Intl.ListFormat('en-US', {
9
+ style: 'long',
10
+ type: 'conjunction',
11
+ })
12
+
13
+ export function formatAuthors(authors: string[]): string {
14
+ if (!authors.length) return 'Unknown'
15
+ return listJoiner.format(authors)
16
+ }
17
+
18
+ /** Metadata parsed from blog post frontmatter */
19
+ export interface BlogPostMeta {
20
+ slug: string
21
+ title: string
22
+ published: string
23
+ excerpt: string
24
+ authors: string[]
25
+ draft?: boolean
26
+ headerImage?: string
27
+ }
28
+
29
+ /** A fully loaded blog post with content */
30
+ export interface LoadedBlogPost {
31
+ slug: string
32
+ title: string
33
+ published: string
34
+ excerpt: string
35
+ authors: string[]
36
+ draft?: boolean
37
+ headerImage?: string
38
+ content: string
39
+ locale: string
40
+ isFallback: boolean
41
+ filePath: string
42
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Config utilities for parsing and validating docs config.json files.
3
+ *
4
+ * Follows the same schema as tanstack.com's config.json:
5
+ * - sections: array of sidebar sections
6
+ * - Each section has label, children, optional frameworks, collapsible, defaultCollapsed
7
+ * - Children have label, to, optional badge
8
+ */
9
+
10
+ import type { DocsConfig, DocsConfigSection, DocsConfigItem, DocsConfigFramework } from '~/types'
11
+
12
+ /**
13
+ * Validate and normalize a raw config.json object into a typed DocsConfig.
14
+ * Returns null if the input is invalid.
15
+ */
16
+ export function parseDocsConfig(raw: unknown): DocsConfig | null {
17
+ if (!raw || typeof raw !== 'object') return null
18
+
19
+ const obj = raw as Record<string, unknown>
20
+ if (!Array.isArray(obj.sections)) return null
21
+
22
+ const sections: DocsConfigSection[] = []
23
+
24
+ for (const section of obj.sections) {
25
+ if (!section || typeof section !== 'object') continue
26
+ const s = section as Record<string, unknown>
27
+
28
+ if (typeof s.label !== 'string') continue
29
+ if (!Array.isArray(s.children)) continue
30
+
31
+ const children = parseConfigItems(s.children)
32
+ const frameworks = s.frameworks ? parseFrameworks(s.frameworks) : undefined
33
+
34
+ sections.push({
35
+ label: s.label,
36
+ children,
37
+ collapsible: typeof s.collapsible === 'boolean' ? s.collapsible : undefined,
38
+ defaultCollapsed: typeof s.defaultCollapsed === 'boolean' ? s.defaultCollapsed : undefined,
39
+ frameworks,
40
+ })
41
+ }
42
+
43
+ return { sections }
44
+ }
45
+
46
+ function parseConfigItems(items: unknown[]): DocsConfigItem[] {
47
+ const result: DocsConfigItem[] = []
48
+
49
+ for (const item of items) {
50
+ if (!item || typeof item !== 'object') continue
51
+ const i = item as Record<string, unknown>
52
+
53
+ if (typeof i.label !== 'string' || typeof i.to !== 'string') continue
54
+
55
+ result.push({
56
+ label: i.label,
57
+ to: i.to,
58
+ badge: typeof i.badge === 'string' ? i.badge : undefined,
59
+ })
60
+ }
61
+
62
+ return result
63
+ }
64
+
65
+ function parseFrameworks(frameworks: unknown): DocsConfigFramework[] | undefined {
66
+ if (!Array.isArray(frameworks)) return undefined
67
+
68
+ const result: DocsConfigFramework[] = []
69
+
70
+ for (const fw of frameworks) {
71
+ if (!fw || typeof fw !== 'object') continue
72
+ const f = fw as Record<string, unknown>
73
+
74
+ if (typeof f.label !== 'string') continue
75
+ if (!Array.isArray(f.children)) continue
76
+
77
+ result.push({
78
+ label: f.label,
79
+ children: parseConfigItems(f.children),
80
+ })
81
+ }
82
+
83
+ return result.length > 0 ? result : undefined
84
+ }
85
+
86
+ /**
87
+ * Build sidebar menu items from a DocsConfig, resolving paths
88
+ * relative to the current route.
89
+ */
90
+ export function buildSidebarItems(
91
+ config: DocsConfig,
92
+ basePath: string,
93
+ framework?: string,
94
+ ) {
95
+ return config.sections.map((section) => {
96
+ const children = section.children.map((item) => ({
97
+ ...item,
98
+ to: `${basePath}/${item.to}`,
99
+ }))
100
+
101
+ // If framework is specified, also include framework-specific items
102
+ let frameworkChildren: DocsConfigItem[] = []
103
+ if (framework && section.frameworks) {
104
+ const fwSection = section.frameworks.find(
105
+ (fw) => fw.label.toLowerCase() === framework.toLowerCase(),
106
+ )
107
+ if (fwSection) {
108
+ frameworkChildren = fwSection.children.map((item) => ({
109
+ ...item,
110
+ to: `${basePath}/framework/${framework}/${item.to}`,
111
+ }))
112
+ }
113
+ }
114
+
115
+ return {
116
+ ...section,
117
+ children: [...children, ...frameworkChildren],
118
+ }
119
+ })
120
+ }