@@ -16,8 +16,256 @@ const [product, module] = Astro.url.pathname.split("/").filter(Boolean);
1616 </span >
1717</a >
1818
19+ <!-- Search Input -->
20+ <div class =" relative" >
21+ <input
22+ type =" text"
23+ id =" sidebar-search"
24+ placeholder =" Search sidebar..."
25+ class =" w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-orange-500 dark:focus:border-orange-500 transition-colors duration-200"
26+ />
27+ </div >
28+
29+ <!-- No Results Message -->
30+ <div
31+ id =" sidebar-no-results"
32+ class =" text-sm text-gray-500 dark:text-gray-400 text-center p-4 hidden"
33+ >
34+ No results found. Try a different search term, or use our <button id =" global-search-link" class =" text-blue-500 dark:text-orange-500 underline hover:no-underline cursor-pointer bg-transparent border-none p-0 font-inherit" >global search</button >.
35+ </div >
36+
1937<Default ><slot /></Default >
2038
39+ <script >
40+ function initSidebarSearch() {
41+ const searchInput = document.getElementById('sidebar-search') as HTMLInputElement;
42+ const noResultsMessage = document.getElementById('sidebar-no-results') as HTMLElement;
43+ const globalSearchLink = document.getElementById('global-search-link') as HTMLButtonElement;
44+ const sidebarContent = document.querySelector('.sidebar-content');
45+
46+ if (!searchInput || !sidebarContent || !noResultsMessage || !globalSearchLink) return;
47+
48+ const originalState: Map<Element, boolean> = new Map();
49+
50+ // Store original state of details elements
51+ function storeOriginalState() {
52+ if (originalState.size === 0) {
53+ const detailsElements = sidebarContent!.querySelectorAll('details');
54+ detailsElements.forEach(details => {
55+ originalState.set(details, details.open);
56+ });
57+ }
58+ }
59+
60+
61+ // Check if text matches all search terms (for multi-word searches)
62+ function matchesAllTerms(text: string, searchTerms: string[]): boolean {
63+ const lowerText = text.toLowerCase();
64+ return searchTerms.every(term => lowerText.includes(term));
65+ }
66+
67+ // Show only direct children of a folder (not recursive)
68+ function showDirectChildren(details: HTMLDetailsElement) {
69+ details.open = true;
70+ const directList = details.querySelector(':scope > ul');
71+ if (directList) {
72+ const directChildren = directList.querySelectorAll(':scope > li');
73+ directChildren.forEach(child => {
74+ (child as HTMLElement).style.display = '';
75+ });
76+ }
77+ }
78+
79+ // Show parent chain for a specific item
80+ function showParentChain(element: Element) {
81+ let parent = element.parentElement;
82+ while (parent && parent !== sidebarContent) {
83+ if (parent.tagName === 'LI') {
84+ (parent as HTMLElement).style.display = '';
85+ }
86+ if (parent.tagName === 'DETAILS') {
87+ (parent as HTMLDetailsElement).open = true;
88+ }
89+ parent = parent.parentElement;
90+ }
91+ }
92+
93+ // Filter sidebar items based on search query
94+ function filterSidebarItems(query: string) {
95+ const items = sidebarContent!.querySelectorAll('li');
96+ const detailsElements = sidebarContent!.querySelectorAll('details');
97+
98+ if (!query.trim()) {
99+ // Reset to original state
100+ items.forEach(item => {
101+ (item as HTMLElement).style.display = '';
102+ });
103+
104+ // Restore original details state
105+ detailsElements.forEach(details => {
106+ const originalOpen = originalState.get(details);
107+ if (originalOpen !== undefined) {
108+ (details as HTMLDetailsElement).open = originalOpen;
109+ }
110+ });
111+
112+ // Hide no results message
113+ noResultsMessage.classList.add('hidden');
114+
115+ return;
116+ }
117+
118+ // Split search query into terms for more precise matching
119+ const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0);
120+
121+ // First, hide all items and close all details
122+ items.forEach(item => {
123+ (item as HTMLElement).style.display = 'none';
124+ });
125+ detailsElements.forEach(details => {
126+ (details as HTMLDetailsElement).open = false;
127+ });
128+
129+ // Track what we've matched to avoid duplicates
130+ const matchedItems = new Set<Element>();
131+
132+ // 1. Check for folder/subfolder matches first (highest priority)
133+ detailsElements.forEach(details => {
134+ const summary = details.querySelector('summary');
135+ if (summary) {
136+ const summaryText = summary.textContent || '';
137+
138+ if (matchesAllTerms(summaryText, searchTerms)) {
139+ // This is a folder match - show the folder and its direct children
140+ const parentLi = details.closest('li');
141+ if (parentLi && !matchedItems.has(parentLi)) {
142+ (parentLi as HTMLElement).style.display = '';
143+ showDirectChildren(details);
144+ showParentChain(parentLi);
145+ matchedItems.add(parentLi);
146+ }
147+ }
148+ }
149+ });
150+
151+ // 2. Check for specific page matches (show page + parent chain)
152+ items.forEach(item => {
153+ if (matchedItems.has(item)) return; // Skip if already matched as folder
154+
155+ const link = item.querySelector('a');
156+ const summary = item.querySelector('summary');
157+
158+ // Skip if this is a folder (has summary) - those are handled above
159+ if (summary) return;
160+
161+ if (link) {
162+ const linkText = link.textContent || '';
163+
164+ if (matchesAllTerms(linkText, searchTerms)) {
165+ // This is a specific page match - show page + parent chain
166+ (item as HTMLElement).style.display = '';
167+ showParentChain(item);
168+ matchedItems.add(item);
169+ }
170+ }
171+ });
172+
173+ // 3. Fallback: if no exact matches, show partial matches (less specific)
174+ if (matchedItems.size === 0) {
175+ items.forEach(item => {
176+ const textContent = item.textContent?.toLowerCase() || '';
177+ const link = item.querySelector('a');
178+ const linkText = link?.textContent?.toLowerCase() || '';
179+ const summary = item.querySelector('summary');
180+ const summaryText = summary?.textContent?.toLowerCase() || '';
181+
182+ // Check if any search term is found (partial matching)
183+ const hasPartialMatch = searchTerms.some(term =>
184+ textContent.includes(term) || linkText.includes(term) || summaryText.includes(term)
185+ );
186+
187+ if (hasPartialMatch) {
188+ (item as HTMLElement).style.display = '';
189+
190+ // If it's a folder, show direct children only
191+ if (summary) {
192+ const details = item.querySelector('details');
193+ if (details) {
194+ showDirectChildren(details);
195+ }
196+ }
197+
198+ showParentChain(item);
199+ matchedItems.add(item);
200+ }
201+ });
202+ }
203+
204+ // Show/hide no results message based on matches
205+ if (matchedItems.size === 0) {
206+ noResultsMessage.classList.remove('hidden');
207+ } else {
208+ noResultsMessage.classList.add('hidden');
209+ }
210+ }
211+
212+ // Event listeners
213+ searchInput.addEventListener('input', (e) => {
214+ storeOriginalState();
215+ const query = (e.target as HTMLInputElement).value;
216+ filterSidebarItems(query);
217+ });
218+
219+ // Clear search on Escape key
220+ searchInput.addEventListener('keydown', (e) => {
221+ if (e.key === 'Escape') {
222+ searchInput.value = '';
223+ filterSidebarItems('');
224+ }
225+ });
226+
227+ // Global search link click handler
228+ globalSearchLink.addEventListener('click', () => {
229+ const currentQuery = searchInput.value.trim();
230+ if (currentQuery) {
231+ // Try multiple selectors for DocSearch
232+ const docSearchButton = document.querySelector('#docsearch button') as HTMLButtonElement ||
233+ document.querySelector('.DocSearch-Button') as HTMLButtonElement ||
234+ document.querySelector('[data-docsearch-button]') as HTMLButtonElement;
235+
236+ if (docSearchButton) {
237+ // Click the DocSearch button to open the modal
238+ docSearchButton.click();
239+
240+ // Wait for modal to open and set the search term
241+ setTimeout(() => {
242+ const searchInput = document.querySelector('.DocSearch-Input') as HTMLInputElement ||
243+ document.querySelector('#docsearch-input') as HTMLInputElement ||
244+ document.querySelector('[data-docsearch-input]') as HTMLInputElement;
245+
246+ if (searchInput) {
247+ searchInput.value = currentQuery;
248+ searchInput.focus();
249+ // Trigger search
250+ searchInput.dispatchEvent(new Event('input', { bubbles: true }));
251+ }
252+ }, 100);
253+ }
254+ }
255+ });
256+ }
257+
258+ // Initialize when DOM is loaded
259+ if (document.readyState === 'loading') {
260+ document.addEventListener('DOMContentLoaded', initSidebarSearch);
261+ } else {
262+ initSidebarSearch();
263+ }
264+
265+ // Re-initialize on navigation (for SPA-like behavior)
266+ initSidebarSearch();
267+ </script >
268+
21269<style is:global >
22270 :root {
23271 .sidebar-content {
0 commit comments