66c534330b
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
167 lines
4.3 KiB
Svelte
167 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import { milestoneEvent, dailyGoalEvent } from "../stores/timer";
|
|
import { config } from "../stores/config";
|
|
|
|
const showMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
|
|
const showGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
|
|
const streakDays = $derived($milestoneEvent ?? 0);
|
|
|
|
// Generate confetti particles on milestone
|
|
const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"];
|
|
const confettiParticles = $derived(
|
|
showMilestone
|
|
? Array.from({ length: 24 }, (_, i) => ({
|
|
id: i,
|
|
color: confettiColors[i % confettiColors.length],
|
|
angle: (i / 24) * 360 + Math.random() * 15,
|
|
distance: 60 + Math.random() * 80,
|
|
delay: Math.random() * 0.3,
|
|
size: 4 + Math.random() * 4,
|
|
}))
|
|
: [],
|
|
);
|
|
</script>
|
|
|
|
{#if showMilestone}
|
|
<div class="celebration-overlay" role="alert" aria-live="assertive">
|
|
<!-- Confetti burst -->
|
|
<div class="confetti-container">
|
|
{#each confettiParticles as p (p.id)}
|
|
<div
|
|
class="confetti-particle"
|
|
style="
|
|
--angle: {p.angle}deg;
|
|
--distance: {p.distance}px;
|
|
--delay: {p.delay}s;
|
|
--size: {p.size}px;
|
|
background: {p.color};
|
|
"
|
|
></div>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Milestone text -->
|
|
<div class="celebration-text">
|
|
<div class="text-[32px] font-bold text-white mb-1">{streakDays}</div>
|
|
<div class="text-[13px] font-medium tracking-wider uppercase text-white opacity-80">
|
|
day streak!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if showGoal && !showMilestone}
|
|
<div class="goal-overlay" role="alert" aria-live="assertive">
|
|
<div class="goal-badge">
|
|
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2.5">
|
|
<path d="M20 6L9 17l-5-5"/>
|
|
</svg>
|
|
<span class="text-[14px] font-medium text-[#3fb950] ml-2">Daily goal reached!</span>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.celebration-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
animation: celebration-fade 3.5s ease forwards;
|
|
}
|
|
|
|
@keyframes celebration-fade {
|
|
0%, 70% { opacity: 1; }
|
|
100% { opacity: 0; }
|
|
}
|
|
|
|
.confetti-container {
|
|
position: absolute;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.confetti-particle {
|
|
position: absolute;
|
|
width: var(--size);
|
|
height: var(--size);
|
|
border-radius: 2px;
|
|
animation: confetti-burst 1.2s ease-out var(--delay) forwards;
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes confetti-burst {
|
|
0% {
|
|
transform: translate(0, 0) rotate(0deg) scale(0);
|
|
opacity: 1;
|
|
}
|
|
30% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform:
|
|
translate(
|
|
calc(cos(var(--angle)) * var(--distance)),
|
|
calc(sin(var(--angle)) * var(--distance) + 40px)
|
|
)
|
|
rotate(720deg)
|
|
scale(1);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.celebration-text {
|
|
text-align: center;
|
|
animation: celebration-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
@keyframes celebration-pop {
|
|
0% { transform: scale(0.5); opacity: 0; }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.goal-overlay {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
animation: goal-slide 3.5s ease forwards;
|
|
}
|
|
|
|
@keyframes goal-slide {
|
|
0% { transform: translateX(-50%) translateY(-20px); opacity: 0; }
|
|
10% { transform: translateX(-50%) translateY(0); opacity: 1; }
|
|
75% { opacity: 1; }
|
|
100% { transform: translateX(-50%) translateY(-10px); opacity: 0; }
|
|
}
|
|
|
|
.goal-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
background: rgba(63, 185, 80, 0.12);
|
|
border: 1px solid rgba(63, 185, 80, 0.25);
|
|
border-radius: 12px;
|
|
padding: 10px 18px;
|
|
backdrop-filter: blur(16px);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.celebration-overlay,
|
|
.confetti-particle,
|
|
.celebration-text,
|
|
.goal-overlay {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
.confetti-particle {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|