boltdocs 2.6.1 → 2.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 (177) hide show
  1. package/bin/boltdocs.js +0 -1
  2. package/dist/cache-CQKlT4fI.mjs +6 -0
  3. package/dist/cache-DorPMFgW.cjs +6 -0
  4. package/dist/cards-BLoSiRuL.d.ts +30 -0
  5. package/dist/cards-CQn9mXZS.d.cts +30 -0
  6. package/dist/chunk-Ds5LZdWN.cjs +6 -0
  7. package/dist/client/index.cjs +1 -1
  8. package/dist/client/index.d.cts +173 -1328
  9. package/dist/client/index.d.ts +172 -1327
  10. package/dist/client/index.js +1 -1
  11. package/dist/{package-c99Cs7mD.cjs → client/mdx.cjs} +1 -1
  12. package/dist/client/mdx.d.cts +128 -0
  13. package/dist/client/mdx.d.ts +129 -0
  14. package/dist/client/mdx.js +6 -0
  15. package/dist/client/primitives.cjs +6 -0
  16. package/dist/client/primitives.d.cts +818 -0
  17. package/dist/client/primitives.d.ts +818 -0
  18. package/dist/client/primitives.js +6 -0
  19. package/dist/client/theme/neutral.css +74 -361
  20. package/dist/client/theme/reset.css +189 -0
  21. package/dist/docs-layout-BlDhcQRv.cjs +6 -0
  22. package/dist/docs-layout-BvAOWEJw.js +6 -0
  23. package/dist/doctor-BQiQhCTl.cjs +6 -0
  24. package/dist/doctor-COpf35L2.cjs +20 -0
  25. package/dist/doctor-Dh1XP7Pz.mjs +20 -0
  26. package/dist/generator-DGW6pkCC.cjs +22 -0
  27. package/dist/generator-Dv3wEmhZ.mjs +22 -0
  28. package/dist/icons-dev-CrQLjoQp.js +6 -0
  29. package/dist/icons-dev-rzdz6Lf3.cjs +6 -0
  30. package/dist/image-BkIfa9oo.js +6 -0
  31. package/dist/image-DIGjCPe6.cjs +6 -0
  32. package/dist/mdx-K0WYBAJ3.js +7 -0
  33. package/dist/mdx-hpErbRUe.cjs +7 -0
  34. package/dist/meta-loader-0gJ4PtBC.cjs +6 -0
  35. package/dist/meta-loader-9IpAHWDS.mjs +6 -0
  36. package/dist/node/cli-entry.cjs +1 -2
  37. package/dist/node/cli-entry.mjs +1 -2
  38. package/dist/node/index.cjs +1 -1
  39. package/dist/node/index.d.cts +66 -13
  40. package/dist/node/index.d.mts +66 -14
  41. package/dist/node/index.mjs +1 -1
  42. package/dist/node/routes/worker.cjs +6 -0
  43. package/dist/node/routes/worker.d.cts +2 -0
  44. package/dist/node/routes/worker.d.mts +2 -0
  45. package/dist/node/routes/worker.mjs +6 -0
  46. package/dist/node-C2nWXElP.mjs +112 -0
  47. package/dist/node-CinkUtxV.cjs +112 -0
  48. package/dist/package-BMYLDBBP.cjs +6 -0
  49. package/dist/{package-DukYeKmD.mjs → package-HegMOTL_.mjs} +1 -1
  50. package/dist/parser-Bh11BsdA.cjs +6 -0
  51. package/dist/parser-D8eQvE7N.mjs +6 -0
  52. package/dist/parser-DYRzXWmA.cjs +6 -0
  53. package/dist/routes-CHf76Ye4.cjs +6 -0
  54. package/dist/routes-CMUZGI6T.mjs +6 -0
  55. package/dist/routes-Co1mRM58.cjs +6 -0
  56. package/dist/search-dialog-BACuzoVX.cjs +6 -0
  57. package/dist/search-dialog-BKagVT17.js +6 -0
  58. package/dist/search-dialog-C8w12eUx.js +6 -0
  59. package/dist/search-dialog-CGyrozZE.cjs +6 -0
  60. package/dist/search-dialog-D26rUnJ_.cjs +6 -0
  61. package/dist/sidebar-DKvg6KOc.d.cts +491 -0
  62. package/dist/sidebar-Dr1TiRIy.d.ts +491 -0
  63. package/dist/utils-BxNAXhZZ.mjs +7 -0
  64. package/dist/utils-Clzu7jvb.cjs +7 -0
  65. package/dist/worker-pool-Bd8Y9KDv.mjs +6 -0
  66. package/dist/worker-pool-BwU8ckrg.cjs +6 -0
  67. package/package.json +27 -8
  68. package/src/client/app/doc-page.tsx +9 -5
  69. package/src/client/app/docs-layout.tsx +17 -3
  70. package/src/client/app/head.tsx +122 -0
  71. package/src/client/app/helmet-compat.tsx +36 -0
  72. package/src/client/app/mdx-component.tsx +5 -52
  73. package/src/client/app/mdx-components-context.tsx +32 -8
  74. package/src/client/app/routes-context.tsx +2 -2
  75. package/src/client/app/scroll-handler.tsx +1 -1
  76. package/src/client/app/theme-context.tsx +5 -5
  77. package/src/client/app/ui-context.tsx +42 -0
  78. package/src/client/components/docs-layout-default.tsx +85 -0
  79. package/src/client/components/icons-dev.tsx +38 -15
  80. package/src/client/components/mdx/callout.tsx +97 -0
  81. package/src/client/components/mdx/card.tsx +73 -98
  82. package/src/client/components/mdx/cards.tsx +27 -0
  83. package/src/client/components/mdx/code-block.tsx +37 -17
  84. package/src/client/components/mdx/field.tsx +24 -56
  85. package/src/client/components/mdx/image.tsx +36 -15
  86. package/src/client/components/mdx/index.ts +19 -53
  87. package/src/client/components/mdx/table.tsx +46 -148
  88. package/src/client/components/mdx/typographics.tsx +120 -0
  89. package/src/client/components/mdx/{hooks/use-code-block.ts → use-code-block.ts} +5 -7
  90. package/src/client/components/primitives/breadcrumbs.tsx +5 -24
  91. package/src/client/components/primitives/button.tsx +3 -142
  92. package/src/client/components/primitives/code-block.tsx +104 -97
  93. package/src/client/components/{docs-layout.tsx → primitives/docs-layout.tsx} +15 -24
  94. package/src/client/components/primitives/error-boundary.tsx +107 -0
  95. package/src/client/components/primitives/heading.tsx +128 -0
  96. package/src/client/components/primitives/helpers/observer.ts +62 -32
  97. package/src/client/components/primitives/image.tsx +26 -0
  98. package/src/client/components/primitives/link.tsx +50 -52
  99. package/src/client/components/primitives/menu.tsx +25 -49
  100. package/src/client/components/primitives/navbar.tsx +234 -59
  101. package/src/client/components/primitives/on-this-page.tsx +169 -40
  102. package/src/client/components/primitives/page-nav.tsx +11 -39
  103. package/src/client/components/primitives/popover.tsx +12 -30
  104. package/src/client/components/primitives/search-dialog.tsx +77 -71
  105. package/src/client/components/primitives/sidebar.tsx +312 -119
  106. package/src/client/components/primitives/skeleton.tsx +1 -1
  107. package/src/client/components/primitives/tabs.tsx +5 -16
  108. package/src/client/components/primitives/tooltip.tsx +1 -1
  109. package/src/client/components/ui-base/banner.tsx +66 -0
  110. package/src/client/components/ui-base/breadcrumbs.tsx +26 -20
  111. package/src/client/components/ui-base/copy-markdown.tsx +43 -35
  112. package/src/client/components/ui-base/error-boundary.tsx +9 -46
  113. package/src/client/components/ui-base/github-stars.tsx +5 -3
  114. package/src/client/components/ui-base/index.ts +3 -3
  115. package/src/client/components/ui-base/last-updated.tsx +27 -0
  116. package/src/client/components/ui-base/navbar.tsx +183 -89
  117. package/src/client/components/ui-base/not-found.tsx +11 -9
  118. package/src/client/components/ui-base/on-this-page.tsx +8 -104
  119. package/src/client/components/ui-base/page-nav.tsx +23 -9
  120. package/src/client/components/ui-base/search-dialog.tsx +111 -36
  121. package/src/client/components/ui-base/search-highlight.tsx +10 -0
  122. package/src/client/components/ui-base/sidebar.tsx +77 -154
  123. package/src/client/components/ui-base/tabs.tsx +20 -7
  124. package/src/client/components/ui-base/theme-toggle.tsx +88 -10
  125. package/src/client/components/ui-base/version-i18n.tsx +80 -0
  126. package/src/client/hooks/index.ts +2 -1
  127. package/src/client/hooks/use-analytics.ts +272 -0
  128. package/src/client/hooks/use-i18n.ts +120 -53
  129. package/src/client/hooks/use-localized-to.ts +70 -30
  130. package/src/client/hooks/use-navbar.ts +69 -39
  131. package/src/client/hooks/use-page-nav.ts +28 -25
  132. package/src/client/hooks/use-routes.ts +64 -81
  133. package/src/client/hooks/use-search-highlight.ts +185 -0
  134. package/src/client/hooks/use-search.ts +12 -3
  135. package/src/client/hooks/use-sidebar.ts +183 -77
  136. package/src/client/hooks/use-tabs.ts +3 -4
  137. package/src/client/hooks/use-version.ts +46 -18
  138. package/src/client/index.ts +13 -86
  139. package/src/client/mdx.ts +2 -0
  140. package/src/client/primitives.ts +19 -0
  141. package/src/client/ssg/boltdocs-shell.tsx +78 -57
  142. package/src/client/ssg/create-routes.tsx +290 -50
  143. package/src/client/ssg/mdx-page.tsx +2 -1
  144. package/src/client/store/boltdocs-context.tsx +83 -12
  145. package/src/client/theme/neutral.css +74 -361
  146. package/src/client/theme/reset.css +189 -0
  147. package/src/client/types.ts +10 -2
  148. package/src/client/utils/path.ts +9 -0
  149. package/src/client/utils/react-to-text.ts +24 -24
  150. package/src/client/virtual.d.ts +1 -1
  151. package/src/shared/types.ts +97 -21
  152. package/dist/node-CWN8U_p8.mjs +0 -88
  153. package/dist/node-D5iosYXv.cjs +0 -88
  154. package/dist/search-dialog-3lvKsbVG.js +0 -6
  155. package/dist/search-dialog-DMK5OpgH.cjs +0 -6
  156. package/dist/use-search-C9bxCqfF.js +0 -6
  157. package/dist/use-search-DcfZSunO.cjs +0 -6
  158. package/src/client/components/mdx/admonition.tsx +0 -91
  159. package/src/client/components/mdx/badge.tsx +0 -41
  160. package/src/client/components/mdx/button.tsx +0 -35
  161. package/src/client/components/mdx/component-preview.tsx +0 -37
  162. package/src/client/components/mdx/component-props.tsx +0 -83
  163. package/src/client/components/mdx/file-tree.tsx +0 -325
  164. package/src/client/components/mdx/hooks/use-component-preview.ts +0 -16
  165. package/src/client/components/mdx/hooks/useTable.ts +0 -74
  166. package/src/client/components/mdx/hooks/useTabs.ts +0 -68
  167. package/src/client/components/mdx/link.tsx +0 -38
  168. package/src/client/components/mdx/list.tsx +0 -192
  169. package/src/client/components/mdx/tabs.tsx +0 -135
  170. package/src/client/components/mdx/video.tsx +0 -68
  171. package/src/client/components/primitives/index.ts +0 -19
  172. package/src/client/components/primitives/navigation-menu.tsx +0 -114
  173. package/src/client/components/ui-base/head.tsx +0 -76
  174. package/src/client/components/ui-base/loading.tsx +0 -57
  175. package/src/client/components/ui-base/powered-by.tsx +0 -25
  176. package/src/client/hooks/use-onthispage.ts +0 -23
  177. package/src/client/utils/use-on-change.ts +0 -15
