boltdocs 1.7.0 → 1.8.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 (42) hide show
  1. package/dist/{SearchDialog-UOAW6IR3.css → SearchDialog-4ANHNJTL.css} +213 -28
  2. package/dist/{SearchDialog-YOXMFGH6.mjs → SearchDialog-6Z7CUAYJ.mjs} +8 -1
  3. package/dist/{chunk-MULKZFVN.mjs → chunk-SFVOGJ2W.mjs} +269 -165
  4. package/dist/client/index.css +211 -26
  5. package/dist/client/index.d.mts +25 -6
  6. package/dist/client/index.d.ts +25 -6
  7. package/dist/client/index.js +614 -336
  8. package/dist/client/index.mjs +134 -5
  9. package/dist/client/ssr.css +211 -26
  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/index.ts +2 -0
  23. package/src/client/theme/components/mdx/Field.tsx +60 -0
  24. package/src/client/theme/components/mdx/Table.tsx +108 -10
  25. package/src/client/theme/components/mdx/index.ts +3 -0
  26. package/src/client/theme/components/mdx/mdx-components.css +174 -0
  27. package/src/client/theme/styles/variables.css +25 -1
  28. package/src/client/theme/ui/ErrorBoundary/ErrorBoundary.tsx +46 -0
  29. package/src/client/theme/ui/ErrorBoundary/index.ts +1 -0
  30. package/src/client/theme/ui/Layout/Layout.tsx +8 -1
  31. package/src/client/theme/ui/Link/link-preview.css +1 -20
  32. package/src/client/theme/ui/Navbar/Tabs.tsx +37 -12
  33. package/src/client/theme/ui/Navbar/navbar.css +26 -18
  34. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +1 -8
  35. package/src/client/theme/ui/ProgressBar/ProgressBar.css +17 -0
  36. package/src/client/theme/ui/ProgressBar/ProgressBar.tsx +51 -0
  37. package/src/client/theme/ui/ProgressBar/index.ts +1 -0
  38. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +11 -1
  39. package/src/client/types.ts +2 -0
  40. package/src/node/routes/index.ts +1 -0
  41. package/src/node/routes/parser.ts +11 -0
  42. 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.8.0",
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;
@@ -30,6 +30,7 @@ export {
30
30
  List,
31
31
  FileTree,
32
32
  Table,
33
+ Field,
33
34
  } from "./theme/components/mdx";
