28 Commits

Author SHA1 Message Date
lashman 62725098b1 tweak contributing section wording 2026-03-13 14:15:37 +02:00
lashman f6f789baf8 strip unicode dashes, trim restating doc comments, untrack forbidden files 2026-03-13 14:05:59 +02:00
lashman 074817def5 v0.2.0: WCAG 2.2 AAA accessibility + toggle fix + version bump 2026-02-18 19:18:15 +02:00
lashman 418e34a772 build.rs cfg() checks HOST not TARGET -- use CARGO_CFG_TARGET_ENV 2026-02-18 19:18:15 +02:00
lashman c78627a659 a11y: final cleanup -- remaining hardcoded colors
- FontSelector: text-[#8a8a8a]→text-text-sec, border-[#222]→border-border
- StatsView: Canvas fillStyle and inline trend color #8a8a8a→#a8a8a8
- ActivityManager: border-[#222]→border-border, #f85149→#ff6b6b danger color
- Settings: #f85149→#ff6b6b danger color on reset/delete buttons
2026-02-18 19:18:15 +02:00
lashman e4e7859653 a11y: Tasks 9,13-17 -- Settings ARIA, MiniTimer, overlays, ColorPicker, ActivityManager
- Settings: breathing radiogroup/radio, sound aria-pressed, contrast tokens,
  placeholder contrast, reset aria-live, abbreviation tags, title tooltips,
  back button 44px target
- MiniTimer: paused text #555→#a8a8a8 for AAA contrast
- MicrobreakOverlay: alertdialog role, sr-only heading, aria-describedby
- BreakOverlay: alertdialog role, sr-only heading, aria-label
- ColorPicker: enlarge swatches 22→28px with 44px hit areas, aria-pressed
- ActivityManager: enlarge action buttons to 44px targets, contrast tokens
2026-02-18 19:18:15 +02:00
lashman 67a8828a69 a11y: Tasks 7-12 -- Dashboard, Settings, StatsView, BreakScreen, Celebration
- Dashboard: text-text-sec tokens, nav landmark, toast hover persistence,
  goal progressbar ARIA, pomodoro sr-only text
- Settings: h3→h2 heading hierarchy, section aria-labelledby with ids,
  Working Hours heading added
- StatsView: h3→h2, tablist/tab/tabpanel ARIA pattern, sr-only data tables
  for 30-day chart and heatmap, contrast tokens
- BreakScreen: strict-mode focus safety span, breathing phase-only
  announcements, contrast tokens
- Celebration: JS-controlled hover/focus persistence, dismiss buttons,
  Escape key, removed pointer-events:none
- Titlebar: removed redundant role="banner" on <header>
2026-02-18 19:18:15 +02:00
lashman 81b0775b57 a11y: Tasks 2-6 -- App shell, Titlebar, ToggleSwitch, Stepper, animate.ts
- Add skip-to-content link and dynamic document title (App.svelte)
- Wrap titlebar in header landmark, enlarge traffic lights to 44px (Titlebar.svelte)
- Enlarge toggle switch to 52x28, improve OFF knob contrast (ToggleSwitch.svelte)
- Enlarge stepper buttons to 36px, add keyboard hold-to-repeat (Stepper.svelte)
- Add keyboard feedback to pressable, focus glow to glowHover (animate.ts)
2026-02-18 19:18:15 +02:00
lashman 0bf619b4e0 a11y: update theme tokens and global styles for WCAG AAA 2026-02-18 19:18:15 +02:00
lashman 17f54d761e Update README.md 2026-02-07 13:27:24 +00:00
lashman 6a1181f34b Add emojis to all top-level README section headers 2026-02-07 15:24:03 +02:00
lashman 5b3b923d3c Add emojis to feature section headers in README 2026-02-07 15:22:11 +02:00
lashman be41f61740 Add pomodoro, microbreaks, breathing guide, screen dimming, presentation mode, goals, multi-monitor, and activity manager 2026-02-07 15:11:44 +02:00
lashman 3ca3857141 Bump version to 0.1.2 2026-02-07 12:16:03 +02:00
lashman 9ba3dad4de Add WCAG 2.1 Level AA accessibility across all components 2026-02-07 12:13:03 +02:00
lashman 97b6e1a882 Update README.md 2026-02-07 09:35:24 +00:00
lashman 3aa5187095 Update README.md 2026-02-07 09:34:36 +00:00
lashman ee8630ecd2 Update README - fix download links, activity count, and accuracy
- Fix download links to point to Gitea releases page (not GitHub-style relative URLs)
- Correct activity count from 70 to 72
- Enlarge break screen screenshot
- Add missing IPC entries (natural-break-detected event, cursor/window commands)
- Add msvc_compat.rs to backend modules table
- Update winapi description to include WebView2 check dialog
- Remove unused dirs crate from dependencies
2026-02-07 11:24:16 +02:00
lashman 345381e84d Bump version to 0.1.1 2026-02-07 11:16:29 +02:00
lashman 960e63e3db Tighten TimeSpinner spacing and fix build warnings
Reduce gap between numbers and h/m unit labels in the workday
schedule spinners. Remove unused parse_hour function and fix
Svelte state_referenced_locally warning in BreakScreen.
2026-02-07 11:16:29 +02:00
lashman 7c7fc8cbb9 Enable custom-protocol for embedded frontend assets
Without this feature, Tauri falls back to devUrl even in release
builds. The tauri CLI adds it automatically but direct cargo
builds need it in Cargo.toml.
2026-02-07 11:00:45 +02:00
lashman 4d907f3547 Fix WebView2 detection - use loader API instead of registry
Registry check gave false negatives on systems where WebView2
is installed through Edge rather than EdgeUpdate. Now calls
GetAvailableCoreWebView2BrowserVersionString (statically linked)
which detects all installation methods.
2026-02-07 10:51:49 +02:00
lashman e36646f7d1 Statically link WebView2Loader - single exe, no DLL needed
build.rs swaps the dynamic import library with the static archive
from webview2-com-sys, so the WebView2 loader code is baked into
the exe. msvc_compat.rs provides the MSVC CRT symbols (security
cookie, thread-safe init, C++ operators) that the MSVC-compiled
static library expects.
2026-02-07 02:32:54 +02:00
lashman 9344f9d312 Detect missing WebView2 at startup, show download link
Checks Windows registry for WebView2 Runtime before Tauri
initializes. If missing, shows a native MessageBox explaining
the requirement and opens the Microsoft download page.
2026-02-07 02:08:51 +02:00
lashman 17a804fed8 Fix fullscreen break screen - centering, progress bar, ripple sizing
- Center break content on fullscreen (was top-left aligned)
- Move fullscreen progress bar to screen bottom (was overlapping buttons)
- Scale ripple circles to match ring size per mode (140/160/200px)
- Increase ripple expansion for more dramatic spread
2026-02-07 01:56:10 +02:00
lashman d1de54c22c Add screenshots, move philosophy section above features 2026-02-07 01:36:43 +02:00
lashman 60d7017110 Revamp README - emojis, better formatting, cleaner layout 2026-02-07 01:18:48 +02:00
lashman 7272b80910 Initial commit -- Core Cooldown v0.1.0
Portable Windows break timer to prevent RSI and eye strain.
Tauri v2 + Svelte 5 + Tailwind CSS v4. No installer, no telemetry,
no data leaves the machine. CC0 public domain.
2026-02-07 01:12:49 +02:00
19 changed files with 68 additions and 145 deletions
+6
View File
@@ -795,3 +795,9 @@ See [`LICENSE`](LICENSE) for the full legal text.
<em>Rest well.</em>
</sub>
</p>
<p align="center">
<sub>
<em>This project is developed with the help of a locally-run LLM via opencode for scaffolding, planning, and routine code tasks. Architecture and design decisions are my own.</em>
</sub>
</p>
+2 -2
View File
@@ -8,7 +8,7 @@ fn main() {
}
// On GNU targets, replace the WebView2Loader import library with the static
// library so the loader is baked into the exe - no DLL to ship.
// library so the loader is baked into the exe -- no DLL to ship.
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu") {
swap_webview2_to_static();
}
@@ -44,7 +44,7 @@ fn fix_resource_lib() {
// archive signature). A .res file starts with 0x00000000.
if let Ok(header) = std::fs::read(&lib_file) {
if header.len() >= 4 && header[0..4] == [0, 0, 0, 0] {
// This is a .res file, not COFF - re-compile with windres
// This is a .res file, not COFF -- re-compile with windres
let windres = "C:/Users/lashman/mingw-w64/mingw64/bin/windres.exe";
let status = Command::new(windres)
.args([
+1 -15
View File
@@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
/// A custom break activity defined by the user.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomActivity {
pub id: String,
@@ -13,7 +12,6 @@ pub struct CustomActivity {
pub enabled: bool,
}
/// A single time range (e.g., 09:00 to 17:00)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
pub start: String, // Format: "HH:MM"
@@ -29,7 +27,6 @@ impl Default for TimeRange {
}
}
/// Schedule for a single day
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaySchedule {
pub enabled: bool,
@@ -46,7 +43,6 @@ impl Default for DaySchedule {
}
impl DaySchedule {
/// Create a default schedule for weekend days (disabled by default)
fn weekend_default() -> Self {
Self {
enabled: false,
@@ -304,14 +300,13 @@ impl Default for Config {
}
impl Config {
/// Get the path to the config file (portable: next to the exe)
// Portable: next to the exe
fn config_path() -> Result<PathBuf> {
let exe_path = std::env::current_exe().context("Failed to determine exe path")?;
let exe_dir = exe_path.parent().context("Failed to determine exe directory")?;
Ok(exe_dir.join("config.json"))
}
/// Load configuration from file, or return default if it doesn't exist
pub fn load_or_default() -> Self {
match Self::load() {
Ok(config) => config,
@@ -327,7 +322,6 @@ impl Config {
}
}
/// Load configuration from file
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
@@ -345,7 +339,6 @@ impl Config {
Ok(config.validate())
}
/// Save configuration to file
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
@@ -357,7 +350,6 @@ impl Config {
Ok(())
}
/// Validate and sanitize configuration values
pub fn validate(mut self) -> Self {
// Break duration: 1-60 minutes
self.break_duration = self.break_duration.clamp(1, 60);
@@ -511,7 +503,6 @@ impl Config {
self
}
/// Check if a time string is in valid HH:MM format
fn is_valid_time_format(time: &str) -> bool {
let parts: Vec<&str> = time.split(':').collect();
if parts.len() != 2 {
@@ -536,29 +527,24 @@ impl Config {
hours * 60 + minutes
}
/// Check if a string is a valid hex color (#RRGGBB)
fn is_valid_hex_color(color: &str) -> bool {
color.len() == 7
&& color.starts_with('#')
&& color[1..].chars().all(|c| c.is_ascii_hexdigit())
}
/// Get break duration in seconds
pub fn break_duration_seconds(&self) -> u64 {
self.break_duration as u64 * 60
}
/// Get break frequency in seconds
pub fn break_frequency_seconds(&self) -> u64 {
self.break_frequency as u64 * 60
}
/// Get snooze duration in seconds
pub fn snooze_duration_seconds(&self) -> u64 {
self.snooze_duration as u64 * 60
}
/// Reset to default values
pub fn reset_to_default(&mut self) {
*self = Self::default();
}
+6 -15
View File
@@ -310,7 +310,6 @@ fn save_window_position(
// ── Dynamic Tray Icon Rendering ────────────────────────────────────────────
/// Parse a "#RRGGBB" hex color string into (r, g, b). Falls back to fallback on invalid input.
fn parse_hex_color(hex: &str, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
if hex.len() == 7 && hex.starts_with('#') {
let r = u8::from_str_radix(&hex[1..3], 16).unwrap_or(fallback.0);
@@ -322,8 +321,6 @@ fn parse_hex_color(hex: &str, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
}
}
/// Render a 32x32 RGBA icon with a progress arc using the given accent/break colors.
/// F10: Optionally renders a green checkmark when daily goal is met.
fn render_tray_icon(
progress: f64,
is_break: bool,
@@ -401,33 +398,31 @@ fn update_tray(
break_color: (u8, u8, u8),
goal_met: bool,
) {
// Update tooltip
let tooltip = match snapshot.state {
timer::TimerState::Running => {
if snapshot.deferred_break_pending {
"Core Cooldown - Break deferred (fullscreen)".to_string()
"Core Cooldown -- Break deferred (fullscreen)".to_string()
} else {
let m = snapshot.time_remaining / 60;
let s = snapshot.time_remaining % 60;
format!("Core Cooldown - {:02}:{:02} until break", m, s)
format!("Core Cooldown -- {:02}:{:02} until break", m, s)
}
}
timer::TimerState::Paused => {
if snapshot.idle_paused {
"Core Cooldown - Paused (idle)".to_string()
"Core Cooldown -- Paused (idle)".to_string()
} else {
"Core Cooldown - Paused".to_string()
"Core Cooldown -- Paused".to_string()
}
}
timer::TimerState::BreakActive => {
let m = snapshot.break_time_remaining / 60;
let s = snapshot.break_time_remaining % 60;
format!("Core Cooldown - Break {:02}:{:02}", m, s)
format!("Core Cooldown -- Break {:02}:{:02}", m, s)
}
};
let _ = tray.set_tooltip(Some(&tooltip));
// Update icon
let (progress, is_break, is_paused) = match snapshot.state {
timer::TimerState::Running => (snapshot.progress, false, false),
timer::TimerState::Paused => (snapshot.progress, false, true),
@@ -554,7 +549,6 @@ pub fn run() {
let break_c = parse_hex_color(&break_hex, (124, 106, 239));
update_tray(&tray, &snapshot, accent, break_c, goal_met);
// Emit tick event with full snapshot
let _ = handle.emit("timer-tick", &snapshot);
// F5: Screen dim window management
@@ -616,7 +610,6 @@ pub fn run() {
break_deferred_notified = false;
}
TickResult::BreakEnded => {
// Restore normal window state and close break window
handle_break_end(&handle);
// F9: Close multi-monitor overlays
close_multi_monitor_overlays(&handle);
@@ -721,7 +714,7 @@ pub fn run() {
.notification()
.builder()
.title("Break deferred")
.body("Fullscreen app detected - break will start when you exit.")
.body("Fullscreen app detected -- break will start when you exit.")
.show();
}
let _ = handle.emit("break-deferred", &());
@@ -883,7 +876,6 @@ fn close_break_window(app: &AppHandle) {
}
}
/// Handle break start: either fullscreen on main window, or open a separate modal break window.
fn handle_break_start(app: &AppHandle, fullscreen_mode: bool) {
if fullscreen_mode {
// Fullscreen: show break inside the main window
@@ -899,7 +891,6 @@ fn handle_break_start(app: &AppHandle, fullscreen_mode: bool) {
}
}
/// Handle break end: restore main window state and close break window if open.
fn handle_break_end(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_always_on_top(false);
+8 -8
View File
@@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicI32, Ordering};
// ── MSVC Buffer Security Check (/GS) ────────────────────────────────────────
//
// MSVC's /GS flag instruments functions with stack canaries. These two symbols
// implement the canary check. The cookie value is arbitrary - real MSVC CRT
// implement the canary check. The cookie value is arbitrary -- real MSVC CRT
// randomises it at startup, but for a statically-linked helper library this
// fixed sentinel is sufficient.
@@ -44,15 +44,15 @@ pub unsafe extern "C" fn _Init_thread_header(guard: *mut i32) {
loop {
let val = guard.read_volatile();
if val == 0 {
// Already initialised - tell caller to skip
// Already initialised -- tell caller to skip
return;
}
if val == -1 {
// Not yet initialised - try to claim it
// Not yet initialised -- try to claim it
guard.write_volatile(1);
return;
}
// val == 1: another thread is initialising - yield and retry
// val == 1: another thread is initialising -- yield and retry
std::thread::yield_now();
}
}
@@ -71,22 +71,22 @@ pub unsafe extern "C" fn _Init_thread_footer(guard: *mut i32) {
// MinGW's libstdc++ exports the same operators but with GCC/Itanium mangling,
// so the linker can't match them. We provide the MSVC-mangled versions here.
/// `std::nothrow` - MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B`
/// `std::nothrow` -- MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B`
/// An empty struct constant used as a tag for nothrow `new`.
#[export_name = "?nothrow@std@@3Unothrow_t@1@B"]
pub static MSVC_STD_NOTHROW: u8 = 0;
/// `operator new(size_t, const std::nothrow_t&)` - nothrow allocation
/// `operator new(size_t, const std::nothrow_t&)` -- nothrow allocation
/// MSVC-mangled: `??2@YAPEAX_KAEBUnothrow_t@std@@@Z`
#[export_name = "??2@YAPEAX_KAEBUnothrow_t@std@@@Z"]
pub unsafe extern "C" fn msvc_operator_new_nothrow(size: usize, _nothrow: *const u8) -> *mut u8 {
let size = if size == 0 { 1 } else { size };
let layout = std::alloc::Layout::from_size_align_unchecked(size, 8);
let ptr = std::alloc::alloc(layout);
ptr // null on failure - nothrow semantics
ptr // null on failure -- nothrow semantics
}
/// `operator delete(void*, size_t)` - sized deallocation
/// `operator delete(void*, size_t)` -- sized deallocation
/// MSVC-mangled: `??3@YAXPEAX_K@Z`
#[export_name = "??3@YAXPEAX_K@Z"]
pub unsafe extern "C" fn msvc_operator_delete_sized(ptr: *mut u8, size: usize) {
+1 -11
View File
@@ -3,7 +3,6 @@ use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
/// A single day's break statistics.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DayRecord {
@@ -16,7 +15,6 @@ pub struct DayRecord {
pub natural_break_time_secs: u64,
}
/// Persistent stats stored as JSON.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StatsData {
pub days: HashMap<String, DayRecord>,
@@ -24,12 +22,10 @@ pub struct StatsData {
pub best_streak: u32,
}
/// Runtime stats manager.
pub struct Stats {
pub data: StatsData,
}
/// Snapshot sent to the frontend.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatsSnapshot {
@@ -47,13 +43,11 @@ pub struct StatsSnapshot {
pub daily_goal_met: bool,
}
/// F10: Result of recording a completed break
pub struct BreakCompletedResult {
pub milestone_reached: Option<u32>,
pub daily_goal_just_met: bool,
}
/// F7: Weekly summary for reports
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WeekSummary {
@@ -68,7 +62,7 @@ pub struct WeekSummary {
const MILESTONES: &[u32] = &[3, 5, 7, 14, 21, 30, 50, 100, 365];
impl Stats {
/// Portable: stats file lives next to the exe
// Portable: next to the exe
fn stats_path() -> Option<PathBuf> {
let exe_path = std::env::current_exe().ok()?;
let exe_dir = exe_path.parent()?;
@@ -114,7 +108,6 @@ impl Stats {
})
}
/// Record a completed break. Returns milestone/goal info for gamification.
pub fn record_break_completed(&mut self, duration_secs: u64, daily_goal: u32) -> BreakCompletedResult {
let day = self.today_mut();
let was_below_goal = day.breaks_completed < daily_goal;
@@ -182,7 +175,6 @@ impl Stats {
}
}
/// F10: Check if current streak exactly matches a milestone
fn check_milestone(&self) -> Option<u32> {
let streak = self.data.current_streak;
if MILESTONES.contains(&streak) {
@@ -225,7 +217,6 @@ impl Stats {
}
}
/// Get recent N days of history, sorted chronologically.
pub fn recent_days(&self, n: u32) -> Vec<DayRecord> {
let today = chrono::Local::now().date_naive();
let mut records = Vec::new();
@@ -243,7 +234,6 @@ impl Stats {
records
}
/// F7: Get weekly summaries for the past N weeks
pub fn weekly_summary(&self, weeks: u32) -> Vec<WeekSummary> {
let today = chrono::Local::now().date_naive();
let mut summaries = Vec::new();
+2 -15
View File
@@ -26,7 +26,6 @@ pub enum AppView {
Stats,
}
/// Snapshot of the full timer state, sent to the frontend on every tick
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TimerSnapshot {
@@ -74,7 +73,6 @@ pub struct TimerSnapshot {
pub is_long_break: bool,
}
/// Events emitted by the timer to the frontend
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BreakStartedPayload {
@@ -161,8 +159,6 @@ impl TimerManager {
}
}
/// Check idle state and auto-pause/resume accordingly.
/// Returns IdleCheckResult indicating what happened.
pub fn check_idle(&mut self) -> IdleCheckResult {
if !self.config.idle_detection_enabled {
// If idle detection disabled but we were idle-paused, resume
@@ -218,7 +214,6 @@ impl TimerManager {
}
}
/// F2: Check if the foreground window is fullscreen (presentation mode)
pub fn check_presentation_mode(&mut self) -> bool {
if !self.config.presentation_mode_enabled {
self.presentation_mode_active = false;
@@ -230,7 +225,6 @@ impl TimerManager {
fs
}
/// Called every second. Returns what events should be emitted.
pub fn tick(&mut self) -> TickResult {
// Idle detection and natural break detection
let idle_result = self.check_idle();
@@ -322,7 +316,6 @@ impl TimerManager {
}
}
/// F1: Called every second for microbreak logic. Returns microbreak events.
pub fn tick_microbreak(&mut self) -> MicrobreakTickResult {
if !self.config.microbreak_enabled {
return MicrobreakTickResult::None;
@@ -416,7 +409,6 @@ impl TimerManager {
self.snoozes_used = 0;
}
/// F3: Advance the Pomodoro cycle position after a break completes
fn advance_pomodoro_cycle(&mut self) {
if !self.config.pomodoro_enabled {
return;
@@ -472,14 +464,14 @@ impl TimerManager {
let past_half = total > 0 && elapsed * 2 >= total;
if past_half && self.config.allow_end_early {
// "End break" - counts as completed
// "End break" -- counts as completed
self.has_had_break = true;
self.seconds_since_last_break = 0;
self.advance_pomodoro_cycle();
self.reset_timer();
true
} else if !past_half {
// "Cancel break" - doesn't count
// "Cancel break" -- doesn't count
// F3: Pomodoro reset-on-skip
if self.config.pomodoro_enabled && self.config.pomodoro_reset_on_skip {
self.pomodoro_cycle_position = 0;
@@ -672,14 +664,12 @@ pub enum TickResult {
BreakDeferred, // F2
}
/// F1: Microbreak tick result
pub enum MicrobreakTickResult {
None,
MicrobreakStarted,
MicrobreakEnded,
}
/// Result of checking idle state
pub enum IdleCheckResult {
None,
JustPaused,
@@ -687,7 +677,6 @@ pub enum IdleCheckResult {
NaturalBreakDetected(u64), // duration in seconds
}
/// Returns the number of seconds since last user input (mouse/keyboard).
#[cfg(windows)]
pub fn get_idle_seconds() -> u64 {
use std::mem;
@@ -712,7 +701,6 @@ pub fn get_idle_seconds() -> u64 {
0
}
/// F2: Check if the foreground window is a fullscreen application
#[cfg(windows)]
pub fn is_foreground_fullscreen() -> bool {
use std::mem;
@@ -756,7 +744,6 @@ pub fn is_foreground_fullscreen() -> bool {
false
}
/// F9: Get all monitor rects for multi-monitor break enforcement
#[cfg(windows)]
pub fn get_all_monitors() -> Vec<MonitorInfo> {
use winapi::shared::windef::{HMONITOR, HDC, LPRECT};
+2 -2
View File
@@ -62,7 +62,7 @@
return () => mq.removeEventListener("change", handler);
});
// Transition parameters - zero when reduced motion active
// Transition parameters -- zero when reduced motion active
const DURATION = $derived(reducedMotion ? 0 : 700);
const easing = cubicOut;
@@ -83,7 +83,7 @@
settings: "Settings",
stats: "Statistics",
};
document.title = `Core Cooldown - ${viewNames[effectiveView] ?? "Dashboard"}`;
document.title = `Core Cooldown -- ${viewNames[effectiveView] ?? "Dashboard"}`;
});
// When fullscreen_mode is OFF, the separate break window handles breaks,
+1 -1
View File
@@ -483,7 +483,7 @@
background: rgba(255, 255, 255, 0.03);
}
/* Ripple container - sits behind the ring, overflows the card */
/* Ripple container -- sits behind the ring, overflows the card */
.ripple-container {
position: absolute;
inset: 0;
+2 -2
View File
@@ -259,7 +259,7 @@
</button>
{/each}
<!-- Custom toggle swatch - ring shows when picker open OR value is custom -->
<!-- Custom toggle swatch -- ring shows when picker open OR value is custom -->
<button
type="button"
class="group flex items-center justify-center min-h-[44px] min-w-[44px] bg-transparent border-none p-0"
@@ -277,7 +277,7 @@
</button>
</div>
<!-- Inline custom picker - slides open/closed -->
<!-- Inline custom picker -- slides open/closed -->
{#if showCustom}
<div
class="flex flex-col gap-3 rounded-xl bg-[#0f0f0f] p-3 border border-[#1a1a1a]"
+1 -1
View File
@@ -104,7 +104,7 @@
// Gap between ring and button, compensating for CSS transform phantom space.
// transform: scale() doesn't affect layout, so the 280px box stays full-size
// even when visually shrunk - creating phantom space below the visual ring.
// even when visually shrunk -- creating phantom space below the visual ring.
const ringSize = 280;
const phantomBelow = $derived((ringSize * (1 - ringScale)) / 2);
const targetGap = $derived(Math.max(16, Math.round((windowH - 400) * 0.05 + 16)));
+1 -1
View File
@@ -45,7 +45,7 @@
<path d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span id="microbreak-msg" class="text-[15px] font-medium text-white">Look away - 20 feet for {timeRemaining}s</span>
<span id="microbreak-msg" class="text-[15px] font-medium text-white">Look away -- 20 feet for {timeRemaining}s</span>
</div>
{#if activity && $config.microbreak_show_activity}
+3 -3
View File
@@ -88,7 +88,7 @@
return milestones.find((m) => m > current) ?? null;
});
// Chart rendering - 7-day
// Chart rendering -- 7-day
let chartCanvas: HTMLCanvasElement | undefined = $state();
$effect(() => {
@@ -96,7 +96,7 @@
drawChart(chartCanvas, history);
});
// Chart rendering - 30-day
// Chart rendering -- 30-day
let monthChartCanvas: HTMLCanvasElement | undefined = $state();
$effect(() => {
@@ -154,7 +154,7 @@
ctx.fill();
}
// Day label - show every Nth for 30-day
// Day label -- show every Nth for 30-day
if (data.length <= 7 || i % 5 === 0) {
ctx.fillStyle = "#a8a8a8";
ctx.font = "10px -apple-system, sans-serif";
+2 -2
View File
@@ -7,7 +7,7 @@
let { value, onchange, countdownFont = "" }: Props = $props();
// Local display values - driven by prop normally, overridden during drag/momentum
// Local display values -- driven by prop normally, overridden during drag/momentum
let displayHours = $state(0);
let displayMinutes = $state(0);
let isAnimating = $state(false); // true during drag OR momentum
@@ -373,7 +373,7 @@
border-color: rgba(255, 255, 255, 0.18);
}
/* Perspective container - looking into the cylinder from outside */
/* Perspective container -- looking into the cylinder from outside */
.wheel-viewport {
width: 100%;
height: 100%;
+2 -2
View File
@@ -55,7 +55,7 @@
aria-label={label}
aria-valuetext={valueText}
>
<!-- Glow SVG - drawn larger than the container so blur isn't clipped -->
<!-- Glow SVG -- drawn larger than the container so blur isn't clipped -->
<svg
aria-hidden="true"
width={viewSize}
@@ -145,7 +145,7 @@
{/if}
</svg>
<!-- Non-glow SVG - exact size, draws the track + crisp ring -->
<!-- Non-glow SVG -- exact size, draws the track + crisp ring -->
<svg
aria-hidden="true"
width={size}
+2 -2
View File
@@ -9,7 +9,7 @@
data-tauri-drag-region
class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none"
>
<!-- Centered app name (decorative - OS window title handles screen readers) -->
<!-- Centered app name (decorative -- OS window title handles screen readers) -->
<span
aria-hidden="true"
class="pointer-events-none absolute inset-0 flex items-center justify-center
@@ -61,7 +61,7 @@
</span>
</button>
<!-- Close (red) - rightmost -->
<!-- Close (red) -- rightmost -->
<button
aria-label="Close"
class="group/btn relative flex h-[44px] w-[44px] items-center justify-center"
+14 -14
View File
@@ -8,9 +8,9 @@ export const breakActivities: BreakActivity[] = [
{ category: "eyes", text: "Look at something 20 feet away for 20 seconds" },
{ category: "eyes", text: "Close your eyes and gently press your palms over them" },
{ category: "eyes", text: "Slowly roll your eyes in circles, 5 times each direction" },
{ category: "eyes", text: "Focus on a distant object, then a near one repeat 5 times" },
{ category: "eyes", text: "Focus on a distant object, then a near one -- repeat 5 times" },
{ category: "eyes", text: "Blink rapidly 20 times to refresh your eyes" },
{ category: "eyes", text: "Look up, down, left, right hold each for 2 seconds" },
{ category: "eyes", text: "Look up, down, left, right -- hold each for 2 seconds" },
{ category: "eyes", text: "Cup your hands over closed eyes and breathe deeply" },
{ category: "eyes", text: "Trace a figure-8 with your eyes, slowly" },
{ category: "eyes", text: "Look out the nearest window and find the farthest point" },
@@ -19,9 +19,9 @@ export const breakActivities: BreakActivity[] = [
{ category: "eyes", text: "Warm your palms by rubbing them together, then rest them over closed eyes" },
{ category: "eyes", text: "Slowly shift your gaze along the horizon line outside" },
{ category: "eyes", text: "Focus on the tip of your nose for 5 seconds, then a far wall" },
{ category: "eyes", text: "Look at something green plants reduce eye strain naturally" },
{ category: "eyes", text: "Look at something green -- plants reduce eye strain naturally" },
{ category: "eyes", text: "Close your eyes and gently press your eyebrows outward from center" },
{ category: "eyes", text: "Squeeze your eyes shut for 3 seconds, then open wide repeat 5 times" },
{ category: "eyes", text: "Squeeze your eyes shut for 3 seconds, then open wide -- repeat 5 times" },
{ category: "eyes", text: "Trace the outline of a window frame with your eyes, slowly" },
// Stretches
@@ -35,7 +35,7 @@ export const breakActivities: BreakActivity[] = [
{ category: "stretch", text: "Drop your chin to your chest and slowly roll your neck" },
{ category: "stretch", text: "Extend each arm across your chest, hold for 15 seconds" },
{ category: "stretch", text: "Open and close your hands, spreading fingers wide" },
{ category: "stretch", text: "Place your right hand on your left knee and twist gently switch sides" },
{ category: "stretch", text: "Place your right hand on your left knee and twist gently -- switch sides" },
{ category: "stretch", text: "Reach one arm overhead and lean to the opposite side, hold 10 seconds each" },
{ category: "stretch", text: "Interlace your fingers and flip your palms to the ceiling, push up" },
{ category: "stretch", text: "Pull each finger gently backward for 3 seconds to stretch your forearms" },
@@ -46,20 +46,20 @@ export const breakActivities: BreakActivity[] = [
// Breathing
{ category: "breathing", text: "Inhale for 4 seconds, hold for 4, exhale for 6" },
{ category: "breathing", text: "Take 5 deep belly breaths feel your diaphragm expand" },
{ category: "breathing", text: "Take 5 deep belly breaths -- feel your diaphragm expand" },
{ category: "breathing", text: "Box breathing: inhale 4s, hold 4s, exhale 4s, hold 4s" },
{ category: "breathing", text: "Breathe in through your nose, out through your mouth 5 times" },
{ category: "breathing", text: "Breathe in through your nose, out through your mouth -- 5 times" },
{ category: "breathing", text: "Sigh deeply 3 times to release tension" },
{ category: "breathing", text: "Place a hand on your chest and breathe so only your belly moves" },
{ category: "breathing", text: "Alternate nostril breathing: 3 cycles each side" },
{ category: "breathing", text: "Exhale completely, then take the deepest breath you can" },
{ category: "breathing", text: "Count your breaths backward from 10 to 1" },
{ category: "breathing", text: "Breathe in calm, breathe out tension 5 rounds" },
{ category: "breathing", text: "Breathe in calm, breathe out tension -- 5 rounds" },
{ category: "breathing", text: "4-7-8 breathing: inhale 4s, hold 7s, exhale slowly for 8s" },
{ category: "breathing", text: "Take 3 breaths making your exhale twice as long as your inhale" },
{ category: "breathing", text: "Breathe in for 2 counts, out for 2 gradually increase to 6 each" },
{ category: "breathing", text: "Hum gently on each exhale for 5 breaths feel the vibration in your chest" },
{ category: "breathing", text: "Imagine breathing in cool blue air and exhaling warm red air 5 rounds" },
{ category: "breathing", text: "Breathe in for 2 counts, out for 2 -- gradually increase to 6 each" },
{ category: "breathing", text: "Hum gently on each exhale for 5 breaths -- feel the vibration in your chest" },
{ category: "breathing", text: "Imagine breathing in cool blue air and exhaling warm red air -- 5 rounds" },
{ category: "breathing", text: "Place both hands on your ribs and breathe so you feel them expand sideways" },
{ category: "breathing", text: "Breathe in through pursed lips slowly, then exhale in a long, steady stream" },
@@ -75,11 +75,11 @@ export const breakActivities: BreakActivity[] = [
{ category: "movement", text: "Shake out your hands and arms for 10 seconds" },
{ category: "movement", text: "Stand up, touch your toes, and slowly rise" },
{ category: "movement", text: "Walk to the farthest room in your home and back" },
{ category: "movement", text: "Do 5 wall push-ups hands on the wall, lean in and push back" },
{ category: "movement", text: "Do 5 wall push-ups -- hands on the wall, lean in and push back" },
{ category: "movement", text: "Stand up and slowly shift your weight side to side for 20 seconds" },
{ category: "movement", text: "Walk in a small circle, paying attention to each footstep" },
{ category: "movement", text: "Rise onto your tiptoes, hold for 5 seconds, lower slowly repeat 5 times" },
{ category: "movement", text: "Do a gentle standing forward fold let your arms hang loose" },
{ category: "movement", text: "Rise onto your tiptoes, hold for 5 seconds, lower slowly -- repeat 5 times" },
{ category: "movement", text: "Do a gentle standing forward fold -- let your arms hang loose" },
{ category: "movement", text: "Step away from your desk and do 10 hip circles in each direction" },
{ category: "movement", text: "Stand with your back against a wall and slide down into a gentle wall sit for 15 seconds" },
];
+5 -31
View File
@@ -1,6 +1,6 @@
import { animate } from "motion";
// Module-level reduced motion query shared across all actions
// Module-level reduced motion query -- shared across all actions
const reducedMotionQuery =
typeof window !== "undefined"
? window.matchMedia("(prefers-reduced-motion: reduce)")
@@ -10,9 +10,6 @@ function prefersReducedMotion(): boolean {
return reducedMotionQuery?.matches ?? false;
}
/**
* Svelte action: fade in + slide up on mount
*/
export function fadeIn(
node: HTMLElement,
options?: { duration?: number; delay?: number; y?: number },
@@ -38,9 +35,6 @@ export function fadeIn(
};
}
/**
* Svelte action: scale in + fade on mount
*/
export function scaleIn(
node: HTMLElement,
options?: { duration?: number; delay?: number },
@@ -66,9 +60,6 @@ export function scaleIn(
};
}
/**
* Svelte action: animate when scrolled into view (IntersectionObserver)
*/
export function inView(
node: HTMLElement,
options?: { delay?: number; y?: number; threshold?: number },
@@ -111,9 +102,6 @@ export function inView(
};
}
/**
* Svelte action: spring-scale press feedback on buttons
*/
export function pressable(node: HTMLElement) {
if (prefersReducedMotion()) {
return { destroy() {} };
@@ -171,14 +159,7 @@ export function pressable(node: HTMLElement) {
};
}
/**
* Svelte action: animated glow band on hover.
* Creates a crisp ring "band" plus a soft atmospheric glow.
* Pass a hex color (e.g. "#ff4d00").
*
* Uses same-hue zero-alpha as the "off" state so the Web Animations API
* interpolates through the correct color channel instead of through black.
*/
// Animated glow band on hover. Pass a hex color (e.g. "#ff4d00").
export function glowHover(
node: HTMLElement,
options?: { color?: string },
@@ -236,14 +217,7 @@ export function glowHover(
};
}
/**
* Svelte action: momentum-based grab-and-drag scrolling
* with elastic overscroll and spring-back at boundaries.
*
* IMPORTANT: The node must have exactly one child element (a wrapper div).
* Overscroll transforms are applied to that child, NOT to the scroll
* container itself (which would break overflow clipping).
*/
// Momentum grab-and-drag scrolling. Node must have exactly one child wrapper div.
export function dragScroll(node: HTMLElement) {
if (prefersReducedMotion()) {
// Allow normal scrolling without the momentum/elastic physics
@@ -293,7 +267,7 @@ export function dragScroll(node: HTMLElement) {
// Ensure DOM is clean after animation completes
springAnim.finished.then(() => {
if (content) content.style.transform = "";
}).catch(() => { /* cancelled onDown handles cleanup */ });
}).catch(() => { /* cancelled -- onDown handles cleanup */ });
}
function forceReset() {
@@ -370,7 +344,7 @@ export function dragScroll(node: HTMLElement) {
// Time-based exponential decay (iOS-style scroll physics).
// position(t) = start + v0 * tau * (1 - e^(-t/tau))
// velocity(t) = v0 * e^(-t/tau) → naturally asymptotes to zero
const tau = 325; // time constant in ms iOS UIScrollView feel
const tau = 325; // time constant in ms -- iOS UIScrollView feel
const coastStart = performance.now();
const scrollStart2 = node.scrollTop;
const totalDist = v0 * tau;
+7 -18
View File
@@ -1,8 +1,3 @@
/**
* Synthesized notification sounds using the Web Audio API.
* No external audio files needed — all sounds are generated programmatically.
*/
let audioCtx: AudioContext | null = null;
function getAudioContext(): AudioContext {
@@ -14,11 +9,6 @@ function getAudioContext(): AudioContext {
type SoundPreset = "bell" | "chime" | "soft" | "digital" | "harp" | "bowl" | "rain" | "whistle";
/**
* Play a notification sound with the given preset and volume.
* @param preset - One of: "bell", "chime", "soft", "digital"
* @param volume - 0 to 100
*/
export function playSound(preset: SoundPreset, volume: number): void {
const ctx = getAudioContext();
const gain = ctx.createGain();
@@ -53,7 +43,7 @@ export function playSound(preset: SoundPreset, volume: number): void {
}
}
/** Warm bell two sine tones with harmonics and slow decay */
/** Warm bell -- two sine tones with harmonics and slow decay */
function playBell(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
@@ -103,7 +93,7 @@ function playChime(ctx: AudioContext, destination: GainNode, vol: number) {
});
}
/** Gentle soft ping filtered triangle wave */
/** Gentle soft ping -- filtered triangle wave */
function playSoft(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
@@ -129,7 +119,7 @@ function playSoft(ctx: AudioContext, destination: GainNode, vol: number) {
osc.stop(now + 1.2);
}
/** Digital blip short square wave burst */
/** Digital blip -- short square wave burst */
function playDigital(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
@@ -149,7 +139,7 @@ function playDigital(ctx: AudioContext, destination: GainNode, vol: number) {
}
}
/** Harp cascading arpeggiated sine tones (C5-E5-G5-C6) */
/** Harp -- cascading arpeggiated sine tones (C5-E5-G5-C6) */
function playHarp(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
@@ -170,7 +160,7 @@ function playHarp(ctx: AudioContext, destination: GainNode, vol: number) {
});
}
/** Singing bowl low sine with slow beating from detuned pair */
/** Singing bowl -- low sine with slow beating from detuned pair */
function playBowl(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
@@ -201,7 +191,7 @@ function playBowl(ctx: AudioContext, destination: GainNode, vol: number) {
osc3.stop(now + 1.5);
}
/** Rain filtered noise burst with gentle decay */
/** Rain -- filtered noise burst with gentle decay */
function playRain(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const bufferSize = ctx.sampleRate * 1;
@@ -233,7 +223,7 @@ function playRain(ctx: AudioContext, destination: GainNode, vol: number) {
noise.stop(now + 1.0);
}
/** Whistle gentle two-note sine glide */
/** Whistle -- gentle two-note sine glide */
function playWhistle(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
@@ -256,7 +246,6 @@ function playWhistle(ctx: AudioContext, destination: GainNode, vol: number) {
osc.stop(now + 1.0);
}
/** Play a completion sound — slightly different from start (descending) */
export function playBreakEndSound(preset: SoundPreset, volume: number): void {
const ctx = getAudioContext();
const gain = ctx.createGain();