boltdocs 1.7.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.
Files changed (38) hide show
  1. package/dist/{SearchDialog-YOXMFGH6.mjs → SearchDialog-6Z7CUAYJ.mjs} +8 -1
  2. package/dist/{SearchDialog-UOAW6IR3.css → SearchDialog-GOZ6X53X.css} +129 -14
  3. package/dist/{chunk-MULKZFVN.mjs → chunk-SFVOGJ2W.mjs} +269 -165
  4. package/dist/client/index.css +129 -14
  5. package/dist/client/index.d.mts +5 -7
  6. package/dist/client/index.d.ts +5 -7
  7. package/dist/client/index.js +586 -337
  8. package/dist/client/index.mjs +106 -5
  9. package/dist/client/ssr.css +129 -14
  10. package/dist/client/ssr.d.mts +1 -1
  11. package/dist/client/ssr.d.ts +1 -1
  12. package/dist/client/ssr.js +378 -230
  13. package/dist/client/ssr.mjs +1 -1
  14. package/dist/node/index.d.mts +2 -0
  15. package/dist/node/index.d.ts +2 -0
  16. package/dist/node/index.js +4 -1
  17. package/dist/node/index.mjs +4 -1
  18. package/dist/{types-CviV0GbX.d.ts → types-BbceAHA0.d.mts} +2 -0
  19. package/dist/{types-CviV0GbX.d.mts → types-BbceAHA0.d.ts} +2 -0
  20. package/package.json +1 -1
  21. package/src/client/app/index.tsx +8 -7
  22. package/src/client/theme/components/mdx/Table.tsx +108 -10
  23. package/src/client/theme/components/mdx/mdx-components.css +79 -0
  24. package/src/client/theme/styles/variables.css +24 -0
  25. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +46 -0
  26. package/src/client/theme/ui/ErrorBoundary/index.ts +1 -0
  27. package/src/client/theme/ui/Layout/Layout.tsx +8 -1
  28. package/src/client/theme/ui/Navbar/Tabs.tsx +37 -12
  29. package/src/client/theme/ui/Navbar/navbar.css +26 -18
  30. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +1 -8
  31. package/src/client/theme/ui/ProgressBar/ProgressBar.css +17 -0
  32. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +51 -0
  33. package/src/client/theme/ui/ProgressBar/index.ts +1 -0
  34. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +11 -1
  35. package/src/client/types.ts +2 -0
  36. package/src/node/routes/index.ts +1 -0
  37. package/src/node/routes/parser.ts +11 -0
  38. package/src/node/routes/types.ts +2 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AppShell
3
- } from "../chunk-MULKZFVN.mjs";
3
+ } from "../chunk-SFVOGJ2W.mjs";
4
4
  import "../chunk-FMTOYQLO.mjs";
5
5
 
6
6
  // src/client/ssr.tsx
@@ -80,6 +80,8 @@ interface RouteMeta {
80
80
  icon?: string;
81
81
  /** The tab this route belongs to, if tabs are configured */
82
82
  tab?: string;
83
+ /** The extracted plain-text content of the page for search indexing */
84
+ _content?: string;
83
85
  }
84
86
 
85
87
  declare function boltdocs(options?: BoltdocsPluginOptions): Promise<Plugin[]>;
@@ -80,6 +80,8 @@ interface RouteMeta {
80
80
  icon?: string;
81
81
  /** The tab this route belongs to, if tabs are configured */
82
82
  tab?: string;
83
+ /** The extracted plain-text content of the page for search indexing */
84
+ _content?: string;
83
85
  }
84
86
 
85
87
  declare function boltdocs(options?: BoltdocsPluginOptions): Promise<Plugin[]>;
@@ -529,6 +529,7 @@ function parseDocFile(file, docsDir, basePath, config) {
529
529
  }
530
530
  const sanitizedBadge = data.badge ? data.badge : void 0;
531
531
  const icon = data.icon ? String(data.icon) : void 0;
