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.
- package/dist/{SearchDialog-YOXMFGH6.mjs → SearchDialog-6Z7CUAYJ.mjs} +8 -1
- package/dist/{SearchDialog-UOAW6IR3.css → SearchDialog-GOZ6X53X.css} +129 -14
- package/dist/{chunk-MULKZFVN.mjs → chunk-SFVOGJ2W.mjs} +269 -165
- package/dist/client/index.css +129 -14
- package/dist/client/index.d.mts +5 -7
- package/dist/client/index.d.ts +5 -7
- package/dist/client/index.js +586 -337
- package/dist/client/index.mjs +106 -5
- package/dist/client/ssr.css +129 -14
- package/dist/client/ssr.d.mts +1 -1
- package/dist/client/ssr.d.ts +1 -1
- package/dist/client/ssr.js +378 -230
- package/dist/client/ssr.mjs +1 -1
- package/dist/node/index.d.mts +2 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +4 -1
- package/dist/node/index.mjs +4 -1
- package/dist/{types-CviV0GbX.d.ts → types-BbceAHA0.d.mts} +2 -0
- package/dist/{types-CviV0GbX.d.mts → types-BbceAHA0.d.ts} +2 -0
- package/package.json +1 -1
- package/src/client/app/index.tsx +8 -7
- package/src/client/theme/components/mdx/Table.tsx +108 -10
- package/src/client/theme/components/mdx/mdx-components.css +79 -0
- package/src/client/theme/styles/variables.css +24 -0
- 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 +8 -1
- package/src/client/theme/ui/Navbar/Tabs.tsx +37 -12
- package/src/client/theme/ui/Navbar/navbar.css +26 -18
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +1 -8
- 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/types.ts +2 -0
- package/src/node/routes/index.ts +1 -0
- package/src/node/routes/parser.ts +11 -0
- package/src/node/routes/types.ts +2 -0
package/dist/client/ssr.mjs
CHANGED
package/dist/node/index.d.mts
CHANGED
|
@@ -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[]>;
|
package/dist/node/index.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/node/index.js
CHANGED
|
@@ -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
|
package/dist/node/index.mjs
CHANGED
|
@@ -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
package/src/client/app/index.tsx
CHANGED
|
@@ -216,26 +216,27 @@ function ScrollHandler() {
|
|
|
216
216
|
const { pathname, hash } = useLocation();
|
|
217
217
|
|
|
218
218
|
useLayoutEffect(() => {
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
227
|
+
const containerRect = container.getBoundingClientRect().top;
|
|
227
228
|
const elementRect = element.getBoundingClientRect().top;
|
|
228
|
-
const elementPosition = elementRect -
|
|
229
|
-
const offsetPosition = elementPosition - offset;
|
|
229
|
+
const elementPosition = elementRect - containerRect;
|
|
230
|
+
const offsetPosition = elementPosition - offset + container.scrollTop;
|
|
230
231
|
|
|
231
|
-
|
|
232
|
+
container.scrollTo({
|
|
232
233
|
top: offsetPosition,
|
|
233
234
|
behavior: "smooth",
|
|
234
235
|
});
|
|
235
236
|
return;
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
|
-
|
|
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
|
|
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
|
-
{
|
|
92
|
+
{paginatedData && (
|
|
35
93
|
<tbody>
|
|
36
|
-
{
|
|
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
|
-
<
|
|
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">
|
|
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
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
253
|
+
position: relative;
|
|
254
254
|
background: var(--ld-navbar-bg);
|
|
255
255
|
padding: 0;
|
|
256
|
-
height: 46px;
|
|
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);
|
|
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-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
}
|