boltdocs 1.6.0 → 1.7.1
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.
- package/dist/{SearchDialog-J3KNRGNO.mjs → SearchDialog-6Z7CUAYJ.mjs} +8 -1
- package/dist/{SearchDialog-3QICRMWF.css → SearchDialog-GOZ6X53X.css} +385 -113
- package/dist/{chunk-HSPDIRTW.mjs → chunk-SFVOGJ2W.mjs} +955 -737
- package/dist/client/index.css +385 -113
- package/dist/client/index.d.mts +19 -7
- package/dist/client/index.d.ts +19 -7
- package/dist/client/index.js +964 -577
- package/dist/client/index.mjs +118 -1
- package/dist/client/ssr.css +385 -113
- package/dist/client/ssr.d.mts +3 -1
- package/dist/client/ssr.d.ts +3 -1
- package/dist/client/ssr.js +743 -474
- package/dist/client/ssr.mjs +3 -2
- package/dist/{config-DkZg5aCf.d.ts → config-D68h41CA.d.mts} +21 -2
- package/dist/{config-DkZg5aCf.d.mts → config-D68h41CA.d.ts} +21 -2
- package/dist/node/index.d.mts +12 -2
- package/dist/node/index.d.ts +12 -2
- package/dist/node/index.js +48 -21
- package/dist/node/index.mjs +48 -21
- package/dist/{types-DGIo1VKD.d.mts → types-BbceAHA0.d.mts} +15 -0
- package/dist/{types-DGIo1VKD.d.ts → types-BbceAHA0.d.ts} +15 -0
- package/package.json +1 -1
- package/src/client/app/index.tsx +16 -11
- package/src/client/index.ts +2 -0
- package/src/client/ssr.tsx +4 -1
- package/src/client/theme/components/mdx/Table.tsx +151 -0
- package/src/client/theme/components/mdx/index.ts +3 -0
- package/src/client/theme/components/mdx/mdx-components.css +128 -0
- package/src/client/theme/styles/markdown.css +8 -3
- package/src/client/theme/styles/variables.css +34 -9
- package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +46 -0
- package/src/client/theme/ui/ErrorBoundary/index.ts +1 -0
- package/src/client/theme/ui/Layout/Layout.tsx +10 -11
- package/src/client/theme/ui/Layout/base.css +15 -3
- package/src/client/theme/ui/Link/Link.tsx +2 -2
- package/src/client/theme/ui/Link/LinkPreview.tsx +9 -14
- package/src/client/theme/ui/Link/link-preview.css +30 -27
- package/src/client/theme/ui/Navbar/Navbar.tsx +65 -17
- package/src/client/theme/ui/Navbar/Tabs.tsx +99 -0
- package/src/client/theme/ui/Navbar/navbar.css +119 -5
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +66 -57
- package/src/client/theme/ui/OnThisPage/toc.css +30 -10
- package/src/client/theme/ui/ProgressBar/ProgressBar.css +17 -0
- package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +51 -0
- package/src/client/theme/ui/ProgressBar/index.ts +1 -0
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +11 -1
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +97 -57
- package/src/client/theme/ui/Sidebar/sidebar.css +61 -67
- package/src/client/types.ts +12 -0
- package/src/node/config.ts +19 -1
- package/src/node/plugin/entry.ts +5 -1
- package/src/node/plugin/index.ts +2 -1
- package/src/node/routes/index.ts +13 -1
- package/src/node/routes/parser.ts +32 -7
- package/src/node/routes/types.ts +11 -1
- package/src/node/ssg/index.ts +2 -1
- package/src/node/ssg/options.ts +2 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef, useCallback } from "react";
|
|
2
2
|
import { useLocation } from "react-router-dom";
|
|
3
|
-
import { Pencil, CircleHelp } from "lucide-react";
|
|
3
|
+
import { Pencil, CircleHelp, TextAlignStart } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
interface TocHeading {
|
|
6
6
|
id: string;
|
|
@@ -19,16 +19,16 @@ export function OnThisPage({
|
|
|
19
19
|
communityHelp?: string;
|
|
20
20
|
filePath?: string;
|
|
21
21
|
}) {
|
|
22
|
-
const [activeId, setActiveId] = useState<string>(
|
|
22
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
23
23
|
const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({});
|
|
24
24
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
25
25
|
const location = useLocation();
|
|
26
26
|
const listRef = useRef<HTMLUListElement>(null);
|
|
27
|
+
const visibleIdsRef = useRef<Set<string>>(new Set());
|
|
27
28
|
|
|
28
29
|
// Reset active ID when path changes
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
if (headings.length > 0) {
|
|
31
|
-
// Check if there's a hash in the URL
|
|
32
32
|
const hash = window.location.hash.substring(1);
|
|
33
33
|
if (hash && headings.some((h) => h.id === hash)) {
|
|
34
34
|
setActiveId(hash);
|
|
@@ -38,19 +38,21 @@ export function OnThisPage({
|
|
|
38
38
|
}
|
|
39
39
|
}, [location.pathname, headings]);
|
|
40
40
|
|
|
41
|
-
// Update indicator position
|
|
41
|
+
// Update indicator position for the single active ID
|
|
42
42
|
useEffect(() => {
|
|
43
43
|
if (!activeId || !listRef.current) return;
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const activeLink = listRef.current.querySelector(
|
|
46
46
|
`a[href="#${activeId}"]`,
|
|
47
47
|
) as HTMLElement;
|
|
48
48
|
|
|
49
|
-
if (
|
|
50
|
-
const
|
|
49
|
+
if (activeLink) {
|
|
50
|
+
const top = activeLink.offsetTop;
|
|
51
|
+
const height = activeLink.offsetHeight;
|
|
52
|
+
|
|
51
53
|
setIndicatorStyle({
|
|
52
|
-
transform: `translateY(${
|
|
53
|
-
height: `${
|
|
54
|
+
transform: `translateY(${top}px)`,
|
|
55
|
+
height: `${height}px`,
|
|
54
56
|
opacity: 1,
|
|
55
57
|
});
|
|
56
58
|
}
|
|
@@ -60,30 +62,49 @@ export function OnThisPage({
|
|
|
60
62
|
useEffect(() => {
|
|
61
63
|
if (headings.length === 0) return;
|
|
62
64
|
|
|
65
|
+
// Reset visible tracking on re-run
|
|
66
|
+
visibleIdsRef.current.clear();
|
|
67
|
+
|
|
63
68
|
if (observerRef.current) {
|
|
64
69
|
observerRef.current.disconnect();
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
const callback: IntersectionObserverCallback = (entries) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
entries.forEach((entry) => {
|
|
74
|
+
if (entry.isIntersecting) {
|
|
75
|
+
visibleIdsRef.current.add(entry.target.id);
|
|
76
|
+
} else {
|
|
77
|
+
visibleIdsRef.current.delete(entry.target.id);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Selection logic: first visible heading in document order
|
|
82
|
+
const firstVisible = headings.find((h) => visibleIdsRef.current.has(h.id));
|
|
83
|
+
|
|
84
|
+
if (firstVisible) {
|
|
85
|
+
setActiveId(firstVisible.id);
|
|
86
|
+
} else {
|
|
87
|
+
// Fallback: If nothing is visible, determine if we are at the top or bottom
|
|
88
|
+
const firstEl = document.getElementById(headings[0].id);
|
|
89
|
+
if (firstEl) {
|
|
90
|
+
const rect = firstEl.getBoundingClientRect();
|
|
91
|
+
if (rect.top > 200) {
|
|
92
|
+
setActiveId(headings[0].id);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If we are deep in the doc, don't change it (keep previous)
|
|
80
98
|
}
|
|
81
99
|
};
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
const observerOptions: IntersectionObserverInit = {
|
|
102
|
+
root: document.querySelector(".boltdocs-content"),
|
|
103
|
+
rootMargin: "-20% 0px -70% 0px",
|
|
85
104
|
threshold: [0, 1],
|
|
86
|
-
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
observerRef.current = new IntersectionObserver(callback, observerOptions);
|
|
87
108
|
|
|
88
109
|
const observeHeadings = () => {
|
|
89
110
|
headings.forEach(({ id }) => {
|
|
@@ -94,18 +115,13 @@ export function OnThisPage({
|
|
|
94
115
|
});
|
|
95
116
|
};
|
|
96
117
|
|
|
97
|
-
// Initial observation
|
|
98
118
|
observeHeadings();
|
|
99
|
-
|
|
100
|
-
// Re-observe if content changes
|
|
101
119
|
const timeoutId = setTimeout(observeHeadings, 1000);
|
|
102
120
|
|
|
103
|
-
// Scroll listener to detect bottom of page
|
|
104
121
|
const handleScroll = () => {
|
|
105
122
|
const scrollPosition = window.innerHeight + window.pageYOffset;
|
|
106
123
|
const bodyHeight = document.documentElement.scrollHeight;
|
|
107
124
|
|
|
108
|
-
// If we're within 50px of the bottom, activate the last heading
|
|
109
125
|
if (scrollPosition >= bodyHeight - 50) {
|
|
110
126
|
setActiveId(headings[headings.length - 1].id);
|
|
111
127
|
}
|
|
@@ -120,7 +136,7 @@ export function OnThisPage({
|
|
|
120
136
|
};
|
|
121
137
|
}, [headings, location.pathname]);
|
|
122
138
|
|
|
123
|
-
// Autoscroll TOC list when activeId changes
|
|
139
|
+
// Autoscroll TOC list when the activeId changes
|
|
124
140
|
useEffect(() => {
|
|
125
141
|
if (!activeId || !listRef.current) return;
|
|
126
142
|
|
|
@@ -129,9 +145,7 @@ export function OnThisPage({
|
|
|
129
145
|
) as HTMLElement;
|
|
130
146
|
|
|
131
147
|
if (activeLink) {
|
|
132
|
-
const container = listRef.current.
|
|
133
|
-
".boltdocs-on-this-page",
|
|
134
|
-
) as HTMLElement;
|
|
148
|
+
const container = listRef.current.parentElement as HTMLElement;
|
|
135
149
|
if (!container) return;
|
|
136
150
|
|
|
137
151
|
const linkRect = activeLink.getBoundingClientRect();
|
|
@@ -143,7 +157,7 @@ export function OnThisPage({
|
|
|
143
157
|
|
|
144
158
|
if (!isVisible) {
|
|
145
159
|
activeLink.scrollIntoView({
|
|
146
|
-
behavior: "
|
|
160
|
+
behavior: "auto",
|
|
147
161
|
block: "nearest",
|
|
148
162
|
});
|
|
149
163
|
}
|
|
@@ -155,14 +169,7 @@ export function OnThisPage({
|
|
|
155
169
|
e.preventDefault();
|
|
156
170
|
const el = document.getElementById(id);
|
|
157
171
|
if (el) {
|
|
158
|
-
|
|
159
|
-
const bodyRect = document.body.getBoundingClientRect().top;
|
|
160
|
-
const elementRect = el.getBoundingClientRect().top;
|
|
161
|
-
const elementPosition = elementRect - bodyRect;
|
|
162
|
-
const offsetPosition = elementPosition - offset;
|
|
163
|
-
|
|
164
|
-
window.scrollTo({
|
|
165
|
-
top: offsetPosition,
|
|
172
|
+
el.scrollIntoView({
|
|
166
173
|
behavior: "smooth",
|
|
167
174
|
});
|
|
168
175
|
|
|
@@ -179,21 +186,23 @@ export function OnThisPage({
|
|
|
179
186
|
<nav className="boltdocs-on-this-page" aria-label="Table of contents">
|
|
180
187
|
<p className="on-this-page-title">On this page</p>
|
|
181
188
|
<div className="on-this-page-container">
|
|
182
|
-
<div className="
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
<div className="on-this-page-list-container">
|
|
190
|
+
<div className="toc-indicator" style={indicatorStyle} />
|
|
191
|
+
<ul className="on-this-page-list" ref={listRef}>
|
|
192
|
+
{headings.map((h) => (
|
|
193
|
+
<li key={h.id} className={h.level === 3 ? "toc-indent" : ""}>
|
|
194
|
+
<a
|
|
195
|
+
href={`#${h.id}`}
|
|
196
|
+
className={`toc-link ${activeId === h.id ? "active" : ""}`}
|
|
197
|
+
aria-current={activeId === h.id ? "true" : undefined}
|
|
198
|
+
onClick={(e) => handleClick(e, h.id)}
|
|
199
|
+
>
|
|
200
|
+
{h.text}
|
|
201
|
+
</a>
|
|
202
|
+
</li>
|
|
203
|
+
))}
|
|
204
|
+
</ul>
|
|
205
|
+
</div>
|
|
197
206
|
</div>
|
|
198
207
|
|
|
199
208
|
{/* Need help? section */}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
width: var(--ld-toc-width);
|
|
6
6
|
flex-shrink: 0;
|
|
7
7
|
padding: 1.5rem 1rem;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
height: 100%;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
/* Hardware acceleration */
|
|
12
|
+
transform: translate3d(0, 0, 0);
|
|
13
|
+
backface-visibility: hidden;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.on-this-page-title {
|
|
@@ -21,11 +21,28 @@
|
|
|
21
21
|
color: var(--ld-text-dim);
|
|
22
22
|
margin: 0 0 0.75rem;
|
|
23
23
|
padding-left: 0.75rem;
|
|
24
|
+
flex-shrink: 0;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
.on-this-page-container {
|
|
27
28
|
position: relative;
|
|
28
29
|
padding-left: 2px;
|
|
30
|
+
flex: 0 1 auto;
|
|
31
|
+
min-height: 0; /* Important for children overflow */
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.on-this-page-list-container {
|
|
37
|
+
overflow-y: auto;
|
|
38
|
+
max-height: 50vh; /* Constrain height for large docs */
|
|
39
|
+
flex: 0 1 auto;
|
|
40
|
+
position: relative;
|
|
41
|
+
scrollbar-width: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.on-this-page-list-container::-webkit-scrollbar {
|
|
45
|
+
display: none;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
.on-this-page-list {
|
|
@@ -67,7 +84,9 @@
|
|
|
67
84
|
border-left: 2px solid transparent;
|
|
68
85
|
margin-left: -2px;
|
|
69
86
|
line-height: 1.4;
|
|
70
|
-
transition:
|
|
87
|
+
transition:
|
|
88
|
+
color 0.2s,
|
|
89
|
+
font-weight 0.2s;
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
.toc-link:hover {
|
|
@@ -81,9 +100,10 @@
|
|
|
81
100
|
|
|
82
101
|
/* ─── Need Help Section ──────────────────────────────────── */
|
|
83
102
|
.toc-help {
|
|
84
|
-
margin-top:
|
|
103
|
+
margin-top: 1.5rem;
|
|
85
104
|
padding-top: 1rem;
|
|
86
105
|
border-top: 1px solid var(--ld-border-subtle);
|
|
106
|
+
flex-shrink: 0;
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
.toc-help-title {
|
|
@@ -121,8 +141,8 @@
|
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
.toc-help-link:hover {
|
|
124
|
-
color: var(--ld-text-
|
|
125
|
-
background-color:
|
|
144
|
+
color: var(--ld-text-main);
|
|
145
|
+
background-color: var(--ld-bg-mute);
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
.toc-help-link svg {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.boltdocs-progress-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 2px;
|
|
7
|
+
z-index: 1000;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
background: transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.boltdocs-progress-bar {
|
|
13
|
+
height: 100%;
|
|
14
|
+
background: linear-gradient(90deg, var(--ld-color-primary), var(--ld-gradient-to, var(--ld-color-primary)));
|
|
15
|
+
box-shadow: 0 0 8px var(--ld-color-primary-glow);
|
|
16
|
+
transition: width 0.1s ease-out;
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import "./ProgressBar.css";
|
|
3
|
+
|
|
4
|
+
export function ProgressBar() {
|
|
5
|
+
const [progress, setProgress] = useState(0);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let container: Element | null = null;
|
|
9
|
+
let timer: any;
|
|
10
|
+
|
|
11
|
+
const handleScroll = () => {
|
|
12
|
+
if (!container) return;
|
|
13
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
14
|
+
if (scrollHeight <= clientHeight) {
|
|
15
|
+
setProgress(0);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const scrollPercent = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
19
|
+
setProgress(Math.min(100, Math.max(0, scrollPercent)));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const attachListener = () => {
|
|
23
|
+
container = document.querySelector(".boltdocs-content");
|
|
24
|
+
if (container) {
|
|
25
|
+
container.addEventListener("scroll", handleScroll);
|
|
26
|
+
handleScroll();
|
|
27
|
+
if (timer) clearInterval(timer);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (!attachListener()) {
|
|
34
|
+
timer = setInterval(attachListener, 100);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
if (container) container.removeEventListener("scroll", handleScroll);
|
|
39
|
+
if (timer) clearInterval(timer);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="boltdocs-progress-container">
|
|
45
|
+
<div
|
|
46
|
+
className="boltdocs-progress-bar"
|
|
47
|
+
style={{ width: `${progress}%` }}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ProgressBar";
|
|
@@ -49,7 +49,7 @@ export function SearchDialog({ routes }: { routes: ComponentRoute[] }) {
|
|
|
49
49
|
|
|
50
50
|
const results: SearchResult[] = [];
|
|
51
51
|
const lowerQuery = query.toLowerCase();
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
for (const route of routes) {
|
|
54
54
|
if (route.title && route.title.toLowerCase().includes(lowerQuery)) {
|
|
55
55
|
results.push({
|
|
@@ -71,6 +71,16 @@ export function SearchDialog({ routes }: { routes: ComponentRoute[] }) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
if (route._content && route._content.toLowerCase().includes(lowerQuery)) {
|
|
76
|
+
// If it's a content match but not a title/heading match, add it
|
|
77
|
+
// We only add the page itself for now
|
|
78
|
+
results.push({
|
|
79
|
+
title: route.title,
|
|
80
|
+
path: route.path,
|
|
81
|
+
groupTitle: route.groupTitle,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
// Deduplicate results by path
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import { useLocation } from "react-router-dom";
|
|
3
3
|
import { Link } from "../Link";
|
|
4
4
|
import { BoltdocsConfig } from "../../../../node/config";
|
|
5
5
|
import { PoweredBy } from "../PoweredBy";
|
|
6
|
-
import { ChevronRight
|
|
6
|
+
import { ChevronRight } from "lucide-react";
|
|
7
|
+
import * as LucideIcons from "lucide-react";
|
|
7
8
|
|
|
8
9
|
interface RouteItem {
|
|
9
10
|
path: string;
|
|
@@ -12,12 +13,16 @@ interface RouteItem {
|
|
|
12
13
|
groupTitle?: string;
|
|
13
14
|
sidebarPosition?: number;
|
|
14
15
|
badge?: string | { text: string; expires?: string };
|
|
16
|
+
icon?: string;
|
|
17
|
+
tab?: string;
|
|
18
|
+
groupIcon?: string;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
interface SidebarGroup {
|
|
18
22
|
slug: string;
|
|
19
23
|
title: string;
|
|
20
24
|
routes: RouteItem[];
|
|
25
|
+
icon?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -50,8 +55,6 @@ function renderBadge(badgeRaw: RouteItem["badge"]) {
|
|
|
50
55
|
|
|
51
56
|
if (!text) return null;
|
|
52
57
|
|
|
53
|
-
if (!text) return null;
|
|
54
|
-
|
|
55
58
|
let typeClass = "badge-default";
|
|
56
59
|
const lowerText = text.toLowerCase();
|
|
57
60
|
if (lowerText === "new") {
|
|
@@ -65,6 +68,43 @@ function renderBadge(badgeRaw: RouteItem["badge"]) {
|
|
|
65
68
|
return <span className={`sidebar-badge ${typeClass}`}>{text}</span>;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Renders an icon from a string (Lucide name or SVG).
|
|
73
|
+
*/
|
|
74
|
+
function renderIcon(iconName?: string, size = 16) {
|
|
75
|
+
if (!iconName) return null;
|
|
76
|
+
|
|
77
|
+
const trimmed = iconName.trim();
|
|
78
|
+
|
|
79
|
+
// Check if it's a raw SVG
|
|
80
|
+
if (trimmed.startsWith("<svg") || trimmed.includes("http")) {
|
|
81
|
+
if (trimmed.startsWith("<svg")) {
|
|
82
|
+
return (
|
|
83
|
+
<span
|
|
84
|
+
className="sidebar-icon svg-icon"
|
|
85
|
+
dangerouslySetInnerHTML={{ __html: trimmed }}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return (
|
|
90
|
+
<img
|
|
91
|
+
src={trimmed}
|
|
92
|
+
className="sidebar-icon"
|
|
93
|
+
style={{ width: size, height: size }}
|
|
94
|
+
alt=""
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if it's a Lucide icon
|
|
100
|
+
const IconComponent = (LucideIcons as any)[iconName];
|
|
101
|
+
if (IconComponent) {
|
|
102
|
+
return <IconComponent size={size} className="sidebar-icon lucide-icon" />;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
68
108
|
/**
|
|
69
109
|
* The sidebar navigation component.
|
|
70
110
|
* Groups documentation routes logically based on the `group` property.
|
|
@@ -76,20 +116,28 @@ function renderBadge(badgeRaw: RouteItem["badge"]) {
|
|
|
76
116
|
export function Sidebar({
|
|
77
117
|
routes,
|
|
78
118
|
config,
|
|
79
|
-
isCollapsed,
|
|
80
|
-
onToggle,
|
|
81
119
|
}: {
|
|
82
120
|
routes: RouteItem[];
|
|
83
121
|
config: BoltdocsConfig;
|
|
84
|
-
isCollapsed?: boolean;
|
|
85
|
-
onToggle?: () => void;
|
|
86
122
|
}) {
|
|
87
123
|
const location = useLocation();
|
|
88
124
|
|
|
125
|
+
// Find active tab based on the current route's metadata
|
|
126
|
+
const currentRoute = routes.find((r) => r.path === location.pathname);
|
|
127
|
+
const activeTabId = currentRoute?.tab?.toLowerCase();
|
|
128
|
+
|
|
129
|
+
// Filter routes by active tab if any tab is active
|
|
130
|
+
const filteredRoutes = activeTabId
|
|
131
|
+
? routes.filter((r) => {
|
|
132
|
+
if (!r.tab) return true; // Fallback for untabbed routes
|
|
133
|
+
return r.tab.toLowerCase() === activeTabId;
|
|
134
|
+
})
|
|
135
|
+
: routes;
|
|
136
|
+
|
|
89
137
|
const ungrouped: RouteItem[] = [];
|
|
90
|
-
const groupMap = new Map<string, SidebarGroup>();
|
|
138
|
+
const groupMap = new Map<string, SidebarGroup & { icon?: string }>();
|
|
91
139
|
|
|
92
|
-
for (const route of
|
|
140
|
+
for (const route of filteredRoutes) {
|
|
93
141
|
if (!route.group) {
|
|
94
142
|
ungrouped.push(route);
|
|
95
143
|
} else {
|
|
@@ -98,6 +146,7 @@ export function Sidebar({
|
|
|
98
146
|
slug: route.group,
|
|
99
147
|
title: route.groupTitle || route.group,
|
|
100
148
|
routes: [],
|
|
149
|
+
icon: (route as any).groupIcon,
|
|
101
150
|
});
|
|
102
151
|
}
|
|
103
152
|
groupMap.get(route.group)!.routes.push(route);
|
|
@@ -108,52 +157,38 @@ export function Sidebar({
|
|
|
108
157
|
|
|
109
158
|
return (
|
|
110
159
|
<aside className="boltdocs-sidebar">
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
<nav aria-label="Main Navigation">
|
|
161
|
+
<ul className="sidebar-list">
|
|
162
|
+
{ungrouped.map((route) => (
|
|
163
|
+
<li key={route.path}>
|
|
164
|
+
<Link
|
|
165
|
+
to={route.path === "" ? "/" : route.path}
|
|
166
|
+
className={`sidebar-link ${location.pathname === route.path ? "active" : ""}`}
|
|
167
|
+
aria-current={
|
|
168
|
+
location.pathname === route.path ? "page" : undefined
|
|
169
|
+
}
|
|
170
|
+
>
|
|
171
|
+
<div className="sidebar-link-content">
|
|
172
|
+
<div className="sidebar-link-title-container">
|
|
173
|
+
{renderIcon((route as any).icon)}
|
|
174
|
+
<span>{route.title}</span>
|
|
175
|
+
</div>
|
|
176
|
+
{renderBadge(route.badge)}
|
|
177
|
+
</div>
|
|
178
|
+
</Link>
|
|
179
|
+
</li>
|
|
180
|
+
))}
|
|
181
|
+
</ul>
|
|
123
182
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
aria-current={
|
|
134
|
-
location.pathname === route.path ? "page" : undefined
|
|
135
|
-
}
|
|
136
|
-
>
|
|
137
|
-
<div className="sidebar-link-content">
|
|
138
|
-
<span>{route.title}</span>
|
|
139
|
-
{renderBadge(route.badge)}
|
|
140
|
-
</div>
|
|
141
|
-
</Link>
|
|
142
|
-
</li>
|
|
143
|
-
))}
|
|
144
|
-
</ul>
|
|
145
|
-
|
|
146
|
-
{groups.map((group) => (
|
|
147
|
-
<SidebarGroupSection
|
|
148
|
-
key={group.slug}
|
|
149
|
-
group={group}
|
|
150
|
-
currentPath={location.pathname}
|
|
151
|
-
/>
|
|
152
|
-
))}
|
|
153
|
-
</nav>
|
|
154
|
-
{config.themeConfig?.poweredBy !== false && <PoweredBy />}
|
|
155
|
-
</>
|
|
156
|
-
)}
|
|
183
|
+
{groups.map((group) => (
|
|
184
|
+
<SidebarGroupSection
|
|
185
|
+
key={group.slug}
|
|
186
|
+
group={group}
|
|
187
|
+
currentPath={location.pathname}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</nav>
|
|
191
|
+
{config.themeConfig?.poweredBy !== false && <PoweredBy />}
|
|
157
192
|
</aside>
|
|
158
193
|
);
|
|
159
194
|
}
|
|
@@ -176,7 +211,9 @@ function SidebarGroupSection({
|
|
|
176
211
|
aria-expanded={open}
|
|
177
212
|
aria-controls={`sidebar-group-${group.slug}`}
|
|
178
213
|
>
|
|
179
|
-
<
|
|
214
|
+
<div className="sidebar-group-header-content">
|
|
215
|
+
<span className="sidebar-group-title">{group.title}</span>
|
|
216
|
+
</div>
|
|
180
217
|
<span className={`sidebar-group-chevron ${open ? "open" : ""}`}>
|
|
181
218
|
<ChevronRight size={16} />
|
|
182
219
|
</span>
|
|
@@ -191,7 +228,10 @@ function SidebarGroupSection({
|
|
|
191
228
|
aria-current={currentPath === route.path ? "page" : undefined}
|
|
192
229
|
>
|
|
193
230
|
<div className="sidebar-link-content">
|
|
194
|
-
<
|
|
231
|
+
<div className="sidebar-link-title-container">
|
|
232
|
+
{renderIcon((route as any).icon)}
|
|
233
|
+
<span>{route.title}</span>
|
|
234
|
+
</div>
|
|
195
235
|
{renderBadge(route.badge)}
|
|
196
236
|
</div>
|
|
197
237
|
</Link>
|