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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user