diff --git a/README.md b/README.md
index ce867d6..a1ce6db 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Runs as a web app or as a browser extension, and keeps every bit of data in your
## What you get
- **Focus timer** with pause, resume, breaks, extend-on-complete and goal history (press ↑/↓ in the goal field).
-- **Notifications** when a session or break ends while the timer tab is in the background (the app offers to turn them on the first time you open it).
+- **Notifications** when a focus session or break runs out. The app offers to turn them on the first time you open it; you can also turn them on, test them or see why they are blocked from **Menu → Settings → Notifications**.
- **Pixel-art progress animations** — a coffee cup that fills, a battery that charges, a rocket that lifts off — plus an auto-shuffle option.
- **To-dos** with time estimates, notes, drag-and-drop ordering, a backlog and archive; start a focus session straight from a to-do.
- **Habits** with 7- and 30-day grids and notes per day.
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 4a0710f..64a57b3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -21,8 +21,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
{children}
+ // Browser extensions (e.g. ColorZilla) add attributes to / before React hydrates.
+
+ {children}
);
}
diff --git a/src/components/focus/notifications.ts b/src/components/focus/notifications.ts
index 1cdd806..41ca231 100644
--- a/src/components/focus/notifications.ts
+++ b/src/components/focus/notifications.ts
@@ -25,11 +25,14 @@ export async function requestNotificationPermission(): Promise (isSupported() ? Notification.permission : "unsupported");
+
+/** Shows a system notification when the user has allowed them; silent because the end bell already plays. */
export function notify(title: string, body: string): void {
- if (!isSupported() || Notification.permission !== "granted" || document.visibilityState === "visible") return;
+ if (!isSupported() || Notification.permission !== "granted") return;
try {
- // Silent because the end bell already plays; the notification is the visual cue.
const notification = new Notification(title, { body, icon: ICON, tag: TAG, silent: true });
notification.onclick = () => {
window.focus();
diff --git a/src/components/shell/StartMenu.tsx b/src/components/shell/StartMenu.tsx
index e89c73e..20ecbaf 100644
--- a/src/components/shell/StartMenu.tsx
+++ b/src/components/shell/StartMenu.tsx
@@ -1,7 +1,8 @@
"use client";
import { useEffect, useRef, useState, type ComponentType } from "react";
-import { ChevronDown, CircleUser, Menu, Settings, Tag } from "../icons";
+import { notificationStatus, notify, requestNotificationPermission, type NotificationStatus } from "../focus/notifications";
+import { Bell, ChevronDown, CircleUser, Menu, Settings, Tag } from "../icons";
import { useHotkey, useHotkeyLabel } from "../hotkeys/HotkeysProvider";
import type { Tab } from "../types";
import { MOBILE_NAV_OFFSET, MOBILE_QUERY, useMediaQuery } from "./use-media-query";
@@ -15,6 +16,34 @@ const SETTINGS: readonly { label: string; Icon: ComponentType<{ className?: stri
{ label: "Tags", Icon: Tag, target: "tags" },
];
+const NOTIFICATION_LABELS: Readonly, string>> = {
+ default: "Turn on notifications",
+ granted: "Notifications on · test",
+ denied: "Notifications blocked",
+};
+
+const BLOCKED_HELP =
+ "Notifications are blocked for this site. Allow them in your browser's site settings (the icon left of the address bar), then reload.";
+
+/** One menu row that shows the current permission and does the useful next step for it. */
+function NotificationsItem() {
+ const [status, setStatus] = useState(notificationStatus);
+ if (status === "unsupported") return null;
+
+ const act = async () => {
+ if (status === "default") setStatus((await requestNotificationPermission()) ?? notificationStatus());
+ else if (status === "granted") notify("Notifications are on", "You'll get one when a focus session or break ends.");
+ else alert(BLOCKED_HELP);
+ };
+
+ return (
+
+ );
+}
+
export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
const [isOpen, setIsOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(true);
@@ -59,6 +88,7 @@ export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
{label}
))}
+
) : null}