@@ -1,54 +1,52 @@
1
- import { Link } from './link'
1
+ import {
2
+ type ReactNode,
3
+ useRef,
4
+ useLayoutEffect,
5
+ useEffect,
6
+ useState,
7
+ } from 'react'
2
8
  import * as RAC from 'react-aria-components'
3
- import { ChevronRight } from 'lucide-react'
4
9
  import { cn } from '../../utils/cn'
10
+ import { useUI } from '../../app/ui-context'
11
+ import { Link } from './link'
12
+ import { ChevronRight } from 'lucide-react'
5
13
  import type { ComponentBase } from './types'
6
14
  import type { ComponentRoute } from '../../types'
15
+ import { useSidebar } from '../../hooks/use-sidebar'
16
+ import { useLocalizedTo } from '../../hooks/use-localized-to'
17
+ import * as LucideIcons from 'lucide-react'
18
+ import virtualIcons from 'virtual:boltdocs-icons'
7
19
 
8
- export interface SidebarGroupProps extends ComponentBase {
9
- title?: string
10
- icon?: React.ElementType
11
- }
20
+ // Persistent scroll position across navigation (SPA)
21
+ let sidebarScrollPos = 0
12
22
 
13
- export interface SidebarSubGroupProps extends SidebarLinkProps {
14
- isOpen?: boolean
15
- onToggle?: () => void
16
- children?: React.ReactNode
17
- }
18
-
19
- export interface SidebarLinkProps extends ComponentBase {
20
- label: string
21
- href: string
22
- active?: boolean
23
- icon?: React.ElementType
24
- badge?: ComponentRoute['badge']
23
+ function getIcon(iconName?: string): React.ElementType | undefined {
24
+ if (!iconName) return undefined
25
+ const icons = { ...LucideIcons, ...virtualIcons } as unknown as Record<
26
+ string,
27
+ React.ElementType
28
+ >
29
+ const IconComponent = icons[iconName] || icons[iconName + 'Icon']
30
+ return IconComponent || undefined
25
31
  }
26
32
 
33
+ /**
34
+ * Internal Badge component for links
35
+ */
27
36
  const Badge = ({ badge }: { badge: ComponentRoute['badge'] }) => {
28
37
  const colors = {
29
- new: 'bg-primary-500/20 text-primary-500',
30
- updated: 'bg-gray-500/20 text-gray-500',
31
- deprecated: 'bg-red-500/20 text-red-500',
32
- }
33
-
34
- // Expire Badge
35
- if (typeof badge === 'object' && badge?.expires) {
36
- const expireDate = new Date(badge.expires)
37
- const today = new Date()
38
- const diffTime = expireDate.getTime() - today.getTime()
39
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
40
-
41
- if (diffDays === 0) {
42
- return null
43
- }
38
+ new: 'bg-primary-500/10 text-primary-500 border border-primary-500/20',
39
+ updated: 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20',
40
+ deprecated: 'bg-danger-500/10 text-danger-500 border border-danger-500/20',
44
41
  }
45
42
 
46
43
  const text = typeof badge === 'string' ? badge : badge?.text
44
+ if (!text) return null
47
45
 
48
46
  return (
49
47
  <span
50
48
  className={cn(
51
- 'ml-auto flex h-4.5 items-center rounded-full text-[9px] font-medium px-1.5 py-0.5 text-center whitespace-nowrap',
49
+ 'ml-auto flex h-5 items-center rounded-md text-[10px] font-bold px-1.5 py-0.5 uppercase tracking-wider',
52
50
  colors[text as keyof typeof colors] || colors.new,
53
51
  )}
54
52
  >
@@ -57,53 +55,140 @@ const Badge = ({ badge }: { badge: ComponentRoute['badge'] }) => {
57
55
  )
58
56
  }
59
57
 
60
- export const Sidebar = ({ children, className }: ComponentBase) => {
58
+ /**
59
+ * Desktop Sidebar Container
60
+ */
61
+ export function SidebarRoot({ children, className }: ComponentBase) {
61
62
  return (
62
63
  <aside
63
64
  className={cn(
64
- 'boltdocs-sidebar sticky top-navbar hidden lg:flex flex-col shrink-0',
65
- 'w-sidebar h-full',
66
- 'overflow-y-auto border-r border-border-subtle bg-bg-main',
67
- 'py-6 px-4',
65
+ 'hidden lg:flex flex-col w-sidebar sticky top-navbar h-[calc(100vh-var(--spacing-navbar))] border-r border-subtle bg-main',
68
66
  className,
69
67
  )}
70
68
  >
71
- <nav className="flex-1 space-y-6">{children}</nav>
69
+ {children}
72
70
  </aside>
73
71
  )
74
72
  }
75
73
 
76
- const SidebarGroup = ({
77
- children,
74
+ /**
75
+ * Mobile Sidebar Modal
76
+ */
77
+ export function SidebarMobile({ children, className }: ComponentBase) {
78
+ const { isSidebarOpen, closeSidebar } = useUI()
79
+
80
+ return (
81
+ <RAC.ModalOverlay
82
+ isOpen={isSidebarOpen}
83
+ onOpenChange={(open) => !open && closeSidebar()}
84
+ isDismissable={true}
85
+ className={cn(
86
+ 'fixed inset-0 z-50 bg-black/20 backdrop-blur-sm lg:hidden',
87
+ 'entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out duration-300',
88
+ )}
89
+ >
90
+ <RAC.Modal
91
+ className={cn(
92
+ 'fixed top-0 left-0 bottom-0 w-80 bg-main border-r border-subtle shadow-2xl outline-none',
93
+ 'entering:animate-in entering:slide-in-from-left exiting:animate-out exiting:slide-out-to-left duration-300',
94
+ className,
95
+ )}
96
+ >
97
+ <RAC.Dialog className="h-full focus:outline-none outline-none flex flex-col">
98
+ {children}
99
+ </RAC.Dialog>
100
+ </RAC.Modal>
101
+ </RAC.ModalOverlay>
102
+ )
103
+ }
104
+
105
+ /**
106
+ * Shared Header for Sidebar
107
+ */
108
+ export function SidebarHeader({ children, className }: ComponentBase) {
109
+ return (
110
+ <div
111
+ className={cn(
112
+ 'flex items-center justify-between p-4 border-b border-subtle',
113
+ className,
114
+ )}
115
+ >
116
+ {children}
117
+ </div>
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Scrollable Content Wrapper
123
+ */
124
+ export function SidebarContent({ children, className }: ComponentBase) {
125
+ const scrollRef = useRef<HTMLDivElement>(null)
126
+
127
+ // Restore scroll position
128
+ useLayoutEffect(() => {
129
+ if (scrollRef.current) {
130
+ scrollRef.current.scrollTop = sidebarScrollPos
131
+ }
132
+ }, [])
133
+
134
+ // Save scroll position
135
+ useEffect(() => {
136
+ const el = scrollRef.current
137
+ if (!el) return
138
+ const handleScroll = () => {
139
+ sidebarScrollPos = el.scrollTop
140
+ }
141
+ el.addEventListener('scroll', handleScroll, { passive: true })
142
+ return () => el.removeEventListener('scroll', handleScroll)
143
+ }, [])
144
+
145
+ return (
146
+ <div
147
+ ref={scrollRef}
148
+ className={cn(
149
+ 'flex-1 overflow-y-auto p-4 pb-16 custom-scrollbar',
150
+ className,
151
+ )}
152
+ >
153
+ <nav className="flex flex-col gap-6">{children}</nav>
154
+ </div>
155
+ )
156
+ }
157
+
158
+ /**
159
+ * Navigation Group
160
+ */
161
+ export const SidebarGroup = ({
78
162
  title,
79
163
  icon: Icon,
164
+ children,
80
165
  className,
81
- }: SidebarGroupProps) => {
166
+ }: { title?: string; icon?: React.ElementType } & ComponentBase) => {
82
167
  return (
83
- <div className={cn('space-y-1', className)}>
168
+ <div className={className}>
84
169
  {title && (
85
- <div
86
- className={cn(
87
- 'flex w-full items-center justify-between px-2 py-1.5 text-sm font-semibold',
88
- 'text-text-main',
89
- )}
90
- >
91
- <div className="flex items-center gap-2">
92
- {Icon && <Icon size={14} />}
93
- {title}
94
- </div>
95
- </div>
170
+ <h4 className="px-2 mb-2 flex items-center gap-2 text-[11px] font-bold uppercase tracking-widest text-muted/50">
171
+ {Icon && <Icon size={12} />}
172
+ {title}
173
+ </h4>
96
174
  )}
97
- {children && <div className="space-y-0.5">{children}</div>}
175
+ <div className="flex flex-col gap-0.5">{children}</div>
98
176
  </div>
99
177
  )
100
178
  }
101
179
 
102
- const SidebarGroupItem = ({ children, className }: ComponentBase) => {
103
- return <div className={cn(className)}>{children}</div>
180
+ /**
181
+ * Sidebar Link
182
+ */
183
+ export interface SidebarLinkProps extends ComponentBase {
184
+ label: string
185
+ href: string
186
+ active?: boolean
187
+ icon?: React.ElementType
188
+ badge?: ComponentRoute['badge']
104
189
  }
105
190
 
106
- const SidebarLink = ({
191
+ export const SidebarLink = ({
107
192
  label,
108
193
  href,
109
194
  active,
@@ -115,13 +200,10 @@ const SidebarLink = ({
115
200
  <Link
116
201
  href={href}
117
202
  className={cn(
118
- 'group flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm outline-none',
119
- 'transition-all duration-200 ease-in-out',
120
- 'focus-visible:ring-2 focus-visible:ring-primary-500/30',
121
- {
122
- 'bg-primary-500/10 text-primary-500 font-medium': active,
123
- 'text-text-muted hover:bg-bg-muted hover:text-text-main': !active,
124
- },
203
+ 'group flex items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm transition-all outline-none',
204
+ active
205
+ ? 'bg-primary-500/10 text-primary-500 font-medium shadow-sm'
206
+ : 'text-muted hover:bg-surface hover:text-body',
125
207
  className,
126
208
  )}
127
209
  >
@@ -129,9 +211,7 @@ const SidebarLink = ({
129
211
  <Icon
130
212
  size={16}
131
213
  className={cn(
132
- active
133
- ? 'text-primary-500'
134
- : 'text-text-muted group-hover:text-text-main',
214
+ active ? 'text-primary-500' : 'text-muted group-hover:text-body',
135
215
  )}
136
216
  />
137
217
  )}
@@ -141,62 +221,54 @@ const SidebarLink = ({
141
221
  )
142
222
  }
143
223
 
144
- const SidebarSubGroup = ({
224
+ /**
225
+ * Nested SubGroup
226
+ */
227
+ export const SidebarSubGroup = ({
145
228
  label,
146
229
  href,
147
230
  active,
148
231
  icon: Icon,
149
232
  badge,
150
- className,
151
- isOpen = false,
233
+ isOpen,
152
234
  onToggle,
153
235
  children,
154
- }: SidebarSubGroupProps) => {
236
+ className,
237
+ }: SidebarLinkProps & {
238
+ isOpen: boolean
239
+ onToggle: () => void
240
+ children: ReactNode
241
+ }) => {
155
242
  return (
156
- <div className="space-y-0.5">
157
- <div className="flex items-center w-full">
158
- <Link
243
+ <div className="flex flex-col gap-0.5">
244
+ <div className="group relative flex items-center">
245
+ <SidebarLink
246
+ label={label}
159
247
  href={href}
160
- className={cn(
161
- 'group flex flex-1 items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm outline-none',
162
- 'transition-all duration-200 ease-in-out',
163
- 'focus-visible:ring-2 focus-visible:ring-primary-500/30',
164
- active
165
- ? 'bg-primary-500/10 text-primary-500 font-medium'
166
- : 'text-text-muted hover:bg-bg-surface hover:text-text-main hover:translate-x-1',
167
- className,
168
- )}
248
+ active={active}
249
+ icon={Icon}
250
+ badge={badge}
251
+ className={cn('flex-1 pr-8', className)}
252
+ />
253
+ <button
254
+ onClick={(e) => {
255
+ e.preventDefault()
256
+ e.stopPropagation()
257
+ onToggle()
258
+ }}
259
+ className="absolute right-1 p-1.5 text-muted hover:text-body transition-colors outline-none cursor-pointer"
169
260
  >
170
- {Icon && (
171
- <Icon
172
- size={16}
173
- className={cn(
174
- active
175
- ? 'text-primary-500'
176
- : 'text-text-muted group-hover:text-text-main',
177
- )}
178
- />
179
- )}
180
- <span className="truncate">{label}</span>
181
- {badge && <Badge badge={badge} />}
182
- </Link>
183
- {children && (
184
- <RAC.Button
185
- onPress={onToggle}
186
- className="flex items-center justify-center p-1.5 ml-1 rounded-md text-text-muted hover:bg-bg-surface hover:text-text-main transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30 cursor-pointer"
187
- >
188
- <ChevronRight
189
- size={16}
190
- className={cn(
191
- 'transition-transform duration-200',
192
- isOpen && 'rotate-90',
193
- )}
194
- />
195
- </RAC.Button>
196
- )}
261
+ <ChevronRight
262
+ size={14}
263
+ className={cn(
264
+ 'transition-transform duration-200',
265
+ isOpen && 'rotate-90',
266
+ )}
267
+ />
268
+ </button>
197
269
  </div>
198
- {isOpen && children && (
199
- <div className="pl-4 ml-[7px] border-l border-border-subtle/50 space-y-0.5 mt-0.5">
270
+ {isOpen && (
271
+ <div className="ml-4 pl-3 border-l border-subtle/50 mt-0.5 flex flex-col gap-0.5">
200
272
  {children}
201
273
  </div>
202
274
  )}
@@ -204,8 +276,129 @@ const SidebarSubGroup = ({
204
276
  )
205
277
  }
206
278
 
207
- Sidebar.Root = Sidebar
208
- Sidebar.Group = SidebarGroup
209
- Sidebar.SubGroup = SidebarSubGroup
210
- Sidebar.GroupItem = SidebarGroupItem
211
- Sidebar.Link = SidebarLink
279
+ /**
280
+ * Automated single-route rendering primitive
281
+ */
282
+ export interface SidebarItemProps extends ComponentBase {
283
+ route: ComponentRoute
284
+ activePath: string
285
+ }
286
+
287
+ export function SidebarItem({
288
+ route,
289
+ activePath,
290
+ className,
291
+ }: SidebarItemProps) {
292
+ const localizedHref = useLocalizedTo(route.path)
293
+ const isCurrent =
294
+ activePath ===
295
+ (localizedHref.endsWith('/') ? localizedHref.slice(0, -1) : localizedHref)
296
+ const hasChildren = !!route.routes?.length || !!route.subRoutes?.length
297
+ const children = route.routes || route.subRoutes
298
+
299
+ const [isOpen, setIsOpen] = useState(() =>
300
+ activePath.startsWith(localizedHref),
301
+ )
302
+ const [prevActivePath, setPrevActivePath] = useState(activePath)
303
+
304
+ if (activePath !== prevActivePath) {
305
+ setPrevActivePath(activePath)
306
+ if (activePath.startsWith(localizedHref)) {
307
+ setIsOpen(true)
308
+ }
309
+ }
310
+
311
+ if (hasChildren) {
312
+ return (
313
+ <SidebarSubGroup
314
+ label={route.title}
315
+ href={route.path}
316
+ active={isCurrent}
317
+ icon={getIcon(route.icon)}
318
+ badge={route.badge}
319
+ isOpen={isOpen}
320
+ onToggle={() => setIsOpen(!isOpen)}
321
+ className={className}
322
+ >
323
+ {children?.map((subRoute) => (
324
+ <SidebarItem
325
+ key={subRoute.path}
326
+ route={subRoute}
327
+ activePath={activePath}
328
+ />
329
+ ))}
330
+ </SidebarSubGroup>
331
+ )
332
+ }
333
+
334
+ return (
335
+ <SidebarLink
336
+ label={route.title}
337
+ href={route.path}
338
+ active={isCurrent}
339
+ icon={getIcon(route.icon)}
340
+ badge={route.badge}
341
+ className={className}
342
+ />
343
+ )
344
+ }
345
+
346
+ /**
347
+ * High-level automated routes data rendering primitive
348
+ */
349
+ export interface SidebarItemsProps extends ComponentBase {
350
+ routes: ComponentRoute[]
351
+ }
352
+
353
+ export function SidebarItems({ routes, className }: SidebarItemsProps) {
354
+ const { groups, ungrouped, activePath } = useSidebar(routes)
355
+
356
+ return (
357
+ <div className={cn('flex flex-col gap-6', className)}>
358
+ {ungrouped.length > 0 && (
359
+ <SidebarGroup>
360
+ {ungrouped.map((route) => (
361
+ <SidebarItem
362
+ key={route.path}
363
+ route={route}
364
+ activePath={activePath}
365
+ />
366
+ ))}
367
+ </SidebarGroup>
368
+ )}
369
+
370
+ {groups.map((group) => (
371
+ <SidebarGroup
372
+ key={group.title}
373
+ title={group.title}
374
+ icon={getIcon(group.icon)}
375
+ >
376
+ {group.routes.map((route) => (
377
+ <SidebarItem
378
+ key={route.path}
379
+ route={route}
380
+ activePath={activePath}
381
+ />
382
+ ))}
383
+ </SidebarGroup>
384
+ ))}
385
+ </div>
386
+ )
387
+ }
388
+
389
+ /**
390
+ * Main Sidebar Export
391
+ */
392
+ export const Sidebar = Object.assign(SidebarRoot, {
393
+ Root: SidebarRoot,
394
+ Mobile: SidebarMobile,
395
+ Header: SidebarHeader,
396
+ Content: SidebarContent,
397
+ Group: SidebarGroup,
398
+ Link: SidebarLink,
399
+ SubGroup: SidebarSubGroup,
400
+ Item: SidebarItem,
401
+ Items: SidebarItems,
402
+ })
403
+
404
+ export default Sidebar
@@ -16,7 +16,7 @@ export function Skeleton({
16
16
  return (
17
17
  <div
18
18
  className={cn(
19
- 'animate-pulse bg-bg-muted',
19
+ 'animate-pulse bg-soft',
20
20
  variant === 'circle' ? 'rounded-full' : 'rounded-md',
21
21
  className,
22
22
  )}
@@ -25,10 +25,7 @@ const TabsList = ({ children, className = '' }: ComponentBase) => {
25
25
  return (
26
26
  <div
27
27
  role="tablist"
28
- className={cn(
29
- 'relative flex flex-row items-center border-b border-border-subtle',
30
- className,
31
- )}
28
+ className={cn('relative flex flex-row items-center', className)}
32
29
  >
33
30
  {children}
34
31
  </div>
@@ -46,9 +43,9 @@ const TabsItem = ({
46
43
  <button
47
44
  role="tab"
48
45
  aria-selected={selected}
46
+ data-selected={selected}
49
47
  className={cn(
50
- 'flex items-center gap-2 px-4 py-2 text-sm font-medium transition-colors outline-none cursor-pointer bg-transparent border-none',
51
- selected ? 'text-primary-500' : 'text-text-muted hover:text-text-main',
48
+ 'outline-none cursor-pointer bg-transparent border-none',
52
49
  className,
53
50
  )}
54
51
  {...props}
@@ -59,19 +56,11 @@ const TabsItem = ({
59
56
  }
60
57
 
61
58
  const TabsContent = ({ children, className = '' }: ComponentBase) => {
62
- return <div className={cn('p-4 outline-none', className)}>{children}</div>
59
+ return <div className={cn('outline-none', className)}>{children}</div>
63
60
  }
64
61
 
65
62
  const TabsIndicator = ({ className = '', style }: TabsIndicatorProps) => {
66
- return (
67
- <div
68
- className={cn(
69
- 'absolute bottom-0 h-0.5 bg-primary-500 transition-all duration-300',
70
- className,
71
- )}
72
- style={style}
73
- />
74
- )
63
+ return <div className={cn('absolute bottom-0', className)} style={style} />
75
64
  }
76
65
 
77
66
  Tabs.Root = Tabs
@@ -31,7 +31,7 @@ const TooltipContent = ({
31
31
  offset={8}
32
32
  className={(values) =>
33
33
  cn(
34
- 'group z-50 overflow-visible rounded-md bg-bg-surface/90 px-2.5 py-1.5 text-xs font-medium text-text-main shadow-lg backdrop-blur-md ring-1 ring-border-subtle outline-hidden select-none',
34
+ 'group z-50 overflow-visible rounded-md bg-surface px-2.5 py-1.5 text-xs font-medium text-body ring-1 ring-subtle outline-hidden select-none',
35
35
  'data-entering:animate-in data-entering:fade-in data-entering:zoom-in-95 data-entering:duration-100',
36
36
  'data-exiting:animate-out data-exiting:fade-out data-exiting:zoom-out-95 data-exiting:duration-75',
37
37
  'data-[placement=top]:slide-in-from-bottom-1',
@@ -0,0 +1,66 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { X } from 'lucide-react'
3
+
4
+ export interface BannerProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * If true, shows a close button to dismiss the banner.
7
+ */
8
+ dismissible?: boolean
9
+ /**
10
+ * Unique identifier for the banner. If provided and dismissible is true,
11
+ * the dismissed state will be saved in localStorage so it doesn't reappear
12
+ * on subsequent visits.
13
+ */
14
+ id?: string
15
+ }
16
+
17
+ export function Banner({
18
+ children,
19
+ className = '',
20
+ dismissible = false,
21
+ id = 'boltdocs-banner',
22
+ ...props
23
+ }: BannerProps) {
24
+ const [isVisible, setIsVisible] = useState(true)
25
+
26
+ useEffect(() => {
27
+ if (dismissible && id) {
28
+ const isDismissed = localStorage.getItem(
29
+ `boltdocs-banner-dismissed-${id}`,
30
+ )
31
+ if (isDismissed === 'true') {
32
+ setIsVisible(false)
33
+ }
34
+ }
35
+ }, [dismissible, id])
36
+
37
+ const handleDismiss = () => {
38
+ setIsVisible(false)
39
+ if (dismissible && id) {
40
+ localStorage.setItem(`boltdocs-banner-dismissed-${id}`, 'true')
41
+ }
42
+ }
43
+
44
+ if (!isVisible) return null
45
+
46
+ return (
47
+ <div
48
+ className={`relative flex items-center justify-center px-4 py-2.5 text-xs font-semibold tracking-wide bg-primary-500/10 dark:bg-primary-500/15 text-primary-700 dark:text-primary-300 border-b border-primary-500/20 select-none animate-in fade-in duration-300 ${className}`}
49
+ {...props}
50
+ >
51
+ <div className="flex-1 text-center flex items-center justify-center gap-2">
52
+ {children}
53
+ </div>
54
+ {dismissible && (
55
+ <button
56
+ onClick={handleDismiss}
57
+ className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 opacity-70 hover:opacity-100 transition-all duration-300 rounded-xl hover:bg-primary-500/10 cursor-pointer border-none bg-transparent flex items-center justify-center outline-none"
58
+ aria-label="Dismiss banner"
59
+ >
60
+ <X className="w-3.5 h-3.5" />
61
+ </button>
62
+ )}
63
+ </div>
64
+ )
65
+ }
66
+ export default Banner