boltdocs 1.10.2 → 1.11.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 (225) hide show
  1. package/package.json +29 -7
  2. package/src/client/app/config-context.tsx +18 -0
  3. package/src/client/app/docs-layout.tsx +14 -0
  4. package/src/client/app/index.tsx +132 -260
  5. package/src/client/app/mdx-component.tsx +52 -0
  6. package/src/client/app/mdx-components-context.tsx +23 -0
  7. package/src/client/app/mdx-page.tsx +20 -0
  8. package/src/client/app/preload.tsx +38 -30
  9. package/src/client/app/router.tsx +30 -0
  10. package/src/client/app/scroll-handler.tsx +40 -0
  11. package/src/client/app/theme-context.tsx +75 -0
  12. package/src/client/components/default-layout.tsx +80 -0
  13. package/src/client/components/docs-layout.tsx +105 -0
  14. package/src/client/components/icons-dev.tsx +74 -0
  15. package/src/client/components/mdx/admonition.tsx +107 -0
  16. package/src/client/components/mdx/badge.tsx +41 -0
  17. package/src/client/components/mdx/button.tsx +35 -0
  18. package/src/client/components/mdx/card.tsx +124 -0
  19. package/src/client/components/mdx/code-block.tsx +119 -0
  20. package/src/client/components/mdx/component-preview.tsx +47 -0
  21. package/src/client/components/mdx/component-props.tsx +83 -0
  22. package/src/client/components/mdx/field.tsx +66 -0
  23. package/src/client/components/mdx/file-tree.tsx +287 -0
  24. package/src/client/components/mdx/hooks/use-code-block.ts +56 -0
  25. package/src/client/components/mdx/hooks/use-component-preview.ts +16 -0
  26. package/src/client/components/mdx/hooks/useTable.ts +74 -0
  27. package/src/client/components/mdx/hooks/useTabs.ts +68 -0
  28. package/src/client/components/mdx/image.tsx +23 -0
  29. package/src/client/components/mdx/index.ts +53 -0
  30. package/src/client/components/mdx/link.tsx +38 -0
  31. package/src/client/components/mdx/list.tsx +192 -0
  32. package/src/client/components/mdx/table.tsx +156 -0
  33. package/src/client/components/mdx/tabs.tsx +135 -0
  34. package/src/client/components/mdx/video.tsx +68 -0
  35. package/src/client/components/primitives/breadcrumbs.tsx +79 -0
  36. package/src/client/components/primitives/button-group.tsx +54 -0
  37. package/src/client/components/primitives/button.tsx +145 -0
  38. package/src/client/components/primitives/helpers/observer.ts +120 -0
  39. package/src/client/components/primitives/index.ts +17 -0
  40. package/src/client/components/primitives/link.tsx +122 -0
  41. package/src/client/components/primitives/menu.tsx +159 -0
  42. package/src/client/components/primitives/navbar.tsx +359 -0
  43. package/src/client/components/primitives/navigation-menu.tsx +116 -0
  44. package/src/client/components/primitives/on-this-page.tsx +461 -0
  45. package/src/client/components/primitives/page-nav.tsx +87 -0
  46. package/src/client/components/primitives/popover.tsx +47 -0
  47. package/src/client/components/primitives/search-dialog.tsx +183 -0
  48. package/src/client/components/primitives/sidebar.tsx +154 -0
  49. package/src/client/components/primitives/tabs.tsx +90 -0
  50. package/src/client/components/primitives/tooltip.tsx +83 -0
  51. package/src/client/components/primitives/types.ts +11 -0
  52. package/src/client/components/ui-base/breadcrumbs.tsx +42 -0
  53. package/src/client/components/ui-base/copy-markdown.tsx +112 -0
  54. package/src/client/components/ui-base/error-boundary.tsx +52 -0
  55. package/src/client/components/ui-base/github-stars.tsx +27 -0
  56. package/src/client/components/ui-base/head.tsx +69 -0
  57. package/src/client/components/ui-base/loading.tsx +87 -0
  58. package/src/client/components/ui-base/navbar.tsx +138 -0
  59. package/src/client/components/ui-base/not-found.tsx +24 -0
  60. package/src/client/components/ui-base/on-this-page.tsx +152 -0
  61. package/src/client/components/ui-base/page-nav.tsx +39 -0
  62. package/src/client/components/ui-base/powered-by.tsx +19 -0
  63. package/src/client/components/ui-base/progress-bar.tsx +67 -0
  64. package/src/client/components/ui-base/search-dialog.tsx +82 -0
  65. package/src/client/components/ui-base/sidebar.tsx +104 -0
  66. package/src/client/components/ui-base/tabs.tsx +65 -0
  67. package/src/client/components/ui-base/theme-toggle.tsx +32 -0
  68. package/src/client/hooks/index.ts +12 -0
  69. package/src/client/hooks/use-breadcrumbs.ts +22 -0
  70. package/src/client/hooks/use-i18n.ts +84 -0
  71. package/src/client/hooks/use-localized-to.ts +95 -0
  72. package/src/client/hooks/use-location.ts +5 -0
  73. package/src/client/hooks/use-navbar.ts +60 -0
  74. package/src/client/hooks/use-onthispage.ts +23 -0
  75. package/src/client/hooks/use-page-nav.ts +22 -0
  76. package/src/client/hooks/use-routes.ts +72 -0
  77. package/src/client/hooks/use-search.ts +71 -0
  78. package/src/client/hooks/use-sidebar.ts +49 -0
  79. package/src/client/hooks/use-tabs.ts +43 -0
  80. package/src/client/hooks/use-version.ts +78 -0
  81. package/src/client/index.ts +55 -17
  82. package/src/client/integrations/codesandbox.ts +179 -0
  83. package/src/client/ssr.tsx +27 -16
  84. package/src/client/theme/neutral.css +360 -0
  85. package/src/client/types.ts +131 -27
  86. package/src/client/utils/cn.ts +6 -0
  87. package/src/client/utils/copy-clipboard.ts +22 -0
  88. package/src/client/utils/get-base-file-path.ts +21 -0
  89. package/src/client/utils/github.ts +121 -0
  90. package/src/client/utils/use-on-change.ts +15 -0
  91. package/src/client/virtual.d.ts +24 -0
  92. package/src/node/cache.ts +156 -156
  93. package/src/node/config.ts +159 -103
  94. package/src/node/index.ts +13 -13
  95. package/src/node/mdx.ts +213 -61
  96. package/src/node/plugin/entry.ts +29 -18
  97. package/src/node/plugin/html.ts +11 -11
  98. package/src/node/plugin/index.ts +161 -83
  99. package/src/node/plugin/types.ts +2 -4
  100. package/src/node/routes/cache.ts +6 -6
  101. package/src/node/routes/index.ts +206 -113
  102. package/src/node/routes/parser.ts +106 -81
  103. package/src/node/routes/sorter.ts +15 -15
  104. package/src/node/routes/types.ts +24 -24
  105. package/src/node/ssg/index.ts +46 -46
  106. package/src/node/ssg/meta.ts +4 -4
  107. package/src/node/ssg/options.ts +5 -5
  108. package/src/node/ssg/sitemap.ts +14 -14
  109. package/src/node/utils.ts +31 -31
  110. package/tsconfig.json +25 -20
  111. package/tsup.config.ts +23 -14
  112. package/dist/PackageManagerTabs-NVT7G625.mjs +0 -99
  113. package/dist/SearchDialog-AGVF6JBO.mjs +0 -194
  114. package/dist/SearchDialog-YPDOM7Q6.css +0 -2847
  115. package/dist/Video-KNTY5BNO.mjs +0 -6
  116. package/dist/cache-KNL5B4EE.mjs +0 -12
  117. package/dist/chunk-7SFUJWTB.mjs +0 -211
  118. package/dist/chunk-FFBNU6IJ.mjs +0 -386
  119. package/dist/chunk-FMTOYQLO.mjs +0 -37
  120. package/dist/chunk-TKLQWU7H.mjs +0 -1920
  121. package/dist/chunk-Z7JHYNAS.mjs +0 -57
  122. package/dist/client/index.css +0 -2847
  123. package/dist/client/index.d.mts +0 -372
  124. package/dist/client/index.d.ts +0 -372
  125. package/dist/client/index.js +0 -3630
  126. package/dist/client/index.mjs +0 -697
  127. package/dist/client/ssr.css +0 -2847
  128. package/dist/client/ssr.d.mts +0 -27
  129. package/dist/client/ssr.d.ts +0 -27
  130. package/dist/client/ssr.js +0 -2928
  131. package/dist/client/ssr.mjs +0 -33
  132. package/dist/config-BsFQ-ErD.d.mts +0 -159
  133. package/dist/config-BsFQ-ErD.d.ts +0 -159
  134. package/dist/node/index.d.mts +0 -91
  135. package/dist/node/index.d.ts +0 -91
  136. package/dist/node/index.js +0 -1187
  137. package/dist/node/index.mjs +0 -762
  138. package/dist/types-Dj-bfnC3.d.mts +0 -74
  139. package/dist/types-Dj-bfnC3.d.ts +0 -74
  140. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -61
  141. package/src/client/theme/components/CodeBlock/index.ts +0 -1
  142. package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +0 -131
  143. package/src/client/theme/components/PackageManagerTabs/index.ts +0 -1
  144. package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +0 -64
  145. package/src/client/theme/components/Playground/Playground.tsx +0 -180
  146. package/src/client/theme/components/Playground/index.ts +0 -1
  147. package/src/client/theme/components/Playground/playground.css +0 -238
  148. package/src/client/theme/components/Video/Video.tsx +0 -84
  149. package/src/client/theme/components/Video/index.ts +0 -1
  150. package/src/client/theme/components/Video/video.css +0 -41
  151. package/src/client/theme/components/mdx/Admonition.tsx +0 -80
  152. package/src/client/theme/components/mdx/Badge.tsx +0 -31
  153. package/src/client/theme/components/mdx/Button.tsx +0 -50
  154. package/src/client/theme/components/mdx/Card.tsx +0 -80
  155. package/src/client/theme/components/mdx/Field.tsx +0 -60
  156. package/src/client/theme/components/mdx/FileTree.tsx +0 -229
  157. package/src/client/theme/components/mdx/List.tsx +0 -57
  158. package/src/client/theme/components/mdx/Table.tsx +0 -151
  159. package/src/client/theme/components/mdx/Tabs.tsx +0 -123
  160. package/src/client/theme/components/mdx/index.ts +0 -27
  161. package/src/client/theme/components/mdx/mdx-components.css +0 -764
  162. package/src/client/theme/icons/bun.tsx +0 -62
  163. package/src/client/theme/icons/deno.tsx +0 -20
  164. package/src/client/theme/icons/discord.tsx +0 -12
  165. package/src/client/theme/icons/github.tsx +0 -15
  166. package/src/client/theme/icons/npm.tsx +0 -13
  167. package/src/client/theme/icons/pnpm.tsx +0 -72
  168. package/src/client/theme/icons/twitter.tsx +0 -12
  169. package/src/client/theme/styles/markdown.css +0 -394
  170. package/src/client/theme/styles/variables.css +0 -175
  171. package/src/client/theme/styles.css +0 -39
  172. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +0 -68
  173. package/src/client/theme/ui/Breadcrumbs/index.ts +0 -1
  174. package/src/client/theme/ui/CopyMarkdown/CopyMarkdown.tsx +0 -82
  175. package/src/client/theme/ui/CopyMarkdown/copy-markdown.css +0 -112
  176. package/src/client/theme/ui/CopyMarkdown/index.ts +0 -1
  177. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +0 -50
  178. package/src/client/theme/ui/ErrorBoundary/error-boundary.css +0 -55
  179. package/src/client/theme/ui/ErrorBoundary/index.ts +0 -1
  180. package/src/client/theme/ui/Footer/footer.css +0 -32
  181. package/src/client/theme/ui/Head/Head.tsx +0 -69
  182. package/src/client/theme/ui/Head/index.ts +0 -1
  183. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +0 -125
  184. package/src/client/theme/ui/LanguageSwitcher/index.ts +0 -1
  185. package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +0 -98
  186. package/src/client/theme/ui/Layout/Layout.tsx +0 -203
  187. package/src/client/theme/ui/Layout/base.css +0 -106
  188. package/src/client/theme/ui/Layout/index.ts +0 -2
  189. package/src/client/theme/ui/Layout/pagination.css +0 -72
  190. package/src/client/theme/ui/Layout/responsive.css +0 -47
  191. package/src/client/theme/ui/Link/Link.tsx +0 -392
  192. package/src/client/theme/ui/Link/LinkPreview.tsx +0 -59
  193. package/src/client/theme/ui/Link/index.ts +0 -2
  194. package/src/client/theme/ui/Link/link-preview.css +0 -48
  195. package/src/client/theme/ui/Loading/Loading.tsx +0 -10
  196. package/src/client/theme/ui/Loading/index.ts +0 -1
  197. package/src/client/theme/ui/Loading/loading.css +0 -30
  198. package/src/client/theme/ui/Navbar/GithubStars.tsx +0 -27
  199. package/src/client/theme/ui/Navbar/Navbar.tsx +0 -193
  200. package/src/client/theme/ui/Navbar/Tabs.tsx +0 -99
  201. package/src/client/theme/ui/Navbar/index.ts +0 -2
  202. package/src/client/theme/ui/Navbar/navbar.css +0 -347
  203. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -19
  204. package/src/client/theme/ui/NotFound/index.ts +0 -1
  205. package/src/client/theme/ui/NotFound/not-found.css +0 -64
  206. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +0 -244
  207. package/src/client/theme/ui/OnThisPage/index.ts +0 -1
  208. package/src/client/theme/ui/OnThisPage/toc.css +0 -152
  209. package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +0 -18
  210. package/src/client/theme/ui/PoweredBy/index.ts +0 -1
  211. package/src/client/theme/ui/PoweredBy/powered-by.css +0 -76
  212. package/src/client/theme/ui/ProgressBar/ProgressBar.css +0 -17
  213. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +0 -51
  214. package/src/client/theme/ui/ProgressBar/index.ts +0 -1
  215. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +0 -209
  216. package/src/client/theme/ui/SearchDialog/index.ts +0 -1
  217. package/src/client/theme/ui/SearchDialog/search.css +0 -152
  218. package/src/client/theme/ui/Sidebar/Sidebar.tsx +0 -244
  219. package/src/client/theme/ui/Sidebar/index.ts +0 -1
  220. package/src/client/theme/ui/Sidebar/sidebar.css +0 -230
  221. package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +0 -69
  222. package/src/client/theme/ui/ThemeToggle/index.ts +0 -1
  223. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +0 -136
  224. package/src/client/theme/ui/VersionSwitcher/index.ts +0 -1
  225. package/src/client/utils.ts +0 -49
