Skip to main content

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

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}"`;
}

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

  1. Initialize Fuse.js:
const fuse = new Fuse(searchableItems, fuseOptions);
  1. Add keyboard shortcut listener:
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search').focus();
}
});
  1. Add input listener with debounce:
const debouncedSearch = debounce(performSearch, 300);
document.getElementById('global-search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
  1. 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