create-trellis-docs 1.0.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 (148) hide show
  1. package/bin/index.js +29 -0
  2. package/lib/index.js +178 -0
  3. package/package.json +23 -0
  4. package/template/_gitignore +6 -0
  5. package/template/blog/2025-01-01-welcome.md +15 -0
  6. package/template/design-tokens.json +218 -0
  7. package/template/docs/faq/general.mdx +16 -0
  8. package/template/docs/faq/index.mdx +13 -0
  9. package/template/docs/getting-started.mdx +54 -0
  10. package/template/docs/guides/writing-docs.mdx +90 -0
  11. package/template/docusaurus.config.js.tpl +200 -0
  12. package/template/package.json.tpl +60 -0
  13. package/template/packages/faq-index-plugin/README.md +104 -0
  14. package/template/packages/faq-index-plugin/index.js +91 -0
  15. package/template/packages/faq-index-plugin/package.json +15 -0
  16. package/template/packages/redirects-plugin/README.md +186 -0
  17. package/template/packages/redirects-plugin/index.js +167 -0
  18. package/template/packages/redirects-plugin/package.json +15 -0
  19. package/template/packages/redirects-plugin/yarn.lock +31 -0
  20. package/template/redirects.json +1 -0
  21. package/template/scripts/build-tokens.js +34 -0
  22. package/template/sidebars.js +17 -0
  23. package/template/src/components/CustomSearch/CustomSearch.js +241 -0
  24. package/template/src/components/CustomSearch/CustomSearchContent.js +171 -0
  25. package/template/src/components/CustomSearch/NavbarSearch.js +211 -0
  26. package/template/src/components/CustomSearch/SearchContext.js +26 -0
  27. package/template/src/components/CustomSearch/styles.module.css +171 -0
  28. package/template/src/components/CustomSearch/wrapperInit.js +11 -0
  29. package/template/src/components/FaqTableOfContents/index.jsx +176 -0
  30. package/template/src/components/FaqTableOfContents/styles.module.css +167 -0
  31. package/template/src/components/Feedback/Feedback.js +310 -0
  32. package/template/src/components/Feedback/api.js +77 -0
  33. package/template/src/components/FlippingCard/FlippingCard.js +197 -0
  34. package/template/src/components/FlippingCard/styles.module.css +248 -0
  35. package/template/src/components/Glossary.js +57 -0
  36. package/template/src/css/custom.css +765 -0
  37. package/template/src/data/glossary.json +1 -0
  38. package/template/src/pages/index.js.tpl +38 -0
  39. package/template/src/theme/Admonition/Icon/Danger.js +11 -0
  40. package/template/src/theme/Admonition/Icon/Info.js +11 -0
  41. package/template/src/theme/Admonition/Icon/Note.js +11 -0
  42. package/template/src/theme/Admonition/Icon/Tip.js +11 -0
  43. package/template/src/theme/Admonition/Icon/Warning.js +11 -0
  44. package/template/src/theme/Admonition/Layout/index.js +39 -0
  45. package/template/src/theme/Admonition/Layout/styles.module.css +36 -0
  46. package/template/src/theme/Admonition/Type/Caution.js +28 -0
  47. package/template/src/theme/Admonition/Type/Danger.js +26 -0
  48. package/template/src/theme/Admonition/Type/Info.js +26 -0
  49. package/template/src/theme/Admonition/Type/Note.js +26 -0
  50. package/template/src/theme/Admonition/Type/Tip.js +26 -0
  51. package/template/src/theme/Admonition/Type/Warning.js +26 -0
  52. package/template/src/theme/Admonition/Types.js +27 -0
  53. package/template/src/theme/Admonition/index.js +18 -0
  54. package/template/src/theme/AnnouncementBar/CloseButton/index.js +20 -0
  55. package/template/src/theme/AnnouncementBar/CloseButton/styles.module.css +4 -0
  56. package/template/src/theme/AnnouncementBar/Content/index.js +17 -0
  57. package/template/src/theme/AnnouncementBar/Content/styles.module.css +10 -0
  58. package/template/src/theme/AnnouncementBar/index.js +33 -0
  59. package/template/src/theme/AnnouncementBar/styles.module.css +55 -0
  60. package/template/src/theme/BlogSidebar/Content/index.js +35 -0
  61. package/template/src/theme/BlogSidebar/Desktop/index.js +44 -0
  62. package/template/src/theme/BlogSidebar/Desktop/styles.module.css +60 -0
  63. package/template/src/theme/BlogSidebar/Mobile/index.js +38 -0
  64. package/template/src/theme/BlogSidebar/Mobile/styles.module.css +3 -0
  65. package/template/src/theme/BlogSidebar/index.js +15 -0
  66. package/template/src/theme/Details/index.js +23 -0
  67. package/template/src/theme/Details/styles.module.css +52 -0
  68. package/template/src/theme/DocBreadcrumbs/Items/Home/index.js +22 -0
  69. package/template/src/theme/DocBreadcrumbs/Items/Home/styles.module.css +7 -0
  70. package/template/src/theme/DocBreadcrumbs/StructuredData/index.js +15 -0
  71. package/template/src/theme/DocBreadcrumbs/index.js +75 -0
  72. package/template/src/theme/DocBreadcrumbs/styles.module.css +5 -0
  73. package/template/src/theme/DocCard/index.js +93 -0
  74. package/template/src/theme/DocCard/styles.module.css +53 -0
  75. package/template/src/theme/DocCardList/index.js +27 -0
  76. package/template/src/theme/DocCardList/styles.module.css +7 -0
  77. package/template/src/theme/DocItem/Content/index.js +121 -0
  78. package/template/src/theme/DocItem/Content/styles.module.css +96 -0
  79. package/template/src/theme/DocItem/Footer/index.js +43 -0
  80. package/template/src/theme/DocItem/Footer/styles.module.css +19 -0
  81. package/template/src/theme/DocItem/Layout/index.js +55 -0
  82. package/template/src/theme/DocItem/Layout/styles.module.css +14 -0
  83. package/template/src/theme/DocItem/Metadata/index.js +17 -0
  84. package/template/src/theme/DocItem/Paginator/index.js +17 -0
  85. package/template/src/theme/DocItem/TOC/Desktop/index.js +15 -0
  86. package/template/src/theme/DocItem/TOC/Mobile/index.js +17 -0
  87. package/template/src/theme/DocItem/TOC/Mobile/styles.module.css +12 -0
  88. package/template/src/theme/DocItem/index.js +19 -0
  89. package/template/src/theme/DocItem/styles.module.css +28 -0
  90. package/template/src/theme/DocRoot/Layout/Main/index.js +23 -0
  91. package/template/src/theme/DocRoot/Layout/Main/styles.module.css +31 -0
  92. package/template/src/theme/DocRoot/Layout/Sidebar/ExpandButton/index.js +28 -0
  93. package/template/src/theme/DocRoot/Layout/Sidebar/ExpandButton/styles.module.css +27 -0
  94. package/template/src/theme/DocRoot/Layout/Sidebar/index.js +70 -0
  95. package/template/src/theme/DocRoot/Layout/Sidebar/styles.module.css +32 -0
  96. package/template/src/theme/DocRoot/Layout/index.js +27 -0
  97. package/template/src/theme/DocRoot/Layout/styles.module.css +9 -0
  98. package/template/src/theme/DocRoot/index.js +25 -0
  99. package/template/src/theme/DocSidebar/Desktop/CollapseButton/index.js +28 -0
  100. package/template/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css +40 -0
  101. package/template/src/theme/DocSidebar/Desktop/Content/index.js +44 -0
  102. package/template/src/theme/DocSidebar/Desktop/Content/styles.module.css +16 -0
  103. package/template/src/theme/DocSidebar/Desktop/index.js +28 -0
  104. package/template/src/theme/DocSidebar/Desktop/styles.module.css +37 -0
  105. package/template/src/theme/DocSidebar/Mobile/index.js +39 -0
  106. package/template/src/theme/DocSidebar/index.js +18 -0
  107. package/template/src/theme/DocSidebarItem/Category/index.js +203 -0
  108. package/template/src/theme/DocSidebarItem/Html/index.js +20 -0
  109. package/template/src/theme/DocSidebarItem/Html/styles.module.css +6 -0
  110. package/template/src/theme/DocSidebarItem/Link/index.js +49 -0
  111. package/template/src/theme/DocSidebarItem/Link/styles.module.css +3 -0
  112. package/template/src/theme/DocSidebarItem/index.js +15 -0
  113. package/template/src/theme/EditMetaRow/index.js +25 -0
  114. package/template/src/theme/EditMetaRow/styles.module.css +11 -0
  115. package/template/src/theme/EditThisPage/index.js +29 -0
  116. package/template/src/theme/ErrorPageContent.js +34 -0
  117. package/template/src/theme/Footer/Copyright/index.js +11 -0
  118. package/template/src/theme/Footer/Layout/index.js +21 -0
  119. package/template/src/theme/Footer/LinkItem/index.js +26 -0
  120. package/template/src/theme/Footer/Links/MultiColumn/index.js +44 -0
  121. package/template/src/theme/Footer/Links/Simple/index.js +32 -0
  122. package/template/src/theme/Footer/Links/index.js +11 -0
  123. package/template/src/theme/Footer/Logo/index.js +35 -0
  124. package/template/src/theme/Footer/Logo/styles.module.css +9 -0
  125. package/template/src/theme/Footer/index.js +22 -0
  126. package/template/src/theme/MDXComponents/Heading.js +120 -0
  127. package/template/src/theme/MDXComponents/index.js +17 -0
  128. package/template/src/theme/MDXComponents/styles.module.css +110 -0
  129. package/template/src/theme/MDXContent/index.js +6 -0
  130. package/template/src/theme/NavbarItem/DropdownNavbarItem/Desktop/index.js +110 -0
  131. package/template/src/theme/NavbarItem/DropdownNavbarItem/Mobile/index.js +136 -0
  132. package/template/src/theme/NavbarItem/DropdownNavbarItem/Mobile/styles.module.css +3 -0
  133. package/template/src/theme/NavbarItem/DropdownNavbarItem/index.js +7 -0
  134. package/template/src/theme/NotFound/Content/index.js +46 -0
  135. package/template/src/theme/NotFound/index.js +19 -0
  136. package/template/src/theme/NotFound.js +49 -0
  137. package/template/src/theme/PaginatorNavLink/index.js +17 -0
  138. package/template/src/theme/TOC/index.js +19 -0
  139. package/template/src/theme/TOC/styles.module.css +16 -0
  140. package/template/src/theme/TOCItems/Tree.js +30 -0
  141. package/template/src/theme/TOCItems/index.js +47 -0
  142. package/template/src/theme/TabItem/index.js +15 -0
  143. package/template/src/theme/TabItem/styles.module.css +10 -0
  144. package/template/src/theme/Tabs/index.js +189 -0
  145. package/template/src/theme/Tabs/styles.module.css +74 -0
  146. package/template/static/img/favicon.svg +4 -0
  147. package/template/static/img/oops-404.svg +11 -0
  148. package/template/static/searchIndex.json +1 -0
