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

This commit is contained in:
2026-02-07 15:11:44 +02:00
parent ccfa03d8d2
commit adaf82d839
27 changed files with 3714 additions and 448 deletions

View File

@@ -7,10 +7,36 @@
import ColorPicker from "./ColorPicker.svelte";
import FontSelector from "./FontSelector.svelte";
import TimeSpinner from "./TimeSpinner.svelte";
import ActivityManager from "./ActivityManager.svelte";
import { fadeIn, inView, pressable, dragScroll } from "../utils/animate";
import { playSound } from "../utils/sounds";
import type { TimeRange } from "../stores/config";
const breathingPatternMeta = [
{ id: "box", label: "Box", desc: "4s in \u00b7 4s hold \u00b7 4s out \u00b7 4s hold" },
{ id: "relaxing", label: "Relaxing", desc: "4s in \u00b7 7s hold \u00b7 8s out" },
{ id: "energizing", label: "Energizing", desc: "6s in \u00b7 2s hold \u00b7 6s out \u00b7 2s hold" },
{ id: "calm", label: "Calm", desc: "4s in \u00b7 4s hold \u00b7 6s out" },
{ id: "deep", label: "Deep", desc: "5s in \u00b7 5s out" },
] as const;
// F8: Auto-start on login
let autoStartEnabled = $state(false);
async function loadAutoStartStatus() {
try {
autoStartEnabled = await invoke<boolean>("get_auto_start_status");
} catch {}
}
async function toggleAutoStart() {
try {
await invoke("set_auto_start", { enabled: !autoStartEnabled });
autoStartEnabled = !autoStartEnabled;
} catch (e) {
console.error("Failed to set auto-start:", e);
}
}
$effect(() => { loadAutoStartStatus(); });
const soundPresets = ["bell", "chime", "soft", "digital", "harp", "bowl", "rain", "whistle"] as const;
const daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] as const;
@@ -131,11 +157,10 @@
use:dragScroll
>
<div class="space-y-3">
<!-- Timer -->
<!-- 1. Timer -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Timer
</h3>
@@ -188,11 +213,137 @@
</div>
</section>
<!-- Break Screen -->
<!-- 2. Pomodoro Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.03 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Pomodoro Mode
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Enable Pomodoro</div>
<div class="text-[11px] text-[#8a8a8a]">Short breaks then a long break</div>
</div>
<ToggleSwitch bind:checked={$config.pomodoro_enabled} label="Pomodoro mode" onchange={markChanged} />
</div>
{#if $config.pomodoro_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Short breaks before long</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.pomodoro_short_breaks} short + 1 long</div>
</div>
<Stepper bind:value={$config.pomodoro_short_breaks} label="Short breaks" min={1} max={10} onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Long break duration</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.pomodoro_long_break_duration} min</div>
</div>
<Stepper bind:value={$config.pomodoro_long_break_duration} label="Long break duration" min={5} max={60} onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] text-white" for="pomo-title">Long break title</label>
<input id="pomo-title" type="text" maxlength={100}
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#2a2a2a] focus:border-[#333]"
bind:value={$config.pomodoro_long_break_title} oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] text-white" for="pomo-msg">Long break message</label>
<input id="pomo-msg" type="text" maxlength={500}
class="rounded-xl border border-[#161616] bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#2a2a2a] focus:border-[#333]"
bind:value={$config.pomodoro_long_break_message} oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Reset on skip</div>
<div class="text-[11px] text-[#8a8a8a]">Reset cycle when skipping a break</div>
</div>
<ToggleSwitch bind:checked={$config.pomodoro_reset_on_skip} label="Reset on skip" onchange={markChanged} />
</div>
{/if}
</section>
<!-- 3. Microbreaks -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Microbreaks
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">20-20-20 eye breaks</div>
<div class="text-[11px] text-[#8a8a8a]">Quick eye rest reminders</div>
</div>
<ToggleSwitch
bind:checked={$config.microbreak_enabled}
label="Microbreaks"
onchange={markChanged}
/>
</div>
{#if $config.microbreak_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Frequency</div>
<div class="text-[11px] text-[#8a8a8a]">Every {$config.microbreak_frequency} min</div>
</div>
<Stepper bind:value={$config.microbreak_frequency} label="Microbreak frequency" min={5} max={60} onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Duration</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.microbreak_duration} seconds</div>
</div>
<Stepper bind:value={$config.microbreak_duration} label="Microbreak duration" min={10} max={60} step={5} onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Sound</div>
<div class="text-[11px] text-[#8a8a8a]">Play sound on eye break</div>
</div>
<ToggleSwitch bind:checked={$config.microbreak_sound_enabled} label="Microbreak sound" onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Show activity</div>
<div class="text-[11px] text-[#8a8a8a]">Activity suggestion during eye break</div>
</div>
<ToggleSwitch bind:checked={$config.microbreak_show_activity} label="Microbreak activity" onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Pause during breaks</div>
<div class="text-[11px] text-[#8a8a8a]">No eye breaks during main breaks</div>
</div>
<ToggleSwitch bind:checked={$config.microbreak_pause_during_break} label="Pause during breaks" onchange={markChanged} />
</div>
{/if}
</section>
<!-- 4. Break Screen (stripped down) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.09 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Break Screen
</h3>
@@ -261,13 +412,90 @@
onchange={markChanged}
/>
</div>
{#if $config.fullscreen_mode}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Block all monitors</div>
<div class="text-[11px] text-[#8a8a8a]">Show overlay on all screens during breaks</div>
</div>
<ToggleSwitch
bind:checked={$config.multi_monitor_break}
label="Block all monitors"
onchange={markChanged}
/>
</div>
{/if}
</section>
<!-- Behavior -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<!-- 5. Break Activities (own card, conditional) -->
{#if $config.show_break_activities}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Break Activities
</h3>
<ActivityManager />
</section>
{/if}
<!-- 6. Breathing Guide (own card) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.15 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Breathing Guide
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Guided breathing</div>
<div class="text-[11px] text-[#8a8a8a]">Visual breathing guide during breaks</div>
</div>
<ToggleSwitch
bind:checked={$config.breathing_guide_enabled}
label="Guided breathing"
onchange={markChanged}
/>
</div>
{#if $config.breathing_guide_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div>
<div class="mb-3 text-[13px] text-white">Breathing pattern</div>
<div class="flex flex-col gap-1.5">
{#each breathingPatternMeta as bp}
<button
use:pressable
class="flex items-center gap-3 rounded-xl px-3.5 py-2.5 text-left
transition-all duration-200
{$config.breathing_pattern === bp.id
? 'bg-[#1a1a1a] border border-[#333]'
: 'bg-[#0a0a0a] border border-[#161616] hover:border-[#333]'}"
onclick={() => {
$config.breathing_pattern = bp.id;
markChanged();
}}
>
<div
class="w-3 h-3 rounded-full border-2 flex-shrink-0 transition-colors duration-200"
style="border-color: {$config.breathing_pattern === bp.id ? $config.accent_color : '#333'};
background: {$config.breathing_pattern === bp.id ? $config.accent_color : 'transparent'};"
></div>
<span class="text-[12px] font-medium {$config.breathing_pattern === bp.id ? 'text-white' : 'text-[#8a8a8a]'}">
{bp.label}
</span>
<span class="ml-auto text-[11px] text-[#8a8a8a] opacity-60 tabular-nums">
{bp.desc}
</span>
</button>
{/each}
</div>
</div>
{/if}
</section>
<!-- 7. Behavior -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Behavior
</h3>
@@ -357,114 +585,152 @@
</div>
</section>
<!-- Working Hours -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
<!-- 8. Alerts (MERGED: Notifications + Pre-Break Nudge) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Alerts
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Working hours</div>
<div class="text-[11px] text-[#8a8a8a]">
Only show breaks during your configured work schedule
</div>
<div class="text-[13px] text-white">Pre-break alert</div>
<div class="text-[11px] text-[#8a8a8a]">Warn before breaks</div>
</div>
<ToggleSwitch
bind:checked={$config.working_hours_enabled}
label="Working hours"
bind:checked={$config.notification_enabled}
label="Pre-break alert"
onchange={markChanged}
/>
</div>
{#if $config.working_hours_enabled}
{#if $config.notification_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
{#each $config.working_hours_schedule as daySchedule, dayIndex}
{@const dayName = daysOfWeek[dayIndex]}
<div class="mb-4">
<!-- Day header with toggle -->
<div class="flex items-center gap-3 mb-3">
<ToggleSwitch
bind:checked={$config.working_hours_schedule[dayIndex].enabled}
label={dayName}
onchange={markChanged}
/>
<span class="text-[13px] text-white w-20">{dayName}</span>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Alert timing</div>
<div class="text-[11px] text-[#8a8a8a]">
{$config.notification_before_break}s before
</div>
{#if daySchedule.enabled}
<!-- Time ranges for this day -->
<div class="space-y-2">
{#each daySchedule.ranges as range, rangeIndex}
<div class="flex items-center gap-2">
<TimeSpinner
value={range.start}
countdownFont={$config.countdown_font}
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "start", v)}
/>
<span class="text-[#555] text-[13px]">to</span>
<TimeSpinner
value={range.end}
countdownFont={$config.countdown_font}
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "end", v)}
/>
<!-- Add range button -->
{#if rangeIndex === daySchedule.ranges.length - 1}
<button
use:pressable
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full text-[#444] hover:text-white hover:bg-[#1a1a1a] transition-colors"
onclick={() => addTimeRange(dayIndex)}
aria-label="Add time range"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
{/if}
<!-- Clone button -->
<button
use:pressable
class="w-8 h-8 flex items-center justify-center rounded-full text-[#444] hover:text-white hover:bg-[#1a1a1a] transition-colors"
onclick={() => cloneTimeRange(dayIndex, rangeIndex)}
aria-label="Clone time range"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<!-- Delete button (never show for first range) -->
{#if rangeIndex > 0}
<button
use:pressable
class="w-8 h-8 flex items-center justify-center rounded-full text-[#444] hover:text-[#f85149] hover:bg-[#f8514915] transition-colors"
onclick={() => removeTimeRange(dayIndex, rangeIndex)}
aria-label="Remove time range"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{#if dayIndex < 6}
<div class="my-4 h-px bg-[#161616]"></div>
{/if}
{/each}
<Stepper
bind:value={$config.notification_before_break}
label="Alert timing"
min={0}
max={300}
step={10}
onchange={markChanged}
/>
</div>
{/if}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Screen dimming</div>
<div class="text-[11px] text-[#8a8a8a]">Gradually dim screen before breaks</div>
</div>
<ToggleSwitch bind:checked={$config.screen_dim_enabled} label="Screen dimming" onchange={markChanged} />
</div>
{#if $config.screen_dim_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Start dimming</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.screen_dim_seconds}s before break</div>
</div>
<Stepper bind:value={$config.screen_dim_seconds} label="Dim start" min={3} max={60} onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Max dimming</div>
<div class="text-[11px] text-[#8a8a8a]">{Math.round($config.screen_dim_max_opacity * 100)}%</div>
</div>
<Stepper
bind:value={$config.screen_dim_max_opacity}
label="Max dim opacity"
min={0.1}
max={0.7}
step={0.05}
formatValue={(v) => `${Math.round(v * 100)}%`}
onchange={markChanged}
/>
</div>
{/if}
</section>
<!-- Idle Detection -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
Idle Detection
<!-- 9. Sound -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Sound
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Sound effects</div>
<div class="text-[11px] text-[#8a8a8a]">Play sounds on break events</div>
</div>
<ToggleSwitch
bind:checked={$config.sound_enabled}
label="Sound effects"
onchange={markChanged}
/>
</div>
{#if $config.sound_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Volume</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.sound_volume}%</div>
</div>
<Stepper
bind:value={$config.sound_volume}
label="Volume"
min={0}
max={100}
step={10}
formatValue={(v) => `${v}%`}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div>
<div class="mb-3 text-[13px] text-white">Sound preset</div>
<div class="grid grid-cols-4 gap-2">
{#each soundPresets as preset}
<button
use:pressable
class="rounded-xl py-2.5 text-[11px] tracking-wider uppercase
transition-all duration-200
{$config.sound_preset === preset
? 'bg-[#1a1a1a] text-white border border-[#333]'
: 'bg-[#0a0a0a] text-[#8a8a8a] border border-[#161616] hover:border-[#333] hover:text-white'}"
onclick={() => {
$config.sound_preset = preset;
markChanged();
playSound(preset, $config.sound_volume);
}}
>
{preset}
</button>
{/each}
</div>
</div>
{/if}
</section>
<!-- 10. Idle & Smart Breaks (MERGED) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Idle & Smart Breaks
</h3>
<div class="flex items-center">
@@ -500,19 +766,12 @@
/>
</div>
{/if}
</section>
<!-- Smart Breaks -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
Smart Breaks
</h3>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Enable smart breaks</div>
<div class="text-[13px] text-white">Smart breaks</div>
<div class="text-[11px] text-[#8a8a8a]">Auto-reset timer when you step away</div>
</div>
<ToggleSwitch
@@ -561,119 +820,90 @@
{/if}
</section>
<!-- Notifications -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
Notifications
<!-- 11. Presentation Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.30 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Presentation Mode
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Pre-break alert</div>
<div class="text-[11px] text-[#8a8a8a]">Warn before breaks</div>
<div class="text-[13px] text-white">Auto-detect fullscreen</div>
<div class="text-[11px] text-[#8a8a8a]">Defer breaks during fullscreen apps</div>
</div>
<ToggleSwitch
bind:checked={$config.notification_enabled}
label="Pre-break alert"
onchange={markChanged}
/>
<ToggleSwitch bind:checked={$config.presentation_mode_enabled} label="Presentation mode" onchange={markChanged} />
</div>
{#if $config.notification_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Alert timing</div>
<div class="text-[11px] text-[#8a8a8a]">
{$config.notification_before_break}s before
{#if $config.presentation_mode_enabled}
{#if $config.microbreak_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Defer microbreaks</div>
<div class="text-[11px] text-[#8a8a8a]">Also pause eye breaks</div>
</div>
<ToggleSwitch bind:checked={$config.presentation_mode_defer_microbreaks} label="Defer microbreaks" onchange={markChanged} />
</div>
<Stepper
bind:value={$config.notification_before_break}
label="Alert timing"
min={0}
max={300}
step={10}
onchange={markChanged}
/>
{/if}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Notification</div>
<div class="text-[11px] text-[#8a8a8a]">Show toast when break is deferred</div>
</div>
<ToggleSwitch bind:checked={$config.presentation_mode_notification} label="Deferral notification" onchange={markChanged} />
</div>
{/if}
</section>
<!-- Sound -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
Sound
<!-- 12. Goals & Streaks -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Goals & Streaks
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Sound effects</div>
<div class="text-[11px] text-[#8a8a8a]">Play sounds on break events</div>
<div class="text-[13px] text-white">Daily goal</div>
<div class="text-[11px] text-[#8a8a8a]">Track daily break target</div>
</div>
<ToggleSwitch
bind:checked={$config.sound_enabled}
label="Sound effects"
onchange={markChanged}
/>
<ToggleSwitch bind:checked={$config.daily_goal_enabled} label="Daily goal" onchange={markChanged} />
</div>
{#if $config.sound_enabled}
{#if $config.daily_goal_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Volume</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.sound_volume}%</div>
</div>
<Stepper
bind:value={$config.sound_volume}
label="Volume"
min={0}
max={100}
step={10}
formatValue={(v) => `${v}%`}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div>
<div class="mb-3 text-[13px] text-white">Sound preset</div>
<div class="grid grid-cols-4 gap-2">
{#each soundPresets as preset}
<button
use:pressable
class="rounded-xl py-2.5 text-[11px] tracking-wider uppercase
transition-all duration-200
{$config.sound_preset === preset
? 'bg-[#1a1a1a] text-white border border-[#333]'
: 'bg-[#0a0a0a] text-[#555] border border-[#161616] hover:border-[#333] hover:text-[#999]'}"
onclick={() => {
$config.sound_preset = preset;
markChanged();
playSound(preset, $config.sound_volume);
}}
>
{preset}
</button>
{/each}
<div class="text-[13px] text-white">Target breaks</div>
<div class="text-[11px] text-[#8a8a8a]">{$config.daily_goal_breaks} per day</div>
</div>
<Stepper bind:value={$config.daily_goal_breaks} label="Daily goal breaks" min={1} max={30} onchange={markChanged} />
</div>
{/if}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Celebrations</div>
<div class="text-[11px] text-[#8a8a8a]">Confetti on milestones and goals</div>
</div>
<ToggleSwitch bind:checked={$config.milestone_celebrations} label="Celebrations" onchange={markChanged} />
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Streak notifications</div>
<div class="text-[11px] text-[#8a8a8a]">Toast on streak milestones</div>
</div>
<ToggleSwitch bind:checked={$config.streak_notifications} label="Streak notifications" onchange={markChanged} />
</div>
</section>
<!-- Appearance -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.3 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<!-- 13. Appearance -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Appearance
</h3>
@@ -741,11 +971,111 @@
</div>
</section>
<!-- Mini Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<!-- 14. Working Hours -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.39 }}>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Working hours</div>
<div class="text-[11px] text-[#8a8a8a]">
Only show breaks during your configured work schedule
</div>
</div>
<ToggleSwitch
bind:checked={$config.working_hours_enabled}
label="Working hours"
onchange={markChanged}
/>
</div>
{#if $config.working_hours_enabled}
<div class="my-4 h-px bg-[#161616]"></div>
{#each $config.working_hours_schedule as daySchedule, dayIndex}
{@const dayName = daysOfWeek[dayIndex]}
<div class="mb-4">
<!-- Day header with toggle -->
<div class="flex items-center gap-3 mb-3">
<ToggleSwitch
bind:checked={$config.working_hours_schedule[dayIndex].enabled}
label={dayName}
onchange={markChanged}
/>
<span class="text-[13px] text-white w-20">{dayName}</span>
</div>
{#if daySchedule.enabled}
<!-- Time ranges for this day -->
<div class="space-y-2">
{#each daySchedule.ranges as range, rangeIndex}
<div class="flex items-center gap-2">
<TimeSpinner
value={range.start}
countdownFont={$config.countdown_font}
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "start", v)}
/>
<span class="text-[#8a8a8a] text-[13px]">to</span>
<TimeSpinner
value={range.end}
countdownFont={$config.countdown_font}
onchange={(v) => updateTimeRange(dayIndex, rangeIndex, "end", v)}
/>
<!-- Add range button -->
{#if rangeIndex === daySchedule.ranges.length - 1}
<button
use:pressable
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-white hover:bg-[#1a1a1a] transition-colors"
onclick={() => addTimeRange(dayIndex)}
aria-label="Add time range"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
{/if}
<!-- Clone button -->
<button
use:pressable
class="w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-white hover:bg-[#1a1a1a] transition-colors"
onclick={() => cloneTimeRange(dayIndex, rangeIndex)}
aria-label="Clone time range"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<!-- Delete button (never show for first range) -->
{#if rangeIndex > 0}
<button
use:pressable
class="w-8 h-8 flex items-center justify-center rounded-full text-[#8a8a8a] hover:text-[#f85149] hover:bg-[#f8514915] transition-colors"
onclick={() => removeTimeRange(dayIndex, rangeIndex)}
aria-label="Remove time range"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{#if dayIndex < 6}
<div class="my-4 h-px bg-[#161616]"></div>
{/if}
{/each}
{/if}
</section>
<!-- 15. Mini Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.42 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Mini Mode
</h3>
@@ -784,11 +1114,28 @@
{/if}
</section>
<!-- Shortcuts -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
<h3
class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase"
>
<!-- 16. General -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.45 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
General
</h3>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Start on Windows login</div>
<div class="text-[11px] text-[#8a8a8a]">Launch automatically at startup</div>
</div>
<ToggleSwitch
checked={autoStartEnabled}
label="Start on login"
onchange={toggleAutoStart}
/>
</div>
</section>
<!-- 17. Keyboard Shortcuts -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.48 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Keyboard Shortcuts
</h3>
@@ -808,8 +1155,8 @@
</div>
</section>
<!-- Reset -->
<div class="pt-2 pb-6" use:inView={{ delay: 0.39 }}>
<!-- 18. Reset -->
<div class="pt-2 pb-6" use:inView={{ delay: 0.51 }}>
<button
use:pressable
class="w-full rounded-full border py-3 text-[12px]
@@ -817,7 +1164,7 @@
transition-all duration-200
{resetConfirming
? 'border-[#f85149] text-[#f85149] hover:bg-[#f85149] hover:text-white'
: 'border-[#1a1a1a] text-[#444] hover:border-[#333] hover:text-white'}"
: 'border-[#1a1a1a] text-[#8a8a8a] hover:border-[#333] hover:text-white'}"
onclick={handleReset}
>
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}