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