Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b584efb40 | ||
|
|
b01dbd6c0b | ||
|
|
d93d231a45 | ||
|
|
4f4599c4c9 | ||
|
|
e9021e51e5 | ||
|
|
37d0d638d5 | ||
|
|
6bba2835bb | ||
|
|
3aeb83f69b |
73
README.md
73
README.md
@@ -41,6 +41,62 @@ Core Cooldown is a single portable `.exe`. No installer, no account, no telemetr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💡 Philosophy
|
||||||
|
|
||||||
|
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
|
||||||
|
|
||||||
|
This application:
|
||||||
|
|
||||||
|
- **Collects nothing.** No analytics, no telemetry, no usage tracking, no crash reports phoned home. Your break habits are your own business.
|
||||||
|
- **Costs nothing.** Not "free tier with limitations." Not "free for personal use." Free, unconditionally, for everyone.
|
||||||
|
- **Requires nothing.** No account, no email, no app store, no internet connection after download. It runs on your machine and answers to you alone.
|
||||||
|
- **Owns nothing.** Released under CC0 - the most complete relinquishment of rights possible under law. There is no owner. There are no restrictions. The code belongs to the commons.
|
||||||
|
|
||||||
|
Tools for human wellbeing should never be enclosed, never be scarce, and never serve a master other than the person using them.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼️ Screenshots
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/01-dashboard.png" alt="Dashboard - Main Timer" width="420" /><br />
|
||||||
|
<sub><strong>Dashboard</strong> - Focus timer with countdown ring, status pill, and quick controls</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/02-stats.png" alt="Statistics" width="420" /><br />
|
||||||
|
<sub><strong>Statistics</strong> - Daily summary, compliance rate, streaks, and 7-day history chart</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/03-settings.png" alt="Settings" width="420" /><br />
|
||||||
|
<sub><strong>Settings</strong> - Grouped configuration cards with live preview</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/04-break.png" alt="Break Screen" width="420" /><br />
|
||||||
|
<sub><strong>Break Screen</strong> - Always-on-top break overlay with activity suggestions</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshots/05-mini.png" alt="Mini Mode" width="300" /><br />
|
||||||
|
<sub><strong>Mini Mode</strong> - Compact floating timer, click-through until hovered</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### ⏱️ Timer & Breaks
|
### ⏱️ Timer & Breaks
|
||||||
@@ -465,23 +521,6 @@ The best software is built through mutual aid - people helping people because it
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 Philosophy
|
|
||||||
|
|
||||||
Core Cooldown exists because rest is not a luxury. It is a fundamental need that no productivity framework, no employer, and no software platform should gatekeep behind a paywall or a subscription.
|
|
||||||
|
|
||||||
This application:
|
|
||||||
|
|
||||||
- **Collects nothing.** No analytics, no telemetry, no usage tracking, no crash reports phoned home. Your break habits are your own business.
|
|
||||||
- **Costs nothing.** Not "free tier with limitations." Not "free for personal use." Free, unconditionally, for everyone.
|
|
||||||
- **Requires nothing.** No account, no email, no app store, no internet connection after download. It runs on your machine and answers to you alone.
|
|
||||||
- **Owns nothing.** Released under CC0 - the most complete relinquishment of rights possible under law. There is no owner. There are no restrictions. The code belongs to the commons.
|
|
||||||
|
|
||||||
Tools for human wellbeing should never be enclosed, never be scarce, and never serve a master other than the person using them.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "core-cooldown",
|
"name": "core-cooldown",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
BIN
screenshots/01-dashboard.png
Normal file
BIN
screenshots/01-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
BIN
screenshots/02-stats.png
Normal file
BIN
screenshots/02-stats.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
screenshots/03-settings.png
Normal file
BIN
screenshots/03-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
screenshots/04-break.png
Normal file
BIN
screenshots/04-break.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
screenshots/05-mini.png
Normal file
BIN
screenshots/05-mini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "core-cooldown"
|
name = "core-cooldown"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -11,7 +11,7 @@ crate-type = ["lib", "staticlib"]
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["tray-icon"] }
|
tauri = { version = "2", features = ["tray-icon", "custom-protocol"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
@@ -22,4 +22,4 @@ chrono = "0.4"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["winuser", "sysinfoapi", "windef"] }
|
winapi = { version = "0.3", features = ["winuser", "sysinfoapi", "windef", "shellapi"] }
|
||||||
|
|||||||
@@ -7,5 +7,75 @@ fn main() {
|
|||||||
std::env::set_var("PATH", mingw_bin);
|
std::env::set_var("PATH", mingw_bin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On GNU targets, replace the WebView2Loader import library with the static
|
||||||
|
// library so the loader is baked into the exe — no DLL to ship.
|
||||||
|
#[cfg(target_env = "gnu")]
|
||||||
|
swap_webview2_to_static();
|
||||||
|
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace `WebView2Loader.dll.lib` (dynamic import lib) with the contents of
|
||||||
|
/// `WebView2LoaderStatic.lib` (static archive) in the webview2-com-sys build
|
||||||
|
/// output. The linker then statically links the WebView2 loader code, removing
|
||||||
|
/// the runtime dependency on WebView2Loader.dll.
|
||||||
|
#[cfg(target_env = "gnu")]
|
||||||
|
fn swap_webview2_to_static() {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap_or_default();
|
||||||
|
// OUT_DIR = target/.../build/core-cooldown-HASH/out
|
||||||
|
// We need: target/.../build/ (two levels up)
|
||||||
|
let build_dir = PathBuf::from(&out_dir)
|
||||||
|
.parent() // core-cooldown-HASH
|
||||||
|
.and_then(|p| p.parent()) // build/
|
||||||
|
.map(|p| p.to_path_buf());
|
||||||
|
|
||||||
|
let build_dir = match build_dir {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"x86_64" => "x64",
|
||||||
|
"x86" => "x86",
|
||||||
|
"aarch64" => "arm64",
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = match fs::read_dir(&build_dir) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
if !name.starts_with("webview2-com-sys-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lib_dir = entry.path().join("out").join(target_arch);
|
||||||
|
let import_lib = lib_dir.join("WebView2Loader.dll.lib");
|
||||||
|
let static_lib = lib_dir.join("WebView2LoaderStatic.lib");
|
||||||
|
|
||||||
|
if static_lib.exists() && import_lib.exists() {
|
||||||
|
if let Ok(static_bytes) = fs::read(&static_lib) {
|
||||||
|
match fs::write(&import_lib, &static_bytes) {
|
||||||
|
Ok(_) => println!(
|
||||||
|
"cargo:warning=Swapped WebView2Loader to static linking ({})",
|
||||||
|
lib_dir.display()
|
||||||
|
),
|
||||||
|
Err(e) => println!(
|
||||||
|
"cargo:warning=Failed to swap WebView2Loader lib: {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
mod config;
|
mod config;
|
||||||
|
#[cfg(all(windows, target_env = "gnu"))]
|
||||||
|
mod msvc_compat;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod timer;
|
mod timer;
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,66 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if !webview2_check::is_webview2_installed() {
|
||||||
|
webview2_check::show_missing_dialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
core_cooldown_lib::run()
|
core_cooldown_lib::run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod webview2_check {
|
||||||
|
use std::ptr;
|
||||||
|
use winapi::um::winuser::{MessageBoxW, MB_OK, MB_ICONWARNING};
|
||||||
|
use winapi::um::shellapi::ShellExecuteW;
|
||||||
|
|
||||||
|
// Statically linked from WebView2LoaderStatic.lib via msvc_compat shims
|
||||||
|
extern "system" {
|
||||||
|
fn GetAvailableCoreWebView2BrowserVersionString(
|
||||||
|
browser_executable_folder: *const u16,
|
||||||
|
version_info: *mut *mut u16,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "system" {
|
||||||
|
fn CoTaskMemFree(pv: *mut std::ffi::c_void);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_wide(s: &str) -> Vec<u16> {
|
||||||
|
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_webview2_installed() -> bool {
|
||||||
|
unsafe {
|
||||||
|
let mut version_info: *mut u16 = ptr::null_mut();
|
||||||
|
let hr = GetAvailableCoreWebView2BrowserVersionString(
|
||||||
|
ptr::null(),
|
||||||
|
&mut version_info,
|
||||||
|
);
|
||||||
|
let installed = hr == 0 && !version_info.is_null();
|
||||||
|
if !version_info.is_null() {
|
||||||
|
CoTaskMemFree(version_info as *mut _);
|
||||||
|
}
|
||||||
|
installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_missing_dialog() {
|
||||||
|
let title = to_wide("Core Cooldown - WebView2 Required");
|
||||||
|
let message = to_wide(
|
||||||
|
"Microsoft WebView2 Runtime is required to run Core Cooldown, \
|
||||||
|
but it was not found on this system.\n\n\
|
||||||
|
Click OK to open the download page in your browser.\n\n\
|
||||||
|
After installing WebView2, restart Core Cooldown."
|
||||||
|
);
|
||||||
|
let url = to_wide("https://developer.microsoft.com/en-us/microsoft-edge/webview2/consumer");
|
||||||
|
let open = to_wide("open");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
MessageBoxW(ptr::null_mut(), message.as_ptr(), title.as_ptr(), MB_OK | MB_ICONWARNING);
|
||||||
|
ShellExecuteW(ptr::null_mut(), open.as_ptr(), url.as_ptr(), ptr::null(), ptr::null(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
98
src-tauri/src/msvc_compat.rs
Normal file
98
src-tauri/src/msvc_compat.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//! Compatibility shims for MSVC CRT symbols required by WebView2LoaderStatic.lib.
|
||||||
|
//!
|
||||||
|
//! When statically linking the WebView2 loader on GNU/MinGW, the MSVC-compiled
|
||||||
|
//! object code references these symbols. We provide minimal implementations so
|
||||||
|
//! the linker can resolve them.
|
||||||
|
|
||||||
|
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
|
||||||
|
// randomises it at startup, but for a statically-linked helper library this
|
||||||
|
// fixed sentinel is sufficient.
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static __security_cookie: u64 = 0x00002B992DDFA232;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn __security_check_cookie(cookie: u64) {
|
||||||
|
if cookie != __security_cookie {
|
||||||
|
std::process::abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MSVC Thread-Safe Static Initialisation ───────────────────────────────────
|
||||||
|
//
|
||||||
|
// C++11 guarantees that function-local statics are initialised exactly once,
|
||||||
|
// even under concurrent access. MSVC implements this with an epoch counter and
|
||||||
|
// a set of helper functions. The WebView2 loader uses a few statics internally.
|
||||||
|
//
|
||||||
|
// Simplified implementation: uses an atomic spin for the guard. This is safe
|
||||||
|
// because WebView2 initialisation runs on the main thread in practice.
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static _Init_thread_epoch: AtomicI32 = AtomicI32::new(0);
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn _Init_thread_header(guard: *mut i32) {
|
||||||
|
if guard.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Spin until we can claim the guard (-1 = uninitialized, 0 = done, 1 = in progress)
|
||||||
|
loop {
|
||||||
|
let val = guard.read_volatile();
|
||||||
|
if val == 0 {
|
||||||
|
// Already initialised — tell caller to skip
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if val == -1 {
|
||||||
|
// Not yet initialised — try to claim it
|
||||||
|
guard.write_volatile(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// val == 1: another thread is initialising — yield and retry
|
||||||
|
std::thread::yield_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn _Init_thread_footer(guard: *mut i32) {
|
||||||
|
if !guard.is_null() {
|
||||||
|
guard.write_volatile(0); // Mark initialisation complete
|
||||||
|
_Init_thread_epoch.fetch_add(1, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MSVC C++ Runtime Operators (mangled names) ───────────────────────────────
|
||||||
|
//
|
||||||
|
// The static library is compiled with MSVC, which uses its own C++ name mangling.
|
||||||
|
// 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`
|
||||||
|
/// 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
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `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) {
|
||||||
|
if !ptr.is_null() {
|
||||||
|
let size = if size == 0 { 1 } else { size };
|
||||||
|
let layout = std::alloc::Layout::from_size_align_unchecked(size, 8);
|
||||||
|
std::alloc::dealloc(ptr, layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -462,14 +462,6 @@ pub enum IdleCheckResult {
|
|||||||
NaturalBreakDetected(u64), // duration in seconds
|
NaturalBreakDetected(u64), // duration in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_hour(time_str: &str) -> u32 {
|
|
||||||
time_str
|
|
||||||
.split(':')
|
|
||||||
.next()
|
|
||||||
.and_then(|h| h.parse().ok())
|
|
||||||
.unwrap_or(9)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of seconds since last user input (mouse/keyboard).
|
/// Returns the number of seconds since last user input (mouse/keyboard).
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn get_idle_seconds() -> u64 {
|
pub fn get_idle_seconds() -> u64 {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||||
"productName": "Core Cooldown",
|
"productName": "Core Cooldown",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"identifier": "com.corecooldown.app",
|
"identifier": "com.corecooldown.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
let { standalone = false }: Props = $props();
|
let { standalone = false }: Props = $props();
|
||||||
|
|
||||||
const appWindow = standalone ? getCurrentWebviewWindow() : null;
|
const appWindow = $derived(standalone ? getCurrentWebviewWindow() : null);
|
||||||
|
|
||||||
let currentActivity = $state<BreakActivity>(pickRandomActivity());
|
let currentActivity = $state<BreakActivity>(pickRandomActivity());
|
||||||
let activityCycleTimer: ReturnType<typeof setInterval> | null = null;
|
let activityCycleTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -168,23 +168,19 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- ── In-app break screen: full-screen fill OR modal with backdrop ── -->
|
<!-- ── In-app break screen: full-screen fill OR modal with backdrop ── -->
|
||||||
<div
|
<div
|
||||||
class="relative h-full"
|
class="relative h-full flex items-center justify-center"
|
||||||
class:flex={isModal}
|
style="background: #000;"
|
||||||
class:items-center={isModal}
|
|
||||||
class:justify-center={isModal}
|
|
||||||
style={isModal ? `background: #000;` : ""}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col"
|
class="relative flex flex-col items-center"
|
||||||
class:h-full={!isModal}
|
|
||||||
class:break-modal={isModal}
|
class:break-modal={isModal}
|
||||||
>
|
>
|
||||||
<!-- Break ring with breathing pulse + ripples -->
|
<!-- Break ring with breathing pulse + ripples -->
|
||||||
<div class="mb-6 relative" use:scaleIn={{ duration: 0.6, delay: 0.1 }}>
|
<div class="mb-6 relative" use:scaleIn={{ duration: 0.6, delay: 0.1 }}>
|
||||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<div class="break-ripple ripple-1" style="--ripple-color: {$config.break_color}"></div>
|
<div class="break-ripple ripple-1" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
|
||||||
<div class="break-ripple ripple-2" style="--ripple-color: {$config.break_color}"></div>
|
<div class="break-ripple ripple-2" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
|
||||||
<div class="break-ripple ripple-3" style="--ripple-color: {$config.break_color}"></div>
|
<div class="break-ripple ripple-3" style="--ripple-color: {$config.break_color}; --ripple-size: {isModal ? 160 : 200}px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="break-breathe relative">
|
<div class="break-breathe relative">
|
||||||
@@ -267,7 +263,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Bottom progress bar for modal - uses clip-path to respect border radius -->
|
<!-- Bottom progress bar for modal -->
|
||||||
|
{#if isModal}
|
||||||
<div class="break-modal-progress-container">
|
<div class="break-modal-progress-container">
|
||||||
<div class="break-modal-progress-track">
|
<div class="break-modal-progress-track">
|
||||||
<div
|
<div
|
||||||
@@ -276,7 +273,20 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Fullscreen progress bar - anchored to bottom of screen -->
|
||||||
|
{#if !isModal}
|
||||||
|
<div class="absolute bottom-8 left-12 right-12 h-[3px] rounded-full overflow-hidden">
|
||||||
|
<div class="w-full h-full" style="background: rgba(255,255,255,0.03);">
|
||||||
|
<div
|
||||||
|
class="h-full transition-[width] duration-1000 ease-linear"
|
||||||
|
style="width: {$timer.breakProgress * 100}%; background: {barGradient};"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -393,8 +403,8 @@
|
|||||||
/* ── Ripple circles ── */
|
/* ── Ripple circles ── */
|
||||||
.break-ripple {
|
.break-ripple {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 140px;
|
width: var(--ripple-size, 140px);
|
||||||
height: 140px;
|
height: var(--ripple-size, 140px);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1.5px solid var(--ripple-color);
|
border: 1.5px solid var(--ripple-color);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -407,10 +417,10 @@
|
|||||||
@keyframes ripple-expand {
|
@keyframes ripple-expand {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 0.3;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scale(2.2);
|
transform: scale(2.5);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,14 +324,14 @@
|
|||||||
.time-spinner {
|
.time-spinner {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wheel-field {
|
.wheel-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 50px;
|
width: 44px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
@@ -376,13 +376,13 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
padding-right: 12px;
|
padding-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unit label pinned to the right of the field */
|
/* Unit label pinned to the right of the field */
|
||||||
.unit-badge {
|
.unit-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5px;
|
right: 3px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user