@@ -0,0 +1,211 @@
1
+ // src/components/CustomSearch/NavbarSearch.js
2
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { Input } from 'antd';
4
+ import { SearchOutlined } from '@ant-design/icons';
5
+ import { useHistory } from '@docusaurus/router';
6
+ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
7
+
8
+ let Fuse = null;
9
+ if (ExecutionEnvironment.canUseDOM) {
10
+ import('fuse.js').then(module => {
11
+ Fuse = module.default;
12
+ }).catch(err => {
13
+ console.warn('Error loading Fuse.js:', err);
14
+ });
15
+ }
16
+ import styles from './styles.module.css';
17
+
18
+ const fuseOptions = {
19
+ includeScore: true,
20
+ threshold: 0.4,
21
+ minMatchCharLength: 2,
22
+ keys: [
23
+ { name: 'keywords', weight: 0.7 },
24
+ { name: 'title', weight: 0.5 },
25
+ { name: 'description', weight: 0.3 }
26
+ ]
27
+ };
28
+
29
+ function NavbarSearch() {
30
+ const [isExpanded, setIsExpanded] = useState(false);
31
+ const [searchTerm, setSearchTerm] = useState('');
32
+ const [searchResults, setSearchResults] = useState([]);
33
+ const [searchIndex, setSearchIndex] = useState([]);
34
+ const [fuse, setFuse] = useState(null);
35
+ const inputRef = useRef(null);
36
+ const dropdownRef = useRef(null);
37
+ const history = useHistory();
38
+
39
+ useEffect(() => {
40
+ const loadSearchIndex = async () => {
41
+ try {
42
+ const response = await fetch('/searchIndex.json');
43
+ if (!response.ok) throw new Error('Failed to load search index');
44
+ const data = await response.json();
45
+
46
+ const processedData = data.map(item => ({
47
+ ...item,
48
+ keywords: Array.isArray(item.keywords)
49
+ ? item.keywords.map(k => k?.toLowerCase()?.trim()).filter(Boolean)
50
+ : [],
51
+ }));
52
+
53
+ setSearchIndex(processedData);
54
+ setFuse(new Fuse(processedData, fuseOptions));
55
+ } catch (error) {
56
+ console.error('Error loading search index:', error);
57
+ }
58
+ };
59
+
60
+ loadSearchIndex();
61
+
62
+ // Click outside handler
63
+ const handleClickOutside = (event) => {
64
+ // Check if the click is on the theme toggle or its children
65
+ if (event.target.closest('.navbar__items--right')) {
66
+ return;
67
+ }
68
+
69
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target) &&
70
+ !event.target.closest('.navbar-search-container')) {
71
+ setIsExpanded(false);
72
+ setSearchTerm('');
73
+ setSearchResults([]);
74
+ }
75
+ };
76
+
77
+ document.addEventListener('mousedown', handleClickOutside);
78
+ return () => document.removeEventListener('mousedown', handleClickOutside);
79
+ }, []);
80
+
81
+ const performSearch = useCallback((value) => {
82
+ if (!value?.trim() || !fuse) {
83
+ setSearchResults([]);
84
+ return;
85
+ }
86
+
87
+ try {
88
+ const results = fuse.search(value.trim());
89
+ const processedResults = Array.from(
90
+ new Map(results.map(result => [result.item.id, result.item])).values()
91
+ );
92
+ setSearchResults(processedResults);
93
+ } catch (error) {
94
+ console.error('Search error:', error);
95
+ setSearchResults([]);
96
+ }
97
+ }, [fuse]);
98
+
99
+ const handleSearchChange = useCallback((e) => {
100
+ const value = e.target.value;
101
+ setSearchTerm(value);
102
+ performSearch(value);
103
+ }, [performSearch]);
104
+
105
+ const navigateToPage = (url) => {
106
+ setIsExpanded(false);
107
+ setSearchTerm('');
108
+ setSearchResults([]);
109
+ const formattedUrl = url.startsWith('/') ? url : `/${url}`;
110
+ history.replace(formattedUrl);
111
+ };
112
+
113
+ const formatDate = (lastUpdate) => {
114
+ if (!lastUpdate) return null;
115
+ try {
116
+ const dateStr = typeof lastUpdate === 'string'
117
+ ? lastUpdate
118
+ : lastUpdate.date || lastUpdate.lastUpdatedAt;
119
+
120
+ if (!dateStr) return null;
121
+
122
+ const date = new Date(dateStr);
123
+ return isNaN(date.getTime()) ? null : {
124
+ date: date.toLocaleDateString('en-US', {
125
+ year: 'numeric',
126
+ month: 'long',
127
+ day: 'numeric'
128
+ }),
129
+ author: typeof lastUpdate === 'object' ? lastUpdate.author : null
130
+ };
131
+ } catch (error) {
132
+ return null;
133
+ }
134
+ };
135
+
136
+ const handleSearchIconClick = (e) => {
137
+ e.preventDefault();
138
+ if (isExpanded && !searchTerm) {
139
+ setIsExpanded(false);
140
+ // Remove focus from the input field without selecting nearby elements
141
+ inputRef.current?.blur();
142
+ } else {
143
+ setIsExpanded(true);
144
+ // Add a slight delay to ensure smooth transition before focusing
145
+ setTimeout(() => {
146
+ inputRef.current?.focus();
147
+ }, 100);
148
+ }
149
+ };
150
+
151
+ return (
152
+ <div className={styles.searchContainer}>
153
+ <div className={`${styles.searchWrapper} navbar-search-container ${isExpanded ? styles.expanded : ''}`}>
154
+ <SearchOutlined
155
+ className={styles.searchIcon}
156
+ onClick={handleSearchIconClick}
157
+ />
158
+ <Input
159
+ ref={inputRef}
160
+ placeholder="Search documentation..."
161
+ value={searchTerm}
162
+ onChange={handleSearchChange}
163
+ className={`${styles.searchInput} ${isExpanded ? styles.visible : ''}`}
164
+ onKeyDown={(e) => {
165
+ if (e.key === 'Enter' && searchResults.length > 0) {
166
+ navigateToPage(searchResults[0].url);
167
+ } else if (e.key === 'Escape') {
168
+ setIsExpanded(false);
169
+ setSearchTerm('');
170
+ setSearchResults([]);
171
+ }
172
+ }}
173
+ />
174
+ </div>
175
+
176
+ {isExpanded && searchTerm && (
177
+ <div className={styles.dropdownResults} ref={dropdownRef}>
178
+ <div className={styles.resultCount}>
179
+ {searchResults.length === 0 ? 'No results found' :
180
+ `Found ${searchResults.length} result${searchResults.length === 1 ? '' : 's'}`}
181
+ </div>
182
+ {searchResults.map((item) => (
183
+ <div
184
+ key={item.id}
185
+ className={styles.resultItem}
186
+ onClick={() => navigateToPage(item.url)}
187
+ onKeyDown={(e) => {
188
+ if (e.key === 'Enter') {
189
+ navigateToPage(item.url);
190
+ }
191
+ }}
192
+ tabIndex={0}
193
+ role="button"
194
+ >
195
+ <div className={styles.resultTitle}>{item.title}</div>
196
+ <div className={styles.resultDescription}>{item.description}</div>
197
+ {item.last_update && (
198
+ <div className={styles.resultMeta}>
199
+ {formatDate(item.last_update)?.date &&
200
+ `Last updated: ${formatDate(item.last_update).date}`}
201
+ </div>
202
+ )}
203
+ </div>
204
+ ))}
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ }
210
+
211
+ export default NavbarSearch;
@@ -0,0 +1,26 @@
1
+ // src/components/CustomSearch/SearchContext.js
2
+ import React, { createContext, useContext, useState } from 'react';
3
+
4
+ const SearchContext = createContext();
5
+
6
+ export function SearchProvider({ children }) {
7
+ const [isModalOpen, setIsModalOpen] = useState(false);
8
+
9
+ return (
10
+ <SearchContext.Provider value={{ isModalOpen, setIsModalOpen }}>
11
+ {children}
12
+ </SearchContext.Provider>
13
+ );
14
+ }
15
+
16
+ export function useModalSearch() {
17
+ const context = useContext(SearchContext);
18
+ if (!context) {
19
+ throw new Error('useModalSearch must be used within a SearchProvider');
20
+ }
21
+ return {
22
+ isSearchModalOpen: context.isModalOpen,
23
+ openSearchModal: () => context.setIsModalOpen(true),
24
+ closeSearchModal: () => context.setIsModalOpen(false)
25
+ };
26
+ }
@@ -0,0 +1,171 @@
1
+ /* src/components/CustomSearch/styles.module.css */
2
+ .searchContainer {
3
+ position: relative;
4
+ display: flex;
5
+ align-items: center;
6
+ z-index: var(--ifm-z-index-fixed);
7
+ }
8
+
9
+ /* Position the search elements relative to navbar */
10
+ .searchContainer :global(.navbar__items--right) {
11
+ margin-left: 1rem;
12
+ }
13
+
14
+ :global(.theme-toggle-button) {
15
+ padding: 0.5rem;
16
+ border-radius: 50%;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ transition: background-color 0.2s;
21
+ }
22
+
23
+ :global(.theme-toggle-button:hover) {
24
+ background-color: var(--ifm-color-emphasis-200);
25
+ }
26
+
27
+ /* Ensure other navbar items remain accessible */
28
+ :global(.navbar__items--right) {
29
+ position: relative;
30
+ z-index: calc(var(--ifm-z-index-fixed) + 1);
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 0.5rem;
34
+ }
35
+
36
+ .searchWrapper {
37
+ display: flex;
38
+ align-items: center;
39
+ position: relative;
40
+ transition: all 0.3s ease;
41
+ }
42
+
43
+ .searchIcon {
44
+ font-size: 20px;
45
+ color: var(--ifm-navbar-link-color);
46
+ cursor: pointer;
47
+ padding: 8px;
48
+ z-index: 2;
49
+ user-select: none;
50
+ -webkit-user-select: none;
51
+ -moz-user-select: none;
52
+ -ms-user-select: none;
53
+ }
54
+
55
+ .searchInput {
56
+ position: absolute;
57
+ right: 100%;
58
+ width: 0;
59
+ opacity: 0;
60
+ transition: all 0.3s ease;
61
+ background: var(--ifm-background-color);
62
+ border-radius: 4px;
63
+ border: 1px solid var(--ifm-color-emphasis-300);
64
+ }
65
+
66
+ .searchWrapper.expanded .searchInput {
67
+ width: 300px;
68
+ opacity: 1;
69
+ padding-right: 32px;
70
+ }
71
+
72
+ .searchInput.visible {
73
+ width: 300px;
74
+ opacity: 1;
75
+ }
76
+
77
+ .dropdownResults {
78
+ position: absolute;
79
+ top: 100%;
80
+ right: 0;
81
+ width: 400px;
82
+ max-height: 70vh;
83
+ overflow-y: auto;
84
+ background: var(--ifm-background-color);
85
+ border: 1px solid var(--ifm-color-emphasis-300);
86
+ border-radius: 4px;
87
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
88
+ z-index: 1000;
89
+ margin-top: 8px;
90
+ scrollbar-width: thin;
91
+ scrollbar-color: var(--ifm-color-emphasis-500) var(--ifm-color-emphasis-200);
92
+ }
93
+
94
+ .dropdownResults::-webkit-scrollbar {
95
+ width: 8px;
96
+ }
97
+
98
+ .dropdownResults::-webkit-scrollbar-track {
99
+ background: var(--ifm-color-emphasis-200);
100
+ border-radius: 4px;
101
+ }
102
+
103
+ .dropdownResults::-webkit-scrollbar-thumb {
104
+ background-color: var(--ifm-color-emphasis-500);
105
+ border-radius: 4px;
106
+ }
107
+
108
+ .resultCount {
109
+ padding: 8px 16px;
110
+ background-color: var(--ifm-color-emphasis-100);
111
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
112
+ font-size: 0.875rem;
113
+ color: var(--ifm-color-emphasis-700);
114
+ font-weight: 500;
115
+ }
116
+
117
+ .resultItem {
118
+ padding: 12px 16px;
119
+ cursor: pointer;
120
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
121
+ transition: background-color 0.2s ease;
122
+ }
123
+
124
+ .resultItem:last-child {
125
+ border-bottom: none;
126
+ }
127
+
128
+ .resultItem:hover {
129
+ background-color: var(--ifm-hover-overlay);
130
+ }
131
+
132
+ .resultTitle {
133
+ color: var(--ifm-color-primary);
134
+ font-size: 1rem;
135
+ font-weight: 600;
136
+ margin-bottom: 4px;
137
+ }
138
+
139
+ .resultDescription {
140
+ font-size: 0.875rem;
141
+ color: var(--ifm-color-emphasis-700);
142
+ margin-bottom: 4px;
143
+ line-height: 1.4;
144
+ }
145
+
146
+ .resultMeta {
147
+ font-size: 0.75rem;
148
+ color: var(--ifm-color-emphasis-600);
149
+ }
150
+
151
+ @media (max-width: 996px) {
152
+ .searchWrapper.expanded .searchInput {
153
+ width: 200px;
154
+ }
155
+
156
+ .dropdownResults {
157
+ width: 300px;
158
+ right: -40px;
159
+ }
160
+ }
161
+
162
+ /* Dark mode adjustments */
163
+ [data-theme='dark'] .searchInput {
164
+ background: var(--ifm-background-color);
165
+ color: var(--ifm-color-white);
166
+ }
167
+
168
+ [data-theme='dark'] .dropdownResults {
169
+ background: var(--ifm-background-color);
170
+ border-color: var(--ifm-color-emphasis-300);
171
+ }
@@ -0,0 +1,11 @@
1
+ // src/components/CustomSearch/wrapperInit.js
2
+ import React from 'react';
3
+ import { SearchProvider } from './SearchContext';
4
+
5
+ const wrapRootElement = ({ element }) => (
6
+ <SearchProvider>
7
+ {element}
8
+ </SearchProvider>
9
+ );
10
+
11
+ export default wrapRootElement;
@@ -0,0 +1,176 @@
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import Link from '@docusaurus/Link';
3
+ import useGlobalData from '@docusaurus/useGlobalData';
4
+ import styles from './styles.module.css';
5
+
6
+ /**
7
+ * Retrieves FAQ topics from the faq-index-plugin's global data.
8
+ * Returns an empty array if the plugin data is unavailable.
9
+ */
10
+ function useFaqTopics(pluginId) {
11
+ try {
12
+ const globalData = useGlobalData();
13
+ const pluginData = globalData?.[pluginId]?.default;
14
+ return pluginData?.topics || [];
15
+ } catch {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Searchable FAQ table of contents for Docusaurus.
22
+ *
23
+ * Reads the FAQ index produced by faq-index-plugin and renders a
24
+ * filterable list of every question, grouped by topic file.
25
+ *
26
+ * @param {Object} props
27
+ * @param {string} [props.pluginId='docusaurus-plugin-faq-index'] - Plugin name to look up in global data.
28
+ * @param {string} [props.searchPlaceholder='Search FAQs...'] - Placeholder for the search input.
29
+ * @param {string} [props.title] - Optional heading above the TOC.
30
+ */
31
+ export default function FaqTableOfContents({
32
+ pluginId = 'docusaurus-plugin-faq-index',
33
+ searchPlaceholder = 'Search FAQs...',
34
+ title,
35
+ }) {
36
+ const [searchQuery, setSearchQuery] = useState('');
37
+ const topics = useFaqTopics(pluginId);
38
+
39
+ // Total question count
40
+ const totalQuestions = useMemo(
41
+ () => topics.reduce((sum, t) => sum + t.questions.length, 0),
42
+ [topics],
43
+ );
44
+
45
+ // Filter topics and questions by search query
46
+ const filtered = useMemo(() => {
47
+ if (!searchQuery.trim()) return topics;
48
+
49
+ const query = searchQuery.toLowerCase();
50
+ return topics
51
+ .map((topic) => {
52
+ const topicMatches = topic.title.toLowerCase().includes(query);
53
+ const matchingQuestions = topic.questions.filter((q) =>
54
+ q.text.toLowerCase().includes(query),
55
+ );
56
+
57
+ if (topicMatches) return topic;
58
+ if (matchingQuestions.length > 0) {
59
+ return { ...topic, questions: matchingQuestions };
60
+ }
61
+ return null;
62
+ })
63
+ .filter(Boolean);
64
+ }, [topics, searchQuery]);
65
+
66
+ const filteredQuestionCount = useMemo(
67
+ () => filtered.reduce((sum, t) => sum + t.questions.length, 0),
68
+ [filtered],
69
+ );
70
+
71
+ const handleSearchChange = useCallback((e) => {
72
+ setSearchQuery(e.target.value);
73
+ }, []);
74
+
75
+ const handleClear = useCallback(() => {
76
+ setSearchQuery('');
77
+ }, []);
78
+
79
+ if (topics.length === 0) {
80
+ return (
81
+ <div className={styles.faqToc}>
82
+ <p className={styles.emptyState}>
83
+ No FAQ topics found. Verify that the{' '}
84
+ <code>faq-index-plugin</code> is registered in{' '}
85
+ <code>docusaurus.config.js</code> and that <code>docs/faq/</code>{' '}
86
+ contains <code>.mdx</code> files with <code>###</code> question
87
+ headings.
88
+ </p>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className={styles.faqToc}>
95
+ {title && <h2 className={styles.title}>{title}</h2>}
96
+
97
+ {/* Search input */}
98
+ <div className={styles.searchContainer}>
99
+ <svg
100
+ className={styles.searchIcon}
101
+ width="18"
102
+ height="18"
103
+ viewBox="0 0 24 24"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ strokeWidth="2"
107
+ strokeLinecap="round"
108
+ strokeLinejoin="round"
109
+ >
110
+ <circle cx="11" cy="11" r="8" />
111
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
112
+ </svg>
113
+ <input
114
+ type="text"
115
+ className={styles.searchInput}
116
+ placeholder={searchPlaceholder}
117
+ value={searchQuery}
118
+ onChange={handleSearchChange}
119
+ aria-label="Search FAQ entries"
120
+ />
121
+ {searchQuery && (
122
+ <button
123
+ className={styles.clearButton}
124
+ onClick={handleClear}
125
+ aria-label="Clear search"
126
+ type="button"
127
+ >
128
+
129
+ </button>
130
+ )}
131
+ </div>
132
+
133
+ {/* Result count */}
134
+ {searchQuery && (
135
+ <p className={styles.resultCount}>
136
+ {filteredQuestionCount} of {totalQuestions}{' '}
137
+ {totalQuestions === 1 ? 'question' : 'questions'} match
138
+ </p>
139
+ )}
140
+
141
+ {/* FAQ list */}
142
+ {filtered.length === 0 ? (
143
+ <p className={styles.noResults}>
144
+ No FAQs match "<strong>{searchQuery}</strong>". Try a different search
145
+ term.
146
+ </p>
147
+ ) : (
148
+ filtered.map((topic) => (
149
+ <div key={topic.slug} className={styles.topicGroup}>
150
+ <h2 className={styles.topicHeading}>
151
+ {topic.title}
152
+ <span className={styles.questionCount}>
153
+ {topic.questions.length}
154
+ </span>
155
+ </h2>
156
+ {topic.description && (
157
+ <p className={styles.topicDescription}>{topic.description}</p>
158
+ )}
159
+ <ul className={styles.questionList}>
160
+ {topic.questions.map((q) => (
161
+ <li key={q.anchor} className={styles.questionItem}>
162
+ <Link
163
+ to={`${topic.permalink}#${q.anchor}`}
164
+ className={styles.questionLink}
165
+ >
166
+ {q.text}
167
+ </Link>
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ </div>
172
+ ))
173
+ )}
174
+ </div>
175
+ );
176
+ }