Files
core-cooldown/src/lib/components/Celebration.svelte
T
Your Name 66c534330b 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
2026-02-07 15:11:44 +02:00

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>