+
+
+
+
+
+ handleNlChange(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter' && nlResult?.success) handleNlCreate(); }}
+ placeholder='e.g. "meeting in 30 min" or "alarm at 3pm"'
+ className="flex-1 px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
+ style={{
+ backgroundColor: 'var(--cm-surface-card)',
+ borderColor: nlResult?.success ? 'var(--cm-accent-secondary)' : 'var(--cm-border)',
+ color: 'var(--cm-text-primary)',
+ }}
+ />
+ {nlResult?.success && (
+
+ )}
+
+ {nlResult && nlInput.trim() && (
+
+ {nlResult.success && nlResult.timer ? (
+
+ {nlResult.timer.type === 'pomodoro' ? 'Pomodoro' : nlResult.timer.type === 'alarm' ? 'Alarm' : 'Countdown'}
+ {' — '}{nlResult.timer.label}
+ {nlResult.timer.durationMs ? ` (${Math.round(nlResult.timer.durationMs / 60000)}m)` : ''}
+ {nlResult.timer.urgency !== 'standard' ? ` [${nlResult.timer.urgency}]` : ''}
+
+ ) : (
+ {nlResult.error}
+ )}
+
+ )}
+
+
{/* Tabs */}
{tabs.map((t) => (
diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx
index 877cb0f..beb01f1 100644
--- a/web/src/components/Dashboard.tsx
+++ b/web/src/components/Dashboard.tsx
@@ -11,7 +11,7 @@ import { CreateTimerModal } from './CreateTimerModal';
import { AlarmOverlay } from './AlarmOverlay';
import { requestNotificationPermission } from '@/lib/notifications';
import { formatTime, formatDate } from '@/lib/format';
-import { Plus, Clock, Bell, Keyboard, Sun, Moon, Settings } from 'lucide-react';
+import { Plus, Clock, Bell, Keyboard, Sun, Moon, Settings, Eye } from 'lucide-react';
import Link from 'next/link';
import { FeedbackButton } from './FeedbackButton';
import { useTheme } from '@/lib/use-theme';
@@ -135,6 +135,14 @@ export function Dashboard() {
>
{theme === 'dark' ?
:
}
+
+
+
void;
+}
+
+export function FocusView({ onExit }: FocusViewProps) {
+ const [session, setSession] = useState
(null);
+ const [now, setNow] = useState(Date.now());
+ const rafRef = useRef(0);
+ const nextTimer = useTimerStore((s) => s.getNextFiringTimer());
+
+ // Tick loop for countdown
+ useEffect(() => {
+ if (!session?.isActive) return;
+ const tick = () => {
+ setNow(Date.now());
+ rafRef.current = requestAnimationFrame(tick);
+ };
+ rafRef.current = requestAnimationFrame(tick);
+ return () => cancelAnimationFrame(rafRef.current);
+ }, [session?.isActive]);
+
+ const startFocus = useCallback((mode: FocusMode, durationMs: number) => {
+ setSession({
+ mode,
+ durationMs,
+ startedAt: Date.now(),
+ pausedAt: null,
+ elapsedBeforePause: 0,
+ isActive: true,
+ isPaused: false,
+ isCompleted: false,
+ blockedCount: 0,
+ });
+ }, []);
+
+ const pauseFocus = useCallback(() => {
+ setSession((prev) => {
+ if (!prev || !prev.isActive || prev.isPaused) return prev;
+ const elapsed = prev.elapsedBeforePause + (Date.now() - prev.startedAt);
+ return { ...prev, isPaused: true, pausedAt: Date.now(), elapsedBeforePause: elapsed };
+ });
+ }, []);
+
+ const resumeFocus = useCallback(() => {
+ setSession((prev) => {
+ if (!prev || !prev.isPaused) return prev;
+ return { ...prev, isPaused: false, pausedAt: null, startedAt: Date.now() };
+ });
+ }, []);
+
+ const endFocus = useCallback(() => {
+ setSession((prev) => {
+ if (!prev) return prev;
+ return { ...prev, isActive: false, isCompleted: true };
+ });
+ }, []);
+
+ // Auto-complete when duration is up
+ useEffect(() => {
+ if (!session?.isActive || session.isPaused || session.mode === 'until_next') return;
+ const elapsed = session.elapsedBeforePause + (now - session.startedAt);
+ if (elapsed >= session.durationMs) {
+ endFocus();
+ }
+ }, [now, session, endFocus]);
+
+ // Auto-complete "until next timer" when timer fires
+ useEffect(() => {
+ if (!session?.isActive || session.mode !== 'until_next') return;
+ if (!nextTimer) return;
+ if (now >= nextTimer.targetTime) {
+ endFocus();
+ }
+ }, [now, session, nextTimer, endFocus]);
+
+ // ── Not started: show setup ──────────────────────────────────
+ if (!session) {
+ const untilNextMs = nextTimer ? Math.max(0, nextTimer.targetTime - Date.now()) : 0;
+
+ return (
+
+
+
+
+
+
+ Focus Mode
+
+
+ Minimize distractions and stay in the zone. Only critical alerts will break through.
+
+
+ {/* Duration presets */}
+
+
+ Choose duration
+
+
+ {FOCUS_PRESETS.map((preset) => (
+
+ ))}
+
+
+
+ {/* Until next timer */}
+ {nextTimer && untilNextMs > 0 && (
+
+ )}
+
+ {/* Pomodoro shortcut */}
+
+
+
+ );
+ }
+
+ // ── Completed: show summary ──────────────────────────────────
+ if (session.isCompleted) {
+ const totalElapsed = session.elapsedBeforePause + (session.isPaused ? 0 : (now - session.startedAt));
+ const focusMinutes = Math.round(totalElapsed / 60_000);
+
+ return (
+
+
+
+
+
+
+ Focus Complete!
+
+
+ Great work staying focused.
+
+
+ {/* Stats */}
+
+
+
+
+ {focusMinutes}m
+
+
+ Time focused
+
+
+
+
+ {session.blockedCount}
+
+
+ Distractions blocked
+
+
+
+
+
+
+
+ {session.mode === 'pomodoro' ? 'Pomodoro' :
+ session.mode === 'until_next' ? 'Until next timer' :
+ formatDurationCompact(session.durationMs)} focus session
+
+
+
+
+
+
+
+ {onExit && (
+
+ )}
+
+
+
+ );
+ }
+
+ // ── Active session: full-screen focus ────────────────────────
+ const elapsed = session.isPaused
+ ? session.elapsedBeforePause
+ : session.elapsedBeforePause + (now - session.startedAt);
+
+ const remaining = session.mode === 'until_next'
+ ? (nextTimer ? Math.max(0, nextTimer.targetTime - now) : 0)
+ : Math.max(0, session.durationMs - elapsed);
+
+ const total = session.mode === 'until_next'
+ ? (nextTimer ? nextTimer.targetTime - session.startedAt : 1)
+ : session.durationMs;
+
+ const progress = total > 0 ? Math.max(0, remaining / total) : 0;
+
+ return (
+
+ {/* Shield badge */}
+
+
+
+ Focus Active — Only critical alerts
+
+
+
+
+ {/* Countdown ring */}
+
+
+
+
+ {formatDuration(remaining)}
+
+
+ {session.isPaused ? 'Paused' :
+ session.mode === 'until_next' ? `Until ${nextTimer?.label ?? 'next timer'}` :
+ session.mode === 'pomodoro' ? 'Pomodoro Focus' :
+ 'Focus Time'}
+
+
+
+
+
+ {/* Controls */}
+
+ {session.isPaused ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Elapsed time */}
+
+ Focused for {formatDurationCompact(elapsed)}
+
+
+
+ );
+}