boltdocs 1.6.0 → 1.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 (51) hide show
  1. package/dist/{SearchDialog-3QICRMWF.css → SearchDialog-UOAW6IR3.css} +270 -113
  2. package/dist/{SearchDialog-J3KNRGNO.mjs → SearchDialog-YOXMFGH6.mjs} +1 -1
  3. package/dist/{chunk-HSPDIRTW.mjs → chunk-MULKZFVN.mjs} +872 -758
  4. package/dist/client/index.css +270 -113
  5. package/dist/client/index.d.mts +21 -7
  6. package/dist/client/index.d.ts +21 -7
  7. package/dist/client/index.js +637 -499
  8. package/dist/client/index.mjs +17 -1
  9. package/dist/client/ssr.css +270 -113
  10. package/dist/client/ssr.d.mts +3 -1
  11. package/dist/client/ssr.d.ts +3 -1
  12. package/dist/client/ssr.js +533 -412
  13. package/dist/client/ssr.mjs +3 -2
  14. package/dist/{config-DkZg5aCf.d.ts → config-D68h41CA.d.mts} +21 -2
  15. package/dist/{config-DkZg5aCf.d.mts → config-D68h41CA.d.ts} +21 -2
  16. package/dist/node/index.d.mts +10 -2
  17. package/dist/node/index.d.ts +10 -2
  18. package/dist/node/index.js +45 -21
  19. package/dist/node/index.mjs +45 -21
  20. package/dist/{types-DGIo1VKD.d.mts → types-CviV0GbX.d.mts} +13 -0
  21. package/dist/{types-DGIo1VKD.d.ts → types-CviV0GbX.d.ts} +13 -0
  22. package/package.json +1 -1
  23. package/src/client/app/index.tsx +8 -4
  24. package/src/client/index.ts +2 -0
  25. package/src/client/ssr.tsx +4 -1
  26. package/src/client/theme/components/mdx/Table.tsx +53 -0
  27. package/src/client/theme/components/mdx/index.ts +3 -0
  28. package/src/client/theme/components/mdx/mdx-components.css +49 -0
  29. package/src/client/theme/styles/markdown.css +8 -3
  30. package/src/client/theme/styles/variables.css +10 -9
  31. package/src/client/theme/ui/Layout/Layout.tsx +2 -10
  32. package/src/client/theme/ui/Layout/base.css +15 -3
  33. package/src/client/theme/ui/Link/Link.tsx +2 -2
  34. package/src/client/theme/ui/Link/LinkPreview.tsx +9 -14
  35. package/src/client/theme/ui/Link/link-preview.css +30 -27
  36. package/src/client/theme/ui/Navbar/Navbar.tsx +65 -17
  37. package/src/client/theme/ui/Navbar/Tabs.tsx +74 -0
  38. package/src/client/theme/ui/Navbar/navbar.css +111 -5
  39. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +65 -49
  40. package/src/client/theme/ui/OnThisPage/toc.css +30 -10
  41. package/src/client/theme/ui/Sidebar/Sidebar.tsx +97 -57
  42. package/src/client/theme/ui/Sidebar/sidebar.css +61 -67
  43. package/src/client/types.ts +10 -0
  44. package/src/node/config.ts +19 -1
  45. package/src/node/plugin/entry.ts +5 -1
  46. package/src/node/plugin/index.ts +2 -1
  47. package/src/node/routes/index.ts +12 -1
  48. package/src/node/routes/parser.ts +21 -7
  49. package/src/node/routes/types.ts +9 -1
  50. package/src/node/ssg/index.ts +2 -1
  51. package/src/node/ssg/options.ts +2 -0
@@ -3,19 +3,25 @@
3
3
  ═══════════════════════════════════════════════════════════ */
4
4
  .boltdocs-navbar {
5
5
  display: flex;
6
- align-items: center;
6
+ flex-direction: column;
7
7
  justify-content: center;
8
- height: var(--ld-navbar-height);
9
- padding: 0; /* Padding moved to container */
10
8
  background-color: var(--ld-navbar-bg);
11
- backdrop-filter: blur(var(--ld-navbar-blur));
12
- -webkit-backdrop-filter: blur(var(--ld-navbar-blur));
13
9
  border-bottom: 1px solid var(--ld-border-subtle);
14
10
  position: sticky;
15
11
  top: 0;
16
12
  z-index: 100;
17
13
  }
