Task 18: Add command palette (Ctrl+K)

- Create CommandPalette.tsx with overlay, search input, grouped results,
  keyboard navigation (arrows, Enter, Escape), and footer hints
- Rebuild search.ts with PaletteItem model: 24 entries across 6 sections
  (Experience, Core Skills, Active Projects, Achievements, Education,
  Quick Actions) matching concept HTML structure
- Fuzzy search via fuse.js with weighted keys (title, subtitle, keywords)
- Wire into DashboardLayout with global Ctrl+K listener and TopBar click
- Action system: scroll-to-tile, expand-item, external links, download CV
- Add data-tile-id to all Card/tile components for scroll targeting
- CSS animations: palette-overlay-in, palette-modal-in with
  prefers-reduced-motion support
- Maintain backward-compatible legacy exports for ClinicalSidebar
  (will be removed in Task 21)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 17:54:31 +00:00
parent acee97a579
commit aafdeba93e
13 changed files with 836 additions and 80 deletions
+61 -3
View File
@@ -1,7 +1,8 @@
import { useState } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { TopBar } from './TopBar'
import Sidebar from './Sidebar'
import { CommandPalette } from './CommandPalette'
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
import { LatestResultsTile } from './tiles/LatestResultsTile'
import { CoreSkillsTile } from './tiles/CoreSkillsTile'
@@ -9,6 +10,7 @@ import { LastConsultationTile } from './tiles/LastConsultationTile'
import { CareerActivityTile } from './tiles/CareerActivityTile'
import { EducationTile } from './tiles/EducationTile'
import { ProjectsTile } from './tiles/ProjectsTile'
import type { PaletteAction } from '@/lib/search'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
@@ -45,12 +47,63 @@ const contentVariants = {
}
export function DashboardLayout() {
const [, setCommandPaletteOpen] = useState(false)
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
const handleSearchClick = () => {
setCommandPaletteOpen(true)
}
const handlePaletteClose = useCallback(() => {
setCommandPaletteOpen(false)
}, [])
// Global Ctrl+K listener to open command palette
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault()
setCommandPaletteOpen(prev => !prev)
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
// Handle palette actions (scroll to tile, expand item, open link, download)
const handlePaletteAction = useCallback((action: PaletteAction) => {
switch (action.type) {
case 'scroll': {
const tileEl = document.querySelector(`[data-tile-id="${action.tileId}"]`)
if (tileEl) {
tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
break
}
case 'expand': {
const tileEl = document.querySelector(`[data-tile-id="${action.tileId}"]`)
if (tileEl) {
tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
// Dispatch a custom event that the tile can listen for to expand the item
const expandEvent = new CustomEvent('palette-expand', {
detail: { tileId: action.tileId, itemId: action.itemId },
})
document.dispatchEvent(expandEvent)
}
break
}
case 'link': {
window.open(action.url, '_blank', 'noopener,noreferrer')
break
}
case 'download': {
// For now, open the CV file or trigger a download
// This can be wired to an actual PDF when available
window.open('/References/CV_v4.md', '_blank')
break
}
}
}, [])
return (
<div
className="font-ui"
@@ -123,7 +176,12 @@ export function DashboardLayout() {
</motion.main>
</div>
{/* Command palette will be rendered here (Task 18) */}
{/* Command palette overlay */}
<CommandPalette
isOpen={commandPaletteOpen}
onClose={handlePaletteClose}
onAction={handlePaletteAction}
/>
</div>
)
}