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,145 @@
1
+ import * as RAC from 'react-aria-components'
2
+ import { cn } from '@client/utils/cn'
3
+ import { cva } from 'class-variance-authority'
4
+ import type { VariantProps } from 'class-variance-authority'
5
+
6
+ export const buttonVariants = cva(
7
+ 'flex flex-row items-center justify-center w-auto font-semibold tracking-tight no-underline whitespace-nowrap select-none outline-none transition-all duration-200 cursor-pointer pressed:scale-[0.97] hover:-translate-y-px leading-none',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ primary:
12
+ 'bg-primary-500 text-white shadow-md hover:brightness-110 hover:shadow-lg',
13
+ secondary:
14
+ 'bg-bg-surface text-text-main border border-border-subtle hover:bg-bg-muted hover:border-border-strong',
15
+ outline:
16
+ 'bg-transparent text-text-main border border-border-strong hover:bg-bg-surface hover:border-primary-500',
17
+ ghost:
18
+ 'bg-transparent text-text-muted hover:bg-bg-surface hover:text-text-main',
19
+ danger:
20
+ 'bg-[var(--color-danger-500)]/10 text-[var(--color-danger-500)] border border-[var(--color-danger-500)]/20 hover:bg-[var(--color-danger-500)]/15',
21
+ success:
22
+ 'bg-[var(--color-success-500)]/10 text-[var(--color-success-500)] border border-[var(--color-success-500)]/20 hover:bg-[var(--color-success-500)]/15',
23
+ warning:
24
+ 'bg-[var(--color-warning-500)]/10 text-[var(--color-warning-500)] border border-[var(--color-warning-500)]/20 hover:bg-[var(--color-warning-500)]/15',
25
+ info: 'bg-[var(--color-info-500)]/10 text-[var(--color-info-500)] border border-[var(--color-info-500)]/20 hover:bg-[var(--color-info-500)]/15',
26
+ subtle: 'bg-primary-500/10 text-primary-500 hover:bg-primary-500/20',
27
+ link: 'bg-transparent text-primary-500 !p-0 !min-h-0 hover:underline',
28
+ },
29
+ size: {
30
+ sm: 'min-h-8 px-3.5 text-[0.8125rem] gap-1.5',
31
+ md: 'min-h-10 px-5 text-[0.9375rem] gap-2',
32
+ lg: 'min-h-12 px-7 text-[1.05rem] gap-2.5',
33
+ },
34
+ rounded: {
35
+ none: 'rounded-none',
36
+ sm: 'rounded-sm',
37
+ md: 'rounded-md',
38
+ lg: 'rounded-lg',
39
+ full: 'rounded-full',
40
+ },
41
+ iconSize: {
42
+ sm: 'w-8 h-8 p-0',
43
+ md: 'w-10 h-10 p-0',
44
+ lg: 'w-12 h-12 p-0',
45
+ },
46
+ disabled: {
47
+ true: 'opacity-50 cursor-not-allowed pointer-events-none',
48
+ false: null,
49
+ },
50
+ },
51
+ defaultVariants: {
52
+ variant: 'primary',
53
+ size: 'md',
54
+ rounded: 'md',
55
+ },
56
+ },
57
+ )
58
+ type ButtonVariantType = VariantProps<typeof buttonVariants>
59
+
60
+ export interface ButtonProps
61
+ extends Omit<RAC.ButtonProps, 'children' | 'className'>,
62
+ ButtonVariantType {
63
+ icon?: React.ReactNode
64
+ iconPosition?: 'left' | 'right'
65
+ href?: string
66
+ children?: React.ReactNode
67
+ className?: string
68
+ isIconOnly?: boolean
69
+ }
70
+
71
+ export const Button = ({
72
+ href,
73
+ icon,
74
+ iconPosition = 'left',
75
+ isIconOnly,
76
+ children,
77
+ className,
78
+ variant,
79
+ size,
80
+ rounded,
81
+ iconSize,
82
+ disabled,
83
+ ...props
84
+ }: ButtonProps) => {
85
+ const isOnlyIcon = isIconOnly || (!children && !!icon)
86
+
87
+ const content = isOnlyIcon ? (
88
+ <span className="inline-flex items-center justify-center [&>svg]:w-[1.2rem] [&>svg]:h-[1.2rem]">
89
+ {icon}
90
+ </span>
91
+ ) : (
92
+ <>
93
+ {icon && iconPosition === 'left' && (
94
+ <span className="inline-flex items-center shrink-0 [&>svg]:w-[1.1rem] [&>svg]:h-[1.1rem]">
95
+ {icon}
96
+ </span>
97
+ )}
98
+ <span className="flex items-center">{children}</span>
99
+ {icon && iconPosition === 'right' && (
100
+ <span className="inline-flex items-center shrink-0 [&>svg]:w-[1.1rem] [&>svg]:h-[1.1rem]">
101
+ {icon}
102
+ </span>
103
+ )}
104
+ </>
105
+ )
106
+
107
+ if (href) {
108
+ return (
109
+ <RAC.Link
110
+ href={href}
111
+ className={cn(
112
+ buttonVariants({
113
+ variant,
114
+ size,
115
+ rounded,
116
+ iconSize: isOnlyIcon ? iconSize : undefined,
117
+ disabled,
118
+ }),
119
+ className,
120
+ )}
121
+ {...(props as RAC.LinkProps)}
122
+ >
123
+ {content}
124
+ </RAC.Link>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <RAC.Button
130
+ className={cn(
131
+ buttonVariants({
132
+ variant,
133
+ size,
134
+ rounded,
135
+ iconSize: isOnlyIcon ? iconSize : undefined,
136
+ disabled,
137
+ }),
138
+ className,
139
+ )}
140
+ {...(props as RAC.ButtonProps)}
141
+ >
142
+ {content}
143
+ </RAC.Button>
144
+ )
145
+ }
@@ -0,0 +1,120 @@
1
+ import type { TOCItemInfo, TOCItemType } from '../on-this-page'
2
+
3
+ export function getItemId(url: string) {
4
+ if (url.startsWith('#')) return url.slice(1)
5
+ return null
6
+ }
7
+
8
+ export class Observer {
9
+ items: TOCItemInfo[] = []
10
+ single = false
11
+ private observer: IntersectionObserver | null = null
12
+ onChange?: () => void
13
+
14
+ private callback(entries: IntersectionObserverEntry[]) {
15
+ if (entries.length === 0) return
16
+
17
+ let hasActive = false
18
+ this.items = this.items.map((item) => {
19
+ const entry = entries.find((entry) => entry.target.id === item.id)
20
+ let active = entry ? entry.isIntersecting : item.active && !item.fallback
21
+ if (this.single && hasActive) active = false
22
+
23
+ if (item.active !== active) {
24
+ item = {
25
+ ...item,
26
+ t: Date.now(),
27
+ active,
28
+ fallback: false,
29
+ }
30
+ }
31
+
32
+ if (active) hasActive = true
33
+ return item
34
+ })
35
+
36
+ if (!hasActive && entries[0].rootBounds) {
37
+ const viewTop = entries[0].rootBounds.top
38
+ let min = Number.MAX_VALUE
39
+ let fallbackIdx = -1
40
+
41
+ for (let i = 0; i < this.items.length; i++) {
42
+ const element = document.getElementById(this.items[i].id)
43
+ if (!element) continue
44
+
45
+ const d = Math.abs(viewTop - element.getBoundingClientRect().top)
46
+ if (d < min) {
47
+ fallbackIdx = i
48
+ min = d
49
+ }
50
+ }
51
+
52
+ if (fallbackIdx !== -1) {
53
+ this.items[fallbackIdx] = {
54
+ ...this.items[fallbackIdx],
55
+ active: true,
56
+ fallback: true,
57
+ t: Date.now(),
58
+ }
59
+ }
60
+ }
61
+
62
+ this.onChange?.()
63
+ }
64
+
65
+ setItems(newItems: TOCItemType[]) {
66
+ const observer = this.observer
67
+ if (observer) {
68
+ for (const item of this.items) {
69
+ const element = document.getElementById(item.id)
70
+ if (!element) continue
71
+ observer.unobserve(element)
72
+ }
73
+ }
74
+
75
+ this.items = []
76
+ for (const item of newItems) {
77
+ const id = getItemId(item.url)
78
+ if (!id) continue
79
+
80
+ this.items.push({
81
+ id,
82
+ active: false,
83
+ fallback: false,
84
+ t: 0,
85
+ original: item,
86
+ })
87
+ }
88
+ this.watchItems()
89
+
90
+ // In an SPA, the TOC might update before the MDX content is in the DOM.
91
+ // We perform a few delayed scans to ensure we catch those elements.
92
+ if (typeof window !== 'undefined') {
93
+ setTimeout(() => this.watchItems(), 100)
94
+ setTimeout(() => this.watchItems(), 500)
95
+ setTimeout(() => this.watchItems(), 1000)
96
+ }
97
+
98
+ this.onChange?.()
99
+ }
100
+
101
+ watch(options?: IntersectionObserverInit) {
102
+ if (this.observer) return
103
+ this.observer = new IntersectionObserver(this.callback.bind(this), options)
104
+ this.watchItems()
105
+ }
106
+
107
+ private watchItems() {
108
+ if (!this.observer) return
109
+ for (const item of this.items) {
110
+ const element = document.getElementById(item.id)
111
+ if (!element) continue
112
+ this.observer.observe(element)
113
+ }
114
+ }
115
+
116
+ unwatch() {
117
+ this.observer?.disconnect()
118
+ this.observer = null
119
+ }
120
+ }
@@ -0,0 +1,17 @@
1
+ export * from './navbar'
2
+ export * from './navigation-menu'
3
+ export * from './search-dialog'
4
+ export * from './on-this-page'
5
+ export * from './page-nav'
6
+ export * from './tabs'
7
+ export * from './sidebar'
8
+ export * from './breadcrumbs'
9
+ export * from './button'
10
+ export * from './button-group'
11
+ export * from './menu'
12
+ export * from './popover'
13
+ export * from './tooltip'
14
+ export * from './link'
15
+ export { Separator, ToggleButton } from 'react-aria-components'
16
+
17
+ export { cn } from '../../utils/cn'
@@ -0,0 +1,122 @@
1
+ import React from 'react'
2
+ import {
3
+ Link as RACLink,
4
+ type LinkProps as RACLinkProps,
5
+ } from 'react-aria-components'
6
+ import { useLocation } from 'react-router-dom'
7
+ import { useLocalizedTo } from '@hooks/use-localized-to'
8
+ import { usePreload } from '@client/app/preload'
9
+ import { cn } from '@client/utils/cn'
10
+
11
+ export interface LinkProps extends RACLinkProps {
12
+ /** Should prefetch the page on hover? Default 'hover' */
13
+ prefetch?: 'hover' | 'none'
14
+ }
15
+
16
+ /**
17
+ * A primitive Link component that wraps React Aria Components' Link
18
+ * and adds framework-specific logic for path localization and preloading.
19
+ *
20
+ * It uses the global navigation configuration from BoltdocsRouterProvider
21
+ * to handle seamless client-side transitions.
22
+ */
23
+ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
24
+ (props, ref) => {
25
+ const { href, prefetch = 'hover', onMouseEnter, onFocus, ...rest } = props
26
+
27
+ const localizedHref = useLocalizedTo(href ?? '')
28
+ const { preload } = usePreload()
29
+
30
+ const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
31
+ onMouseEnter?.(e)
32
+ if (
33
+ prefetch === 'hover' &&
34
+ typeof localizedHref === 'string' &&
35
+ localizedHref.startsWith('/')
36
+ ) {
37
+ preload(localizedHref)
38
+ }
39
+ }
40
+
41
+ const handleFocus = (e: React.FocusEvent) => {
42
+ onFocus?.(e as any)
43
+ if (
44
+ prefetch === 'hover' &&
45
+ typeof localizedHref === 'string' &&
46
+ localizedHref.startsWith('/')
47
+ ) {
48
+ preload(localizedHref)
49
+ }
50
+ }
51
+
52
+ return (
53
+ <RACLink
54
+ {...rest}
55
+ ref={ref}
56
+ href={localizedHref as string}
57
+ onMouseEnter={handleMouseEnter}
58
+ onFocus={handleFocus as any}
59
+ />
60
+ )
61
+ },
62
+ )
63
+ Link.displayName = 'Link'
64
+
65
+ /**
66
+ * Props for the NavLink component, extending standard Link props.
67
+ */
68
+ export interface NavLinkProps
69
+ extends Omit<LinkProps, 'className' | 'children'> {
70
+ /**
71
+ * When true, the active state will only be applied if the paths match exactly.
72
+ * Default is false.
73
+ */
74
+ end?: boolean
75
+ /**
76
+ * Provides access to the active state for conditional children rendering.
77
+ */
78
+ children?:
79
+ | React.ReactNode
80
+ | ((props: { isActive: boolean }) => React.ReactNode)
81
+ /**
82
+ * Provides access to the active state for conditional styling.
83
+ */
84
+ className?: string | ((props: { isActive: boolean }) => string)
85
+ }
86
+
87
+ /**
88
+ * A primitive NavLink component that provides active state detection.
89
+ *
90
+ * It combines the Link primitive with path matching logic to determine
91
+ * if the link is currently active based on the browser's location.
92
+ */
93
+ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
94
+ (props, ref) => {
95
+ const { href, end = false, className, children, ...rest } = props
96
+ const location = useLocation()
97
+ const localizedHref = useLocalizedTo(href ?? '')
98
+
99
+ const isActive = end
100
+ ? location.pathname === localizedHref
101
+ : location.pathname.startsWith(localizedHref as string)
102
+
103
+ const resolvedClassName =
104
+ typeof className === 'function'
105
+ ? className({ isActive })
106
+ : cn(className, isActive && 'active')
107
+ const resolvedChildren =
108
+ typeof children === 'function' ? children({ isActive }) : children
109
+
110
+ return (
111
+ <Link
112
+ {...rest}
113
+ ref={ref}
114
+ href={href}
115
+ className={resolvedClassName as any}
116
+ >
117
+ {resolvedChildren as any}
118
+ </Link>
119
+ )
120
+ },
121
+ )
122
+ NavLink.displayName = 'NavLink'
@@ -0,0 +1,159 @@
1
+ 'use client'
2
+
3
+ import { Check, ChevronRight, Dot } from 'lucide-react'
4
+ import React from 'react'
5
+ import * as RAC from 'react-aria-components'
6
+ import { Popover, type PopoverProps } from './popover'
7
+ import { cn } from '@client/utils/cn'
8
+
9
+ /**
10
+ * MenuTrigger wraps a trigger (usually a Button) and a Menu.
11
+ */
12
+ export interface MenuTriggerProps extends RAC.MenuTriggerProps {
13
+ placement?: PopoverProps['placement']
14
+ }
15
+
16
+ export function MenuTrigger(props: MenuTriggerProps) {
17
+ const [trigger, menu] = (
18
+ React.Children.toArray(props.children) as React.ReactElement[]
19
+ ).slice(0, 2)
20
+ return (
21
+ <RAC.MenuTrigger {...props}>
22
+ {trigger as any}
23
+ <Popover placement={props.placement} className="min-w-[200px]">
24
+ {menu as any}
25
+ </Popover>
26
+ </RAC.MenuTrigger>
27
+ )
28
+ }
29
+
30
+ /**
31
+ * SubmenuTrigger for nested menus.
32
+ */
33
+ export function SubmenuTrigger(props: RAC.SubmenuTriggerProps) {
34
+ const [trigger, menu] = (
35
+ React.Children.toArray(props.children) as React.ReactElement[]
36
+ ).slice(0, 2)
37
+ return (
38
+ <RAC.SubmenuTrigger {...props}>
39
+ {trigger as any}
40
+ <Popover offset={-4} crossOffset={-4}>
41
+ {menu as any}
42
+ </Popover>
43
+ </RAC.SubmenuTrigger>
44
+ )
45
+ }
46
+
47
+ /**
48
+ * The Menu container.
49
+ */
50
+ export function Menu<T extends object>(props: RAC.MenuProps<T>) {
51
+ return (
52
+ <RAC.Menu
53
+ {...props}
54
+ className={RAC.composeRenderProps(props.className, (className) =>
55
+ cn('p-1.5 outline-none max-h-[inherit] overflow-auto', className),
56
+ )}
57
+ />
58
+ )
59
+ }
60
+
61
+ /**
62
+ * MenuItem with support for selection states and submenus.
63
+ */
64
+ export function MenuItem(props: RAC.MenuItemProps) {
65
+ const textValue =
66
+ props.textValue ||
67
+ (typeof props.children === 'string' ? props.children : undefined)
68
+
69
+ return (
70
+ <RAC.MenuItem
71
+ {...props}
72
+ textValue={textValue}
73
+ className={RAC.composeRenderProps(
74
+ props.className,
75
+ (className, { isFocused, isPressed, isDisabled }) =>
76
+ cn(
77
+ 'group relative flex flex-row items-center gap-2.5 px-3 py-1.5 rounded-lg outline-none cursor-default transition-all duration-200',
78
+ 'text-text-main text-[0.8125rem]',
79
+ isFocused && 'bg-primary-500/10 text-primary-600 shadow-sm',
80
+ isPressed && 'scale-[0.98] bg-primary-500/15',
81
+ isDisabled && 'opacity-40 grayscale pointer-events-none',
82
+ className,
83
+ ),
84
+ )}
85
+ >
86
+ {RAC.composeRenderProps(
87
+ props.children,
88
+ (children, { selectionMode, isSelected, hasSubmenu }) => (
89
+ <>
90
+ {selectionMode !== 'none' && (
91
+ <span className="flex items-center w-4 h-4 shrink-0 justify-center">
92
+ {isSelected && selectionMode === 'multiple' && (
93
+ <Check className="w-3.5 h-3.5 stroke-[2.5px] text-primary-500 animate-in zoom-in-50 duration-200" />
94
+ )}
95
+ {isSelected && selectionMode === 'single' && (
96
+ <Dot className="w-6 h-6 text-primary-500 animate-in zoom-in-50 duration-200" />
97
+ )}
98
+ </span>
99
+ )}
100
+ <div className="flex-1 flex flex-row items-center gap-2.5 truncate font-medium">
101
+ {children}
102
+ </div>
103
+ {hasSubmenu && (
104
+ <ChevronRight className="w-4 h-4 ml-auto text-text-muted group-focused:text-primary-500/70 transition-colors" />
105
+ )}
106
+ </>
107
+ ),
108
+ )}
109
+ </RAC.MenuItem>
110
+ )
111
+ }
112
+
113
+ /**
114
+ * MenuSection for grouping items with an optional header.
115
+ */
116
+ export interface MenuSectionProps<T> extends RAC.MenuSectionProps<T> {
117
+ title?: string
118
+ }
119
+
120
+ export function MenuSection<T extends object>({
121
+ title,
122
+ ...props
123
+ }: MenuSectionProps<T>) {
124
+ return (
125
+ <RAC.MenuSection
126
+ {...props}
127
+ className={cn('flex flex-col gap-0.5', props.className)}
128
+ >
129
+ {title && (
130
+ <RAC.Header className="px-3 py-2 text-[10px] font-bold uppercase tracking-[0.075em] text-text-muted/50 select-none">
131
+ {title}
132
+ </RAC.Header>
133
+ )}
134
+ <RAC.Collection items={props.items}>{props.children}</RAC.Collection>
135
+ </RAC.MenuSection>
136
+ )
137
+ }
138
+
139
+ /**
140
+ * MenuSeparator for visual division.
141
+ */
142
+ export function MenuSeparator(props: RAC.SeparatorProps) {
143
+ return (
144
+ <RAC.Separator
145
+ {...props}
146
+ className="mx-2 my-1.5 border-t border-border-subtle/50"
147
+ />
148
+ )
149
+ }
150
+
151
+ // Default export for convenience
152
+ export default {
153
+ Root: Menu,
154
+ Item: MenuItem,
155
+ Trigger: MenuTrigger,
156
+ SubTrigger: SubmenuTrigger,
157
+ Section: MenuSection,
158
+ Separator: MenuSeparator,
159
+ }