diff --git a/src/index.css b/src/index.css
index c175242..4098b8d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -337,3 +337,25 @@ html {
grid-template-columns: 1fr;
}
}
+
+/* ===== COMMAND PALETTE ANIMATIONS ===== */
+@keyframes palette-overlay-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes palette-modal-in {
+ from { transform: scale(0.97) translateY(-8px); opacity: 0; }
+ to { transform: scale(1) translateY(0); opacity: 1; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ @keyframes palette-overlay-in {
+ from { opacity: 1; }
+ to { opacity: 1; }
+ }
+ @keyframes palette-modal-in {
+ from { transform: none; opacity: 1; }
+ to { transform: none; opacity: 1; }
+ }
+}
diff --git a/src/lib/search.ts b/src/lib/search.ts
index 8762042..c47a69b 100644
--- a/src/lib/search.ts
+++ b/src/lib/search.ts
@@ -1,12 +1,298 @@
import Fuse, { type FuseResult } from 'fuse.js'
import type { ViewId } from '@/types/pmr'
-// Import all data sources
import { consultations } from '@/data/consultations'
import { medications } from '@/data/medications'
import { problems } from '@/data/problems'
import { investigations } from '@/data/investigations'
import { documents } from '@/data/documents'
+import { skills } from '@/data/skills'
+
+export type PaletteSection = 'Experience' | 'Core Skills' | 'Active Projects' | 'Achievements' | 'Education' | 'Quick Actions'
+
+export type PaletteAction =
+ | { type: 'scroll'; tileId: string }
+ | { type: 'expand'; tileId: string; itemId: string }
+ | { type: 'link'; url: string }
+ | { type: 'download' }
+
+export type IconColorVariant = 'teal' | 'green' | 'amber' | 'purple'
+
+export interface PaletteItem {
+ id: string
+ title: string
+ subtitle: string
+ section: PaletteSection
+ iconVariant: IconColorVariant
+ iconType: 'role' | 'skill' | 'project' | 'achievement' | 'edu' | 'action'
+ keywords: string
+ action: PaletteAction
+}
+
+// Build the full palette dataset matching the concept HTML structure
+export function buildPaletteData(): PaletteItem[] {
+ const items: PaletteItem[] = []
+
+ // Experience — matching concept HTML entries
+ const experienceEntries: Array<{ title: string; sub: string; keywords: string; activityId: string }> = [
+ {
+ title: 'Interim Head, Population Health & Data Analysis',
+ sub: 'NHS Norfolk & Waveney ICB \u00b7 2024\u20132025',
+ keywords: 'head interim population health data analysis nhs norfolk waveney icb 2024 2025 latest current',
+ activityId: 'interim-head',
+ },
+ {
+ title: 'Senior Data Analyst \u2014 Medicines Optimisation',
+ sub: 'NHS Norfolk & Waveney ICB \u00b7 2021\u20132024',
+ keywords: 'senior data analyst medicines optimisation nhs norfolk waveney icb 2021 2024',
+ activityId: 'senior-analyst',
+ },
+ {
+ title: 'Prescribing Data Pharmacist',
+ sub: 'NHS Norwich CCG \u00b7 2018\u20132021',
+ keywords: 'prescribing data pharmacist nhs norwich ccg 2018 2021',
+ activityId: 'prescribing-pharmacist',
+ },
+ {
+ title: 'Community Pharmacist',
+ sub: 'Boots UK \u00b7 2016\u20132018',
+ keywords: 'community pharmacist boots uk 2016 2018',
+ activityId: 'community-pharmacist',
+ },
+ ]
+
+ experienceEntries.forEach((entry, i) => {
+ items.push({
+ id: `exp-${i}`,
+ title: entry.title,
+ subtitle: entry.sub,
+ section: 'Experience',
+ iconVariant: 'teal',
+ iconType: 'role',
+ keywords: entry.keywords,
+ action: { type: 'expand', tileId: 'career-activity', itemId: entry.activityId },
+ })
+ })
+
+ // Core Skills — from skills.ts, matching concept format with proficiency %
+ const skillDescriptions: Record
= {
+ 'Data Analysis': 'Primary expertise \u00b7 NHS population data',
+ 'Python': 'Data pipelines, automation, analytics',
+ 'SQL': 'Advanced queries, database migration',
+ 'Power BI': 'Dashboard design & deployment',
+ 'JavaScript / TypeScript': 'Web development & tooling',
+ }
+
+ skills.forEach((skill) => {
+ items.push({
+ id: `skill-${skill.id}`,
+ title: `${skill.name} \u2014 ${skill.proficiency}%`,
+ subtitle: skillDescriptions[skill.name] ?? `${skill.frequency} \u00b7 Since ${skill.startYear}`,
+ section: 'Core Skills',
+ iconVariant: 'green',
+ iconType: 'skill',
+ keywords: `${skill.name.toLowerCase()} ${skill.proficiency} ${skill.frequency.toLowerCase()}`,
+ action: { type: 'expand', tileId: 'core-skills', itemId: skill.id },
+ })
+ })
+
+ // Active Projects — matching concept HTML entries
+ const projectEntries: Array<{ name: string; sub: string; keywords: string; investigationId: string }> = [
+ {
+ name: '\u00a3220M Prescribing Budget',
+ sub: 'Budget oversight & analytical accountability \u00b7 2024',
+ keywords: '220m prescribing budget oversight analytical accountability 2024',
+ investigationId: 'inv-pharmetrics',
+ },
+ {
+ name: 'SQL Analytics Transformation',
+ sub: 'Legacy migration to modern data stack \u00b7 2025',
+ keywords: 'sql analytics transformation legacy migration modern data stack 2025',
+ investigationId: 'inv-switching-algorithm',
+ },
+ {
+ name: 'Team Data Literacy Programme',
+ sub: 'Upskilling 30+ non-technical staff \u00b7 2024',
+ keywords: 'team data literacy programme upskilling non-technical staff 2024 training',
+ investigationId: 'inv-blueteq-gen',
+ },
+ ]
+
+ projectEntries.forEach((entry) => {
+ items.push({
+ id: `proj-${entry.investigationId}`,
+ title: entry.name,
+ subtitle: entry.sub,
+ section: 'Active Projects',
+ iconVariant: 'amber',
+ iconType: 'project',
+ keywords: entry.keywords,
+ action: { type: 'expand', tileId: 'projects', itemId: entry.investigationId },
+ })
+ })
+
+ // Achievements — matching concept HTML entries
+ const achievementEntries: Array<{ title: string; sub: string; keywords: string }> = [
+ {
+ title: '\u00a314.6M Efficiency Savings Identified',
+ sub: 'Data-driven prescribing interventions',
+ keywords: '14.6m efficiency savings identified data-driven prescribing interventions money cost',
+ },
+ {
+ title: '\u00a3220M Budget Oversight',
+ sub: 'Full analytical accountability to ICB board',
+ keywords: '220m budget oversight analytical accountability icb board',
+ },
+ {
+ title: 'Power BI Dashboards for 200+ Users',
+ sub: 'Clinicians & commissioners across ICB',
+ keywords: 'power bi dashboards 200 users clinicians commissioners',
+ },
+ {
+ title: 'Team of 12 Led',
+ sub: 'Cross-functional data & population health',
+ keywords: 'team 12 led cross-functional data population health leadership management',
+ },
+ ]
+
+ achievementEntries.forEach((entry, i) => {
+ items.push({
+ id: `ach-${i}`,
+ title: entry.title,
+ subtitle: entry.sub,
+ section: 'Achievements',
+ iconVariant: 'amber',
+ iconType: 'achievement',
+ keywords: entry.keywords,
+ action: { type: 'scroll', tileId: 'latest-results' },
+ })
+ })
+
+ // Education — matching concept HTML entries
+ const educationEntries: Array<{ title: string; sub: string; keywords: string }> = [
+ {
+ title: 'MPharm (Hons) \u2014 2:1',
+ sub: 'University of East Anglia \u00b7 2011\u20132015',
+ keywords: 'mpharm hons 2:1 university east anglia uea 2011 2015 pharmacy degree',
+ },
+ {
+ title: 'GPhC Registration',
+ sub: 'General Pharmaceutical Council \u00b7 August 2016',
+ keywords: 'gphc registration general pharmaceutical council 2016 registered',
+ },
+ {
+ title: 'Power BI Data Analyst Associate',
+ sub: 'Microsoft Certified \u00b7 2023',
+ keywords: 'power bi data analyst associate microsoft certified 2023 certification',
+ },
+ {
+ title: 'Clinical Pharmacy Diploma',
+ sub: 'Professional development \u00b7 2019',
+ keywords: 'clinical pharmacy diploma professional development 2019',
+ },
+ ]
+
+ educationEntries.forEach((entry, i) => {
+ items.push({
+ id: `edu-${i}`,
+ title: entry.title,
+ subtitle: entry.sub,
+ section: 'Education',
+ iconVariant: 'purple',
+ iconType: 'edu',
+ keywords: entry.keywords,
+ action: { type: 'scroll', tileId: 'education' },
+ })
+ })
+
+ // Quick Actions
+ const quickActions: Array<{ title: string; sub: string; keywords: string; action: PaletteAction }> = [
+ {
+ title: 'Download CV',
+ sub: 'Export as PDF',
+ keywords: 'download cv export pdf resume',
+ action: { type: 'download' },
+ },
+ {
+ title: 'Send Email',
+ sub: 'andy@charlwood.xyz',
+ keywords: 'send email contact andy charlwood',
+ action: { type: 'link', url: 'mailto:andy@charlwood.xyz' },
+ },
+ {
+ title: 'View LinkedIn',
+ sub: 'Professional profile',
+ keywords: 'view linkedin professional profile social',
+ action: { type: 'link', url: 'https://linkedin.com/in/andycharlwood' },
+ },
+ {
+ title: 'View Projects',
+ sub: 'GitHub & portfolio',
+ keywords: 'view projects github portfolio code repositories',
+ action: { type: 'link', url: 'https://github.com/andycharlwood' },
+ },
+ ]
+
+ quickActions.forEach((entry, i) => {
+ items.push({
+ id: `action-${i}`,
+ title: entry.title,
+ subtitle: entry.sub,
+ section: 'Quick Actions',
+ iconVariant: 'teal',
+ iconType: 'action',
+ keywords: entry.keywords,
+ action: entry.action,
+ })
+ })
+
+ return items
+}
+
+// Build a fuse.js search index from palette items
+export function buildSearchIndex(items: PaletteItem[]): Fuse {
+ return new Fuse(items, {
+ keys: [
+ { name: 'title', weight: 2 },
+ { name: 'subtitle', weight: 1 },
+ { name: 'keywords', weight: 1.5 },
+ ],
+ threshold: 0.3,
+ includeScore: true,
+ minMatchCharLength: 2,
+ })
+}
+
+// Section ordering for grouped display
+const SECTION_ORDER: PaletteSection[] = [
+ 'Experience',
+ 'Core Skills',
+ 'Active Projects',
+ 'Achievements',
+ 'Education',
+ 'Quick Actions',
+]
+
+// Group palette items by section, maintaining defined order
+export function groupBySection(items: PaletteItem[]): Array<{ section: PaletteSection; items: PaletteItem[] }> {
+ const groups = new Map()
+
+ for (const item of items) {
+ const existing = groups.get(item.section)
+ if (existing) {
+ existing.push(item)
+ } else {
+ groups.set(item.section, [item])
+ }
+ }
+
+ return SECTION_ORDER
+ .filter(section => groups.has(section))
+ .map(section => ({ section, items: groups.get(section)! }))
+}
+
+// ===== LEGACY EXPORTS =====
+// Used by ClinicalSidebar.tsx (old component, will be removed in Task 21)
export interface SearchResult {
id: string
@@ -17,83 +303,40 @@ export interface SearchResult {
score?: number
}
-// Build a unified search index from all PMR content
-export function buildSearchIndex(): Fuse {
+/** @deprecated Use buildPaletteData() + buildSearchIndex() instead */
+export function buildLegacySearchIndex(): Fuse {
const searchableItems: SearchResult[] = []
- // Index consultations (Experience)
- consultations.forEach(consultation => {
- searchableItems.push({
- id: consultation.id,
- title: consultation.role,
- section: 'consultations',
- sectionLabel: 'Experience',
- highlight: `${consultation.role} at ${consultation.organization} — ${consultation.history}`,
- })
+ consultations.forEach(c => {
+ searchableItems.push({ id: c.id, title: c.role, section: 'consultations', sectionLabel: 'Experience', highlight: `${c.role} at ${c.organization} — ${c.history}` })
+ })
+ medications.forEach(m => {
+ searchableItems.push({ id: m.id, title: m.name, section: 'medications', sectionLabel: 'Skills', highlight: `${m.name} — ${m.frequency} use since ${m.startYear}` })
+ })
+ problems.forEach(p => {
+ searchableItems.push({ id: p.id, title: p.description, section: 'problems', sectionLabel: 'Achievements', highlight: `[${p.code}] ${p.description} — ${p.narrative}` })
+ })
+ investigations.forEach(inv => {
+ searchableItems.push({ id: inv.id, title: inv.name, section: 'investigations', sectionLabel: 'Projects', highlight: `${inv.name} — ${inv.methodology}` })
+ })
+ documents.forEach(doc => {
+ searchableItems.push({ id: doc.id, title: doc.title, section: 'documents', sectionLabel: 'Education', highlight: `${doc.title} from ${doc.source} (${doc.date})` })
})
- // Index medications (Skills)
- medications.forEach(medication => {
- searchableItems.push({
- id: medication.id,
- title: medication.name,
- section: 'medications',
- sectionLabel: 'Skills',
- highlight: `${medication.name} — ${medication.frequency} use since ${medication.startYear}`,
- })
- })
-
- // Index problems (Achievements)
- problems.forEach(problem => {
- searchableItems.push({
- id: problem.id,
- title: problem.description,
- section: 'problems',
- sectionLabel: 'Achievements',
- highlight: `[${problem.code}] ${problem.description} — ${problem.narrative}`,
- })
- })
-
- // Index investigations (Projects)
- investigations.forEach(investigation => {
- searchableItems.push({
- id: investigation.id,
- title: investigation.name,
- section: 'investigations',
- sectionLabel: 'Projects',
- highlight: `${investigation.name} — ${investigation.methodology}`,
- })
- })
-
- // Index documents (Education)
- documents.forEach(document => {
- searchableItems.push({
- id: document.id,
- title: document.title,
- section: 'documents',
- sectionLabel: 'Education',
- highlight: `${document.title} from ${document.source} (${document.date})`,
- })
- })
-
- // Fuse.js configuration for fuzzy search
- const fuseOptions = {
+ return new Fuse(searchableItems, {
keys: [
- { name: 'title', weight: 2 }, // Primary match on title
- { name: 'highlight', weight: 1 }, // Secondary match on full text
+ { name: 'title', weight: 2 },
+ { name: 'highlight', weight: 1 },
],
- threshold: 0.3, // 0 = exact match, 1 = match anything
+ threshold: 0.3,
includeScore: true,
minMatchCharLength: 2,
- }
-
- return new Fuse(searchableItems, fuseOptions)
+ })
}
-// Group search results by section
+/** @deprecated Use groupBySection() instead */
export function groupResultsBySection(results: FuseResult[]): Map[]> {
const grouped = new Map[]>()
-
results.forEach(result => {
const sectionLabel = result.item.sectionLabel
if (!grouped.has(sectionLabel)) {
@@ -101,6 +344,5 @@ export function groupResultsBySection(results: FuseResult[]): Map<
}
grouped.get(sectionLabel)!.push(result)
})
-
return grouped
}