6a4fc86387
Semantic HTML:
- Changed Card component from div to article element
- Added id="main-content" to main element for skip link target
Keyboard Navigation & ARIA:
- Added skip link to TopBar (visible only on focus, navigates to #main-content)
- Added aria-label="Active session information" to session info container
- Added aria-hidden="true" to all decorative colored dots (CardHeader, CareerActivity, Projects, Sidebar status badge)
- All expandable items already have role="button", tabIndex={0}, aria-expanded
- All KPI cards already have proper aria-label describing flip state
- Command palette already has full ARIA implementation (combobox, listbox, dialog)
Focus Management:
- Added global focus-visible styles in index.css (2px accent outline, 2px offset)
- Buttons, links, inputs all have proper focus rings with accent color
- Command palette focus trap already implemented
Reduced Motion:
- All components already check prefers-reduced-motion at module scope
- Dashboard entrance, tile expansion, KPI flip, palette animations respect reduced motion
- Added reduced motion override for pulse animation (disables pulse, keeps static dot)
Color Contrast:
- All color tokens already meet WCAG AA standards per ref spec
- Tertiary text (#8DA8A5) used only for supplementary labels where information is conveyed elsewhere
Quality checks: typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
212 lines
5.7 KiB
TypeScript
212 lines
5.7 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Home, Search } from 'lucide-react'
|
|
|
|
interface TopBarProps {
|
|
onSearchClick?: () => void
|
|
}
|
|
|
|
export function TopBar({ onSearchClick }: TopBarProps) {
|
|
const [currentTime, setCurrentTime] = useState(() => formatTime(new Date()))
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setCurrentTime(formatTime(new Date()))
|
|
}, 60_000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
return (
|
|
<header
|
|
className="fixed top-0 left-0 right-0 flex items-center justify-between font-ui"
|
|
style={{
|
|
height: 'var(--topbar-height)',
|
|
background: 'var(--surface)',
|
|
borderBottom: '1px solid var(--border)',
|
|
padding: '0 20px',
|
|
zIndex: 100,
|
|
}}
|
|
>
|
|
{/* Skip to main content link (only visible on focus) */}
|
|
<a
|
|
href="#main-content"
|
|
className="skip-link"
|
|
style={{
|
|
position: 'absolute',
|
|
top: '-40px',
|
|
left: '0',
|
|
background: 'var(--accent)',
|
|
color: '#FFFFFF',
|
|
padding: '8px 16px',
|
|
textDecoration: 'none',
|
|
zIndex: 101,
|
|
borderRadius: '0 0 4px 0',
|
|
fontSize: '13px',
|
|
fontWeight: 600,
|
|
}}
|
|
onFocus={(e) => {
|
|
e.currentTarget.style.top = '0'
|
|
}}
|
|
onBlur={(e) => {
|
|
e.currentTarget.style.top = '-40px'
|
|
}}
|
|
>
|
|
Skip to main content
|
|
</a>
|
|
{/* Brand */}
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<Home
|
|
size={18}
|
|
style={{ color: 'var(--accent)' }}
|
|
aria-hidden="true"
|
|
/>
|
|
<span
|
|
className="font-ui hidden sm:inline"
|
|
style={{
|
|
fontSize: '13px',
|
|
fontWeight: 600,
|
|
color: 'var(--text-primary)',
|
|
}}
|
|
>
|
|
Headhunt Medical Center
|
|
</span>
|
|
<span
|
|
className="font-ui sm:hidden"
|
|
style={{
|
|
fontSize: '13px',
|
|
fontWeight: 600,
|
|
color: 'var(--text-primary)',
|
|
}}
|
|
>
|
|
HMC
|
|
</span>
|
|
<span
|
|
className="hidden md:inline"
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 400,
|
|
color: 'var(--text-tertiary)',
|
|
marginLeft: '2px',
|
|
}}
|
|
>
|
|
Remote
|
|
</span>
|
|
</div>
|
|
|
|
{/* Search bar (center) — triggers command palette, no inline search */}
|
|
<button
|
|
type="button"
|
|
onClick={onSearchClick}
|
|
className="hidden md:flex items-center gap-2 cursor-pointer font-ui"
|
|
style={{
|
|
maxWidth: '560px',
|
|
minWidth: '400px',
|
|
height: '42px',
|
|
border: '1.5px solid var(--border)',
|
|
borderRadius: 'var(--radius-card)',
|
|
padding: '0 14px',
|
|
background: 'var(--surface)',
|
|
transition: 'border-color 150ms, box-shadow 150ms',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (document.activeElement !== e.currentTarget) {
|
|
e.currentTarget.style.borderColor = 'var(--border)'
|
|
}
|
|
}}
|
|
onFocus={(e) => {
|
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
|
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(13,110,110,0.12)'
|
|
}}
|
|
onBlur={(e) => {
|
|
e.currentTarget.style.borderColor = 'var(--border)'
|
|
e.currentTarget.style.boxShadow = 'none'
|
|
}}
|
|
aria-label="Search records, experience, skills. Press Control plus K"
|
|
>
|
|
<Search
|
|
size={16}
|
|
style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}
|
|
aria-hidden="true"
|
|
/>
|
|
<span
|
|
className="flex-1 text-left"
|
|
style={{
|
|
fontSize: '13px',
|
|
color: 'var(--text-tertiary)',
|
|
fontFamily: 'var(--font-ui)',
|
|
}}
|
|
>
|
|
Search records, experience, skills...
|
|
</span>
|
|
<kbd
|
|
className="font-geist"
|
|
style={{
|
|
fontSize: '10px',
|
|
color: 'var(--text-tertiary)',
|
|
background: 'var(--bg-dashboard)',
|
|
border: '1px solid var(--border)',
|
|
padding: '2px 6px',
|
|
borderRadius: '4px',
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
Ctrl+K
|
|
</kbd>
|
|
</button>
|
|
|
|
{/* Session info (right) */}
|
|
<div
|
|
className="flex items-center gap-2 sm:gap-3 shrink-0"
|
|
aria-label="Active session information"
|
|
>
|
|
<span
|
|
className="hidden sm:inline"
|
|
style={{
|
|
fontSize: '12px',
|
|
color: 'var(--text-secondary)',
|
|
fontFamily: 'var(--font-ui)',
|
|
}}
|
|
>
|
|
Dr. A.CHARLWOOD
|
|
</span>
|
|
<span
|
|
className="font-geist hidden xs:inline"
|
|
style={{
|
|
fontSize: '11px',
|
|
color: 'var(--text-tertiary)',
|
|
background: 'var(--accent-light)',
|
|
padding: '3px 10px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--accent-border)',
|
|
}}
|
|
>
|
|
Active Session · {currentTime}
|
|
</span>
|
|
<span
|
|
className="font-geist xs:hidden"
|
|
style={{
|
|
fontSize: '11px',
|
|
color: 'var(--text-tertiary)',
|
|
background: 'var(--accent-light)',
|
|
padding: '3px 8px',
|
|
borderRadius: '4px',
|
|
border: '1px solid var(--accent-border)',
|
|
}}
|
|
>
|
|
{currentTime}
|
|
</span>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString('en-GB', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
})
|
|
}
|