34
35
  export type {
35
36
  ButtonProps,
@@ -42,4 +43,5 @@ export type {
42
43
  ListProps,
43
44
  FileTreeProps,
44
45
  TableProps,
46
+ FieldProps,
45
47
  } from "./theme/components/mdx";
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+
3
+ export interface FieldProps {
4
+ /** The name of the property or field */
5
+ name: string;
6
+ /** The data type of the field (e.g., "string", "number", "boolean") */
7
+ type?: string;
8
+ /** The default value if not provided */
9
+ defaultValue?: string;
10
+ /** Whether the field is required */
11
+ required?: boolean;
12
+ /** Description or additional content */
13
+ children: React.ReactNode;
14
+ /** Optional anchor ID for linking */
15
+ id?: string;
16
+ /** Optional extra class name */
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * A highly aesthetic and readable component for documenting API fields/properties.
22
+ */
23
+ export function Field({
24
+ name,
25
+ type,
26
+ defaultValue,
27
+ required = false,
28
+ children,
29
+ id,
30
+ className = "",
31
+ }: FieldProps) {
32
+ return (
33
+ <div className={`ld-field ${className}`.trim()} id={id}>
34
+ <div className="ld-field__header">
35
+ <div className="ld-field__signature">
36
+ <code className="ld-field__name">{name}</code>
37
+ {type && (
38
+ <span className="ld-field__type-badge">
39
+ {type}
40
+ </span>
41
+ )}
42
+ {required && (
43
+ <span className="ld-field__required-badge">required</span>
44
+ )}
45
+ </div>
46
+
47
+ {defaultValue && (
48
+ <div className="ld-field__default">
49
+ <span className="ld-field__default-label">Default:</span>
50
+ <code className="ld-field__default-value">{defaultValue}</code>
51
+ </div>
52
+ )}
53
+ </div>
54
+
55
+ <div className="ld-field__content">
56
+ {children}
57
+ </div>
58
+ </div>
59
+ );
60
+ }
@@ -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
  }
@@ -22,3 +22,6 @@ export type { FileTreeProps } from "./FileTree";
22
22
 
23
23
  export { Table } from "./Table";
24
24
  export type { TableProps } from "./Table";
25
+
26
+ export { Field } from "./Field";
27
+ export type { FieldProps } from "./Field";
@@ -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,141 @@
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
+ }
670
+ /* ─── Field ───────────────────────────────────────────────── */
671
+ .ld-field {
672
+ margin: 1.5rem 0;
673
+ padding: 1.25rem;
674
+ border-radius: var(--ld-radius-lg);
675
+ border: 1px solid var(--ld-border-subtle);
676
+ background: var(--ld-bg-soft);
677
+ position: relative;
678
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
679
+ }
680
+
681
+ .ld-field:hover {
682
+ border-color: var(--ld-color-primary);
683
+ box-shadow: 0 4px 20px -8px rgba(127, 19, 236, 0.15);
684
+ }
685
+
686
+ .ld-field__header {
687
+ display: flex;
688
+ flex-wrap: wrap;
689
+ align-items: center;
690
+ justify-content: space-between;
691
+ gap: 1rem;
692
+ margin-bottom: 0.85rem;
693
+ padding-bottom: 0.85rem;
694
+ border-bottom: 1px solid var(--ld-border-subtle);
695
+ }
696
+
697
+ .ld-field__signature {
698
+ display: flex;
699
+ align-items: center;
700
+ gap: 0.75rem;
701
+ }
702
+
703
+ .ld-field__name {
704
+ font-family: var(--ld-font-mono);
705
+ font-size: 0.95rem;
706
+ font-weight: 700;
707
+ color: var(--ld-text-main);
708
+ background: rgba(127, 19, 236, 0.08);
709
+ padding: 0.2rem 0.6rem;
710
+ border-radius: var(--ld-radius-sm);
711
+ letter-spacing: -0.01em;
712
+ }
713
+
714
+ .ld-field__type-badge {
715
+ font-family: var(--ld-font-mono);
716
+ font-size: 0.75rem;
717
+ font-weight: 500;
718
+ color: var(--ld-color-primary);
719
+ background: var(--ld-color-primary-muted);
720
+ padding: 0.15rem 0.5rem;
721
+ border-radius: var(--ld-radius-full);
722
+ border: 1px solid rgba(127, 19, 236, 0.2);
723
+ }
724
+
725
+ .ld-field__required-badge {
726
+ font-size: 0.7rem;
727
+ font-weight: 700;
728
+ text-transform: uppercase;
729
+ color: var(--ld-ui-danger-text);
730
+ background: var(--ld-ui-danger-bg);
731
+ padding: 0.1rem 0.4rem;
732
+ border-radius: var(--ld-radius-sm);
733
+ letter-spacing: 0.05em;
734
+ }
735
+
736
+ .ld-field__default {
737
+ display: flex;
738
+ align-items: center;
739
+ gap: 0.5rem;
740
+ font-size: 0.8rem;
741
+ }
742
+
743
+ .ld-field__default-label {
744
+ color: var(--ld-text-dim);
745
+ font-weight: 500;
746
+ }
747
+
748
+ .ld-field__default-value {
749
+ font-family: var(--ld-font-mono);
750
+ color: var(--ld-text-main);
751
+ background: var(--ld-bg-mute);
752
+ padding: 0.15rem 0.4rem;
753
+ border-radius: var(--ld-radius-sm);
754
+ }
755
+
756
+ .ld-field__content {
757
+ font-size: 0.875rem;
758
+ line-height: 1.6;
759
+ color: var(--ld-text-muted);
760
+ }
761
+
762
+ .ld-field__content p {
763
+ margin: 0;
764
+ }
@@ -41,7 +41,7 @@
41
41
  --ld-code-text: #1f2937;
42
42
 
43
43
  /* ─ Customization ─ */
44
- --ld-navbar-bg: rgba(255, 255, 255, 0.82);
44
+ --ld-navbar-bg: #ffffff;
45
45
  --ld-navbar-blur: 12px;
46
46
  --ld-sidebar-bg: transparent;
47
47
  --ld-sidebar-blur: 0px;
@@ -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) && (