532
+ const plainText = content.replace(/^#+.*$/gm, "").replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1").replace(/<[^>]+>/g, "").replace(/\{[^\}]+\}/g, "").replace(/[_*`]/g, "").replace(/\n+/g, " ").trim();
532
533
  return {
533
534
  route: {
534
535
  path: finalPath,
@@ -542,7 +543,8 @@ function parseDocFile(file, docsDir, basePath, config) {
542
543
  version,
543
544
  badge: sanitizedBadge,
544
545
  icon,
545
- tab: inferredTab
546
+ tab: inferredTab,
547
+ _content: plainText
546
548
  },
547
549
  relativeDir: cleanDirName,
548
550
  isGroupIndex,
@@ -586,6 +588,7 @@ function compareByGroupPosition(a, b) {
586
588
  // src/node/routes/index.ts
587
589
  async function generateRoutes(docsDir, config, basePath = "/docs") {
588
590
  docCache.load();
591
+ docCache.invalidateAll();
589
592
  const files = await (0, import_fast_glob.default)(["**/*.md", "**/*.mdx"], {
590
593
  cwd: docsDir,
591
594
  absolute: true
@@ -111,6 +111,7 @@ function parseDocFile(file, docsDir, basePath, config) {
111
111
  }
112
112
  const sanitizedBadge = data.badge ? data.badge : void 0;
113
113
  const icon = data.icon ? String(data.icon) : void 0;
114
+ const plainText = content.replace(/^#+.*$/gm, "").replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1").replace(/<[^>]+>/g, "").replace(/\{[^\}]+\}/g, "").replace(/[_*`]/g, "").replace(/\n+/g, " ").trim();
114
115
  return {
115
116
  route: {
116
117
  path: finalPath,
@@ -124,7 +125,8 @@ function parseDocFile(file, docsDir, basePath, config) {
124
125
  version,
125
126
  badge: sanitizedBadge,
126
127
  icon,
127
- tab: inferredTab
128
+ tab: inferredTab,
129
+ _content: plainText
128
130
  },
129
131
  relativeDir: cleanDirName,
130
132
  isGroupIndex,
@@ -168,6 +170,7 @@ function compareByGroupPosition(a, b) {
168
170
  // src/node/routes/index.ts
169
171
  async function generateRoutes(docsDir, config, basePath = "/docs") {
170
172
  docCache.load();
173
+ docCache.invalidateAll();
171
174
  const files = await fastGlob(["**/*.md", "**/*.mdx"], {
172
175
  cwd: docsDir,
173
176
  absolute: true
@@ -44,6 +44,8 @@ interface ComponentRoute {
44
44
  };
45
45
  /** Optional icon for the route's group */
46
46
  groupIcon?: string;
47
+ /** The extracted plain-text content of the page for search indexing */
48
+ _content?: string;
47
49
  }
48
50
  /**
49
51
  * Configuration options for initializing the Boltdocs client app.
@@ -44,6 +44,8 @@ interface ComponentRoute {
44
44
  };
45
45
  /** Optional icon for the route's group */
46
46
  groupIcon?: string;
47
+ /** The extracted plain-text content of the page for search indexing */
48
+ _content?: string;
47
49
  }
48
50
  /**
49
51
  * Configuration options for initializing the Boltdocs client app.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boltdocs",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "A lightweight documentation generator for React projects.",
5
5
  "main": "dist/node/index.js",
6
6
  "module": "dist/node/index.mjs",
@@ -216,26 +216,27 @@ function ScrollHandler() {
216
216
  const { pathname, hash } = useLocation();
217
217
 
218
218
  useLayoutEffect(() => {
219
- // Only scroll if we are not in a pending transition state (if we were using useTransition)
220
- // For now, we ensure the scroll happens.
219
+ const container = document.querySelector(".boltdocs-content");
220
+ if (!container) return;
221
+
221
222
  if (hash) {
222
223
  const id = hash.replace("#", "");
223
224
  const element = document.getElementById(id);
224
225
  if (element) {
225
226
  const offset = 80;
226
- const bodyRect = document.body.getBoundingClientRect().top;
227
+ const containerRect = container.getBoundingClientRect().top;
227
228
  const elementRect = element.getBoundingClientRect().top;
228
- const elementPosition = elementRect - bodyRect;
229
- const offsetPosition = elementPosition - offset;
229
+ const elementPosition = elementRect - containerRect;
230
+ const offsetPosition = elementPosition - offset + container.scrollTop;
230
231
 
231
- window.scrollTo({
232
+ container.scrollTo({
232
233
  top: offsetPosition,
233
234
  behavior: "smooth",
234
235
  });
235
236
  return;
236
237
  }
237
238
  }
238
- window.scrollTo(0, 0);
239
+ container.scrollTo(0, 0);
239
240
  }, [pathname, hash]);
240
241
 
241
242
  return null;
@@ -1,23 +1,72 @@
1
- import React from "react";
1
+ import React, { useState, useMemo } from "react";
2
+ import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
2
3
 
3
4
  export interface TableProps {
4
5
  headers?: string[];
5
6
  data?: (string | React.ReactNode)[][];
6
7
  children?: React.ReactNode;
7
8
  className?: string;
9
+ sortable?: boolean;
10
+ paginated?: boolean;
11
+ pageSize?: number;
8
12
  }
9
13
 
10
- /**
11
- * A consistent, themed table component for documentation.
12
- * Can be used by passing structured 'headers' and 'data' props,
13
- * or by wrapping standard <thead>/<tbody> elements.
14
- */
15
14
  export function Table({
16
15
  headers,
17
16
  data,
18
17
  children,
19
18
  className = "",
19
+ sortable = false,
20
+ paginated = false,
21
+ pageSize = 10,
20
22
  }: TableProps) {
23
+ const [sortConfig, setSortConfig] = useState<{ key: number; direction: 'asc' | 'desc' } | null>(null);
24
+ const [currentPage, setCurrentPage] = useState(1);
25
+
26
+ const processedData = useMemo(() => {
27
+ if (!data) return [];
28
+ let items = [...data];
29
+
30
+ if (sortable && sortConfig !== null) {
31
+ items.sort((a, b) => {
32
+ const aVal = a[sortConfig.key];
33
+ const bVal = b[sortConfig.key];
34
+
35
+ // Simple string comparison for sorting
36
+ const aStr = typeof aVal === 'string' ? aVal : '';
37
+ const bStr = typeof bVal === 'string' ? bVal : '';
38
+
39
+ if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1;
40
+ if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1;
41
+ return 0;
42
+ });
43
+ }
44
+
45
+ return items;
46
+ }, [data, sortConfig, sortable]);
47
+
48
+ const totalPages = Math.ceil(processedData.length / pageSize);
49
+ const paginatedData = useMemo(() => {
50
+ if (!paginated) return processedData;
51
+ const start = (currentPage - 1) * pageSize;
52
+ return processedData.slice(start, start + pageSize);
53
+ }, [processedData, paginated, currentPage, pageSize]);
54
+
55
+ const requestSort = (index: number) => {
56
+ if (!sortable) return;
57
+ let direction: 'asc' | 'desc' = 'asc';
58
+ if (sortConfig && sortConfig.key === index && sortConfig.direction === 'asc') {
59
+ direction = 'desc';
60
+ }
61
+ setSortConfig({ key: index, direction });
62
+ };
63
+
64
+ const renderSortIcon = (index: number) => {
65
+ if (!sortable) return null;
66
+ if (sortConfig?.key !== index) return <ChevronDown size={14} className="ld-table-sort-icon ld-table-sort-icon--hidden" />;
67
+ return sortConfig.direction === 'asc' ? <ChevronUp size={14} className="ld-table-sort-icon" /> : <ChevronDown size={14} className="ld-table-sort-icon" />;
68
+ };
69
+
21
70
  const tableContent = children ? (
22
71
  children
23
72
  ) : (
@@ -26,14 +75,23 @@ export function Table({
26
75
  <thead>
27
76
  <tr>
28
77
  {headers.map((header, i) => (
29
- <th key={i}>{header}</th>
78
+ <th
79
+ key={i}
80
+ onClick={() => requestSort(i)}
81
+ className={sortable ? "ld-table-header--sortable" : ""}
82
+ >
83
+ <div className="ld-table-header-content">
84
+ {header}
85
+ {renderSortIcon(i)}
86
+ </div>
87
+ </th>
30
88
  ))}
31
89
  </tr>
32
90
  </thead>
33
91
  )}
34
- {data && (
92
+ {paginatedData && (
35
93
  <tbody>
36
- {data.map((row, i) => (
94
+ {paginatedData.map((row, i) => (
37
95
  <tr key={i}>
38
96
  {row.map((cell, j) => (
39
97
  <td key={j}>{cell}</td>
@@ -47,7 +105,47 @@ export function Table({
47
105
 
48
106
  return (
49
107
  <div className={`ld-table-container ${className}`.trim()}>
50
- <table className="ld-table">{tableContent}</table>
108
+ <div className="ld-table-wrapper">
109
+ <table className="ld-table">{tableContent}</table>
110
+ </div>
111
+
112
+ {paginated && totalPages > 1 && (
113
+ <div className="ld-table-pagination">
114
+ <div className="ld-table-pagination-info">
115
+ Page {currentPage} of {totalPages}
116
+ </div>
117
+ <div className="ld-table-pagination-controls">
118
+ <button
119
+ onClick={() => setCurrentPage(1)}
120
+ disabled={currentPage === 1}
121
+ className="ld-table-pagination-btn"
122
+ >
123
+ <ChevronsLeft size={16} />
124
+ </button>
125
+ <button
126
+ onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
127
+ disabled={currentPage === 1}
128
+ className="ld-table-pagination-btn"
129
+ >
130
+ <ChevronLeft size={16} />
131
+ </button>
132
+ <button
133
+ onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
134
+ disabled={currentPage === totalPages}
135
+ className="ld-table-pagination-btn"
136
+ >
137
+ <ChevronRight size={16} />
138
+ </button>
139
+ <button
140
+ onClick={() => setCurrentPage(totalPages)}
141
+ disabled={currentPage === totalPages}
142
+ className="ld-table-pagination-btn"
143
+ >
144
+ <ChevronsRight size={16} />
145
+ </button>
146
+ </div>
147
+ </div>
148
+ )}
51
149
  </div>
52
150
  );
53
151
  }
@@ -547,7 +547,13 @@
547
547
  border-radius: var(--ld-radius-lg);
548
548
  overflow: hidden;
549
549
  background: var(--ld-bg-soft);
550
+ display: flex;
551
+ flex-direction: column;
552
+ }
553
+
554
+ .ld-table-wrapper {
550
555
  overflow-x: auto;
556
+ scrollbar-width: thin;
551
557
  }
552
558
 
553
559
  .ld-table {
@@ -570,6 +576,36 @@
570
576
  font-size: 0.8125rem;
571
577
  text-transform: uppercase;
572
578
  letter-spacing: 0.04em;
579
+ white-space: nowrap;
580
+ }
581
+
582
+ .ld-table-header--sortable {
583
+ cursor: pointer;
584
+ transition: background-color 0.2s;
585
+ }
586
+
587
+ .ld-table-header--sortable:hover {
588
+ background-color: var(--ld-bg-soft);
589
+ }
590
+
591
+ .ld-table-header-content {
592
+ display: flex;
593
+ align-items: center;
594
+ gap: 0.5rem;
595
+ }
596
+
597
+ .ld-table-sort-icon {
598
+ opacity: 0.8;
599
+ color: var(--ld-color-primary);
600
+ transition: opacity 0.2s;
601
+ }
602
+
603
+ .ld-table-sort-icon--hidden {
604
+ opacity: 0;
605
+ }
606
+
607
+ .ld-table-header--sortable:hover .ld-table-sort-icon--hidden {
608
+ opacity: 0.3;
573
609
  }
574
610
 
575
611
  .ld-table td {
@@ -588,3 +624,46 @@
588
624
  background: rgba(255, 255, 255, 0.05);
589
625
  border-radius: 4px;
590
626
  }
627
+
628
+ /* Pagination */
629
+ .ld-table-pagination {
630
+ display: flex;
631
+ align-items: center;
632
+ justify-content: space-between;
633
+ padding: 0.75rem 1rem;
634
+ background: var(--ld-bg-mute);
635
+ border-top: 1px solid var(--ld-border-subtle);
636
+ font-size: 0.75rem;
637
+ color: var(--ld-text-dim);
638
+ }
639
+
640
+ .ld-table-pagination-controls {
641
+ display: flex;
642
+ align-items: center;
643
+ gap: 0.25rem;
644
+ }
645
+
646
+ .ld-table-pagination-btn {
647
+ display: flex;
648
+ align-items: center;
649
+ justify-content: center;
650
+ width: 1.75rem;
651
+ height: 1.75rem;
652
+ border-radius: var(--ld-radius-md);
653
+ border: 1px solid var(--ld-border-subtle);
654
+ background: var(--ld-bg-soft);
655
+ color: var(--ld-text-muted);
656
+ cursor: pointer;
657
+ transition: all 0.2s;
658
+ }
659
+
660
+ .ld-table-pagination-btn:hover:not(:disabled) {
661
+ background: var(--ld-bg-mute);
662
+ color: var(--ld-text-main);
663
+ border-color: var(--ld-border-strong);
664
+ }
665
+
666
+ .ld-table-pagination-btn:disabled {
667
+ opacity: 0.4;
668
+ cursor: not-allowed;
669
+ }
@@ -160,4 +160,28 @@
160
160
  --ld-radius-md: 8px;
161
161
  --ld-radius-lg: 12px;
162
162
  --ld-radius-full: 9999px;
163
+
164
+ /* ─ Custom Scrollbar ─ */
165
+ scrollbar-width: thin;
166
+ scrollbar-color: var(--ld-border-strong) transparent;
167
+ }
168
+
169
+ /* Chrome, Edge, and Safari */
170
+ *::-webkit-scrollbar {
171
+ width: 6px;
172
+ height: 6px;
173
+ }
174
+
175
+ *::-webkit-scrollbar-track {
176
+ background: transparent;
177
+ }
178
+
179
+ *::-webkit-scrollbar-thumb {
180
+ background-color: var(--ld-border-strong);
181
+ border-radius: 20px;
182
+ border: transparent;
183
+ }
184
+
185
+ *::-webkit-scrollbar-thumb:hover {
186
+ background-color: var(--ld-text-dim);
163
187
  }
@@ -0,0 +1,46 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from "react";
2
+
3
+ interface Props {
4
+ children?: ReactNode;
5
+ fallback?: ReactNode;
6
+ }
7
+
8
+ interface State {
9
+ hasError: boolean;
10
+ error?: Error;
11
+ }
12
+
13
+ export class ErrorBoundary extends Component<Props, State> {
14
+ public state: State = {
15
+ hasError: false
16
+ };
17
+
18
+ public static getDerivedStateFromError(error: Error): State {
19
+ return { hasError: true, error };
20
+ }
21
+
22
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
23
+ console.error("Uncaught error in Boltdocs Layout:", error, errorInfo);
24
+ }
25
+
26
+ public render() {
27
+ if (this.state.hasError) {
28
+ return this.props.fallback || (
29
+ <div className="boltdocs-error-boundary">
30
+ <div className="boltdocs-error-title">Something went wrong</div>
31
+ <p className="boltdocs-error-message">
32
+ {this.state.error?.message || "An unexpected error occurred while rendering this page."}
33
+ </p>
34
+ <button
35
+ className="boltdocs-error-retry"
36
+ onClick={() => this.setState({ hasError: false })}
37
+ >
38
+ Try again
39
+ </button>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return this.props.children;
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export * from "./ErrorBoundary";
@@ -18,6 +18,8 @@ import { OnThisPage } from "../OnThisPage";
18
18
  import { Head } from "../Head";
19
19
  import { Breadcrumbs } from "../Breadcrumbs";
20
20
  import { BackgroundGradient } from "../BackgroundGradient";
21
+ import { ProgressBar } from "../ProgressBar";
22
+ import { ErrorBoundary } from "../ErrorBoundary";
21
23
  import "../../styles.css";
22
24
 
23
25
  export interface ThemeLayoutProps {
@@ -108,6 +110,7 @@ export function ThemeLayout({
108
110
 
109
111
  return (
110
112
  <div className={`boltdocs-layout ${className}`} style={style}>
113
+ <ProgressBar />
111
114
  {background !== undefined ? background : <BackgroundGradient />}
112
115
  {head !== undefined ? (
113
116
  head
@@ -142,7 +145,11 @@ export function ThemeLayout({
142
145
  ) : (
143
146
  <Breadcrumbs routes={filteredRoutes} config={config} />
144
147
  )}
145
- <div className="boltdocs-page">{children}</div>
148
+ <div className="boltdocs-page">
149
+ <ErrorBoundary>
150
+ {children}
151
+ </ErrorBoundary>
152
+ </div>
146
153
 
147
154
  {/* Prev / Next Navigation */}
148
155
  {(prevPage || nextPage) && (
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { useLocation } from "react-router-dom";
3
3
  import { Link } from "../Link";
4
4
  import * as Icons from "lucide-react";
@@ -16,15 +16,40 @@ interface TabsProps {
16
16
 
17
17
  export function Tabs({ tabs, routes }: TabsProps) {
18
18
  const location = useLocation();
19
+ const containerRef = useRef<HTMLDivElement>(null);
20
+ const tabRefs = useRef<(HTMLAnchorElement | null)[]>([]);
21
+ const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({
22
+ opacity: 0,
23
+ transform: "translateX(0) scaleX(0)",
24
+ width: 0,
25
+ });
26
+
19
27
  const currentRoute = routes.find((r) => r.path === location.pathname);
20
28
  const currentTabId = currentRoute?.tab?.toLowerCase();
21
29
 
30
+ // Find the active index - default to 0 if no tab detected
31
+ const activeIndex = tabs.findIndex((tab) =>
32
+ currentTabId ? currentTabId === tab.id.toLowerCase() : false
33
+ );
34
+
35
+ const finalActiveIndex = activeIndex === -1 ? 0 : activeIndex;
36
+
37
+ useEffect(() => {
38
+ const activeTab = tabRefs.current[finalActiveIndex];
39
+ if (activeTab) {
40
+ setIndicatorStyle({
41
+ opacity: 1,
42
+ width: activeTab.offsetWidth,
43
+ transform: `translateX(${activeTab.offsetLeft}px)`,
44
+ });
45
+ }
46
+ }, [finalActiveIndex, tabs, location.pathname]);
47
+
22
48
  if (!tabs || tabs.length === 0) return null;
23
49
 
24
50
  const renderTabIcon = (iconName?: string) => {
25
51
  if (!iconName) return null;
26
52
 
27
- // 1. Raw SVG
28
53
  if (iconName.trim().startsWith("<svg")) {
29
54
  return (
30
55
  <span
@@ -34,33 +59,31 @@ export function Tabs({ tabs, routes }: TabsProps) {
34
59
  );
35
60
  }
36
61
 
37
- // 2. Lucide Icon
38
62
  const LucideIcon = (Icons as any)[iconName];
39
63
  if (LucideIcon) {
40
64
  return <LucideIcon size={16} className="tab-icon lucide-icon" />;
41
65
  }
42
66
 
43
- // 3. Fallback to image URL
44
67
  return <img src={iconName} alt="" className="tab-icon img-icon" />;
45
68
  };
46
69
 
47
70
  return (
48
71
  <div className="boltdocs-tabs-container">
49
- <div className="boltdocs-tabs">
72
+ <div className="boltdocs-tabs" ref={containerRef}>
50
73
  {tabs.map((tab, index) => {
51
- // If no tab is detected (e.g. root home page), default to the first tab (usually "Guides")
52
- const isActive = currentTabId
53
- ? currentTabId === tab.id.toLowerCase()
54
- : index === 0;
55
-
56
- // Find the first route for this tab to link to it
57
- const firstRoute = routes.find(r => r.tab && r.tab.toLowerCase() === tab.id.toLowerCase());
74
+ const isActive = index === finalActiveIndex;
75
+ const firstRoute = routes.find(
76
+ (r) => r.tab && r.tab.toLowerCase() === tab.id.toLowerCase()
77
+ );
58
78
  const linkTo = firstRoute ? firstRoute.path : "#";
59
79
 
60
80
  return (
61
81
  <Link
62
82
  key={tab.id}
63
83
  to={linkTo}
84
+ ref={(el) => {
85
+ tabRefs.current[index] = el;
86
+ }}
64
87
  className={`boltdocs-tab-item ${isActive ? "active" : ""}`}
65
88
  >
66
89
  {renderTabIcon(tab.icon)}
@@ -68,6 +91,8 @@ export function Tabs({ tabs, routes }: TabsProps) {
68
91
  </Link>
69
92
  );
70
93
  })}
94
+ {/* Sliding Indicator */}
95
+ <div className="boltdocs-tab-indicator" style={indicatorStyle} />
71
96
  </div>
72
97
  </div>
73
98
  );
@@ -250,10 +250,22 @@
250
250
 
251
251
  /* Tabs Styles */
252
252
  .boltdocs-tabs-container {
253
- border-bottom: 1px solid var(--ld-border-subtle);
253
+ position: relative;
254
254
  background: var(--ld-navbar-bg);
255
255
  padding: 0;
256
- height: 46px; /* 102px total = 56px navbar + 46px tabs */
256
+ height: 46px;
257
+ }
258
+
259
+ /* Border pseudo-element to allow indicator overlap */
260
+ .boltdocs-tabs-container::after {
261
+ content: "";
262
+ position: absolute;
263
+ bottom: 0px;
264
+ left: 0;
265
+ right: 0;
266
+ height: 1px;
267
+ background: var(--ld-border-subtle);
268
+ z-index: 10;
257
269
  }
258
270
 
259
271
  .boltdocs-tabs {
@@ -264,6 +276,8 @@
264
276
  overflow-x: auto;
265
277
  scrollbar-width: none;
266
278
  padding: 0 1.5rem;
279
+ position: relative;
280
+ height: 100%;
267
281
  }
268
282
 
269
283
  .boltdocs-tabs::-webkit-scrollbar {
@@ -272,7 +286,7 @@
272
286
 
273
287
  .boltdocs-tab-item {
274
288
  padding: 0.85rem 0;
275
- padding-bottom: calc(0.85rem + 1px); /* avoid clipping the indicator */
289
+ padding-bottom: calc(0.85rem + 1px);
276
290
  font-size: 0.875rem;
277
291
  font-weight: 500;
278
292
  color: var(--ld-text-muted);
@@ -284,6 +298,7 @@
284
298
  align-items: center;
285
299
  gap: 0.6rem;
286
300
  opacity: 0.7;
301
+ z-index: 20;
287
302
  }
288
303
 
289
304
  .boltdocs-tab-item:hover {
@@ -317,23 +332,16 @@
317
332
  text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
318
333
  }
319
334
 
320
- .boltdocs-tab-item::after {
321
- content: "";
335
+ .boltdocs-tab-indicator {
322
336
  position: absolute;
323
- bottom: 0px;
337
+ bottom: 0px; /* Aligned with the border pseudo-element */
324
338
  left: 0;
325
- right: 0;
326
339
  height: 3px;
327
- background: var(--ld-primary);
340
+ background: var(--ld-color-primary);
328
341
  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);
342
+ box-shadow: 0 -2px 15px var(--ld-color-primary-glow);
343
+ transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
344
+ z-index: 100; /* Higher than border pseudo-element */
345
+ pointer-events: none;
346
+ transform-origin: left;
339
347
  }