feat: US-007 - Connection status indicator with animated dots and typing-linked timing
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user