Fix QA findings: rename ambient component, add clock tab-resume sync

W-02: Renamed layout/AmbientBackground → AppAmbientBackground to avoid
naming collision with auth/AmbientBackground (IDE auto-import confusion).

S-01: Added visibilitychange listener to re-sync clock after tab
sleep/resume. Previously the interval would drift after laptop sleep
or long tab backgrounding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-12 18:28:14 +08:00
parent b663455c26
commit 6e0a848c45
3 changed files with 29 additions and 10 deletions

View File

@ -44,16 +44,35 @@ export default function DashboardPage() {
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const [clockNow, setClockNow] = useState(() => new Date()); const [clockNow, setClockNow] = useState(() => new Date());
// Live clock — synced to the minute boundary // Live clock — synced to the minute boundary, re-syncs after tab sleep/resume
useEffect(() => { useEffect(() => {
let intervalId: ReturnType<typeof setInterval>; let intervalId: ReturnType<typeof setInterval>;
// Wait until the next :00 second mark, then tick every 60s let timeoutId: ReturnType<typeof setTimeout>;
const msUntilNextMinute = (60 - new Date().getSeconds()) * 1000 - new Date().getMilliseconds();
const timeoutId = setTimeout(() => { function startClock() {
clearTimeout(timeoutId);
clearInterval(intervalId);
setClockNow(new Date()); setClockNow(new Date());
intervalId = setInterval(() => setClockNow(new Date()), 60_000); const msUntilNextMinute = (60 - new Date().getSeconds()) * 1000 - new Date().getMilliseconds();
}, msUntilNextMinute); timeoutId = setTimeout(() => {
return () => { clearTimeout(timeoutId); clearInterval(intervalId); }; setClockNow(new Date());
intervalId = setInterval(() => setClockNow(new Date()), 60_000);
}, msUntilNextMinute);
}
startClock();
// Re-sync when tab becomes visible again (after sleep/background throttle)
function handleVisibility() {
if (document.visibilityState === 'visible') startClock();
}
document.addEventListener('visibilitychange', handleVisibility);
return () => {
clearTimeout(timeoutId);
clearInterval(intervalId);
document.removeEventListener('visibilitychange', handleVisibility);
};
}, []); }, []);
// Click outside to close dropdown // Click outside to close dropdown

View File

@ -8,7 +8,7 @@
* Also renders a noise texture for tactile depth and a radial vignette * Also renders a noise texture for tactile depth and a radial vignette
* to darken edges and draw focus to center content. * to darken edges and draw focus to center content.
*/ */
export default function AmbientBackground() { export default function AppAmbientBackground() {
return ( return (
<div className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden="true"> <div className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden="true">
{/* Animated gradient orbs — oversized so drift never clips at visible edges */} {/* Animated gradient orbs — oversized so drift never clips at visible edges */}

View File

@ -7,7 +7,7 @@ import { LockProvider } from '@/hooks/useLock';
import { NotificationProvider } from '@/hooks/useNotifications'; import { NotificationProvider } from '@/hooks/useNotifications';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import AmbientBackground from './AmbientBackground'; import AppAmbientBackground from './AppAmbientBackground';
import LockOverlay from './LockOverlay'; import LockOverlay from './LockOverlay';
import NotificationToaster from '@/components/notifications/NotificationToaster'; import NotificationToaster from '@/components/notifications/NotificationToaster';
@ -35,7 +35,7 @@ export default function AppLayout() {
onMobileClose={() => setMobileOpen(false)} onMobileClose={() => setMobileOpen(false)}
/> />
<div className="flex-1 flex flex-col overflow-hidden relative ambient-glass"> <div className="flex-1 flex flex-col overflow-hidden relative ambient-glass">
<AmbientBackground /> <AppAmbientBackground />
{/* Mobile header */} {/* Mobile header */}
<div className="relative z-10 flex md:hidden items-center h-14 border-b bg-card px-4"> <div className="relative z-10 flex md:hidden items-center h-14 border-b bg-card px-4">
<Button variant="ghost" size="icon" onClick={() => setMobileOpen(true)}> <Button variant="ghost" size="icon" onClick={() => setMobileOpen(true)}>