feat: US-007 - Connection status indicator with animated dots and typing-linked timing

This commit is contained in:
2026-02-15 02:09:03 +00:00
parent cfc1c5797d
commit 73a390ce76
+36 -13
View File
@@ -18,6 +18,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
const [typingComplete, setTypingComplete] = useState(false) const [typingComplete, setTypingComplete] = useState(false)
const [buttonHovered, setButtonHovered] = useState(false) const [buttonHovered, setButtonHovered] = useState(false)
const [connectionState, setConnectionState] = useState<'connecting' | 'connected'>('connecting') const [connectionState, setConnectionState] = useState<'connecting' | 'connected'>('connecting')
const [dotCount, setDotCount] = useState(0)
const { requestFocusAfterLogin } = useAccessibility() const { requestFocusAfterLogin } = useAccessibility()
const fullUsername = 'a.recruiter' const fullUsername = 'a.recruiter'
@@ -32,6 +33,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
const passwordIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null) const passwordIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const cursorIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null) const cursorIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([]) const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([])
const dotIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const loginButtonRef = useRef<HTMLButtonElement>(null) const loginButtonRef = useRef<HTMLButtonElement>(null)
const addTimeout = useCallback((fn: () => void, delay: number) => { const addTimeout = useCallback((fn: () => void, delay: number) => {
@@ -107,17 +109,35 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
} }
}, [canLogin]) }, [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(() => { useEffect(() => {
// Cursor blink: 530ms interval // Cursor blink: 530ms interval
cursorIntervalRef.current = setInterval(() => { cursorIntervalRef.current = setInterval(() => {
setShowCursor(prev => !prev) setShowCursor(prev => !prev)
}, 530) }, 530)
// Connection status: transitions to connected after ~2000ms
const connectionTimeout = addTimeout(() => {
setConnectionState('connected')
}, 2000)
// Delay start to allow card entrance + logo animation to complete // Delay start to allow card entrance + logo animation to complete
// Reduced motion: logo shows instantly, so use original 400ms delay // Reduced motion: logo shows instantly, so use original 400ms delay
// Full motion: 400ms card entrance + 1000ms logo animation + 100ms pause = 1500ms // 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 (cursorIntervalRef.current) clearInterval(cursorIntervalRef.current)
if (usernameIntervalRef.current) clearInterval(usernameIntervalRef.current) if (usernameIntervalRef.current) clearInterval(usernameIntervalRef.current)
if (passwordIntervalRef.current) clearInterval(passwordIntervalRef.current) if (passwordIntervalRef.current) clearInterval(passwordIntervalRef.current)
if (dotIntervalRef.current) clearInterval(dotIntervalRef.current)
clearTimeout(startTimeout) clearTimeout(startTimeout)
clearTimeout(connectionTimeout)
pendingTimeouts.forEach(id => clearTimeout(id)) pendingTimeouts.forEach(id => clearTimeout(id))
} }
}, [startLoginSequence, addTimeout, prefersReducedMotion]) }, [startLoginSequence, addTimeout, prefersReducedMotion])
@@ -365,25 +385,28 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
> >
<span <span
style={{ style={{
width: '6px', width: '10px',
height: '6px', height: '10px',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: connectionState === 'connected' ? '#059669' : '#DC2626', backgroundColor: connectionState === 'connected' ? '#059669' : '#DC2626',
transition: prefersReducedMotion ? 'none' : 'background-color 300ms ease', boxShadow: connectionState === 'connected'
? '0 0 6px 1px rgba(5,150,105,0.4)'
: '0 0 6px 1px rgba(220,38,38,0.4)',
transition: prefersReducedMotion ? 'none' : 'background-color 300ms ease, box-shadow 300ms ease',
flexShrink: 0, flexShrink: 0,
}} }}
/> />
<span <span
style={{ style={{
fontFamily: "var(--font-geist-mono, 'Geist Mono', monospace)", fontFamily: "var(--font-geist-mono, 'Geist Mono', monospace)",
fontSize: '10px', fontSize: '12px',
color: connectionState === 'connected' ? '#059669' : '#8DA8A5', color: connectionState === 'connected' ? '#059669' : '#DC2626',
transition: prefersReducedMotion ? 'none' : 'color 300ms ease', transition: prefersReducedMotion ? 'none' : 'color 300ms ease',
}} }}
> >
{connectionState === 'connected' {connectionState === 'connected'
? 'Secure connection established' ? 'Secure connection established, awaiting login'
: 'Awaiting secure connection...'} : `Awaiting secure connection${'.'.repeat(dotCount)}`}
</span> </span>
</div> </div>
</div> </div>