diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx
index 18b0d08..e185c3e 100644
--- a/src/components/DashboardLayout.tsx
+++ b/src/components/DashboardLayout.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { TopBar } from './TopBar'
+import { SubNav } from './SubNav'
import Sidebar from './Sidebar'
import { CommandPalette } from './CommandPalette'
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
@@ -10,6 +11,7 @@ import { LastConsultationTile } from './tiles/LastConsultationTile'
import { CareerActivityTile } from './tiles/CareerActivityTile'
import { EducationTile } from './tiles/EducationTile'
import { ProjectsTile } from './tiles/ProjectsTile'
+import { useActiveSection } from '@/hooks/useActiveSection'
import type { PaletteAction } from '@/lib/search'
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
@@ -48,6 +50,7 @@ const contentVariants = {
export function DashboardLayout() {
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
+ const activeSection = useActiveSection()
const handleSearchClick = () => {
setCommandPaletteOpen(true)
@@ -57,6 +60,11 @@ export function DashboardLayout() {
setCommandPaletteOpen(false)
}, [])
+ const handleSectionClick = useCallback((_sectionId: string) => {
+ // Section click is already handled in SubNav component
+ // This is just a placeholder for any additional logic needed
+ }, [])
+
// Global Ctrl+K listener to open command palette
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
@@ -114,12 +122,15 @@ export function DashboardLayout() {
- {/* Layout below TopBar: Sidebar + Main */}
+ {/* SubNav — sticky below TopBar */}
+
+
+ {/* Layout below TopBar + SubNav: Sidebar + Main */}
{/* Sidebar — hidden on mobile/tablet, visible on desktop */}
diff --git a/src/components/SubNav.tsx b/src/components/SubNav.tsx
new file mode 100644
index 0000000..4824da4
--- /dev/null
+++ b/src/components/SubNav.tsx
@@ -0,0 +1,88 @@
+interface NavSection {
+ id: string
+ label: string
+ tileId: string // data-tile-id to scroll to
+}
+
+interface SubNavProps {
+ activeSection: string
+ onSectionClick: (sectionId: string) => void
+}
+
+const sections: NavSection[] = [
+ { id: 'overview', label: 'Overview', tileId: 'patient-summary' },
+ { id: 'skills', label: 'Skills', tileId: 'core-skills' },
+ { id: 'experience', label: 'Experience', tileId: 'career-activity' },
+ { id: 'projects', label: 'Projects', tileId: 'projects' },
+ { id: 'education', label: 'Education', tileId: 'education' },
+]
+
+export function SubNav({ activeSection, onSectionClick }: SubNavProps) {
+ const handleSectionClick = (section: NavSection) => {
+ // Scroll to the tile
+ const tileEl = document.querySelector(`[data-tile-id="${section.tileId}"]`)
+ if (tileEl) {
+ tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ }
+ // Notify parent of section change
+ onSectionClick(section.id)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/hooks/useActiveSection.ts b/src/hooks/useActiveSection.ts
new file mode 100644
index 0000000..ad193d9
--- /dev/null
+++ b/src/hooks/useActiveSection.ts
@@ -0,0 +1,66 @@
+import { useState, useEffect } from 'react'
+
+// Map tile IDs to section IDs for SubNav
+const sectionTileMap: Record = {
+ 'patient-summary': 'overview',
+ 'core-skills': 'skills',
+ 'career-activity': 'experience',
+ 'projects': 'projects',
+ 'education': 'education',
+}
+
+/**
+ * Hook to track which section is currently visible using IntersectionObserver.
+ * Observes tiles by their data-tile-id attribute and maps them to section IDs.
+ *
+ * @returns The currently active section ID
+ */
+export function useActiveSection(): string {
+ const [activeSection, setActiveSection] = useState('overview')
+
+ useEffect(() => {
+ // Find all tiles with data-tile-id attribute
+ const tiles = Array.from(
+ document.querySelectorAll('[data-tile-id]')
+ ) as HTMLElement[]
+
+ if (tiles.length === 0) return
+
+ // IntersectionObserver to track which tile is visible
+ const observer = new IntersectionObserver(
+ (entries) => {
+ // Find the entry with the highest intersection ratio
+ const visibleEntries = entries.filter((entry) => entry.isIntersecting)
+
+ if (visibleEntries.length === 0) return
+
+ // Get the most visible tile (highest intersection ratio)
+ const mostVisible = visibleEntries.reduce((prev, current) =>
+ current.intersectionRatio > prev.intersectionRatio ? current : prev
+ )
+
+ // Get the tile ID and map to section ID
+ const tileId = mostVisible.target.getAttribute('data-tile-id')
+ if (tileId && sectionTileMap[tileId]) {
+ setActiveSection(sectionTileMap[tileId])
+ }
+ },
+ {
+ // Trigger when tile is 25% visible
+ threshold: [0, 0.25, 0.5, 0.75, 1],
+ // Use viewport as root, with some margin for better UX
+ rootMargin: '-80px 0px -80% 0px',
+ }
+ )
+
+ // Observe all tiles
+ tiles.forEach((tile) => observer.observe(tile))
+
+ // Cleanup
+ return () => {
+ tiles.forEach((tile) => observer.unobserve(tile))
+ }
+ }, [])
+
+ return activeSection
+}