From 73a390ce76afbafb6ef1e5b2c450e66534e9900b Mon Sep 17 00:00:00 2001 From: Andy Charlwood Date: Sun, 15 Feb 2026 02:09:03 +0000 Subject: [PATCH] feat: US-007 - Connection status indicator with animated dots and typing-linked timing --- src/components/LoginScreen.tsx | 49 +++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index 4a96a0a..3559c3d 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -18,6 +18,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { const [typingComplete, setTypingComplete] = useState(false) const [buttonHovered, setButtonHovered] = useState(false) const [connectionState, setConnectionState] = useState<'connecting' | 'connected'>('connecting') + const [dotCount, setDotCount] = useState(0) const { requestFocusAfterLogin } = useAccessibility() const fullUsername = 'a.recruiter' @@ -32,6 +33,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { const passwordIntervalRef = useRef | null>(null) const cursorIntervalRef = useRef | null>(null) const timeoutRefs = useRef[]>([]) + const dotIntervalRef = useRef | null>(null) const loginButtonRef = useRef(null) const addTimeout = useCallback((fn: () => void, delay: number) => { @@ -107,17 +109,35 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { } }, [canLogin]) + // Connection transitions to green 500ms after typing completes + useEffect(() => { + if (!typingComplete) return + const timeout = addTimeout(() => { + setConnectionState('connected') + }, prefersReducedMotion ? 0 : 500) + return () => clearTimeout(timeout) + }, [typingComplete, addTimeout, prefersReducedMotion]) + + // Animated trailing dots while connecting + useEffect(() => { + if (connectionState === 'connected' || prefersReducedMotion) { + if (dotIntervalRef.current) clearInterval(dotIntervalRef.current) + return + } + dotIntervalRef.current = setInterval(() => { + setDotCount(prev => (prev + 1) % 4) + }, 500) + return () => { + if (dotIntervalRef.current) clearInterval(dotIntervalRef.current) + } + }, [connectionState, prefersReducedMotion]) + useEffect(() => { // Cursor blink: 530ms interval cursorIntervalRef.current = setInterval(() => { setShowCursor(prev => !prev) }, 530) - // Connection status: transitions to connected after ~2000ms - const connectionTimeout = addTimeout(() => { - setConnectionState('connected') - }, 2000) - // Delay start to allow card entrance + logo animation to complete // Reduced motion: logo shows instantly, so use original 400ms delay // Full motion: 400ms card entrance + 1000ms logo animation + 100ms pause = 1500ms @@ -132,8 +152,8 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { if (cursorIntervalRef.current) clearInterval(cursorIntervalRef.current) if (usernameIntervalRef.current) clearInterval(usernameIntervalRef.current) if (passwordIntervalRef.current) clearInterval(passwordIntervalRef.current) + if (dotIntervalRef.current) clearInterval(dotIntervalRef.current) clearTimeout(startTimeout) - clearTimeout(connectionTimeout) pendingTimeouts.forEach(id => clearTimeout(id)) } }, [startLoginSequence, addTimeout, prefersReducedMotion]) @@ -365,25 +385,28 @@ export function LoginScreen({ onComplete }: LoginScreenProps) { > {connectionState === 'connected' - ? 'Secure connection established' - : 'Awaiting secure connection...'} + ? 'Secure connection established, awaiting login' + : `Awaiting secure connection${'.'.repeat(dotCount)}`}