diff --git a/src/components/BootSequence.tsx b/src/components/BootSequence.tsx
index 4b6b138..5c093c4 100644
--- a/src/components/BootSequence.tsx
+++ b/src/components/BootSequence.tsx
@@ -24,9 +24,9 @@ interface BootConfig {
lineDelay: number
cursorBlinkInterval: number
holdAfterComplete: number
+ loadingDuration: number
fadeOutDuration: number
cursorShrinkDuration: number
- completionDelay: number
}
colors: {
bright: string
@@ -82,15 +82,15 @@ const BOOT_CONFIG: BootConfig = {
{ type: 'module', text: 'population_health.mod', style: 'dim' },
{ type: 'module', text: 'data_analytics.eng', style: 'dim' },
{ type: 'separator', text: '---', style: 'dim' },
- { type: 'ready', text: 'READY \u2014 Rendering CV..', style: 'bright' },
+ { type: 'ready', text: 'READY \u2014 Launching CV..', style: 'bright' },
],
timing: {
lineDelay: 220,
cursorBlinkInterval: 300,
- holdAfterComplete: 1000,
- fadeOutDuration: 600,
- cursorShrinkDuration: 600,
- completionDelay: 0,
+ holdAfterComplete: 600,
+ loadingDuration: 1200,
+ fadeOutDuration: 500,
+ cursorShrinkDuration: 400,
},
colors: COLORS,
}
@@ -189,13 +189,62 @@ function buildTypedLines(): TypedLine[] {
const TYPED_LINES = buildTypedLines()
const TOTAL_CHARS = TYPED_LINES.reduce((sum, l) => sum + l.totalChars, 0)
+// =============================================================================
+// Progress Bar Component
+// =============================================================================
+
+function ProgressBar({ active }: { active: boolean }) {
+ const [progress, setProgress] = useState(0)
+
+ useEffect(() => {
+ if (!active) return
+ const start = performance.now()
+ let raf: number
+
+ const tick = (now: number) => {
+ const elapsed = now - start
+ const pct = Math.min(elapsed / BOOT_CONFIG.timing.loadingDuration, 1)
+ // Ease-out curve for natural feel
+ setProgress(1 - Math.pow(1 - pct, 2.5))
+ if (pct < 1) raf = requestAnimationFrame(tick)
+ }
+
+ raf = requestAnimationFrame(tick)
+ return () => cancelAnimationFrame(raf)
+ }, [active])
+
+ return (
+
+ )
+}
+
// =============================================================================
// Main Component
// =============================================================================
export function BootSequence({ onComplete }: BootSequenceProps) {
const [typedCount, setTypedCount] = useState(0)
- const [phase, setPhase] = useState<'typing' | 'holding' | 'fading' | 'done'>('typing')
+ const [phase, setPhase] = useState<'typing' | 'holding' | 'loading' | 'fading' | 'done'>('typing')
const [isVisible, setIsVisible] = useState(true)
const cursorAnchorRef = useRef(null)
const containerRef = useRef(null)
@@ -206,7 +255,6 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false
-
// Typing engine — runs as a self-scheduling setTimeout chain
useEffect(() => {
if (reducedMotion || phase !== 'typing') return
@@ -253,31 +301,37 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
}
}, [typedCount, phase, reducedMotion])
- // Hold phase: then start fading
+ // Hold phase → loading
useEffect(() => {
if (phase !== 'holding') return
- const fadeTimer = setTimeout(() => {
- setPhase('fading')
+ const timer = setTimeout(() => {
+ setPhase('loading')
}, BOOT_CONFIG.timing.holdAfterComplete)
- return () => clearTimeout(fadeTimer)
+ return () => clearTimeout(timer)
+ }, [phase])
+
+ // Loading phase → fading (after progress bar completes)
+ useEffect(() => {
+ if (phase !== 'loading') return
+
+ const timer = setTimeout(() => {
+ setPhase('fading')
+ }, BOOT_CONFIG.timing.loadingDuration + 100)
+
+ return () => clearTimeout(timer)
}, [phase])
// Fade phase: wait for animations to finish, then complete
useEffect(() => {
if (phase !== 'fading') return
- const longestFade = Math.max(
- BOOT_CONFIG.timing.fadeOutDuration,
- BOOT_CONFIG.timing.cursorShrinkDuration
- )
-
const completeTimer = setTimeout(() => {
setIsVisible(false)
setPhase('done')
onComplete()
- }, longestFade + BOOT_CONFIG.timing.completionDelay)
+ }, BOOT_CONFIG.timing.fadeOutDuration)
return () => clearTimeout(completeTimer)
}, [phase, onComplete])
@@ -379,6 +433,8 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
return renderedLines
}
+ const isFadingOut = phase === 'fading' || phase === 'done'
+
// Reduced motion: instant render
if (reducedMotion) {
return (
@@ -421,13 +477,13 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
{/* CRT Scanlines */}
- {/* Text fades out independently */}
+ {/* Text content — slides up and fades during exit */}
{renderLines()}
+
+ {/* Progress bar — appears during loading phase */}
+ {(phase === 'loading' || phase === 'fading') && (
+
+ )}
- {/* Cursor rendered outside fading wrapper — shrinks independently */}
- {cursorPos && phase !== 'done' && (
+ {/* Cursor rendered outside fading wrapper — shrinks into progress bar */}
+ {cursorPos && !isFadingOut && (