Skip to content

Commit 1066ec6

Browse files
Redesign cluster search as collapsible sidebar (#650)
* Initial plan * Redesign cluster search as collapsible left sidebar Co-authored-by: LukasWallrich <60155545+LukasWallrich@users.noreply.github.com> * Fix code review feedback: use alert-info for no results and prevent layout shift Co-authored-by: LukasWallrich <60155545+LukasWallrich@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: LukasWallrich <60155545+LukasWallrich@users.noreply.github.com>
1 parent 501c231 commit 1066ec6

1 file changed

Lines changed: 245 additions & 51 deletions

File tree

static/js/clusters-search.js

Lines changed: 245 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,76 @@
2323
}
2424

2525
function createSearchInterface() {
26-
// Find the intro section to insert search box after it
27-
const introSection = document.querySelector('.wg-blank');
28-
if (!introSection) return;
29-
30-
// Create search container
31-
const searchContainer = document.createElement('div');
32-
searchContainer.className = 'cluster-search-container';
33-
searchContainer.innerHTML = `
34-
<div class="container" style="margin-bottom: 30px; margin-top: 20px;">
35-
<div class="row justify-content-center">
36-
<div class="col-lg-8">
37-
<div class="input-group">
38-
<input type="text"
39-
id="clusterSearchInput"
40-
class="form-control"
41-
placeholder="Search within all clusters and sub-categories..."
42-
aria-label="Search clusters">
43-
<div class="input-group-append">
44-
<button class="btn btn-primary" type="button" id="clusterSearchBtn">
45-
<i class="fas fa-search"></i> Search
46-
</button>
47-
<button class="btn btn-outline-secondary" type="button" id="clusterClearBtn" style="display: none;">
48-
<i class="fas fa-times"></i> Clear
49-
</button>
50-
</div>
26+
// Create sidebar container
27+
const sidebar = document.createElement('div');
28+
sidebar.id = 'clusterSearchSidebar';
29+
sidebar.className = 'cluster-search-sidebar';
30+
sidebar.innerHTML = `
31+
<button class="cluster-search-toggle" id="clusterSearchToggle" aria-label="Toggle search panel">
32+
<i class="fas fa-search"></i> Search Clusters
33+
</button>
34+
<div class="cluster-search-panel" id="clusterSearchPanel">
35+
<div class="cluster-search-header">
36+
<h4>Search Clusters</h4>
37+
<button class="cluster-search-close" id="clusterSearchClose" aria-label="Close search">
38+
<i class="fas fa-times"></i>
39+
</button>
40+
</div>
41+
<div class="cluster-search-body">
42+
<div class="input-group">
43+
<input type="text"
44+
id="clusterSearchInput"
45+
class="form-control"
46+
placeholder="Search clusters..."
47+
aria-label="Search clusters">
48+
<div class="input-group-append">
49+
<button class="btn btn-primary btn-sm" type="button" id="clusterSearchBtn">
50+
<i class="fas fa-search"></i>
51+
</button>
5152
</div>
52-
<div id="clusterSearchResults" style="margin-top: 10px; font-size: 0.9rem;"></div>
5353
</div>
54+
<button class="btn btn-outline-secondary btn-sm btn-block mt-2" type="button" id="clusterClearBtn" style="display: none;">
55+
<i class="fas fa-times"></i> Clear Results
56+
</button>
57+
<div id="clusterSearchResults" style="margin-top: 15px; font-size: 0.85rem;"></div>
5458
</div>
5559
</div>
5660
`;
5761

58-
// Insert after intro section
59-
introSection.parentNode.insertBefore(searchContainer, introSection.nextSibling);
62+
// Insert at beginning of body
63+
document.body.insertBefore(sidebar, document.body.firstChild);
6064
}
6165

6266
function setupSearchHandlers() {
6367
const searchInput = document.getElementById('clusterSearchInput');
6468
const searchBtn = document.getElementById('clusterSearchBtn');
6569
const clearBtn = document.getElementById('clusterClearBtn');
6670
const resultsDiv = document.getElementById('clusterSearchResults');
71+
const toggleBtn = document.getElementById('clusterSearchToggle');
72+
const closeBtn = document.getElementById('clusterSearchClose');
73+
const panel = document.getElementById('clusterSearchPanel');
74+
75+
if (!searchInput || !searchBtn || !clearBtn || !toggleBtn || !closeBtn || !panel) return;
76+
77+
// Toggle sidebar
78+
toggleBtn.addEventListener('click', function() {
79+
panel.classList.toggle('open');
80+
if (panel.classList.contains('open')) {
81+
searchInput.focus();
82+
}
83+
});
6784

68-
if (!searchInput || !searchBtn || !clearBtn) return;
85+
// Close sidebar
86+
closeBtn.addEventListener('click', function() {
87+
panel.classList.remove('open');
88+
});
89+
90+
// Close on escape key
91+
document.addEventListener('keydown', function(e) {
92+
if (e.key === 'Escape' && panel.classList.contains('open')) {
93+
panel.classList.remove('open');
94+
}
95+
});
6996

7097
// Search on button click
7198
searchBtn.addEventListener('click', performSearch);
@@ -198,43 +225,46 @@
198225
return;
199226
}
200227

201-
let html = `<div class="alert alert-success">Found ${results.length} tab(s) containing "${escapeHtml(query)}" (${results.reduce((sum, r) => sum + r.matches, 0)} total matches). Click on a result to view it:</div>`;
202-
html += '<div class="list-group" style="margin-top: 10px;">';
228+
let html = `<div class="search-summary">Found ${results.length} tab(s) with ${results.reduce((sum, r) => sum + r.matches, 0)} matches</div>`;
229+
html += '<div class="search-results-list">';
203230

204231
results.forEach(function(result) {
205232
html += `
206-
<a href="#" class="list-group-item list-group-item-action cluster-search-result"
207-
data-tab-id="${result.tabId}">
208-
<div class="d-flex w-100 justify-content-between">
209-
<h6 class="mb-1">${escapeHtml(result.cluster)}${escapeHtml(result.tab)}</h6>
210-
<small>${result.matches} match${result.matches > 1 ? 'es' : ''}</small>
233+
<div class="search-result-item" data-tab-id="${result.tabId}">
234+
<div class="search-result-header">
235+
<strong>${escapeHtml(result.tab)}</strong>
236+
<span class="badge badge-primary">${result.matches}</span>
211237
</div>
212-
<p class="mb-1"><small>${result.snippet}</small></p>
213-
</a>
238+
<div class="search-result-cluster">${escapeHtml(result.cluster)}</div>
239+
<div class="search-result-snippet">${result.snippet}</div>
240+
</div>
214241
`;
215242
});
216243

217244
html += '</div>';
218245
resultsDiv.innerHTML = html;
219246

220247
// Add click handlers to results
221-
const resultLinks = resultsDiv.querySelectorAll('.cluster-search-result');
222-
resultLinks.forEach(function(link) {
223-
link.addEventListener('click', function(e) {
224-
e.preventDefault();
248+
const resultItems = resultsDiv.querySelectorAll('.search-result-item');
249+
resultItems.forEach(function(item) {
250+
item.addEventListener('click', function() {
225251
const tabId = this.getAttribute('data-tab-id');
226252
const result = results.find(r => r.tabId === tabId);
227253
if (result) {
228254
activateTab(result);
229255
highlightMatches(result.tabPane, query);
230256
// Scroll to the section
231257
result.section.scrollIntoView({ behavior: 'smooth', block: 'start' });
258+
// Mark as active
259+
resultItems.forEach(r => r.classList.remove('active'));
260+
this.classList.add('active');
232261
}
233262
});
234263
});
235264

236265
// Auto-expand first result
237266
if (results.length > 0) {
267+
resultItems[0].classList.add('active');
238268
activateTab(results[0]);
239269
highlightMatches(results[0].tabPane, query);
240270
}
@@ -322,31 +352,195 @@
322352
});
323353
}
324354

325-
// Add CSS for highlighting
355+
// Add CSS for sidebar and highlighting
326356
const style = document.createElement('style');
327357
style.textContent = `
358+
/* Highlight styling */
328359
.cluster-highlight {
329360
background-color: #ffeb3b;
330361
padding: 2px 0;
331362
font-weight: bold;
332363
}
333364
334-
.cluster-search-container {
335-
position: sticky;
336-
top: 70px;
365+
/* Sidebar toggle button */
366+
.cluster-search-toggle {
367+
position: fixed;
368+
left: 0;
369+
top: 200px;
370+
background: #007bff;
371+
color: white;
372+
border: none;
373+
border-radius: 0 5px 5px 0;
374+
padding: 12px 15px;
375+
cursor: pointer;
376+
z-index: 1000;
377+
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
378+
font-size: 14px;
379+
transition: background 0.3s;
380+
}
381+
382+
.cluster-search-toggle:hover {
383+
background: #0056b3;
384+
}
385+
386+
.cluster-search-toggle i {
387+
margin-right: 5px;
388+
}
389+
390+
/* Sidebar panel */
391+
.cluster-search-panel {
392+
position: fixed;
393+
left: -350px;
394+
top: 0;
395+
width: 350px;
396+
height: 100vh;
337397
background: white;
338-
z-index: 100;
339-
padding: 20px 0 10px 0;
340-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
398+
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
399+
z-index: 1001;
400+
transition: left 0.3s ease;
401+
display: flex;
402+
flex-direction: column;
403+
overflow: hidden;
404+
}
405+
406+
.cluster-search-panel.open {
407+
left: 0;
408+
}
409+
410+
/* Sidebar header */
411+
.cluster-search-header {
412+
display: flex;
413+
justify-content: space-between;
414+
align-items: center;
415+
padding: 15px 20px;
416+
border-bottom: 1px solid #dee2e6;
417+
background: #f8f9fa;
418+
}
419+
420+
.cluster-search-header h4 {
421+
margin: 0;
422+
font-size: 18px;
423+
color: #333;
341424
}
342425
343-
.cluster-search-result:hover {
426+
.cluster-search-close {
427+
background: none;
428+
border: none;
429+
font-size: 20px;
430+
color: #666;
344431
cursor: pointer;
432+
padding: 5px;
433+
line-height: 1;
434+
}
435+
436+
.cluster-search-close:hover {
437+
color: #333;
438+
}
439+
440+
/* Sidebar body */
441+
.cluster-search-body {
442+
padding: 20px;
443+
flex: 1;
444+
overflow-y: auto;
445+
}
446+
447+
/* Search results summary */
448+
.search-summary {
449+
background: #e7f3ff;
450+
padding: 10px;
451+
border-radius: 5px;
452+
margin-bottom: 15px;
453+
font-size: 0.9rem;
454+
color: #004085;
345455
}
346456
347-
.cluster-search-result mark {
457+
/* Search results list */
458+
.search-results-list {
459+
display: flex;
460+
flex-direction: column;
461+
gap: 10px;
462+
}
463+
464+
/* Individual search result */
465+
.search-result-item {
466+
background: #f8f9fa;
467+
border: 1px solid #dee2e6;
468+
border-radius: 5px;
469+
padding: 12px;
470+
cursor: pointer;
471+
transition: all 0.2s;
472+
}
473+
474+
.search-result-item:hover {
475+
background: #e9ecef;
476+
border-color: #007bff;
477+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
478+
}
479+
480+
.search-result-item.active {
481+
background: #e7f3ff;
482+
border-color: #007bff;
483+
box-shadow: inset 0 0 0 1px #007bff;
484+
}
485+
486+
.search-result-header {
487+
display: flex;
488+
justify-content: space-between;
489+
align-items: center;
490+
margin-bottom: 5px;
491+
}
492+
493+
.search-result-header strong {
494+
font-size: 0.95rem;
495+
color: #333;
496+
flex: 1;
497+
margin-right: 10px;
498+
}
499+
500+
.search-result-header .badge {
501+
font-size: 0.75rem;
502+
}
503+
504+
.search-result-cluster {
505+
font-size: 0.8rem;
506+
color: #666;
507+
margin-bottom: 8px;
508+
}
509+
510+
.search-result-snippet {
511+
font-size: 0.8rem;
512+
color: #555;
513+
line-height: 1.4;
514+
}
515+
516+
.search-result-snippet mark {
348517
background-color: #ffeb3b;
349518
padding: 1px 2px;
519+
font-weight: 600;
520+
}
521+
522+
/* Mobile responsive */
523+
@media (max-width: 768px) {
524+
.cluster-search-panel {
525+
width: 100%;
526+
left: -100%;
527+
}
528+
529+
.cluster-search-panel.open {
530+
left: 0;
531+
}
532+
533+
.cluster-search-toggle {
534+
top: 150px;
535+
font-size: 12px;
536+
padding: 10px 12px;
537+
}
538+
}
539+
540+
/* Alert styling in sidebar */
541+
.cluster-search-body .alert {
542+
font-size: 0.85rem;
543+
padding: 8px 12px;
350544
}
351545
`;
352546
document.head.appendChild(style);

0 commit comments

Comments
 (0)