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 [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(() => {
let intervalId: ReturnType<typeof setInterval>;
// Wait until the next :00 second mark, then tick every 60s
let timeoutId: ReturnType<typeof setTimeout>;
function startClock() {
clearTimeout(timeoutId);
clearInterval(intervalId);
setClockNow(new Date());
const msUntilNextMinute = (60 - new Date().getSeconds()) * 1000 - new Date().getMilliseconds();
const timeoutId = setTimeout(() => {
timeoutId = setTimeout(() => {
setClockNow(new Date());
intervalId = setInterval(() => setClockNow(new Date()), 60_000);
}, msUntilNextMinute);
return () => { clearTimeout(timeoutId); clearInterval(intervalId); };
}
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

View File

@ -8,7 +8,7 @@
* Also renders a noise texture for tactile depth and a radial vignette
* to darken edges and draw focus to center content.
*/
export default function AmbientBackground() {
export default function AppAmbientBackground() {
return (
<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 */}

View File

@ -7,7 +7,7 @@ import { LockProvider } from '@/hooks/useLock';
import { NotificationProvider } from '@/hooks/useNotifications';
import { Button } from '@/components/ui/button';
import Sidebar from './Sidebar';
import AmbientBackground from './AmbientBackground';
import AppAmbientBackground from './AppAmbientBackground';
import LockOverlay from './LockOverlay';
import NotificationToaster from '@/components/notifications/NotificationToaster';
@ -35,7 +35,7 @@ export default function AppLayout() {
onMobileClose={() => setMobileOpen(false)}
/>
<div className="flex-1 flex flex-col overflow-hidden relative ambient-glass">
<AmbientBackground />
<AppAmbientBackground />
{/* Mobile header */}
<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)}>