11. Global Search
Overview
Global omnisearch functionality provides instant access to tasks, projects, checkpoints, and git commits across the entire CODITECT ecosystem. Inspired by Spotlight (macOS), Cmd+K patterns (VS Code, Notion), and modern SaaS search experiences.
Key Features:
- Instant search across 14,321+ messages, 530+ tasks, 46 projects, 421 checkpoints
- Keyboard-first interaction (Cmd/Ctrl+K to activate)
- Fuzzy matching with typo tolerance
- Search result categories with quick filters
- Recent searches and search history
- Deep linking to results
11.1 Search Input Design
Visual Design
Location: Center of top bar (60px height)
- Collapsed state: 400px width, search icon + placeholder text
- Focused state: Expands to 600px width with autocomplete dropdown
- Position: Absolute center of top bar for easy access
Styling:
.global-search {
width: 400px;
height: 40px;
border: 1px solid #E5E7EB;
border-radius: 8px;
background: #F9FAFB;
padding: 0 16px;
font-size: 14px;
transition: all 0.2s ease;
}
.global-search:focus {
width: 600px;
background: #FFFFFF;
border-color: #0066CC;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
outline: none;
}
.search-icon {
width: 20px;
height: 20px;
color: #6B7280;
margin-right: 12px;
}
.search-placeholder {
color: #9CA3AF;
font-size: 14px;
}
Keyboard Shortcut
Activation:
- Mac: Cmd + K
- Windows/Linux: Ctrl + K
- Alternative: Forward slash (/) like Reddit/Gmail
JavaScript Implementation:
document.addEventListener('keydown', (e) => {
// Cmd+K or Ctrl+K
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search').focus();
}
// Forward slash (/) - only if not in input field
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
e.preventDefault();
document.getElementById('global-search').focus();
}
});
11.2 Autocomplete Dropdown
Dropdown Structure
Appears below search input when 2+ characters typed.
Layout:
- Width: Matches search input (600px when focused)
- Max height: 480px (scroll if more results)
- Position: Absolute, 8px below search input
- Shadow: Large shadow for prominence
- Background: White with border
HTML Structure:
<div class="search-dropdown" role="listbox">
<!-- Quick Filters -->
<div class="search-filters">
<button class="filter-chip active" data-category="all">All</button>
<button class="filter-chip" data-category="tasks">Tasks (234)</button>
<button class="filter-chip" data-category="projects">Projects (12)</button>
<button class="filter-chip" data-category="checkpoints">Checkpoints (45)</button>
<button class="filter-chip" data-category="commits">Commits (89)</button>
</div>
<!-- Search Results -->
<div class="search-results">
<!-- Category: Tasks -->
<div class="result-category">
<div class="category-header">Tasks</div>
<div class="result-item" role="option" tabindex="0">
<span class="result-icon">☐</span>
<div class="result-content">
<div class="result-title">Implement user authentication</div>
<div class="result-meta">coditect-cloud-backend • Phase 1 • P0</div>
</div>
<span class="result-status status-pending">Pending</span>
</div>
<!-- More task results... -->
</div>
<!-- Category: Projects -->
<div class="result-category">
<div class="category-header">Projects</div>
<div class="result-item" role="option" tabindex="0">
<span class="result-icon">📦</span>
<div class="result-content">
<div class="result-title">coditect-cloud-backend</div>
<div class="result-meta">530 tasks • 78% complete • P1</div>
</div>
<span class="project-indicator" style="background: green;"></span>
</div>
</div>
<!-- Category: Checkpoints -->
<div class="result-category">
<div class="category-header">Checkpoints</div>
<div class="result-item" role="option" tabindex="0">
<span class="result-icon">🚩</span>
<div class="result-content">
<div class="result-title">Sprint 1 Complete - Backend MVP</div>
<div class="result-meta">2025-11-18 • 127 messages • Week 1</div>
</div>
</div>
</div>
<!-- Category: Git Commits -->
<div class="result-category">
<div class="category-header">Git Commits</div>
<div class="result-item" role="option" tabindex="0">
<span class="result-icon">🔧</span>
<div class="result-content">
<div class="result-title">feat(auth): Add JWT authentication middleware</div>
<div class="result-meta">coditect-cloud-backend • 2025-11-20 • 7c8ac57</div>
</div>
</div>
</div>
</div>
<!-- No Results State -->
<div class="no-results" style="display: none;">
<p>No results found for "<span id="search-query"></span>"</p>
<p class="suggestion">Try a different search term or check spelling</p>
</div>
<!-- Recent Searches (shown when input empty) -->
<div class="recent-searches">
<div class="category-header">Recent Searches</div>
<div class="recent-item">
<span class="recent-icon">🕒</span>
<span class="recent-text">authentication</span>
</div>
<div class="recent-item">
<span class="recent-icon">🕒</span>
<span class="recent-text">dashboard 2.0</span>
</div>
</div>
</div>
CSS Styling
.search-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 600px;
max-height: 480px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
overflow: hidden;
z-index: 1000;
}
/* Quick Filters */
.search-filters {
display: flex;
gap: 8px;
padding: 16px;
border-bottom: 1px solid #E5E7EB;
overflow-x: auto;
}
.filter-chip {
padding: 6px 12px;
border: 1px solid #E5E7EB;
border-radius: 16px;
background: #F9FAFB;
font-size: 13px;
color: #6B7280;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.filter-chip:hover {
background: #F3F4F6;
border-color: #D1D5DB;
}
.filter-chip.active {
background: #0066CC;
color: #FFFFFF;
border-color: #0066CC;
}
/* Search Results */
.search-results {
max-height: 400px;
overflow-y: auto;
}
.result-category {
border-bottom: 1px solid #F3F4F6;
}
.result-category:last-child {
border-bottom: none;
}
.category-header {
padding: 12px 16px 8px;
font-size: 12px;
font-weight: 600;
color: #6B7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.result-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.15s ease;
}
.result-item:hover,
.result-item:focus {
background: #F9FAFB;
outline: none;
}
.result-item.selected {
background: #EFF6FF;
border-left: 3px solid #0066CC;
padding-left: 13px; /* Compensate for border */
}
.result-icon {
font-size: 20px;
flex-shrink: 0;
}
.result-content {
flex: 1;
min-width: 0; /* Allow text truncation */
}
.result-title {
font-size: 14px;
font-weight: 500;
color: #111827;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-meta {
font-size: 12px;
color: #6B7280;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
flex-shrink: 0;
}
/* No Results */
.no-results {
padding: 48px 24px;
text-align: center;
}
.no-results p {
font-size: 14px;
color: #6B7280;
margin-bottom: 8px;
}
.no-results .suggestion {
font-size: 13px;
color: #9CA3AF;
}
/* Recent Searches */
.recent-searches {
padding: 8px 0;
}
.recent-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
cursor: pointer;
transition: background 0.15s ease;
}
.recent-item:hover {
background: #F9FAFB;
}
.recent-icon {
font-size: 16px;
color: #9CA3AF;
}
.recent-text {
font-size: 14px;
color: #111827;
}
11.3 Search Algorithm & Indexing
Fuzzy Matching
Implementation: Fuse.js library for client-side fuzzy search
Configuration:
const fuseOptions = {
keys: [
{ name: 'title', weight: 0.5 }, // Highest weight
{ name: 'content', weight: 0.3 },
{ name: 'projectName', weight: 0.2 }
],
threshold: 0.4, // 0 = exact match, 1 = match anything
distance: 100, // Max character distance for match
minMatchCharLength: 2, // Minimum query length
includeScore: true, // Return relevance scores
useExtendedSearch: true // Enable advanced queries
};
const fuse = new Fuse(searchableItems, fuseOptions);
Search Data Structure
Combined index of all searchable entities:
const searchableItems = [
// Tasks
{
id: 'task-123',
type: 'task',
title: 'Implement user authentication',
content: 'Add JWT-based authentication middleware',
projectName: 'coditect-cloud-backend',
phase: 'Phase 1',
priority: 'P0',
status: 'pending',
url: '/projects/coditect-cloud-backend#task-123'
},
// Projects
{
id: 'project-456',
type: 'project',
title: 'coditect-cloud-backend',
content: 'Rust/Actix backend for CODITECT platform',
taskCount: 530,
completion: 0.78,
priority: 'P1',
url: '/projects/coditect-cloud-backend'
},
// Checkpoints
{
id: 'checkpoint-789',
type: 'checkpoint',
title: 'Sprint 1 Complete - Backend MVP',
content: 'Completed 45 tasks, database schema designed',
date: '2025-11-18',
messageCount: 127,
url: '/checkpoints/2025-11-18-sprint-1-complete'
},
// Git Commits
{
id: 'commit-abc123',
type: 'commit',
title: 'feat(auth): Add JWT authentication middleware',
content: 'Implemented token generation and validation',
projectName: 'coditect-cloud-backend',
date: '2025-11-20',
hash: '7c8ac57',
url: '/commits/7c8ac57'
}
];
Search Function
function performSearch(query) {
if (query.length < 2) {
// Show recent searches
showRecentSearches();
return;
}
// Perform fuzzy search
const results = fuse.search(query);
// Group by category
const grouped = {
tasks: [],
projects: [],
checkpoints: [],
commits: []
};
results.forEach(result => {
const item = result.item;
grouped[item.type + 's'].push({
...item,
score: result.score // Relevance score
});
});
// Limit results per category (top 5)
Object.keys(grouped).forEach(category => {
grouped[category] = grouped[category].slice(0, 5);
});
// Render results
renderSearchResults(grouped, query);
// Save to recent searches
saveRecentSearch(query);
}
// Debounced search (300ms delay)
const debouncedSearch = debounce(performSearch, 300);
document.getElementById('global-search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
11.4 Keyboard Navigation
Arrow Key Navigation
Behavior:
- ↓ (Down): Move selection to next result
- ↑ (Up): Move selection to previous result
- Enter: Navigate to selected result
- Esc: Close dropdown and clear search
Implementation:
let selectedIndex = -1;
const resultItems = document.querySelectorAll('.result-item');
document.getElementById('global-search').addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, resultItems.length - 1);
updateSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection();
} else if (e.key === 'Enter' && selectedIndex >= 0) {
e.preventDefault();
const selectedItem = resultItems[selectedIndex];
const url = selectedItem.dataset.url;
window.location.href = url;
} else if (e.key === 'Escape') {
closeSearchDropdown();
document.getElementById('global-search').value = '';
}
});
function updateSelection() {
// Remove previous selection
resultItems.forEach(item => item.classList.remove('selected'));
// Add selection to current item
if (selectedIndex >= 0) {
resultItems[selectedIndex].classList.add('selected');
resultItems[selectedIndex].scrollIntoView({ block: 'nearest' });
}
}
11.5 Category Filtering
Quick Filter Chips
Behavior: Click chip to filter results by category.
JavaScript:
let activeCategory = 'all';
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
// Update active state
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
// Set active category
activeCategory = chip.dataset.category;
// Re-filter results
filterResultsByCategory(activeCategory);
});
});
function filterResultsByCategory(category) {
const categories = document.querySelectorAll('.result-category');
if (category === 'all') {
// Show all categories
categories.forEach(cat => cat.style.display = 'block');
} else {
// Show only selected category
categories.forEach(cat => {
const categoryType = cat.dataset.category;
cat.style.display = categoryType === category ? 'block' : 'none';
});
}
}
11.6 Recent Searches
LocalStorage Implementation
Save searches:
function saveRecentSearch(query) {
// Get existing searches
let recent = JSON.parse(localStorage.getItem('recentSearches') || '[]');
// Add new search (avoid duplicates)
recent = recent.filter(q => q !== query);
recent.unshift(query);
// Limit to 10 recent searches
recent = recent.slice(0, 10);
// Save back to LocalStorage
localStorage.setItem('recentSearches', JSON.stringify(recent));
}
function showRecentSearches() {
const recent = JSON.parse(localStorage.getItem('recentSearches') || '[]');
if (recent.length === 0) {
document.querySelector('.recent-searches').style.display = 'none';
return;
}
const html = recent.map(query => `
<div class="recent-item" data-query="${query}">
<span class="recent-icon">🕒</span>
<span class="recent-text">${query}</span>
</div>
`).join('');
document.querySelector('.recent-searches').innerHTML = `
<div class="category-header">Recent Searches</div>
${html}
`;
// Click handler
document.querySelectorAll('.recent-item').forEach(item => {
item.addEventListener('click', () => {
const query = item.dataset.query;
document.getElementById('global-search').value = query;
performSearch(query);
});
});
}
11.7 Accessibility (WCAG 2.1 AA)
ARIA Labels
<div class="search-container" role="search">
<input
id="global-search"
type="text"
placeholder="Search tasks, projects, checkpoints... (Cmd+K)"
aria-label="Global search"
aria-autocomplete="list"
aria-controls="search-dropdown"
aria-expanded="false"
/>
<div id="search-dropdown" role="listbox" aria-label="Search results">
<div class="result-item" role="option" tabindex="0" aria-selected="false">
<!-- Result content -->
</div>
</div>
</div>
Screen Reader Announcements
Live region for result counts:
<div aria-live="polite" aria-atomic="true" class="sr-only">
<span id="search-status">Found 23 results for "authentication"</span>
</div>
function announceResults(count, query) {
const status = document.getElementById('search-status');
status.textContent = `Found ${count} results for "${query}"`;
}
11.8 Implementation Prompt - Global Search
Create a global omnisearch system with keyboard-first interaction.
Overall Structure
HTML:
<div class="search-container" role="search">
<!-- Search Input -->
<div class="search-input-wrapper">
<svg class="search-icon" viewBox="0 0 20 20">...</svg>
<input
id="global-search"
type="text"
placeholder="Search tasks, projects, checkpoints... (Cmd+K)"
aria-label="Global search"
/>
<kbd class="search-shortcut">⌘K</kbd>
</div>
<!-- Dropdown (hidden by default) -->
<div id="search-dropdown" class="search-dropdown" style="display: none;">
<!-- Content from 11.2 -->
</div>
</div>
JavaScript Setup
- Initialize Fuse.js:
const fuse = new Fuse(searchableItems, fuseOptions);
- Add keyboard shortcut listener:
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search').focus();
}
});
- Add input listener with debounce:
const debouncedSearch = debounce(performSearch, 300);
document.getElementById('global-search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
- Add arrow key navigation:
// Code from 11.4
CSS Requirements
- Search input: 400px → 600px on focus with smooth transition
- Dropdown: 600px width, max 480px height, white background, large shadow
- Result items: Hover state (#F9FAFB), selected state (#EFF6FF with blue border)
- Filter chips: Pill-shaped, gray inactive, blue active
Data Requirements
Fetch from database:
- Tasks: title, content, project, phase, priority, status, URL
- Projects: title, description, task count, completion %, URL
- Checkpoints: title, content, date, message count, URL
- Commits: title, content, project, date, hash, URL
Total searchable items: ~15,000+ across all categories
Next: 12. Responsive Design Previous: 10. Task Detail Modal Index: Master Index