From 5e1c96edfaec482ea4270175db15dd73f0b8ed66 Mon Sep 17 00:00:00 2001 From: A Charlwood Date: Thu, 12 Feb 2026 23:44:33 +0000 Subject: [PATCH] Task 3: Rebuild LoginScreen with interactive login and premium font - Typing speed: 80ms/char username, 60ms/dot password (was 30ms/20ms) - Login button is now user-interactive (not auto-triggered) - Button disabled/dimmed during typing, fully interactive after - Hover state on button (darkens to #004D9F) - Font changed from Inter to Elvaro Grotesque (var(--font-ui)) - Card shadow upgraded to multi-layered per design system - Added 'done' activeField state for post-typing phase - Proper timer cleanup via tracked timeout refs - Reduced motion: typing instant, button immediately clickable Co-Authored-By: Claude Opus 4.6 --- src/components/LoginScreen.tsx | 112 +++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index 5fc7248..1a0c22a 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -11,9 +11,11 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { const [username, setUsername] = useState('') const [passwordDots, setPasswordDots] = useState(0) const [showCursor, setShowCursor] = useState(true) - const [activeField, setActiveField] = useState<'username' | 'password' | null>('username') + const [activeField, setActiveField] = useState<'username' | 'password' | 'done' | null>('username') const [buttonPressed, setButtonPressed] = useState(false) const [isExiting, setIsExiting] = useState(false) + const [typingComplete, setTypingComplete] = useState(false) + const [buttonHovered, setButtonHovered] = useState(false) const { requestFocusAfterLogin } = useAccessibility() const fullUsername = 'A.CHARLWOOD' @@ -23,34 +25,41 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false - // Refs for interval cleanup + // Refs for interval/timeout cleanup const usernameIntervalRef = useRef | null>(null) const passwordIntervalRef = useRef | null>(null) const cursorIntervalRef = useRef | null>(null) + const timeoutRefs = useRef[]>([]) - const triggerComplete = useCallback(() => { - setIsExiting(true) - setTimeout(() => { - requestFocusAfterLogin() - onComplete() - }, prefersReducedMotion ? 0 : 200) - }, [onComplete, requestFocusAfterLogin, prefersReducedMotion]) + const addTimeout = useCallback((fn: () => void, delay: number) => { + const id = setTimeout(fn, delay) + timeoutRefs.current.push(id) + return id + }, []) + + const handleLogin = useCallback(() => { + if (!typingComplete || isExiting) return + setButtonPressed(true) + addTimeout(() => { + setIsExiting(true) + addTimeout(() => { + requestFocusAfterLogin() + onComplete() + }, prefersReducedMotion ? 0 : 200) + }, 100) + }, [typingComplete, isExiting, onComplete, requestFocusAfterLogin, prefersReducedMotion, addTimeout]) const startLoginSequence = useCallback(() => { if (prefersReducedMotion) { setUsername(fullUsername) setPasswordDots(passwordLength) - setActiveField(null) - setTimeout(() => { - setButtonPressed(true) - setTimeout(() => { - triggerComplete() - }, 100) - }, 300) + setActiveField('done') + setTypingComplete(true) + // Button is immediately available for user to click return } - // Username typing: 30ms per character + // Username typing: 80ms per character let usernameIndex = 0 usernameIntervalRef.current = setInterval(() => { if (usernameIndex <= fullUsername.length) { @@ -62,8 +71,8 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { } setActiveField('password') - // Password dots: 20ms per dot, after 150ms pause - setTimeout(() => { + // Password dots: 60ms per dot, after 300ms pause + addTimeout(() => { let dotCount = 0 passwordIntervalRef.current = setInterval(() => { if (dotCount <= passwordLength) { @@ -73,21 +82,15 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { if (passwordIntervalRef.current) { clearInterval(passwordIntervalRef.current) } - setActiveField(null) - - // Button press: after 150ms pause - setTimeout(() => { - setButtonPressed(true) - setTimeout(() => { - triggerComplete() - }, 200) - }, 150) + setActiveField('done') + setTypingComplete(true) + // Button becomes interactive — user clicks to proceed } - }, 20) - }, 150) + }, 60) + }, 300) } - }, 30) - }, [triggerComplete, prefersReducedMotion]) + }, 80) + }, [prefersReducedMotion, addTimeout]) useEffect(() => { // Cursor blink: 530ms interval @@ -95,18 +98,28 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { setShowCursor(prev => !prev) }, 530) - // Delay start slightly for card entrance - const startTimeout = setTimeout(() => { + // Delay start slightly for card entrance animation + const startTimeout = addTimeout(() => { startLoginSequence() - }, 200) + }, 400) + + // Capture ref value for cleanup + const pendingTimeouts = timeoutRefs.current return () => { if (cursorIntervalRef.current) clearInterval(cursorIntervalRef.current) if (usernameIntervalRef.current) clearInterval(usernameIntervalRef.current) if (passwordIntervalRef.current) clearInterval(passwordIntervalRef.current) clearTimeout(startTimeout) + pendingTimeouts.forEach(id => clearTimeout(id)) } - }, [startLoginSequence]) + }, [startLoginSequence, addTimeout]) + + const buttonBg = buttonPressed + ? '#004494' + : buttonHovered && typingComplete + ? '#004D9F' + : '#005EB8' return (
- {/* Log In Button */} + {/* Log In Button — user clicks to proceed */}