18
14
 
15
+ .boltdocs-navbar.has-tabs {
16
+ --ld-header-height: 102px !important;
17
+ height: 102px;
18
+ }
19
+
20
+ .boltdocs-navbar:not(.has-tabs) {
21
+ --ld-header-height: var(--ld-navbar-height);
22
+ height: var(--ld-navbar-height);
23
+ }
24
+
19
25
  .navbar-container {
20
26
  display: flex;
21
27
  align-items: center;
@@ -114,6 +120,16 @@
114
120
  transition:
115
121
  color 0.2s,
116
122
  background-color 0.2s;
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 0.35rem;
126
+ }
127
+
128
+ .navbar-external-icon {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ opacity: 0.6;
132
+ margin-top: 1px;
117
133
  }
118
134
 
119
135
  .navbar-links a:hover {
@@ -231,3 +247,93 @@
231
247
  width: 18px;
232
248
  height: 18px;
233
249
  }
250
+
251
+ /* Tabs Styles */
252
+ .boltdocs-tabs-container {
253
+ border-bottom: 1px solid var(--ld-border-subtle);
254
+ background: var(--ld-navbar-bg);
255
+ padding: 0;
256
+ height: 46px; /* 102px total = 56px navbar + 46px tabs */
257
+ }
258
+
259
+ .boltdocs-tabs {
260
+ max-width: 1440px;
261
+ margin: 0 auto;
262
+ display: flex;
263
+ gap: 2rem;
264
+ overflow-x: auto;
265
+ scrollbar-width: none;
266
+ padding: 0 1.5rem;
267
+ }
268
+
269
+ .boltdocs-tabs::-webkit-scrollbar {
270
+ display: none;
271
+ }
272
+
273
+ .boltdocs-tab-item {
274
+ padding: 0.85rem 0;
275
+ padding-bottom: calc(0.85rem + 1px); /* avoid clipping the indicator */
276
+ font-size: 0.875rem;
277
+ font-weight: 500;
278
+ color: var(--ld-text-muted);
279
+ text-decoration: none;
280
+ position: relative;
281
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
282
+ white-space: nowrap;
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 0.6rem;
286
+ opacity: 0.7;
287
+ }
288
+
289
+ .boltdocs-tab-item:hover {
290
+ color: var(--ld-text-main);
291
+ opacity: 1;
292
+ }
293
+
294
+ .tab-icon {
295
+ flex-shrink: 0;
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ transition: transform 0.2s ease;
300
+ }
301
+
302
+ .boltdocs-tab-item:hover .tab-icon {
303
+ transform: translateY(-1px);
304
+ }
305
+
306
+ .tab-icon.lucide-icon,
307
+ .tab-icon.svg-icon svg {
308
+ width: 18px;
309
+ height: 18px;
310
+ stroke-width: 2.25px;
311
+ }
312
+
313
+ .boltdocs-tab-item.active {
314
+ color: var(--ld-text-main);
315
+ font-weight: 600;
316
+ opacity: 1;
317
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
318
+ }
319
+
320
+ .boltdocs-tab-item::after {
321
+ content: "";
322
+ position: absolute;
323
+ bottom: 0px;
324
+ left: 0;
325
+ right: 0;
326
+ height: 3px;
327
+ background: var(--ld-primary);
328
+ border-radius: 2px 2px 0 0;
329
+ box-shadow: 0 0 12px rgba(var(--ld-primary-rgb), 0.6);
330
+ opacity: 0;
331
+ transform: scaleX(0);
332
+ transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
333
+ z-index: 10;
334
+ }
335
+
336
+ .boltdocs-tab-item.active::after {
337
+ opacity: 1;
338
+ transform: scaleX(1);
339
+ }
@@ -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 activeElement = listRef.current.querySelector(
45
+ const activeLink = listRef.current.querySelector(
46
46
  `a[href="#${activeId}"]`,
47
47
  ) as HTMLElement;
48
48
 
49
- if (activeElement) {
50
- const { offsetTop, offsetHeight } = activeElement;
49
+ if (activeLink) {
50
+ const top = activeLink.offsetTop;
51
+ const height = activeLink.offsetHeight;
52
+
51
53
  setIndicatorStyle({
52
- transform: `translateY(${offsetTop}px)`,
53
- height: `${offsetHeight}px`,
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
- // Find all entries that are intersecting
69
- const visibleEntries = entries.filter((entry) => entry.isIntersecting);
70
-
71
- if (visibleEntries.length > 0) {
72
- // If we have visible entries, find the one closest to the top of the viewport
73
- const closest = visibleEntries.reduce((prev, curr) => {
74
- return Math.abs(curr.boundingClientRect.top - 100) <
75
- Math.abs(prev.boundingClientRect.top - 100)
76
- ? curr
77
- : prev;
78
- });
79
- setActiveId(closest.target.id);
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
- observerRef.current = new IntersectionObserver(callback, {
84
- rootMargin: "-100px 0px -70% 0px",
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.closest(
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: "smooth",
160
+ behavior: "auto",
147
161
  block: "nearest",
148
162
  });
149
163
  }
@@ -179,21 +193,23 @@ export function OnThisPage({
179
193
  <nav className="boltdocs-on-this-page" aria-label="Table of contents">
180
194
  <p className="on-this-page-title">On this page</p>
181
195
  <div className="on-this-page-container">
182
- <div className="toc-indicator" style={indicatorStyle} />
183
- <ul className="on-this-page-list" ref={listRef}>
184
- {headings.map((h) => (
185
- <li key={h.id} className={h.level === 3 ? "toc-indent" : ""}>
186
- <a
187
- href={`#${h.id}`}
188
- className={`toc-link ${activeId === h.id ? "active" : ""}`}
189
- aria-current={activeId === h.id ? "true" : undefined}
190
- onClick={(e) => handleClick(e, h.id)}
191
- >
192
- {h.text}
193
- </a>
194
- </li>
195
- ))}
196
- </ul>
196
+ <div className="on-this-page-list-container">
197
+ <div className="toc-indicator" style={indicatorStyle} />
198
+ <ul className="on-this-page-list" ref={listRef}>
199
+ {headings.map((h) => (
200
+ <li key={h.id} className={h.level === 3 ? "toc-indent" : ""}>
201
+ <a
202
+ href={`#${h.id}`}
203
+ className={`toc-link ${activeId === h.id ? "active" : ""}`}
204
+ aria-current={activeId === h.id ? "true" : undefined}
205
+ onClick={(e) => handleClick(e, h.id)}
206
+ >
207
+ {h.text}
208
+ </a>
209
+ </li>
210
+ ))}
211
+ </ul>
212
+ </div>
197
213
  </div>
198
214
 
199
215
  {/* 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
- position: sticky;
9
- top: var(--ld-navbar-height);
10
- height: calc(100vh - var(--ld-navbar-height));
11
- overflow-y: auto;
12
- scrollbar-width: thin;
13
- scrollbar-color: var(--ld-bg-mute) transparent;
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: color 0.2s;
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: 2rem;
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-muted);
125
- background-color: rgba(255, 255, 255, 0.03);
144
+ color: var(--ld-text-main);
145
+ background-color: var(--ld-bg-mute);
126
146
  }
127
147
 
128
148
  .toc-help-link svg {
@@ -1,9 +1,10 @@
1
- import React, { useState } from "react";
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, ChevronLeft, PanelLeft } from "lucide-react";
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 routes) {
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
- {onToggle && (
112
- <div className="sidebar-collapse">
113
- <button
114
- className="sidebar-collapse-btn"
115
- onClick={onToggle}
116
- aria-label={isCollapsed ? "Expand Sidebar" : "Collapse Sidebar"}
117
- title={isCollapsed ? "Expand Sidebar" : "Collapse Sidebar"}
118
- >
119
- <PanelLeft size={18} />
120
- </button>
121
- </div>
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
- {!isCollapsed && (
125
- <>
126
- <nav aria-label="Main Navigation">
127
- <ul className="sidebar-list">
128
- {ungrouped.map((route) => (
129
- <li key={route.path}>
130
- <Link
131
- to={route.path === "" ? "/" : route.path}
132
- className={`sidebar-link ${location.pathname === route.path ? "active" : ""}`}
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
- <span className="sidebar-group-title">{group.title}</span>
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
- <span>{route.title}</span>
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>