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,461 @@
1
+ import React, {
2
+ createContext,
3
+ use,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ type RefObject,
11
+ } from 'react'
12
+ import scrollIntoView from 'scroll-into-view-if-needed'
13
+ import { cn } from '../../utils/cn'
14
+ import { useOnChange } from '../../utils/use-on-change'
15
+ import type { ComponentBase, CompoundComponent } from './types'
16
+ import { getItemId } from './helpers/observer'
17
+
18
+ export interface TOCItemType {
19
+ title: ReactNode
20
+ url: string
21
+ depth: number
22
+ _step?: number
23
+ }
24
+
25
+ export type TableOfContents = TOCItemType[]
26
+
27
+ export interface TOCItemInfo {
28
+ id: string
29
+ active: boolean
30
+ /** last time the item is updated */
31
+ t: number
32
+ /** currently active but not intersecting in viewport */
33
+ fallback: boolean
34
+ original?: TOCItemType
35
+ }
36
+
37
+ export interface AnchorProviderProps {
38
+ toc: TOCItemType[]
39
+ /**
40
+ * Only accept one active item at most
41
+ * @defaultValue false
42
+ */
43
+ single?: boolean
44
+ children?: ReactNode
45
+ }
46
+
47
+ export interface ScrollProviderProps {
48
+ /**
49
+ * Scroll into the view of container when active
50
+ */
51
+ containerRef: RefObject<HTMLElement | null>
52
+ children?: ReactNode
53
+ }
54
+
55
+ export interface OnThisPageContentProps extends ComponentBase {
56
+ ref?: React.Ref<HTMLDivElement>
57
+ scrollRef?: RefObject<HTMLElement | null>
58
+ }
59
+
60
+ export interface OnThisPageItemProps extends ComponentBase {
61
+ level?: number
62
+ }
63
+
64
+ export interface OnThisPageLinkProps extends ComponentBase {
65
+ href?: string
66
+ active?: boolean
67
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void
68
+ }
69
+
70
+ export interface OnThisPageIndicatorProps extends ComponentBase {
71
+ style?: React.CSSProperties
72
+ }
73
+
74
+ const ItemsContext = createContext<TOCItemInfo[] | null>(null)
75
+ const ScrollContext = createContext<RefObject<HTMLElement | null> | null>(null)
76
+
77
+ class Observer {
78
+ items: TOCItemInfo[] = []
79
+ single = false
80
+ private observer: IntersectionObserver | null = null
81
+ onChange?: () => void
82
+
83
+ private callback(entries: IntersectionObserverEntry[]) {
84
+ if (entries.length === 0) return
85
+
86
+ let hasActive = false
87
+ this.items = this.items.map((item) => {
88
+ const entry = entries.find((entry) => entry.target.id === item.id)
89
+ let active = entry ? entry.isIntersecting : item.active && !item.fallback
90
+ if (this.single && hasActive) active = false
91
+
92
+ if (item.active !== active) {
93
+ item = {
94
+ ...item,
95
+ t: Date.now(),
96
+ active,
97
+ fallback: false,
98
+ }
99
+ }
100
+
101
+ if (active) hasActive = true
102
+ return item
103
+ })
104
+
105
+ if (!hasActive && entries[0].rootBounds) {
106
+ const viewTop = entries[0].rootBounds.top
107
+ let min = Number.MAX_VALUE
108
+ let fallbackIdx = -1
109
+
110
+ for (let i = 0; i < this.items.length; i++) {
111
+ const element = document.getElementById(this.items[i].id)
112
+ if (!element) continue
113
+
114
+ const d = Math.abs(viewTop - element.getBoundingClientRect().top)
115
+ if (d < min) {
116
+ fallbackIdx = i
117
+ min = d
118
+ }
119
+ }
120
+
121
+ if (fallbackIdx !== -1) {
122
+ this.items[fallbackIdx] = {
123
+ ...this.items[fallbackIdx],
124
+ active: true,
125
+ fallback: true,
126
+ t: Date.now(),
127
+ }
128
+ }
129
+ }
130
+
131
+ this.onChange?.()
132
+ }
133
+
134
+ setItems(newItems: TOCItemType[]) {
135
+ const observer = this.observer
136
+ if (observer) {
137
+ for (const item of this.items) {
138
+ const element = document.getElementById(item.id)
139
+ if (!element) continue
140
+ observer.unobserve(element)
141
+ }
142
+ }
143
+
144
+ this.items = []
145
+ for (const item of newItems) {
146
+ const id = getItemId(item.url)
147
+ if (!id) continue
148
+
149
+ this.items.push({
150
+ id,
151
+ active: false,
152
+ fallback: false,
153
+ t: 0,
154
+ original: item,
155
+ })
156
+ }
157
+ this.watchItems()
158
+
159
+ // In an SPA, the TOC might update before the MDX content is in the DOM.
160
+ // We perform a few delayed scans to ensure we catch those elements.
161
+ if (typeof window !== 'undefined') {
162
+ setTimeout(() => this.watchItems(), 100)
163
+ setTimeout(() => this.watchItems(), 500)
164
+ setTimeout(() => this.watchItems(), 1000)
165
+ }
166
+
167
+ this.onChange?.()
168
+ }
169
+
170
+ watch(options?: IntersectionObserverInit) {
171
+ if (this.observer) return
172
+ this.observer = new IntersectionObserver(this.callback.bind(this), options)
173
+ this.watchItems()
174
+ }
175
+
176
+ private watchItems() {
177
+ if (!this.observer) return
178
+ for (const item of this.items) {
179
+ const element = document.getElementById(item.id)
180
+ if (!element) continue
181
+ this.observer.observe(element)
182
+ }
183
+ }
184
+
185
+ unwatch() {
186
+ this.observer?.disconnect()
187
+ this.observer = null
188
+ }
189
+ }
190
+
191
+ export function useItems() {
192
+ const ctx = use(ItemsContext)
193
+ if (!ctx)
194
+ throw new Error(
195
+ `Component must be used under the <AnchorProvider /> component.`,
196
+ )
197
+ return ctx
198
+ }
199
+
200
+ export function useScrollStatus(ref: RefObject<HTMLElement | null>) {
201
+ const [status, setStatus] = useState({
202
+ isOverflowing: false,
203
+ isAtBottom: false,
204
+ })
205
+
206
+ useEffect(() => {
207
+ const el = ref.current
208
+ if (!el) return
209
+
210
+ const checkStatus = () => {
211
+ const isOverflowing = el.scrollHeight > el.clientHeight
212
+ // We use a 2px threshold for floating point math issues
213
+ const isAtBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 2
214
+ setStatus({ isOverflowing, isAtBottom })
215
+ }
216
+
217
+ checkStatus()
218
+ el.addEventListener('scroll', checkStatus, { passive: true })
219
+ window.addEventListener('resize', checkStatus)
220
+
221
+ const mutationObserver = new MutationObserver(checkStatus)
222
+ mutationObserver.observe(el, { childList: true, subtree: true })
223
+
224
+ return () => {
225
+ el.removeEventListener('scroll', checkStatus)
226
+ window.removeEventListener('resize', checkStatus)
227
+ mutationObserver.disconnect()
228
+ }
229
+ }, [ref])
230
+
231
+ return status
232
+ }
233
+
234
+ export function useActiveAnchor(): string | undefined {
235
+ const items = useItems()
236
+ return useMemo(() => {
237
+ let out: TOCItemInfo | undefined
238
+ for (const item of items) {
239
+ if (!item.active) continue
240
+ if (!out || item.t > out.t) {
241
+ out = item
242
+ }
243
+ }
244
+ return out?.id
245
+ }, [items])
246
+ }
247
+
248
+ export function useActiveAnchors(): string[] {
249
+ const items = useItems()
250
+ return useMemo(() => {
251
+ const out: string[] = []
252
+ for (const item of items) {
253
+ if (item.active) out.push(item.id)
254
+ }
255
+ return out
256
+ }, [items])
257
+ }
258
+
259
+ /** Optional: add auto-scroll to TOC items. */
260
+ export function ScrollProvider({
261
+ containerRef,
262
+ children,
263
+ }: ScrollProviderProps) {
264
+ return (
265
+ <ScrollContext.Provider value={containerRef}>
266
+ {children}
267
+ </ScrollContext.Provider>
268
+ )
269
+ }
270
+
271
+ export function AnchorProvider({
272
+ toc,
273
+ single = false,
274
+ children,
275
+ }: AnchorProviderProps) {
276
+ const observer = useMemo(() => new Observer(), [])
277
+ const [items, setItems] = useState<TOCItemInfo[]>(observer.items)
278
+
279
+ observer.single = single
280
+ useEffect(() => {
281
+ observer.setItems(toc)
282
+ }, [observer, toc])
283
+
284
+ useEffect(() => {
285
+ observer.watch({
286
+ rootMargin: '0px',
287
+ threshold: 0.98,
288
+ })
289
+ observer.onChange = () => setItems([...observer.items])
290
+
291
+ return () => {
292
+ observer.unwatch()
293
+ }
294
+ }, [observer])
295
+
296
+ return <ItemsContext.Provider value={items}>{children}</ItemsContext.Provider>
297
+ }
298
+
299
+ export const OnThisPageRoot = ({ children, className }: ComponentBase) => {
300
+ return (
301
+ <nav
302
+ className={cn(
303
+ 'sticky top-navbar hidden xl:flex flex-col shrink-0',
304
+ 'w-toc',
305
+ 'py-8 pl-6 pr-4',
306
+ className,
307
+ )}
308
+ >
309
+ {children}
310
+ </nav>
311
+ )
312
+ }
313
+
314
+ export const OnThisPageHeader = ({
315
+ children,
316
+ className,
317
+ ...props
318
+ }: ComponentBase) => {
319
+ return (
320
+ <div
321
+ className={cn(
322
+ 'mb-4 text-xs font-bold uppercase tracking-wider text-text-main',
323
+ className,
324
+ )}
325
+ {...props}
326
+ >
327
+ {children}
328
+ </div>
329
+ )
330
+ }
331
+
332
+ export const OnThisPageContent = ({
333
+ children,
334
+ className,
335
+ ref,
336
+ ...props
337
+ }: OnThisPageContentProps) => {
338
+ const internalRef = useRef<HTMLDivElement>(null)
339
+
340
+ useImperativeHandle(ref, () => internalRef.current!)
341
+
342
+ const { isOverflowing, isAtBottom } = useScrollStatus(internalRef)
343
+
344
+ return (
345
+ <div
346
+ ref={internalRef}
347
+ className={cn(
348
+ 'relative overflow-y-auto boltdocs-otp-content',
349
+ isOverflowing &&
350
+ !isAtBottom &&
351
+ 'mask-[linear-gradient(to_bottom,black_85%,transparent_100%)]',
352
+ className,
353
+ )}
354
+ {...props}
355
+ >
356
+ {children}
357
+ </div>
358
+ )
359
+ }
360
+
361
+ OnThisPageContent.displayName = 'OnThisPageContent'
362
+
363
+ export const OnThisPageList = ({ children, className }: ComponentBase) => {
364
+ return (
365
+ <ul
366
+ className={cn(
367
+ 'relative space-y-1 text-sm border-l border-border-subtle',
368
+ className,
369
+ )}
370
+ >
371
+ {children}
372
+ </ul>
373
+ )
374
+ }
375
+
376
+ export const OnThisPageItem = ({
377
+ level,
378
+ children,
379
+ className,
380
+ }: OnThisPageItemProps) => {
381
+ return <li className={cn(level === 3 && 'pl-3', className)}>{children}</li>
382
+ }
383
+
384
+ export const OnThisPageLink = ({
385
+ children,
386
+ href,
387
+ active,
388
+ onClick,
389
+ className,
390
+ }: OnThisPageLinkProps) => {
391
+ const items = use(ItemsContext)
392
+ const containerRef = use(ScrollContext)
393
+ const id = href ? getItemId(href) : null
394
+ const anchorRef = useRef<HTMLAnchorElement>(null)
395
+ const [internalActive, setInternalActive] = useState(active)
396
+
397
+ useOnChange(
398
+ id && items ? items.find((i) => i.id === id)?.active : null,
399
+ (newActive) => {
400
+ if (newActive !== null && newActive !== internalActive) {
401
+ setInternalActive(!!newActive)
402
+
403
+ if (newActive && anchorRef.current && containerRef?.current) {
404
+ scrollIntoView(anchorRef.current, {
405
+ behavior: 'smooth',
406
+ block: 'center',
407
+ inline: 'center',
408
+ scrollMode: 'if-needed',
409
+ boundary: containerRef.current,
410
+ })
411
+ }
412
+ }
413
+ },
414
+ )
415
+
416
+ // Also sync with external active prop if provided
417
+ useEffect(() => {
418
+ if (active !== undefined) setInternalActive(active)
419
+ }, [active])
420
+
421
+ return (
422
+ <a
423
+ ref={anchorRef}
424
+ href={href}
425
+ onClick={onClick}
426
+ data-active={internalActive}
427
+ className={cn(
428
+ 'block py-1 pl-4 text-[13px] outline-none transition-colors hover:text-text-main',
429
+ internalActive ? 'text-primary-500 font-medium' : 'text-text-muted',
430
+ className,
431
+ )}
432
+ >
433
+ {children}
434
+ </a>
435
+ )
436
+ }
437
+
438
+ export const OnThisPageIndicator = ({
439
+ style,
440
+ className,
441
+ }: OnThisPageIndicatorProps) => {
442
+ return (
443
+ <div
444
+ className={cn(
445
+ 'absolute -left-px w-0.5 rounded-full bg-primary-500 transition-all duration-300',
446
+ className,
447
+ )}
448
+ style={style}
449
+ />
450
+ )
451
+ }
452
+
453
+ export default {
454
+ OnThisPageRoot,
455
+ OnThisPageHeader,
456
+ OnThisPageContent,
457
+ OnThisPageList,
458
+ OnThisPageItem,
459
+ OnThisPageLink,
460
+ OnThisPageIndicator,
461
+ }
@@ -0,0 +1,87 @@
1
+ import * as RAC from 'react-aria-components'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { cn } from '@client/utils/cn'
4
+ import type { ComponentBase } from './types'
5
+
6
+ export interface PageNavProps extends ComponentBase {
7
+ to: string
8
+ direction: 'prev' | 'next'
9
+ }
10
+
11
+ export const PageNavRoot = ({ children, className }: ComponentBase) => {
12
+ return (
13
+ <nav
14
+ className={cn(
15
+ 'grid grid-cols-1 sm:grid-cols-2 gap-4 mt-12 pt-8 border-t border-border-subtle',
16
+ className,
17
+ )}
18
+ >
19
+ {children}
20
+ </nav>
21
+ )
22
+ }
23
+
24
+ export const PageNavLink = ({
25
+ children,
26
+ to,
27
+ direction,
28
+ className,
29
+ }: PageNavProps) => {
30
+ const isNext = direction === 'next'
31
+ return (
32
+ <RAC.Link
33
+ href={to}
34
+ className={cn(
35
+ 'flex group items-center p-4 rounded-xl border border-border-subtle bg-bg-surface outline-none',
36
+ 'transition-all hover:bg-bg-main hover:border-primary-500 hover:shadow-lg',
37
+ 'focus-visible:ring-2 focus-visible:ring-primary-500/30',
38
+ isNext ? 'text-right justify-end' : 'text-left justify-start',
39
+ className,
40
+ )}
41
+ >
42
+ {!isNext && (
43
+ <ChevronLeft className="mr-3 h-5 w-5 text-text-muted group-hover:text-primary-500 transition-transform group-hover:-translate-x-1" />
44
+ )}
45
+ <div className="flex flex-col gap-1 flex-1">{children}</div>
46
+ {isNext && (
47
+ <ChevronRight className="ml-3 h-5 w-5 text-text-muted group-hover:text-primary-500 transition-transform group-hover:translate-x-1" />
48
+ )}
49
+ </RAC.Link>
50
+ )
51
+ }
52
+
53
+ export const PageNavTitle = ({ children, className }: ComponentBase) => {
54
+ return (
55
+ <span
56
+ className={cn(
57
+ 'text-xs font-medium uppercase tracking-wider text-text-muted',
58
+ className,
59
+ )}
60
+ >
61
+ {children}
62
+ </span>
63
+ )
64
+ }
65
+
66
+ export const PageNavDescription = ({ children, className }: ComponentBase) => {
67
+ return (
68
+ <span
69
+ className={cn('text-base font-bold text-text-main truncate', className)}
70
+ >
71
+ {children}
72
+ </span>
73
+ )
74
+ }
75
+
76
+ export const PageNavIcon = ({ children }: ComponentBase) => {
77
+ return <>{children}</>
78
+ }
79
+
80
+ export default {
81
+ PageNavRoot,
82
+ PageNavLink: Object.assign(PageNavLink, {
83
+ Title: PageNavTitle,
84
+ Description: PageNavDescription,
85
+ Icon: PageNavIcon,
86
+ }),
87
+ }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import * as RAC from 'react-aria-components'
4
+ import { cn } from '@client/utils/cn'
5
+
6
+ export interface PopoverProps extends Omit<RAC.PopoverProps, 'children'> {
7
+ children: React.ReactNode
8
+ className?: string
9
+ showArrow?: boolean
10
+ }
11
+
12
+ /**
13
+ * A reusable Popover primitive with premium glassmorphism styling and smooth animations.
14
+ */
15
+ export const Popover = ({
16
+ children,
17
+ className,
18
+ showArrow,
19
+ ...props
20
+ }: PopoverProps) => {
21
+ return (
22
+ <RAC.Popover
23
+ offset={8}
24
+ {...props}
25
+ className={RAC.composeRenderProps(className, (className) =>
26
+ cn(
27
+ 'z-50 overflow-auto rounded-xl border border-border-subtle bg-bg-surface/80 shadow-xl backdrop-blur-md outline-none',
28
+ 'entering:animate-in entering:fade-in entering:zoom-in-95 exiting:animate-out exiting:fade-out exiting:zoom-out-95 fill-mode-forwards',
29
+ className,
30
+ ),
31
+ )}
32
+ >
33
+ {showArrow && (
34
+ <RAC.OverlayArrow className="group">
35
+ <svg
36
+ viewBox="0 0 12 12"
37
+ className="block h-3 w-3 fill-bg-surface/80 stroke-border-subtle group-placement-bottom:rotate-180 group-placement-left:-rotate-90 group-placement-right:rotate-90"
38
+ aria-hidden="true"
39
+ >
40
+ <path d="M0 0 L6 6 L12 0" />
41
+ </svg>
42
+ </RAC.OverlayArrow>
43
+ )}
44
+ {children as any}
45
+ </RAC.Popover>
46
+ )
47
+ }