@@ -0,0 +1,287 @@
1
+ import { Children, isValidElement, useMemo } from 'react'
2
+ import * as RAC from 'react-aria-components'
3
+ import {
4
+ Folder,
5
+ FileText,
6
+ File,
7
+ FileCode,
8
+ FileImage,
9
+ ChevronRight,
10
+ } from 'lucide-react'
11
+ import { cn } from '@client/utils/cn'
12
+
13
+ // --- Constants & Types ---
14
+
15
+ const ICON_SIZE = 16
16
+ const STROKE_WIDTH = 2
17
+
18
+ const FILE_REGEXES = {
19
+ CODE: /\.(ts|tsx|js|jsx|json|mjs|cjs|astro|vue|svelte)$/i,
20
+ TEXT: /\.(md|mdx|txt)$/i,
21
+ IMAGE: /\.(png|jpg|jpeg|svg|gif)$/i,
22
+ } as const
23
+
24
+ export interface FileTreeProps {
25
+ children: React.ReactNode
26
+ }
27
+
28
+ interface TreeItemData {
29
+ id: string
30
+ name: string
31
+ comment?: string
32
+ isFolder: boolean
33
+ children?: TreeItemData[]
34
+ }
35
+
36
+ // --- Helpers ---
37
+
38
+ function getTextContent(node: React.ReactNode): string {
39
+ if (typeof node === 'string') return node
40
+ if (typeof node === 'number') return node.toString()
41
+ if (Array.isArray(node)) return node.map(getTextContent).join('')
42
+ if (
43
+ isValidElement(node) &&
44
+ node.props &&
45
+ typeof node.props === 'object' &&
46
+ 'children' in node.props
47
+ ) {
48
+ return getTextContent(
49
+ (node.props as { children?: React.ReactNode }).children,
50
+ )
51
+ }
52
+ return ''
53
+ }
54
+
55
+ function getFileIcon(filename: string, isFolder: boolean) {
56
+ const name = filename.toLowerCase()
57
+ const iconClass = 'shrink-0 transition-colors duration-200'
58
+
59
+ if (isFolder) {
60
+ return (
61
+ <Folder
62
+ size={ICON_SIZE}
63
+ strokeWidth={STROKE_WIDTH}
64
+ className={cn(iconClass, 'text-primary-400')}
65
+ fill="currentColor"
66
+ fillOpacity={0.15}
67
+ />
68
+ )
69
+ }
70
+
71
+ const fileIconClass = cn(
72
+ iconClass,
73
+ 'text-text-dim group-hover:text-text-main',
74
+ )
75
+
76
+ if (FILE_REGEXES.CODE.test(name))
77
+ return (
78
+ <FileCode
79
+ size={ICON_SIZE}
80
+ strokeWidth={STROKE_WIDTH}
81
+ className={fileIconClass}
82
+ />
83
+ )
84
+ if (FILE_REGEXES.TEXT.test(name))
85
+ return (
86
+ <FileText
87
+ size={ICON_SIZE}
88
+ strokeWidth={STROKE_WIDTH}
89
+ className={fileIconClass}
90
+ />
91
+ )
92
+ if (FILE_REGEXES.IMAGE.test(name))
93
+ return (
94
+ <FileImage
95
+ size={ICON_SIZE}
96
+ strokeWidth={STROKE_WIDTH}
97
+ className={fileIconClass}
98
+ />
99
+ )
100
+
101
+ return (
102
+ <File
103
+ size={ICON_SIZE}
104
+ strokeWidth={STROKE_WIDTH}
105
+ className={fileIconClass}
106
+ />
107
+ )
108
+ }
109
+
110
+ function isListElement(
111
+ node: unknown,
112
+ tag: 'ul' | 'li',
113
+ ): node is React.ReactElement<{ children?: React.ReactNode }> {
114
+ if (!isValidElement(node)) return false
115
+
116
+ const type = node.type
117
+ if (typeof type === 'string') return type === tag
118
+ if (typeof type === 'function') {
119
+ return type.name === tag || type.name?.toLowerCase() === tag
120
+ }
121
+
122
+ const props = node.props as any
123
+ return props?.originalType === tag || props?.mdxType === tag
124
+ }
125
+
126
+ function parseLabel(rawLabel: string): { name: string; comment?: string } {
127
+ const commentMatch = rawLabel.match(/\s+(\/\/|#)\s+(.*)$/)
128
+ if (commentMatch) {
129
+ return {
130
+ name: rawLabel.slice(0, commentMatch.index).trim(),
131
+ comment: commentMatch[2],
132
+ }
133
+ }
134
+ return { name: rawLabel.trim() }
135
+ }
136
+
137
+ function parseMdxToData(
138
+ node: React.ReactNode,
139
+ path: string = 'root',
140
+ ): TreeItemData[] {
141
+ if (!isValidElement(node)) return []
142
+
143
+ const items: TreeItemData[] = []
144
+
145
+ if (isListElement(node, 'ul')) {
146
+ Children.forEach(
147
+ (node.props as { children?: React.ReactNode }).children,
148
+ (child, index) => {
149
+ items.push(...parseMdxToData(child, `${path}-${index}`))
150
+ },
151
+ )
152
+ return items
153
+ }
154
+
155
+ if (isListElement(node, 'li')) {
156
+ const children = Children.toArray(
157
+ (node.props as { children?: React.ReactNode }).children,
158
+ )
159
+ const nestedListIndex = children.findIndex((child) =>
160
+ isListElement(child, 'ul'),
161
+ )
162
+
163
+ const hasNested = nestedListIndex !== -1
164
+ const labelNodes = hasNested ? children.slice(0, nestedListIndex) : children
165
+ const nestedNodes = hasNested ? children.slice(nestedListIndex) : []
166
+
167
+ const rawLabelContent = getTextContent(labelNodes)
168
+ const { name, comment } = parseLabel(rawLabelContent)
169
+
170
+ const isExplicitDir = name.endsWith('/')
171
+ const labelText = isExplicitDir ? name.slice(0, -1) : name
172
+ const isFolder = hasNested || isExplicitDir
173
+
174
+ items.push({
175
+ id: `${path}-${labelText}`,
176
+ name: labelText,
177
+ comment,
178
+ isFolder,
179
+ children: hasNested
180
+ ? parseMdxToData(nestedNodes[0], `${path}-${labelText}`)
181
+ : undefined,
182
+ })
183
+ return items
184
+ }
185
+
186
+ if (
187
+ node.props &&
188
+ typeof node.props === 'object' &&
189
+ 'children' in node.props
190
+ ) {
191
+ Children.forEach(
192
+ (node.props as { children?: React.ReactNode }).children,
193
+ (child, index) => {
194
+ items.push(...parseMdxToData(child, `${path}-${index}`))
195
+ },
196
+ )
197
+ }
198
+
199
+ return items
200
+ }
201
+
202
+ // --- Sub-Components ---
203
+
204
+ function FileTreeNode({ item }: { item: TreeItemData }) {
205
+ return (
206
+ <RAC.TreeItem
207
+ id={item.id}
208
+ textValue={item.name}
209
+ className="outline-none group focus-visible:ring-2 focus-visible:ring-primary-500/30 rounded-md"
210
+ >
211
+ <RAC.TreeItemContent>
212
+ {({ isExpanded, hasChildItems }) => (
213
+ <div className="flex items-center gap-2 py-1 px-1.5 rounded-md transition-colors hover:bg-primary-500/5 cursor-pointer">
214
+ <div
215
+ style={{ width: `calc((var(--tree-item-level) - 1) * 1rem)` }}
216
+ className="shrink-0"
217
+ />
218
+ {hasChildItems ? (
219
+ <RAC.Button
220
+ slot="chevron"
221
+ className="outline-none text-text-dim hover:text-primary-400 p-0.5 rounded transition-colors"
222
+ >
223
+ <ChevronRight
224
+ size={14}
225
+ strokeWidth={3}
226
+ className={cn(
227
+ 'transition-transform duration-200',
228
+ isExpanded && 'rotate-90',
229
+ )}
230
+ />
231
+ </RAC.Button>
232
+ ) : (
233
+ <div className="w-[18px]" />
234
+ )}
235
+
236
+ {getFileIcon(item.name, item.isFolder)}
237
+
238
+ <span
239
+ className={cn(
240
+ 'text-sm transition-colors truncate select-none',
241
+ item.isFolder
242
+ ? 'font-semibold text-text-main'
243
+ : 'text-text-muted group-hover:text-text-main',
244
+ )}
245
+ >
246
+ {item.name}
247
+ </span>
248
+
249
+ {item.comment && (
250
+ <span className="ml-2 text-xs italic text-text-dim opacity-70 group-hover:opacity-100 transition-opacity whitespace-nowrap overflow-hidden text-ellipsis font-sans">
251
+ {'//'} {item.comment}
252
+ </span>
253
+ )}
254
+ </div>
255
+ )}
256
+ </RAC.TreeItemContent>
257
+
258
+ {item.children && (
259
+ <RAC.Collection items={item.children}>
260
+ {(child) => <FileTreeNode item={child} />}
261
+ </RAC.Collection>
262
+ )}
263
+ </RAC.TreeItem>
264
+ )
265
+ }
266
+
267
+ // --- Main Component ---
268
+
269
+ export function FileTree({ children }: FileTreeProps) {
270
+ const items = useMemo(() => parseMdxToData(children), [children])
271
+
272
+ return (
273
+ <div className="my-8">
274
+ <RAC.Tree
275
+ items={items}
276
+ aria-label="File Tree"
277
+ className={cn(
278
+ 'rounded-xl border border-border-subtle bg-bg-surface/50 p-4 font-mono text-sm shadow-sm backdrop-blur-sm outline-none',
279
+ 'max-h-[500px] overflow-y-auto scrollbar-thin scrollbar-thumb-border-subtle',
280
+ 'focus-visible:ring-2 focus-visible:ring-primary-500/20',
281
+ )}
282
+ >
283
+ {(item) => <FileTreeNode item={item} />}
284
+ </RAC.Tree>
285
+ </div>
286
+ )
287
+ }
@@ -0,0 +1,56 @@
1
+ import { useConfig } from '@client/app/config-context'
2
+ import { openSandbox } from '@client/integrations/codesandbox'
3
+ import { copyToClipboard } from '@client/utils/copy-clipboard'
4
+ import { useCallback, useEffect, useRef, useState } from 'react'
5
+ import type { CodeBlockProps } from '../code-block'
6
+
7
+ export function useCodeBlock(props: CodeBlockProps) {
8
+ const { title, sandbox: localSandbox } = props
9
+ const [copied, setCopied] = useState(false)
10
+ const [isExpanded, setIsExpanded] = useState(false)
11
+ const [isExpandable, setIsExpandable] = useState(false)
12
+ const preRef = useRef<HTMLPreElement>(null)
13
+ const config = useConfig()
14
+
15
+ const handleCopy = useCallback(async () => {
16
+ const code = preRef.current?.textContent ?? ''
17
+ copyToClipboard(code)
18
+ setCopied(true)
19
+ setTimeout(() => setCopied(false), 2000)
20
+ }, [])
21
+
22
+ const handleSandbox = useCallback(() => {
23
+ const code = preRef.current?.textContent ?? ''
24
+ const globalSandbox = config?.integrations?.sandbox?.config || {}
25
+ const baseOptions =
26
+ typeof localSandbox === 'object' ? localSandbox : globalSandbox
27
+
28
+ const entry = baseOptions.entry || 'src/App.tsx'
29
+
30
+ openSandbox({
31
+ title: title ?? 'Code Snippet',
32
+ ...baseOptions,
33
+ files: {
34
+ ...baseOptions.files,
35
+ [entry]: { content: code },
36
+ },
37
+ })
38
+ }, [title, config, localSandbox])
39
+
40
+ // biome-ignore lint/correctness/useExhaustiveDependencies: updates when content changes
41
+ useEffect(() => {
42
+ const codeLength = preRef.current?.textContent?.length ?? 0
43
+ setIsExpandable(codeLength > 120)
44
+ }, [props.children, props.highlightedHtml])
45
+
46
+ return {
47
+ copied,
48
+ isExpanded,
49
+ setIsExpanded,
50
+ isExpandable,
51
+ preRef,
52
+ handleCopy,
53
+ handleSandbox,
54
+ shouldTruncate: isExpandable && !isExpanded,
55
+ }
56
+ }
@@ -0,0 +1,16 @@
1
+ import { useMemo } from 'react'
2
+ import type { ComponentPreviewProps } from '../component-preview'
3
+
4
+ export function useComponentPreview(props: ComponentPreviewProps) {
5
+ const { code: propsCode, children, preview } = props
6
+ const initialCode = useMemo(() => {
7
+ const base = propsCode ?? (typeof children === 'string' ? children : '')
8
+ return base.trim()
9
+ }, [propsCode, children])
10
+
11
+ const previewElement = useMemo(() => {
12
+ return preview ?? (typeof children !== 'string' ? children : null)
13
+ }, [preview, children])
14
+
15
+ return { initialCode, previewElement }
16
+ }
@@ -0,0 +1,74 @@
1
+ import { useState, useMemo } from 'react'
2
+
3
+ interface SortConfig {
4
+ key: number
5
+ direction: 'asc' | 'desc'
6
+ }
7
+
8
+ interface UseTableProps {
9
+ data?: (string | React.ReactNode)[][]
10
+ sortable?: boolean
11
+ paginated?: boolean
12
+ pageSize?: number
13
+ }
14
+
15
+ export function useTable({
16
+ data,
17
+ sortable = false,
18
+ paginated = false,
19
+ pageSize = 10,
20
+ }: UseTableProps) {
21
+ const [sortConfig, setSortConfig] = useState<SortConfig | null>(null)
22
+ const [currentPage, setCurrentPage] = useState(1)
23
+
24
+ const processedData = useMemo(() => {
25
+ if (!data) return []
26
+ const items = [...data]
27
+
28
+ if (sortable && sortConfig !== null) {
29
+ items.sort((a, b) => {
30
+ const aVal = a[sortConfig.key]
31
+ const bVal = b[sortConfig.key]
32
+
33
+ const aStr = typeof aVal === 'string' ? aVal : ''
34
+ const bStr = typeof bVal === 'string' ? bVal : ''
35
+
36
+ if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1
37
+ if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1
38
+ return 0
39
+ })
40
+ }
41
+
42
+ return items
43
+ }, [data, sortConfig, sortable])
44
+
45
+ const totalPages = Math.ceil(processedData.length / pageSize)
46
+
47
+ const paginatedData = useMemo(() => {
48
+ if (!paginated) return processedData
49
+ const start = (currentPage - 1) * pageSize
50
+ return processedData.slice(start, start + pageSize)
51
+ }, [processedData, paginated, currentPage, pageSize])
52
+
53
+ const requestSort = (index: number) => {
54
+ if (!sortable) return
55
+ let direction: 'asc' | 'desc' = 'asc'
56
+ if (
57
+ sortConfig &&
58
+ sortConfig.key === index &&
59
+ sortConfig.direction === 'asc'
60
+ ) {
61
+ direction = 'desc'
62
+ }
63
+ setSortConfig({ key: index, direction })
64
+ }
65
+
66
+ return {
67
+ sortConfig,
68
+ currentPage,
69
+ setCurrentPage,
70
+ totalPages,
71
+ paginatedData,
72
+ requestSort,
73
+ }
74
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ type ReactElement,
7
+ type KeyboardEvent,
8
+ } from 'react'
9
+
10
+ interface UseTabsProps {
11
+ initialIndex?: number
12
+ tabs: ReactElement<any>[]
13
+ }
14
+
15
+ export function useTabs({ initialIndex = 0, tabs }: UseTabsProps) {
16
+ const defaultActive = tabs[initialIndex]?.props.disabled
17
+ ? tabs.findIndex((t) => !t.props.disabled)
18
+ : initialIndex
19
+
20
+ const [active, setActive] = useState(defaultActive === -1 ? 0 : defaultActive)
21
+ const tabRefs = useRef<(HTMLButtonElement | null)[]>([])
22
+ const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({
23
+ opacity: 0,
24
+ transform: 'translateX(0)',
25
+ width: 0,
26
+ })
27
+
28
+ // biome-ignore lint/correctness/useExhaustiveDependencies: updates when content changes
29
+ useEffect(() => {
30
+ const activeTab = tabRefs.current[active]
31
+ if (activeTab) {
32
+ setIndicatorStyle({
33
+ opacity: 1,
34
+ width: activeTab.offsetWidth,
35
+ transform: `translateX(${activeTab.offsetLeft}px)`,
36
+ })
37
+ }
38
+ }, [active, tabs])
39
+
40
+ const handleKeyDown = useCallback(
41
+ (e: KeyboardEvent<HTMLDivElement>) => {
42
+ let direction = 0
43
+ if (e.key === 'ArrowRight') direction = 1
44
+ else if (e.key === 'ArrowLeft') direction = -1
45
+
46
+ if (direction !== 0) {
47
+ let nextIndex = (active + direction + tabs.length) % tabs.length
48
+ while (tabs[nextIndex].props.disabled && nextIndex !== active) {
49
+ nextIndex = (nextIndex + direction + tabs.length) % tabs.length
50
+ }
51
+
52
+ if (nextIndex !== active && !tabs[nextIndex].props.disabled) {
53
+ setActive(nextIndex)
54
+ tabRefs.current[nextIndex]?.focus()
55
+ }
56
+ }
57
+ },
58
+ [active, tabs],
59
+ )
60
+
61
+ return {
62
+ active,
63
+ setActive,
64
+ tabRefs,
65
+ indicatorStyle,
66
+ handleKeyDown,
67
+ }
68
+ }
@@ -0,0 +1,23 @@
1
+ import type { ImgHTMLAttributes } from 'react'
2
+ import { useTheme } from '@client/app/theme-context'
3
+
4
+ export interface ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
5
+ src: string
6
+ darkSrc?: string
7
+ theme?: 'light' | 'dark'
8
+ }
9
+
10
+ /**
11
+ * A themed Image component for Boltdocs.
12
+ * It supports rendering based on the current active theme.
13
+ */
14
+ export function Image({ src, alt, theme: imageTheme, ...props }: ImageProps) {
15
+ const { theme: currentTheme } = useTheme()
16
+
17
+ // If a specific theme is required for this image, only render if it matches
18
+ if (imageTheme && imageTheme !== currentTheme) {
19
+ return null
20
+ }
21
+
22
+ return <img src={src} alt={alt || ''} {...props} />
23
+ }
@@ -0,0 +1,53 @@
1
+ export { Button } from './button'
2
+ export type { ButtonProps } from './button'
3
+
4
+ export { CodeBlock } from './code-block'
5
+
6
+ export { Tabs, Tab } from './tabs'
7
+ export type { TabsProps, TabProps } from './tabs'
8
+
9
+ export { Video } from './video'
10
+
11
+ export { Badge } from './badge'
12
+ export type { BadgeProps } from './badge'
13
+
14
+ export { Card, Cards } from './card'
15
+ export type { CardProps, CardsProps } from './card'
16
+
17
+ export {
18
+ Admonition,
19
+ Note,
20
+ Tip,
21
+ Warning,
22
+ Danger,
23
+ InfoBox,
24
+ Important,
25
+ Caution,
26
+ } from './admonition'
27
+ export type { AdmonitionProps } from './admonition'
28
+
29
+ export { List } from './list'
30
+ export type { ListProps } from './list'
31
+
32
+ export { FileTree } from './file-tree'
33
+ export type { FileTreeProps } from './file-tree'
34
+
35
+ export { Table } from './table'
36
+ export type { TableProps } from './table'
37
+
38
+ export { Field } from './field'
39
+ export type { FieldProps } from './field'
40
+
41
+ export { Link } from './link'
42
+ export type { LinkProps } from './link'
43
+
44
+ export { Image } from './image'
45
+ export type { ImageProps } from './image'
46
+
47
+ export { ComponentProps } from './component-props'
48
+ export type { ComponentPropsProps } from './component-props'
49
+
50
+ export { ComponentPreview } from './component-preview'
51
+ export type { ComponentPreviewProps } from './component-preview'
52
+ export { CopyMarkdown } from '../ui-base/copy-markdown'
53
+ export type { CopyMarkdownProps } from '../ui-base/copy-markdown'
@@ -0,0 +1,38 @@
1
+ import {
2
+ Link as LinkPrimitive,
3
+ type LinkProps as LinkPrimitiveProps,
4
+ } from '@components/primitives/link'
5
+ import { cn } from '@client/utils/cn'
6
+
7
+ export type LinkProps = LinkPrimitiveProps & {
8
+ to: string
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ /**
13
+ * A premium Link component for Boltdocs that handles internal and external routing.
14
+ */
15
+ export function Link({ to, children, className = '', ...props }: LinkProps) {
16
+ const isExternal =
17
+ to &&
18
+ (to.startsWith('http://') ||
19
+ to.startsWith('https://') ||
20
+ to.startsWith('//'))
21
+
22
+ const combinedClassName = cn(
23
+ 'text-blue-600 hover:text-blue-800 hover:underline cursor-pointer',
24
+ className,
25
+ )
26
+
27
+ return (
28
+ <LinkPrimitive
29
+ href={to}
30
+ className={combinedClassName}
31
+ target={isExternal ? '_blank' : undefined}
32
+ rel={isExternal ? 'noopener noreferrer' : undefined}
33
+ {...props}
34
+ >
35
+ {children}
36
+ </LinkPrimitive>
37
+ )
38
+ }