Files
core-cooldown/src/lib/components/Settings.svelte

1185 lines
45 KiB
Svelte

<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { timer, currentView } from "../stores/timer";
import { config, autoSave, loadConfig, resetConfig } from "../stores/config";
import ToggleSwitch from "./ToggleSwitch.svelte";
import Stepper from "./Stepper.svelte";
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;
function goBack() {
invoke("set_view", { view: "dashboard" });
currentView.set("dashboard");
}
function markChanged() {
autoSave();
}
// Working hours functions
function toggleDayEnabled(dayIndex: number) {
$config.working_hours_schedule[dayIndex].enabled = !$config.working_hours_schedule[dayIndex].enabled;
$config.working_hours_schedule = $config.working_hours_schedule;
markChanged();
}
function addTimeRange(dayIndex: number) {
$config.working_hours_schedule[dayIndex].ranges = [
...$config.working_hours_schedule[dayIndex].ranges,
{ start: "09:00", end: "18:00" }
];
$config.working_hours_schedule = $config.working_hours_schedule;
markChanged();
}
function removeTimeRange(dayIndex: number, rangeIndex: number) {
if ($config.working_hours_schedule[dayIndex].ranges.length > 1) {
$config.working_hours_schedule[dayIndex].ranges = $config.working_hours_schedule[dayIndex].ranges.filter((_, i) => i !== rangeIndex);
$config.working_hours_schedule = $config.working_hours_schedule;
markChanged();
}
}
function cloneTimeRange(dayIndex: number, rangeIndex: number) {
const range = $config.working_hours_schedule[dayIndex].ranges[rangeIndex];
$config.working_hours_schedule[dayIndex].ranges = [
...$config.working_hours_schedule[dayIndex].ranges,
{ ...range }
];
$config.working_hours_schedule = $config.working_hours_schedule;
markChanged();
}
function updateTimeRange(dayIndex: number, rangeIndex: number, field: "start" | "end", value: string) {
$config.working_hours_schedule[dayIndex].ranges[rangeIndex][field] = value;
$config.working_hours_schedule = $config.working_hours_schedule;
markChanged();
}
// Reset button two-click confirmation
let resetConfirming = $state(false);
let resetTimeout: ReturnType<typeof setTimeout> | null = null;
function handleReset() {
if (!resetConfirming) {
resetConfirming = true;
// Auto-cancel after 3 seconds
resetTimeout = setTimeout(() => {
resetConfirming = false;
}, 3000);
} else {
resetConfirming = false;
if (resetTimeout) clearTimeout(resetTimeout);
resetConfig();
autoSave();
}
}
// Reload config when entering settings
$effect(() => {
loadConfig();
});
</script>
<div class="flex h-full flex-col">
<!-- Header -->
<div
data-tauri-drag-region
class="flex items-center px-5 pt-5 pb-4"
use:fadeIn={{ duration: 0.4, y: 8 }}
>
<button
aria-label="Back to dashboard"
use:pressable
class="mr-3 flex h-10 w-10 min-h-[44px] min-w-[44px] items-center justify-center rounded-full
text-text-sec transition-colors hover:text-white"
onclick={goBack}
>
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<h1
data-tauri-drag-region
tabindex="-1"
class="flex-1 text-lg font-medium text-white"
>
Settings
</h1>
</div>
<!-- Scrollable content with drag scroll -->
<div
class="settings-scroll-container flex-1 overflow-y-auto px-5 pb-6"
use:dragScroll
>
<div class="space-y-3">
<!-- 1. Timer -->
<section aria-labelledby="settings-timer" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h2 id="settings-timer" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Timer
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Break frequency</div>
<div class="text-[11px] text-text-sec">
Every {$config.break_frequency} <abbr title="minutes">min</abbr>
</div>
</div>
<Stepper
bind:value={$config.break_frequency}
label="Break frequency"
min={5}
max={120}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Break duration</div>
<div class="text-[11px] text-text-sec">
{$config.break_duration} min
</div>
</div>
<Stepper
bind:value={$config.break_duration}
label="Break duration"
min={1}
max={60}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Auto-start</div>
<div class="text-[11px] text-text-sec">Start timer on launch</div>
</div>
<ToggleSwitch
bind:checked={$config.auto_start}
label="Auto-start"
onchange={markChanged}
/>
</div>
</section>
<!-- 2. Pomodoro Mode -->
<section aria-labelledby="settings-pomodoro" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.03 }}>
<h2 id="settings-pomodoro" title="Pomodoro technique alternates focused work sessions with short and long breaks" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Pomodoro Mode
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Enable Pomodoro</div>
<div class="text-[11px] text-text-sec">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-border"></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-text-sec">{$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-border"></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-text-sec">{$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-border"></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-border bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#555] focus:border-[#333]"
bind:value={$config.pomodoro_long_break_title} oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></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-border bg-black px-3.5 py-2.5 text-[13px] text-white outline-none transition-colors placeholder:text-[#555] focus:border-[#333]"
bind:value={$config.pomodoro_long_break_message} oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></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-text-sec">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 aria-labelledby="settings-microbreaks" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h2 id="settings-microbreaks" title="20-20-20 rule: every 20 minutes, look 20 feet away for 20 seconds" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Microbreaks
</h2>
<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-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Frequency</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Duration</div>
<div class="text-[11px] text-text-sec">{$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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Sound</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Show activity</div>
<div class="text-[11px] text-text-sec">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-border"></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-text-sec">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 aria-labelledby="settings-breakscreen" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.09 }}>
<h2 id="settings-breakscreen" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Break Screen
</h2>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] text-white" for="break-title">
Break title
</label>
<input
id="break-title"
type="text"
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px]
text-white outline-none transition-colors
placeholder:text-[#555] focus:border-[#333]"
placeholder="Enter break title..."
bind:value={$config.break_title}
oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] text-white" for="break-message">
Break message
</label>
<input
id="break-message"
type="text"
class="rounded-xl border border-border bg-black px-3.5 py-2.5 text-[13px]
text-white outline-none transition-colors
placeholder:text-[#555] focus:border-[#333]"
placeholder="Enter break message..."
bind:value={$config.break_message}
oninput={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Fullscreen break</div>
<div class="text-[11px] text-text-sec">
{$config.fullscreen_mode ? "Fills entire screen" : "Centered modal"}
</div>
</div>
<ToggleSwitch
bind:checked={$config.fullscreen_mode}
label="Fullscreen break"
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Activity suggestions</div>
<div class="text-[11px] text-text-sec">
Exercise ideas during breaks
</div>
</div>
<ToggleSwitch
bind:checked={$config.show_break_activities}
label="Activity suggestions"
onchange={markChanged}
/>
</div>
{#if $config.fullscreen_mode}
<div class="my-4 h-px bg-border"></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-text-sec">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>
<!-- 5. Break Activities (own card, conditional) -->
{#if $config.show_break_activities}
<section aria-labelledby="settings-activities" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h2 id="settings-activities" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Break Activities
</h2>
<ActivityManager />
</section>
{/if}
<!-- 6. Breathing Guide (own card) -->
<section aria-labelledby="settings-breathing" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.15 }}>
<h2 id="settings-breathing" title="Visual breathing exercise during breaks to reduce stress" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Breathing Guide
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Guided breathing</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div>
<div class="mb-3 text-[13px] text-white" id="breathing-pattern-label">Breathing pattern</div>
<div class="flex flex-col gap-1.5" role="radiogroup" aria-label="Breathing pattern">
{#each breathingPatternMeta as bp}
<button
use:pressable
role="radio"
aria-checked={$config.breathing_pattern === bp.id}
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-border 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-text-sec'}">
{bp.label}
</span>
<span class="ml-auto text-[11px] text-text-sec opacity-60 tabular-nums">
{bp.desc}
</span>
</button>
{/each}
</div>
</div>
{/if}
</section>
<!-- 7. Behavior -->
<section aria-labelledby="settings-behavior" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
<h2 id="settings-behavior" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Behavior
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Strict mode</div>
<div class="text-[11px] text-text-sec">
Disable skip and snooze
</div>
</div>
<ToggleSwitch
bind:checked={$config.strict_mode}
label="Strict mode"
onchange={markChanged}
/>
</div>
{#if !$config.strict_mode}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Allow end early</div>
<div class="text-[11px] text-text-sec">After 50% of break</div>
</div>
<ToggleSwitch
bind:checked={$config.allow_end_early}
label="Allow end early"
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Snooze duration</div>
<div class="text-[11px] text-text-sec">
{$config.snooze_duration} min
</div>
</div>
<Stepper
bind:value={$config.snooze_duration}
label="Snooze duration"
min={1}
max={30}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Snooze limit</div>
<div class="text-[11px] text-text-sec">
{$config.snooze_limit === 0
? "Unlimited"
: `${$config.snooze_limit} per break`}
</div>
</div>
<Stepper
bind:value={$config.snooze_limit}
label="Snooze limit"
min={0}
max={5}
formatValue={(v) => (v === 0 ? "\u221E" : String(v))}
onchange={markChanged}
/>
</div>
{/if}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Immediate breaks</div>
<div class="text-[11px] text-text-sec">
Skip pre-break warning
</div>
</div>
<ToggleSwitch
bind:checked={$config.immediately_start_breaks}
label="Immediate breaks"
onchange={markChanged}
/>
</div>
</section>
<!-- 8. Alerts (MERGED: Notifications + Pre-Break Nudge) -->
<section aria-labelledby="settings-alerts" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
<h2 id="settings-alerts" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Alerts
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Pre-break alert</div>
<div class="text-[11px] text-text-sec">Warn before breaks</div>
</div>
<ToggleSwitch
bind:checked={$config.notification_enabled}
label="Pre-break alert"
onchange={markChanged}
/>
</div>
{#if $config.notification_enabled}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Alert timing</div>
<div class="text-[11px] text-text-sec">
{$config.notification_before_break}<abbr title="seconds">s</abbr> before
</div>
</div>
<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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Screen dimming</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Start dimming</div>
<div class="text-[11px] text-text-sec">{$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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Max dimming</div>
<div class="text-[11px] text-text-sec">{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>
<!-- 9. Sound -->
<section aria-labelledby="settings-sound" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
<h2 id="settings-sound" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Sound
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Sound effects</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Volume</div>
<div class="text-[11px] text-text-sec">{$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-border"></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
aria-pressed={$config.sound_preset === preset}
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-text-sec border border-border 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 aria-labelledby="settings-idle" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
<h2 id="settings-idle" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Idle & Smart Breaks
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Auto-pause when idle</div>
<div class="text-[11px] text-text-sec">Pause timer when away</div>
</div>
<ToggleSwitch
bind:checked={$config.idle_detection_enabled}
label="Auto-pause when idle"
onchange={markChanged}
/>
</div>
{#if $config.idle_detection_enabled}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Idle timeout</div>
<div class="text-[11px] text-text-sec">
{$config.idle_timeout}s of inactivity
</div>
</div>
<Stepper
bind:value={$config.idle_timeout}
label="Idle timeout"
min={30}
max={600}
step={30}
formatValue={(v) => `${v}s`}
onchange={markChanged}
/>
</div>
{/if}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Smart breaks</div>
<div class="text-[11px] text-text-sec">Auto-reset timer when you step away</div>
</div>
<ToggleSwitch
bind:checked={$config.smart_breaks_enabled}
label="Enable smart breaks"
onchange={markChanged}
/>
</div>
{#if $config.smart_breaks_enabled}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Minimum away time</div>
<div class="text-[11px] text-text-sec">
{$config.smart_break_threshold >= 60
? `${Math.floor($config.smart_break_threshold / 60)} min`
: `${$config.smart_break_threshold}s`} to count as break
</div>
</div>
<Stepper
bind:value={$config.smart_break_threshold}
label="Minimum away time"
min={120}
max={900}
step={60}
formatValue={(v) => `${Math.floor(v / 60)}m`}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Count in statistics</div>
<div class="text-[11px] text-text-sec">Track natural breaks in stats</div>
</div>
<ToggleSwitch
bind:checked={$config.smart_break_count_stats}
label="Count in statistics"
onchange={markChanged}
/>
</div>
{/if}
</section>
<!-- 11. Presentation Mode -->
<section aria-labelledby="settings-presentation" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.30 }}>
<h2 id="settings-presentation" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Presentation Mode
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Auto-detect fullscreen</div>
<div class="text-[11px] text-text-sec">Defer breaks during fullscreen apps</div>
</div>
<ToggleSwitch bind:checked={$config.presentation_mode_enabled} label="Presentation mode" onchange={markChanged} />
</div>
{#if $config.presentation_mode_enabled}
{#if $config.microbreak_enabled}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Defer microbreaks</div>
<div class="text-[11px] text-text-sec">Also pause eye breaks</div>
</div>
<ToggleSwitch bind:checked={$config.presentation_mode_defer_microbreaks} label="Defer microbreaks" onchange={markChanged} />
</div>
{/if}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Notification</div>
<div class="text-[11px] text-text-sec">Show toast when break is deferred</div>
</div>
<ToggleSwitch bind:checked={$config.presentation_mode_notification} label="Deferral notification" onchange={markChanged} />
</div>
{/if}
</section>
<!-- 12. Goals & Streaks -->
<section aria-labelledby="settings-goals" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
<h2 id="settings-goals" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Goals & Streaks
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Daily goal</div>
<div class="text-[11px] text-text-sec">Track daily break target</div>
</div>
<ToggleSwitch bind:checked={$config.daily_goal_enabled} label="Daily goal" onchange={markChanged} />
</div>
{#if $config.daily_goal_enabled}
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Target breaks</div>
<div class="text-[11px] text-text-sec">{$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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Celebrations</div>
<div class="text-[11px] text-text-sec">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-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Streak notifications</div>
<div class="text-[11px] text-text-sec">Toast on streak milestones</div>
</div>
<ToggleSwitch bind:checked={$config.streak_notifications} label="Streak notifications" onchange={markChanged} />
</div>
</section>
<!-- 13. Appearance -->
<section aria-labelledby="settings-appearance" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
<h2 id="settings-appearance" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Appearance
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">UI zoom</div>
<div class="text-[11px] text-text-sec">
{$config.ui_zoom}%
</div>
</div>
<Stepper
bind:value={$config.ui_zoom}
label="UI zoom"
min={50}
max={200}
step={5}
formatValue={(v) => `${v}%`}
onchange={markChanged}
/>
</div>
<div class="my-4 h-px bg-border"></div>
<ColorPicker
label="Accent color"
bind:value={$config.accent_color}
onchange={markChanged}
/>
<div class="my-4 h-px bg-border"></div>
<ColorPicker
label="Break screen color"
bind:value={$config.break_color}
presets={[
"#7c6aef", "#9b5de5", "#4361ee", "#4895ef",
"#2ec4b6", "#06d6a0", "#3fb950", "#80ed99",
"#f72585", "#ff006e", "#e63946", "#ff4d00",
"#fca311", "#ffbe0b", "#ffffff", "#888888",
]}
onchange={markChanged}
/>
<div class="my-4 h-px bg-border"></div>
<FontSelector
bind:value={$config.countdown_font}
onchange={markChanged}
/>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Animated background</div>
<div class="text-[11px] text-text-sec">
Gradient blobs with film grain
</div>
</div>
<ToggleSwitch
bind:checked={$config.background_blobs_enabled}
label="Animated background"
onchange={markChanged}
/>
</div>
</section>
<!-- 14. Working Hours -->
<section aria-labelledby="settings-workinghours" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.39 }}>
<h2 id="settings-workinghours" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Working Hours
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Working hours</div>
<div class="text-[11px] text-text-sec">
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-border"></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-text-sec 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-text-sec 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-text-sec 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-text-sec hover:text-[#ff6b6b] hover:bg-[#ff6b6b15] 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-border"></div>
{/if}
{/each}
{/if}
</section>
<!-- 15. Mini Mode -->
<section aria-labelledby="settings-minimode" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.42 }}>
<h2 id="settings-minimode" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Mini Mode
</h2>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Click-through</div>
<div class="text-[11px] text-text-sec">
Mini timer ignores clicks until you hover over it
</div>
</div>
<ToggleSwitch
bind:checked={$config.mini_click_through}
label="Click-through"
onchange={markChanged}
/>
</div>
{#if $config.mini_click_through}
<div class="mt-4 flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Hover delay</div>
<div class="text-[11px] text-text-sec">
Seconds to hover before it becomes draggable
</div>
</div>
<Stepper
bind:value={$config.mini_hover_threshold}
label="Hover delay"
min={1}
max={10}
step={0.5}
formatValue={(v) => `${v}s`}
onchange={markChanged}
/>
</div>
{/if}
</section>
<!-- 16. General -->
<section aria-labelledby="settings-general" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.45 }}>
<h2 id="settings-general" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
General
</h2>
<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-text-sec">Launch automatically at startup</div>
</div>
<ToggleSwitch
checked={autoStartEnabled}
label="Start on login"
onchange={toggleAutoStart}
/>
</div>
</section>
<!-- 17. Keyboard Shortcuts -->
<section aria-labelledby="settings-shortcuts" class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.48 }}>
<h2 id="settings-shortcuts" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Keyboard Shortcuts
</h2>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-[13px] text-white">Pause / Resume</span>
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+P</kbd>
</div>
<div class="flex items-center justify-between">
<span class="text-[13px] text-white">Start break now</span>
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+B</kbd>
</div>
<div class="flex items-center justify-between">
<span class="text-[13px] text-white">Show / Hide window</span>
<kbd class="rounded-lg bg-border px-2.5 py-1 text-[11px] text-text-sec">Ctrl+Shift+S</kbd>
</div>
</div>
</section>
<!-- 18. Reset -->
<section aria-labelledby="settings-reset" class="pt-2 pb-6" use:inView={{ delay: 0.51 }}>
<h2 id="settings-reset" class="sr-only">Reset</h2>
<button
use:pressable
aria-live="polite"
class="w-full rounded-full border py-3 text-[12px]
tracking-wider uppercase
transition-all duration-200
{resetConfirming
? 'border-[#ff6b6b] text-[#ff6b6b] hover:bg-[#ff6b6b] hover:text-white'
: 'border-[#1a1a1a] text-text-sec hover:border-[#333] hover:text-white'}"
onclick={handleReset}
>
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}
</button>
</section>
</div>
</div>
</div>