feat(web): sleep timer upgrades — undo toast, chain mode, clock stop, wind-down dim (plan §17)

- Undo: a genuine sleep fire now toasts with a Resume action (toast.ts
  grows an optional action button; acting dismisses).
- Chain: 'End of chapter, then 15 min' — leaving the chapter starts a
  15-min countdown instead of fading (SleepState.followUpMinutes).
- Clock: the popover gains a time input + Arm — armClockTime resolves
  HH:MM to its next occurrence and reuses the minutes machinery.
- Dim: the ticker eases a pointer-events-none overlay toward 35% black
  across a countdown's final 30s; stop/finish resets it.
(Fade-out and countdown labels already existed — §3 built them.)
This commit is contained in:
John O'Keefe committed 2026-10-23 11:06:42 -04:00
1 parent 4769e832c4
commit f8de17723a
6 files changed
+218 -21

No files matched your search

+24 -3
View File
@@ -48,15 +48,23 @@ const getToastConfig = (type: ToastType) => {
};
// Create a toast element
const createToastElement = (message: string, type: ToastType): HTMLElement => {
const createToastElement = (
message: string,
type: ToastType,
action?: { label: string; onClick: () => void },
): HTMLElement => {
const toast = document.createElement("div");
const config = getToastConfig(type);
toast.className = `${config.bgClass} text-white p-4 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] max-w-[500px] text-sm leading-relaxed pointer-events-auto opacity-0 -translate-y-5 transition-all duration-300 border border-white/10`;
const actionHtml = action
? `<button class="toast-action bg-transparent border border-white/40 text-white cursor-pointer text-xs font-semibold px-2.5 py-1 rounded-md whitespace-nowrap flex-shrink-0 hover:bg-white/10 transition-colors">${toastEscapeHtml(action.label)}</button>`
: "";
toast.innerHTML = `
<span class="text-xl flex-shrink-0">${config.icon}</span>
<span class="flex-1 break-words">${toastEscapeHtml(message)}</span>
${actionHtml}
<button class="toast-close bg-transparent border-0 text-white cursor-pointer text-lg p-0 w-5 h-5 flex items-center justify-center opacity-70 hover:opacity-100 flex-shrink-0 transition-opacity">
×
</button>
@@ -68,6 +76,16 @@ const createToastElement = (message: string, type: ToastType): HTMLElement => {
closeBtn.onclick = () => removeToast(toast);
}
// Action button: run the handler and dismiss (an action consumes the
// toast — leaving it up after acting invites double-fires).
const actionBtn = toast.querySelector(".toast-action") as HTMLElement | null;
if (actionBtn && action) {
actionBtn.onclick = () => {
removeToast(toast);
action.onClick();
};
}
return toast;
};
@@ -90,14 +108,17 @@ const removeToast = (toast: HTMLElement): void => {
}, 300);
};
// Show toast notification
// Show toast notification. `action` (optional): a labeled button for
// one-click follow-ups (e.g. the sleep timer's "Resume"); acting
// dismisses the toast.
const showToast = (
message: string,
type: ToastType,
duration: number = TOAST_DEFAULT_DURATION,
action?: { label: string; onClick: () => void },
): void => {
const container = createToastContainer();
const toast = createToastElement(message, type);
const toast = createToastElement(message, type, action);
container.appendChild(toast);
animateToastIn(toast);