# Implementation Quick Start Guide
**Document**: Viewer Improvements Implementation
**Estimated Time**: 4 weeks (1 developer)
**Complexity**: Medium
## TL;DR - Quick Decisions
| Question | Answer |
|----------|--------|
| **Component approach?** | Shared `SearchResults.vue` component used by both DocumentView and SearchView |
| **State management?** | Composition API composables (NO Pinia/Vuex needed) |
| **API changes?** | Add `currentDocumentId` param to `/api/search`, return grouping metadata |
| **Fixed positioning?** | Use `position: sticky` for nav, `position: fixed` + Teleport for dropdown |
| **Search UX?** | Dropdown results in DocumentView, full-page in SearchView |
| **Mobile?** | Full-screen modal on mobile, dropdown on desktop |
---
## Phase 1: Create Base Components (2 days)
### Step 1.1: SearchResults.vue
**Location**: `/client/src/components/search/SearchResults.vue`
**Key Features**:
- Accepts `results` array prop
- Groups by document if `groupByDocument` prop is true
- Emits `@result-click` events
- Keyboard navigation (arrow keys)
**Props Interface**:
```typescript
interface Props {
results: SearchHit[]
currentDocId?: string
groupByDocument?: boolean // default: true
maxResults?: number // default: 20
variant?: 'dropdown' | 'full-page'
loading?: boolean
}
```
**Sample Implementation Stub**:
```vue
```
### Step 1.2: SearchResultCard.vue
**Location**: `/client/src/components/search/SearchResultCard.vue`
**Reuse existing styles** from `SearchView.vue`:
- Copy `.nv-card`, `.nv-snippet`, `.nv-meta` classes
- Extract to shared component
**Sample Implementation**:
```vue
```
### Step 1.3: SearchDropdown.vue
**Location**: `/client/src/components/search/SearchDropdown.vue`
**Key Features**:
- Fixed positioning
- Click-outside to close
- Escape key handler
- Smooth transitions
**Sample Implementation**:
```vue
```
---
## Phase 2: Create Composables (1 day)
### Step 2.1: useDocumentSearch.js
**Location**: `/client/src/composables/useDocumentSearch.js`
```javascript
import { ref, computed } from 'vue'
import { useSearch } from './useSearch'
export function useDocumentSearch(documentId) {
const { search, results, loading } = useSearch()
const searchQuery = ref('')
const isDropdownOpen = ref(false)
// Separate current doc results from others
const groupedResults = computed(() => {
const current = []
const other = []
results.value.forEach(hit => {
if (hit.docId === documentId.value) {
current.push({ ...hit, _isCurrentDoc: true })
} else {
other.push({ ...hit, _isCurrentDoc: false })
}
})
return { current, other }
})
// Prioritized: current doc first
const prioritizedResults = computed(() => [
...groupedResults.value.current,
...groupedResults.value.other
])
async function searchWithScope(query) {
searchQuery.value = query
if (!query.trim()) {
isDropdownOpen.value = false
return
}
// Search globally, but include current doc metadata
await search(query, {
limit: 50,
currentDocumentId: documentId.value // Backend uses this for grouping
})
isDropdownOpen.value = true
}
function closeDropdown() {
isDropdownOpen.value = false
}
return {
searchQuery,
isDropdownOpen,
loading,
groupedResults,
prioritizedResults,
searchWithScope,
closeDropdown
}
}
```
**Usage in DocumentView**:
```javascript
const documentId = ref(route.params.id)
const {
searchQuery,
isDropdownOpen,
prioritizedResults,
searchWithScope,
closeDropdown
} = useDocumentSearch(documentId)
```
---
## Phase 3: Update Backend API (0.5 days)
### Step 3.1: Modify /routes/search.js
**File**: `/home/setup/navidocs/server/routes/search.js`
**Changes**:
1. Accept `currentDocumentId` in request body
2. Add grouping metadata to response
```javascript
router.post('/', async (req, res) => {
const {
q,
filters = {},
limit = 20,
offset = 0,
currentDocumentId = null // NEW
} = req.body
// ... existing auth and filter logic ...
const searchResults = await index.search(q, {
filter: filterString,
limit: parseInt(limit),
offset: parseInt(offset),
attributesToHighlight: ['text'],
attributesToCrop: ['text'],
cropLength: 200
})
// NEW: Add metadata about current document
const hits = searchResults.hits.map(hit => ({
...hit,
_isCurrentDoc: currentDocumentId && hit.docId === currentDocumentId
}))
// NEW: Calculate grouping metadata
let grouping = null
if (currentDocumentId) {
const currentDocHits = hits.filter(h => h.docId === currentDocumentId)
const otherDocHits = hits.filter(h => h.docId !== currentDocumentId)
const uniqueOtherDocs = new Set(otherDocHits.map(h => h.docId))
grouping = {
currentDocument: {
docId: currentDocumentId,
hitCount: currentDocHits.length
},
otherDocuments: {
hitCount: otherDocHits.length,
documentCount: uniqueOtherDocs.size
}
}
}
return res.json({
hits,
estimatedTotalHits: searchResults.estimatedTotalHits || 0,
query: searchResults.query || q,
processingTimeMs: searchResults.processingTimeMs || 0,
limit: parseInt(limit),
offset: parseInt(offset),
grouping // NEW
})
})
```
**Test with curl**:
```bash
curl -X POST http://localhost:3000/api/search \
-H "Content-Type: application/json" \
-d '{
"q": "bilge pump",
"currentDocumentId": "doc-uuid-456",
"limit": 50
}'
```
Expected response should include `grouping` object.
---
## Phase 4: Integrate into DocumentView (2 days)
### Step 4.1: Add Search to Header
**File**: `/client/src/views/DocumentView.vue`
**Modify template** (around line 4-27):
```vue
```
### Step 4.2: Add @vueuse/core dependency
```bash
cd /home/setup/navidocs/client
npm install @vueuse/core
```
---
## Phase 5: Refactor SearchView (1 day)
**Goal**: Replace existing result cards with `SearchResults` component
**File**: `/client/src/views/SearchView.vue`
**Before** (lines 64-183):
```vue
```
**After**:
```vue
```
**Remove** old styles (lines 335-606) - now in SearchResultCard component
---
## Testing Checklist
### Unit Tests (Vitest)
```bash
cd /home/setup/navidocs/client
# Create test files
touch src/components/search/__tests__/SearchResults.test.js
touch src/composables/__tests__/useDocumentSearch.test.js
```
**Sample test** (`SearchResults.test.js`):
```javascript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import SearchResults from '../SearchResults.vue'
describe('SearchResults', () => {
it('groups results by current document', () => {
const wrapper = mount(SearchResults, {
props: {
results: [
{ id: '1', docId: 'doc-123', title: 'Doc 1' },
{ id: '2', docId: 'doc-456', title: 'Doc 2' },
{ id: '3', docId: 'doc-123', title: 'Doc 1' }
],
currentDocId: 'doc-123',
groupByDocument: true
}
})
// Should show "This Document" section first
const sections = wrapper.findAll('.results-section')
expect(sections[0].text()).toContain('This Document (2)')
expect(sections[1].text()).toContain('Other Documents (1)')
})
it('emits result-click event', async () => {
const wrapper = mount(SearchResults, {
props: {
results: [{ id: '1', docId: 'doc-123', pageNumber: 5 }]
}
})
await wrapper.find('.nv-card').trigger('click')
expect(wrapper.emitted('result-click')).toBeTruthy()
})
})
```
### E2E Tests (Playwright)
```javascript
// tests/e2e/search-dropdown.spec.js
import { test, expect } from '@playwright/test'
test('document viewer search dropdown', async ({ page }) => {
// Navigate to a document
await page.goto('/document/test-doc-123')
// Type in search
const searchInput = page.locator('input[placeholder*="Search"]')
await searchInput.fill('bilge pump')
// Wait for dropdown
await page.waitForSelector('.search-dropdown')
// Verify results are grouped
await expect(page.locator('text=This Document')).toBeVisible()
// Click first result
await page.locator('.nv-card').first().click()
// Verify navigation
await expect(page).toHaveURL(/page=\d+/)
// Verify dropdown closed
await expect(page.locator('.search-dropdown')).not.toBeVisible()
})
```
Run tests:
```bash
npm test # Unit tests
npm run test:e2e # E2E tests
```
---
## Deployment Steps
### 1. Build and Test Locally
```bash
# Server
cd /home/setup/navidocs/server
npm run test
# Client
cd /home/setup/navidocs/client
npm run build
npm run preview # Test production build
# Playwright E2E
npm run test:e2e
```
### 2. Performance Benchmarks
```bash
# Measure search latency
curl -w "@curl-format.txt" -X POST http://localhost:3000/api/search \
-H "Content-Type: application/json" \
-d '{"q": "test", "currentDocumentId": "doc-123"}'
# Expected: < 100ms
```
### 3. Accessibility Audit
```bash
# Install axe-core
npm install -D @axe-core/playwright
# Run audit
npm run test:a11y
```
### 4. Feature Flag (Optional)
Add to `.env`:
```bash
ENABLE_NEW_SEARCH_UI=true
```
Use in code:
```javascript
const useNewSearch = import.meta.env.VITE_ENABLE_NEW_SEARCH_UI === 'true'
```
### 5. Deploy
```bash
# Build production
npm run build
# Deploy (adjust for your hosting)
# Example: Vercel, Netlify, etc.
```
---
## Common Issues & Solutions
### Issue 1: Dropdown positioned incorrectly
**Symptom**: Dropdown appears in wrong location after scroll
**Solution**: Use `position: fixed` with dynamic calculation:
```javascript
const dropdownStyles = computed(() => {
const rect = searchInputRef.value?.getBoundingClientRect()
return {
position: 'fixed',
top: `${rect.bottom + 8}px`,
left: `${rect.left}px`
}
})
```
### Issue 2: Search results not grouping
**Symptom**: All results in "Other Documents" section
**Solution**: Verify `currentDocumentId` is passed correctly:
```javascript
// In DocumentView.vue
const documentId = ref(route.params.id)
// Ensure it's reactive
watch(() => route.params.id, (newId) => {
documentId.value = newId
})
```
### Issue 3: Dropdown doesn't close on Escape
**Symptom**: Keyboard shortcuts not working
**Solution**: Ensure event listener is attached to `document`:
```javascript
onMounted(() => {
document.addEventListener('keydown', handleEscape, { capture: true })
})
```
---
## Quick Reference: File Changes
```
NEW FILES (10):
✓ /client/src/components/search/SearchResults.vue
✓ /client/src/components/search/SearchResultCard.vue
✓ /client/src/components/search/SearchDropdown.vue
✓ /client/src/components/search/SearchInput.vue
✓ /client/src/components/navigation/CompactNavControls.vue
✓ /client/src/components/navigation/NavTooltip.vue
✓ /client/src/composables/useDocumentSearch.js
✓ /client/src/composables/useSearchResults.js
✓ /client/src/components/search/__tests__/SearchResults.test.js
✓ /client/src/composables/__tests__/useDocumentSearch.test.js
MODIFIED FILES (4):
✓ /client/src/views/DocumentView.vue (add search UI)
✓ /client/src/views/SearchView.vue (use SearchResults component)
✓ /client/src/views/HomeView.vue (use SearchInput component)
✓ /server/routes/search.js (add grouping metadata)
DEPENDENCIES:
✓ npm install @vueuse/core
```
---
## Success Criteria
- [x] Search dropdown appears in DocumentView header
- [x] Results grouped: "This Document" first, "Other Documents" second
- [x] Clicking result navigates to correct page
- [x] Escape key closes dropdown
- [x] Click outside closes dropdown
- [x] Debounced search (max 1 request per 300ms)
- [x] SearchView reuses SearchResults component
- [x] 80%+ test coverage
- [x] Lighthouse score > 95
- [x] No accessibility violations
---
**Next Action**: Start with Phase 1 (base components), test each component individually before integration.