Merge branch 'ralph/login-screen-rework'
# Conflicts: # Ralph/prd.json # Ralph/progress.txt # src/components/TopBar.tsx
This commit is contained in:
+5
-5
@@ -73,16 +73,16 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{phase === 'login' && (
|
||||
<LoginScreen onComplete={() => setPhase('pmr')} />
|
||||
)}
|
||||
|
||||
{phase === 'pmr' && (
|
||||
{(phase === 'login' || phase === 'pmr') && (
|
||||
<DetailPanelProvider>
|
||||
<DashboardLayout />
|
||||
</DetailPanelProvider>
|
||||
)}
|
||||
|
||||
{phase === 'login' && (
|
||||
<LoginScreen onComplete={() => setPhase('pmr')} />
|
||||
)}
|
||||
|
||||
{(phase === 'boot' || phase === 'ecg') && (
|
||||
<SkipButton onSkip={skipToLogin} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion, useReducedMotion } from 'framer-motion'
|
||||
|
||||
interface CvmisLogoProps {
|
||||
size?: number
|
||||
cssHeight?: string
|
||||
animated?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CvmisLogo({ size, cssHeight, animated = false, className }: CvmisLogoProps) {
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const [animationPhase, setAnimationPhase] = useState<'rise' | 'fan' | 'done'>(
|
||||
animated && !prefersReducedMotion ? 'rise' : 'done'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!animated || prefersReducedMotion) return
|
||||
|
||||
const riseTimer = setTimeout(() => setAnimationPhase('fan'), 500)
|
||||
const fanTimer = setTimeout(() => setAnimationPhase('done'), 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(riseTimer)
|
||||
clearTimeout(fanTimer)
|
||||
}
|
||||
}, [animated, prefersReducedMotion])
|
||||
|
||||
const skipAnimation = !animated || prefersReducedMotion
|
||||
const showAll = animationPhase === 'fan' || animationPhase === 'done'
|
||||
|
||||
// The original SVG viewBox is 600pt x 506pt with an internal transform of
|
||||
// scale(0.05, -0.05) translate(0, 506). We keep the original coordinate
|
||||
// system and let the outer viewBox handle scaling.
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 600 506"
|
||||
height={cssHeight ? undefined : size}
|
||||
className={className}
|
||||
role="img"
|
||||
aria-label="CVMIS logo"
|
||||
style={{ overflow: 'visible', ...(cssHeight ? { height: cssHeight, width: 'auto' } : {}) }}
|
||||
>
|
||||
<g transform="translate(0,506) scale(0.05,-0.05)" stroke="none">
|
||||
{/* Capsule: Rx (Pharmacy) - Left — teal, tilts left in fan */}
|
||||
<motion.g
|
||||
id="capsule-rx"
|
||||
fill="#0b7979"
|
||||
initial={skipAnimation ? false : { opacity: 0 }}
|
||||
animate={
|
||||
skipAnimation
|
||||
? { opacity: 1, rotate: 0, x: 0, y: 0 }
|
||||
: showAll
|
||||
? { opacity: 1, rotate: 0, x: 0, y: 0 }
|
||||
: { opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
style={{ transformOrigin: '2500px 3000px' }}
|
||||
>
|
||||
<path d="M2060 6850 c-914 -249 -1279 -1334 -697 -2071 47 -60 198 -225 336 -366 138 -141 256 -265 262 -275 6 -10 150 -160 320 -333 300 -306 1129 -1163 1490 -1542 549 -575 1246 -700 1772 -318 l86 62 -105 35 c-506 172 -872 557 -1036 1089 l-55 179 -11 1003 -12 1003 -550 567 c-780 805 -801 822 -1095 932 -172 65 -531 82 -705 35z m610 -1228 c80 -45 128 -177 97 -261 l-23 -59 99 -21 c147 -31 144 -33 131 87 -10 101 -7 111 54 170 86 83 87 82 102 -73 24 -254 8 -234 213 -270 99 -18 187 -38 194 -45 22 -23 -136 -153 -173 -143 -19 6 -71 16 -115 23 l-82 12 9 -122 c9 -117 6 -126 -57 -191 -80 -83 -99 -74 -99 47 0 52 -6 141 -13 198 l-12 104 -137 31 c-180 41 -244 39 -288 -9 -41 -45 -37 -52 98 -195 l81 -85 -66 -65 -66 -65 -189 200 c-105 110 -232 248 -283 307 l-93 106 159 150 c236 222 314 251 459 169z" />
|
||||
<path d="M2395 5412 c-112 -103 -113 -108 -37 -180 65 -63 93 -57 185 41 73 77 81 140 26 196 -47 46 -70 39 -174 -57z" />
|
||||
</motion.g>
|
||||
|
||||
{/* Capsule: Terminal (Code) - Centre — amber, stays upright */}
|
||||
<motion.g
|
||||
id="capsule-terminal"
|
||||
fill="#d97706"
|
||||
initial={skipAnimation ? false : { opacity: 0 }}
|
||||
animate={
|
||||
skipAnimation
|
||||
? { opacity: 1, rotate: 0, x: 0, y: 0 }
|
||||
: showAll
|
||||
? { opacity: 1, rotate: 0, x: 0, y: 0 }
|
||||
: { opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.5, ease: 'easeOut', delay: skipAnimation ? 0 : 0.05 }}
|
||||
style={{ transformOrigin: '5500px 5000px' }}
|
||||
>
|
||||
<path d="M5740 8362 c-476 -105 -891 -512 -1015 -997 -45 -173 -54 -3865 -11 -4070 50 -233 182 -483 355 -671 185 -201 701 -447 777 -371 11 11 -100 221 -119 267 -19 46 -18 106 -37 200 -66 317 -11 705 143 1010 120 237 111 226 917 1060 255 264 493 513 528 554 l65 74 -8 916 c-9 1115 -24 1196 -286 1542 -286 377 -851 587 -1309 486z m31 -1595 c115 -118 209 -223 209 -236 0 -12 -97 -118 -215 -236 l-215 -215 -55 48 c-75 64 -76 62 103 244 l159 162 -159 158 c-175 174 -176 176 -115 242 62 65 57 68 288 -167z m825 -613 l-6 -64 -295 -6 -295 -5 0 75 0 76 301 -6 301 -6 -6 -64z" />
|
||||
</motion.g>
|
||||
|
||||
{/* Capsule: Data (Analytics) - Right — green, the "rising" capsule */}
|
||||
<motion.g
|
||||
id="capsule-data"
|
||||
fill="#059669"
|
||||
initial={
|
||||
skipAnimation
|
||||
? false
|
||||
: { opacity: 0, scale: 0, y: 2000 }
|
||||
}
|
||||
animate={
|
||||
skipAnimation
|
||||
? { opacity: 1, scale: 1, y: 0, rotate: 0 }
|
||||
: animationPhase === 'rise'
|
||||
? { opacity: 1, scale: 1, y: 0 }
|
||||
: { opacity: 1, scale: 1, y: 0, rotate: 0 }
|
||||
}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: 'easeOut',
|
||||
delay: skipAnimation ? 0 : (animationPhase === 'fan' ? 0.1 : 0),
|
||||
}}
|
||||
style={{ transformOrigin: '9000px 3000px' }}
|
||||
>
|
||||
<path d="M9380 6850 c-351 -63 -390 -94 -1322 -1027 -1753 -1757 -1929 -1943 -2039 -2162 -455 -906 300 -1962 1305 -1822 381 53 567 178 1165 785 2249 2284 2186 2217 2302 2468 432 933 -380 1945 -1411 1758z m35 -1254 l83 -86 -325 -325 -325 -325 -89 91 -89 91 319 319 c369 370 320 342 426 235z m-502 -59 c88 -86 90 -81 -108 -280 l-175 -176 -86 85 -85 84 175 174 c201 201 192 197 279 113z m1036 -132 c11 -8 47 -44 79 -80 l60 -65 -409 -409 -409 -409 -86 84 -86 84 405 405 c223 224 410 406 416 406 5 0 19 -7 30 -16z m-460 -164 l79 -81 -254 -254 -254 -254 -81 79 c-99 97 -115 63 168 349 276 279 243 263 342 161z" />
|
||||
</motion.g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Shield } from 'lucide-react'
|
||||
import { CvmisLogo } from './CvmisLogo'
|
||||
import { useAccessibility } from '../contexts/AccessibilityContext'
|
||||
|
||||
interface LoginScreenProps {
|
||||
@@ -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<ReturnType<typeof setInterval> | null>(null)
|
||||
const cursorIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([])
|
||||
const dotIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const loginButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const addTimeout = useCallback((fn: () => void, delay: number) => {
|
||||
@@ -49,10 +51,11 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
setIsLoading(true)
|
||||
addTimeout(() => {
|
||||
setIsExiting(true)
|
||||
// After dissolve completes (~600ms), remove overlay and reveal dashboard
|
||||
addTimeout(() => {
|
||||
requestFocusAfterLogin()
|
||||
onComplete()
|
||||
}, prefersReducedMotion ? 0 : 200)
|
||||
}, prefersReducedMotion ? 0 : 600)
|
||||
}, prefersReducedMotion ? 0 : 600)
|
||||
}, 100)
|
||||
}, [canLogin, isExiting, isLoading, onComplete, requestFocusAfterLogin, prefersReducedMotion, addTimeout])
|
||||
@@ -107,21 +110,41 @@ 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 slightly for card entrance animation
|
||||
// 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
|
||||
const startTimeout = addTimeout(() => {
|
||||
startLoginSequence()
|
||||
}, 400)
|
||||
}, prefersReducedMotion ? 400 : 1500)
|
||||
|
||||
// Capture ref value for cleanup
|
||||
const pendingTimeouts = timeoutRefs.current
|
||||
@@ -130,11 +153,11 @@ 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])
|
||||
}, [startLoginSequence, addTimeout, prefersReducedMotion])
|
||||
|
||||
const buttonBg = buttonPressed
|
||||
? '#085858'
|
||||
@@ -143,25 +166,36 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
: '#0D6E6E'
|
||||
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
className="fixed inset-0 flex items-center justify-center z-50"
|
||||
style={{ backgroundColor: '#1A2B2A' }}
|
||||
style={{
|
||||
backgroundColor: 'rgba(240, 245, 244, 0.7)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
}}
|
||||
animate={isExiting ? {
|
||||
backgroundColor: 'rgba(240, 245, 244, 0)',
|
||||
backdropFilter: 'blur(0px)',
|
||||
WebkitBackdropFilter: 'blur(0px)',
|
||||
} : {}}
|
||||
transition={isExiting ? { duration: 0.6, ease: 'easeOut' } : {}}
|
||||
role="dialog"
|
||||
aria-label="Clinical system login"
|
||||
aria-modal="true"
|
||||
>
|
||||
<motion.div
|
||||
className="bg-white"
|
||||
style={{
|
||||
width: '320px',
|
||||
padding: '32px',
|
||||
width: 'clamp(320px, 28vw, 480px)',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
padding: 'clamp(24px, 2.5vw, 40px)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #E5E7EB',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03)',
|
||||
border: '1px solid #E4EDEB',
|
||||
boxShadow: '0 1px 2px rgba(26,43,42,0.05)',
|
||||
backgroundColor: 'var(--surface, #FFFFFF)',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={isExiting ? { scale: 1.03, opacity: 0 } : { scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
transition={isExiting ? { duration: 0.4, ease: 'easeOut' } : { duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
@@ -179,7 +213,7 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
border: '3px solid #E5E7EB',
|
||||
border: '3px solid #E4EDEB',
|
||||
borderTopColor: '#0D6E6E',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
@@ -203,18 +237,10 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
className="flex flex-col items-center"
|
||||
style={{ marginBottom: '28px' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(13, 110, 110, 0.08)',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
<Shield
|
||||
size={26}
|
||||
style={{ color: '#0D6E6E' }}
|
||||
strokeWidth={2.5}
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<CvmisLogo
|
||||
cssHeight="clamp(48px, 4vw, 64px)"
|
||||
animated={true}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
@@ -222,22 +248,22 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: '#64748B',
|
||||
color: 'var(--text-secondary, #5B7A78)',
|
||||
letterSpacing: '0.01em',
|
||||
}}
|
||||
>
|
||||
CareerRecord PMR
|
||||
CVMIS
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '11px',
|
||||
fontWeight: 400,
|
||||
color: '#94A3B8',
|
||||
color: 'var(--text-tertiary, #8DA8A5)',
|
||||
marginTop: '2px',
|
||||
}}
|
||||
>
|
||||
Clinical Information System
|
||||
CV Management Information System
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -249,9 +275,9 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
style={{
|
||||
display: 'block',
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '12px',
|
||||
fontSize: 'clamp(12px, 1vw, 14px)',
|
||||
fontWeight: 500,
|
||||
color: '#64748B',
|
||||
color: 'var(--text-secondary, #5B7A78)',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
@@ -262,9 +288,9 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
width: '100%',
|
||||
padding: '9px 11px',
|
||||
fontFamily: "'Geist Mono', 'Fira Code', monospace",
|
||||
fontSize: '13px',
|
||||
backgroundColor: activeField === 'username' ? '#FFFFFF' : '#FAFAFA',
|
||||
border: activeField === 'username' ? '1px solid #0D6E6E' : '1px solid #E5E7EB',
|
||||
fontSize: 'clamp(13px, 1.1vw, 15px)',
|
||||
backgroundColor: activeField === 'username' ? 'var(--surface, #FFFFFF)' : 'var(--bg-dashboard, #F0F5F4)',
|
||||
border: activeField === 'username' ? '1px solid var(--accent, #0D6E6E)' : '1px solid #E4EDEB',
|
||||
borderRadius: '4px',
|
||||
color: '#111827',
|
||||
minHeight: '38px',
|
||||
@@ -291,9 +317,9 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
style={{
|
||||
display: 'block',
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '12px',
|
||||
fontSize: 'clamp(12px, 1vw, 14px)',
|
||||
fontWeight: 500,
|
||||
color: '#64748B',
|
||||
color: 'var(--text-secondary, #5B7A78)',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
@@ -304,9 +330,9 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
width: '100%',
|
||||
padding: '9px 11px',
|
||||
fontFamily: "'Geist Mono', 'Fira Code', monospace",
|
||||
fontSize: '13px',
|
||||
backgroundColor: activeField === 'password' ? '#FFFFFF' : '#FAFAFA',
|
||||
border: activeField === 'password' ? '1px solid #0D6E6E' : '1px solid #E5E7EB',
|
||||
fontSize: 'clamp(13px, 1.1vw, 15px)',
|
||||
backgroundColor: activeField === 'password' ? 'var(--surface, #FFFFFF)' : 'var(--bg-dashboard, #F0F5F4)',
|
||||
border: activeField === 'password' ? '1px solid var(--accent, #0D6E6E)' : '1px solid #E4EDEB',
|
||||
borderRadius: '4px',
|
||||
color: '#111827',
|
||||
letterSpacing: '0.15em',
|
||||
@@ -335,12 +361,12 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
disabled={!canLogin}
|
||||
onMouseEnter={() => setButtonHovered(true)}
|
||||
onMouseLeave={() => setButtonHovered(false)}
|
||||
className="focus-visible:ring-2 focus-visible:ring-[#0D6E6E]/40 focus-visible:ring-offset-2 focus:outline-none"
|
||||
className={`focus-visible:ring-2 focus-visible:ring-[#0D6E6E]/40 focus-visible:ring-offset-2 focus:outline-none${canLogin && !buttonPressed ? ' login-pulse-active' : ''}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 16px',
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '14px',
|
||||
fontSize: 'clamp(14px, 1.1vw, 16px)',
|
||||
fontWeight: 600,
|
||||
color: '#FFFFFF',
|
||||
backgroundColor: buttonBg,
|
||||
@@ -366,25 +392,28 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "var(--font-geist-mono, 'Geist Mono', monospace)",
|
||||
fontSize: '10px',
|
||||
color: connectionState === 'connected' ? '#059669' : '#8DA8A5',
|
||||
fontSize: '12px',
|
||||
color: connectionState === 'connected' ? '#059669' : '#DC2626',
|
||||
transition: prefersReducedMotion ? 'none' : 'color 300ms ease',
|
||||
}}
|
||||
>
|
||||
{connectionState === 'connected'
|
||||
? 'Secure connection established'
|
||||
: 'Awaiting secure connection...'}
|
||||
? 'Secure connection established, awaiting login'
|
||||
: `Awaiting secure connection${'.'.repeat(dotCount)}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -394,14 +423,14 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
style={{
|
||||
marginTop: '22px',
|
||||
paddingTop: '18px',
|
||||
borderTop: '1px solid #E5E7EB',
|
||||
borderTop: '1px solid #E4EDEB',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "var(--font-ui)",
|
||||
fontSize: '11px',
|
||||
color: '#94A3B8',
|
||||
color: 'var(--text-tertiary, #8DA8A5)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -411,6 +440,6 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Home, Search } from 'lucide-react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { CvmisLogo } from './CvmisLogo'
|
||||
|
||||
interface TopBarProps {
|
||||
onSearchClick?: () => void
|
||||
@@ -54,11 +55,7 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
</a>
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Home
|
||||
size={20}
|
||||
style={{ color: 'var(--accent)' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CvmisLogo size={24} />
|
||||
<span
|
||||
className="font-ui hidden sm:inline"
|
||||
style={{
|
||||
|
||||
@@ -235,6 +235,20 @@ html {
|
||||
animation: login-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* Login button pulse — draws attention when button becomes clickable */
|
||||
@keyframes login-pulse {
|
||||
0%, 60%, 100% { transform: scale(1); }
|
||||
30% { transform: scale(1.03); }
|
||||
}
|
||||
|
||||
.login-pulse-active {
|
||||
animation: login-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.login-pulse-active:hover {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for sidebar */
|
||||
.pmr-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
@@ -412,6 +426,11 @@ textarea:focus-visible {
|
||||
border-top-color: #0D6E6E;
|
||||
}
|
||||
|
||||
/* No pulse animation */
|
||||
.login-pulse-active {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Instant SubNav transitions */
|
||||
.subnav-scroll button {
|
||||
transition: none !important;
|
||||
|
||||
Reference in New Issue
Block a user