Add pomodoro, microbreaks, breathing guide, screen dimming, presentation mode, goals, multi-monitor, and activity manager

Major feature release (v0.1.3) adding 15 new features to the break timer:

Backend (Rust):
- Pomodoro cycle tracking with configurable short/long break pattern
- Microbreak scheduling (20-20-20 rule) with independent timer
- Screen dimming events with gradual opacity progression
- Presentation mode detection (fullscreen app deferral)
- Smart break detection (natural idle breaks counting toward goals)
- Daily goal tracking and streak milestone events
- Multi-monitor break overlay support
- Working hours enforcement with per-day schedules
- Weekly summary and natural break stats queries
- Config expanded to 71 validated fields

Frontend (Svelte):
- 6 new components: BreathingGuide, ActivityManager, BreakOverlay,
  MicrobreakOverlay, DimOverlay, Celebration
- Breathing guide with 5 patterns and animated pulsing halo
- Activity manager with favorites, custom activities, momentum scroll
- Confetti celebrations on milestones and goal completion
- Dashboard indicators (pomodoro/microbreak/goal) moved inside ring
- Settings reorganized into 18 logical cards
- Breathing pattern selector redesigned with timing descriptions
- Break activities expanded from 40 to 71 curated exercises
- Sound presets expanded from 4 to 8
- Stats view with weekly summary and natural break tracking

Also: version bump to 0.1.3, CHANGELOG, README and CLAUDE.md updates
This commit is contained in:
Your Name
2026-02-07 15:11:44 +02:00
parent d09affc4c4
commit 66c534330b
28 changed files with 3792 additions and 448 deletions
+102 -8
View File
@@ -10,6 +10,7 @@
import { config } from "../stores/config";
import TimerRing from "./TimerRing.svelte";
import { scaleIn, fadeIn, pressable, glowHover } from "../utils/animate";
import { listen } from "@tauri-apps/api/event";
async function toggleTimer() {
const snap = await invoke<TimerSnapshot>("toggle_timer");
@@ -28,15 +29,45 @@
}
const statusText = $derived(
$timer.idlePaused
? "IDLE"
: $timer.prebreakWarning
? "BREAK SOON"
: $timer.state === "running"
? "FOCUS"
: "PAUSED",
$timer.deferredBreakPending
? "DEFERRED"
: $timer.idlePaused
? "IDLE"
: $timer.prebreakWarning
? "BREAK SOON"
: $timer.state === "running"
? "FOCUS"
: "PAUSED",
);
// F1: Microbreak countdown
const microbreakCountdown = $derived(() => {
if (!$timer.microbreakEnabled || $timer.microbreakActive) return "";
const secs = $timer.microbreakCountdown;
const m = Math.floor(secs / 60);
const s = secs % 60;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
});
// F10: Daily goal from stats
let dailyGoalProgress = $state(0);
let dailyGoalMet = $state(false);
// Load stats for daily goal display
async function loadGoalProgress() {
try {
const stats = await invoke<{ dailyGoalProgress: number; dailyGoalMet: boolean }>("get_stats");
dailyGoalProgress = stats.dailyGoalProgress;
dailyGoalMet = stats.dailyGoalMet;
} catch {}
}
$effect(() => {
// Reload goal progress on each tick (approximately)
const _state = $timer.state;
loadGoalProgress();
});
// Track status changes for aria-live region (announce only on change, not every tick)
let lastAnnouncedStatus = $state("");
let statusAnnouncement = $state("");
@@ -146,11 +177,74 @@
<!-- Status label -->
<span
class="block text-center text-[11px] font-medium tracking-[0.25em]"
class:text-[#8a8a8a]={!$timer.prebreakWarning}
class:text-[#8a8a8a]={!$timer.prebreakWarning && !$timer.deferredBreakPending}
class:text-warning={$timer.prebreakWarning}
class:text-[#fca311]={$timer.deferredBreakPending}
>
{statusText}
</span>
<!-- Indicators inside ring -->
<div class="mt-2 flex flex-col items-center gap-1">
<!-- Pomodoro cycle -->
{#if $timer.pomodoroEnabled}
<div class="flex items-center gap-1.5">
<div class="flex items-center gap-1">
{#each Array($timer.pomodoroTotalInCycle) as _, i}
{@const isLong = i === $timer.pomodoroTotalInCycle - 1}
{@const isFilled = i < $timer.pomodoroCyclePosition}
{@const isCurrent = i === $timer.pomodoroCyclePosition}
<div
class="rounded-full transition-colors duration-300"
style="
width: {isLong ? 8 : 5}px;
height: {isLong ? 8 : 5}px;
background: {isFilled ? $config.accent_color : isCurrent ? $config.accent_color + '60' : '#222'};
{isCurrent ? 'box-shadow: 0 0 4px ' + $config.accent_color + '40;' : ''}
"
></div>
{/each}
</div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
{$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle}
</span>
</div>
{/if}
<!-- Microbreak countdown -->
{#if $timer.microbreakEnabled && !$timer.microbreakActive && $timer.state === "running"}
<div class="flex items-center gap-1 text-[9px] text-[#8a8a8a]">
<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="tabular-nums">{microbreakCountdown()}</span>
</div>
{/if}
<!-- Daily goal -->
{#if $config.daily_goal_enabled}
<div class="flex items-center gap-1.5">
{#if dailyGoalMet}
<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2.5">
<path d="M20 6L9 17l-5-5"/>
</svg>
<span class="text-[9px] text-[#3fb950]">Goal met</span>
{:else}
<span class="text-[9px] text-[#8a8a8a]">Goal</span>
<div class="w-16 h-[2px] rounded-full overflow-hidden" style="background: #161616;">
<div
class="h-full rounded-full transition-[width] duration-500"
style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};"
></div>
</div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
{dailyGoalProgress}/{$config.daily_goal_breaks}
</span>
{/if}
</div>
{/if}
</div>
</div>
</TimerRing>
</div>