feat(night-light): optional auto-on schedule (time window)

Night Light was all-or-nothing. Add an optional schedule with From/To time
inputs so the warm overlay only shows during set hours and toggles itself on/off
automatically (the overlay re-checks every minute; no reload). Overnight windows
that wrap midnight (e.g. 21:00 -> 07:00) are handled.

Window logic is the pure, unit-tested isWithinTimeWindow/parseHHMM in
utils/timeWindow.ts. New settings: nightLightSchedule/Start/End (default
21:00-07:00).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:05:56 -04:00
co-authored by Claude Opus 4.8
parent 23950a6797
commit cf4ee6618c
6 changed files with 157 additions and 16 deletions
+18 -1
View File
@@ -1,4 +1,4 @@
import React, { ReactNode, useEffect } from 'react';
import React, { ReactNode, useEffect, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Provider as JotaiProvider, useAtomValue } from 'jotai';
import {
@@ -23,6 +23,7 @@ import { createRouter } from './Router';
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
import { useCompositionEndTracking } from '../hooks/useComposingCheck';
import { settingsAtom } from '../state/settings';
import { isWithinTimeWindow } from '../utils/timeWindow';
import { LotusToastContainer } from '../features/toast/LotusToastContainer';
import { useTauriNotificationBadge } from '../hooks/useTauriNotificationBadge';
import { useTauriWindowChrome } from '../hooks/useTauriWindowChrome';
@@ -128,7 +129,23 @@ function DesktopChrome({ children }: { children: ReactNode }) {
function NightLightOverlay() {
const settings = useAtomValue(settingsAtom);
const scheduled = settings.nightLightEnabled && settings.nightLightSchedule;
// When scheduled, re-evaluate the active window every minute so the overlay
// turns on/off automatically at the start/end times without a reload.
const [, forceTick] = useState(0);
useEffect(() => {
if (!scheduled) return undefined;
const id = setInterval(() => forceTick((n) => n + 1), 60_000);
return () => clearInterval(id);
}, [scheduled]);
if (!settings.nightLightEnabled) return null;
if (
settings.nightLightSchedule &&
!isWithinTimeWindow(settings.nightLightStart, settings.nightLightEnd)
) {
return null;
}
return (
<div
aria-hidden="true"