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:
parent
b663455c26
commit
6e0a848c45
@ -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
|
||||||
|
|||||||
@ -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 */}
|
||||||
@ -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)}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user