Files
core-cooldown/src/lib/components/Stepper.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

95 lines
2.5 KiB
Svelte

<script lang="ts">
interface Props {
value: number;
min?: number;
max?: number;
step?: number;
formatValue?: (v: number) => string;
onchange?: (value: number) => void;
label?: string;
}
let {
value = $bindable(),
min = 0,
max = 100,
step = 1,
formatValue = (v: number) => String(v),
onchange,
label = "Value",
}: Props = $props();
let holdTimer: ReturnType<typeof setTimeout> | null = null;
let holdInterval: ReturnType<typeof setInterval> | null = null;
function decrement() {
if (value > min) {
value = Math.max(min, value - step);
onchange?.(value);
}
}
function increment() {
if (value < max) {
value = Math.min(max, value + step);
onchange?.(value);
}
}
function startHold(fn: () => void) {
fn();
// Initial delay before repeating
holdTimer = setTimeout(() => {
// Start repeating, accelerate over time
let delay = 150;
function tick() {
fn();
delay = Math.max(40, delay * 0.85);
holdInterval = setTimeout(tick, delay) as unknown as ReturnType<typeof setInterval>;
}
tick();
}, 400);
}
function stopHold() {
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
if (holdInterval) { clearTimeout(holdInterval as unknown as ReturnType<typeof setTimeout>); holdInterval = null; }
}
</script>
<div class="flex items-center gap-1.5" role="group" aria-label={label}>
<button
type="button"
aria-label="Decrease"
class="flex h-7 w-7 items-center justify-center rounded-lg
bg-[#141414] text-[#8a8a8a] transition-colors
hover:bg-[#1c1c1c] hover:text-white
disabled:opacity-20"
onmousedown={() => startHold(decrement)}
onmouseup={stopHold}
onmouseleave={stopHold}
onclick={(e) => { if (e.detail === 0) decrement(); }}
disabled={value <= min}
>
&minus;
</button>
<span class="min-w-[38px] text-center text-[13px] tabular-nums text-white" aria-live="polite" aria-atomic="true">
{formatValue(value)}
</span>
<button
type="button"
aria-label="Increase"
class="flex h-7 w-7 items-center justify-center rounded-lg
bg-[#141414] text-[#8a8a8a] transition-colors
hover:bg-[#1c1c1c] hover:text-white
disabled:opacity-20"
onmousedown={() => startHold(increment)}
onmouseup={stopHold}
onmouseleave={stopHold}
onclick={(e) => { if (e.detail === 0) increment(); }}
disabled={value >= max}
>
+
</button>
</div>