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>
This commit is contained in:
Your Name
2026-02-18 18:15:06 +02:00
parent 95f684450c
commit acf06c8d32
6 changed files with 443 additions and 216 deletions

View File

@@ -74,6 +74,18 @@
let breathCountdown = $state(4);
let breathScale = $state(0.6);
// Only announce phase name changes (not countdown ticks) to screen readers
let breathAnnouncement = $state("");
let lastBreathPhase = $state("");
$effect(() => {
// Extract just the phase name (e.g., "Inhale" from "Inhale 4")
const phaseName = breathPhase?.split(' ')[0] ?? "";
if (phaseName && phaseName !== lastBreathPhase) {
lastBreathPhase = phaseName;
breathAnnouncement = phaseName;
}
});
// Map raw 0.61.0 scale to 0.91.6 range for visible breathing text
const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
@@ -169,11 +181,11 @@
<span
class="block mt-1.5 text-[9px] tracking-wider uppercase text-center font-medium"
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
aria-live="polite" aria-atomic="true"
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
aria-hidden="true"
>
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
</span>
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
{/if}
</div>
</TimerRing>
@@ -185,16 +197,16 @@
<h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1">
{$timer.breakTitle}
</h2>
<p class="text-[12px] leading-relaxed text-[#8a8a8a] mb-4 max-w-[240px]">
<p class="text-[12px] leading-relaxed text-text-sec mb-4 max-w-[240px]">
{$timer.breakMessage}
</p>
{#if $config.show_break_activities}
<div class="rounded-xl border border-[#ffffff08] bg-[#ffffff06] px-4 py-2.5 mb-4 max-w-[260px]">
<div class="text-[9px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase mb-1">
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase mb-1">
{getCategoryLabel(currentActivity.category)}
</div>
<p class="text-[12px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
<p class="text-[12px] leading-relaxed text-text-sec" aria-live="polite">
{currentActivity.text}
</p>
</div>
@@ -205,7 +217,7 @@
<button
use:pressable
class="rounded-full border border-[#333] px-5 py-2 text-[11px]
tracking-wider text-[#8a8a8a] uppercase
tracking-wider text-text-sec uppercase
transition-colors duration-200
hover:border-[#444] hover:text-[#ccc]"
onclick={cancelBreak}
@@ -226,10 +238,15 @@
{/if}
</div>
{#if $config.snooze_limit > 0}
<p class="mt-2 text-[9px] text-[#8a8a8a]">
<p class="mt-2 text-[9px] text-text-sec">
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
</p>
{/if}
{:else}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span tabindex="0" class="sr-only" aria-live="polite">
Break in progress, please wait
</span>
{/if}
</div>
@@ -302,11 +319,11 @@
class:text-[10px]={!isModal}
class:text-[9px]={isModal}
style="transform: scale({textScale}); color: {breathColor}; opacity: {0.5 + breathT * 0.5}; transition: transform 0.15s ease-out, opacity 0.15s ease-out, color 0.15s ease-out;"
aria-live="polite" aria-atomic="true"
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
aria-hidden="true"
>
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
</span>
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
{/if}
</div>
</TimerRing>
@@ -328,7 +345,7 @@
</h2>
<p
class="max-w-[300px] text-center text-[13px] leading-relaxed text-[#8a8a8a]"
class="max-w-[300px] text-center text-[13px] leading-relaxed text-text-sec"
class:mb-4={$config.show_break_activities}
class:mb-8={!$config.show_break_activities}
use:fadeIn={{ delay: 0.35, y: 10 }}
@@ -341,10 +358,10 @@
class="mb-8 mx-auto max-w-[320px] rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] px-5 py-3.5 text-center"
use:fadeIn={{ delay: 0.4, y: 10 }}
>
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<div class="mb-1.5 text-[10px] font-medium tracking-[0.15em] text-text-sec uppercase">
{getCategoryLabel(currentActivity.category)}
</div>
<p class="text-[13px] leading-relaxed text-[#8a8a8a]" aria-live="polite">
<p class="text-[13px] leading-relaxed text-text-sec" aria-live="polite">
{currentActivity.text}
</p>
</div>
@@ -354,8 +371,8 @@
<div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}>
<button
use:pressable
class="rounded-full border border-[#222] px-6 py-2.5 text-[12px]
tracking-wider text-[#8a8a8a] uppercase
class="rounded-full border border-border px-6 py-2.5 text-[12px]
tracking-wider text-text-sec uppercase
transition-colors duration-200
hover:border-[#333] hover:text-[#ccc]"
onclick={cancelBreak}
@@ -376,10 +393,15 @@
{/if}
</div>
{#if $config.snooze_limit > 0}
<p class="mt-3 text-[10px] text-[#8a8a8a]">
<p class="mt-3 text-[10px] text-text-sec">
{$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
</p>
{/if}
{:else}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span tabindex="0" class="sr-only" aria-live="polite">
Break in progress, please wait
</span>
{/if}
<!-- Bottom progress bar for modal -->

View File

@@ -2,10 +2,99 @@
import { milestoneEvent, dailyGoalEvent } from "../stores/timer";
import { config } from "../stores/config";
const showMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
const showGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
const storeMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
const storeGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
const streakDays = $derived($milestoneEvent ?? 0);
// Local visibility state (decoupled from store for hover persistence)
let showMilestone = $state(false);
let showGoal = $state(false);
let milestoneHovering = $state(false);
let goalHovering = $state(false);
let milestoneFading = $state(false);
let goalFading = $state(false);
// Timeout handles for auto-dismiss
let milestoneTimeout: ReturnType<typeof setTimeout> | null = null;
let goalTimeout: ReturnType<typeof setTimeout> | null = null;
const DISMISS_DELAY = 3500; // matches original animation duration
const FADE_DURATION = 600; // fade-out transition time
function dismissMilestone() {
milestoneFading = true;
setTimeout(() => {
showMilestone = false;
milestoneFading = false;
}, FADE_DURATION);
}
function dismissGoal() {
goalFading = true;
setTimeout(() => {
showGoal = false;
goalFading = false;
}, FADE_DURATION);
}
function startMilestoneTimer() {
if (milestoneTimeout) clearTimeout(milestoneTimeout);
milestoneTimeout = setTimeout(() => {
milestoneTimeout = null;
dismissMilestone();
}, DISMISS_DELAY);
}
function startGoalTimer() {
if (goalTimeout) clearTimeout(goalTimeout);
goalTimeout = setTimeout(() => {
goalTimeout = null;
dismissGoal();
}, DISMISS_DELAY);
}
// When the store signals a milestone, show locally and start auto-dismiss
$effect(() => {
if (storeMilestone) {
showMilestone = true;
milestoneFading = false;
startMilestoneTimer();
}
});
// When the store signals daily goal, show locally and start auto-dismiss
$effect(() => {
if (storeGoal) {
showGoal = true;
goalFading = false;
startGoalTimer();
}
});
// Pause/resume milestone dismiss on hover/focus
$effect(() => {
if (milestoneHovering) {
if (milestoneTimeout) {
clearTimeout(milestoneTimeout);
milestoneTimeout = null;
}
} else if (showMilestone && !milestoneFading) {
startMilestoneTimer();
}
});
// Pause/resume goal dismiss on hover/focus
$effect(() => {
if (goalHovering) {
if (goalTimeout) {
clearTimeout(goalTimeout);
goalTimeout = null;
}
} else if (showGoal && !goalFading) {
startGoalTimer();
}
});
// Generate confetti particles on milestone
const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"];
const confettiParticles = $derived(
@@ -23,7 +112,27 @@
</script>
{#if showMilestone}
<div class="celebration-overlay" role="alert" aria-live="assertive">
<!-- svelte-ignore a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
<div
class="celebration-overlay"
class:fading={milestoneFading}
role="alert"
aria-live="assertive"
tabindex="-1"
onmouseenter={() => milestoneHovering = true}
onmouseleave={() => milestoneHovering = false}
onfocusin={() => milestoneHovering = true}
onfocusout={() => milestoneHovering = false}
onkeydown={(e) => { if (e.key === 'Escape') dismissMilestone(); }}
>
<button
onclick={() => dismissMilestone()}
class="absolute top-3 right-3 w-8 h-8 flex items-center justify-center text-white/50 hover:text-white rounded-full"
aria-label="Dismiss notification"
>
&times;
</button>
<!-- Confetti burst -->
<div class="confetti-container">
{#each confettiParticles as p (p.id)}
@@ -51,12 +160,31 @@
{/if}
{#if showGoal && !showMilestone}
<div class="goal-overlay" role="alert" aria-live="assertive">
<!-- svelte-ignore a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
<div
class="goal-overlay"
class:fading={goalFading}
role="alert"
aria-live="assertive"
tabindex="-1"
onmouseenter={() => goalHovering = true}
onmouseleave={() => goalHovering = false}
onfocusin={() => goalHovering = true}
onfocusout={() => goalHovering = false}
onkeydown={(e) => { if (e.key === 'Escape') dismissGoal(); }}
>
<div class="goal-badge">
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2.5">
<path d="M20 6L9 17l-5-5"/>
</svg>
<span class="text-[14px] font-medium text-[#3fb950] ml-2">Daily goal reached!</span>
<button
onclick={() => dismissGoal()}
class="w-6 h-6 flex items-center justify-center text-[#3fb950]/50 hover:text-[#3fb950] rounded-full ml-2"
aria-label="Dismiss notification"
>
&times;
</button>
</div>
</div>
{/if}
@@ -69,13 +197,18 @@
align-items: center;
justify-content: center;
z-index: 9999;
pointer-events: none;
animation: celebration-fade 3.5s ease forwards;
opacity: 1;
transition: opacity 0.6s ease;
animation: celebration-enter 0.3s ease forwards;
}
@keyframes celebration-fade {
0%, 70% { opacity: 1; }
100% { opacity: 0; }
.celebration-overlay.fading {
opacity: 0;
}
@keyframes celebration-enter {
0% { opacity: 0; }
100% { opacity: 1; }
}
.confetti-container {
@@ -130,15 +263,18 @@
left: 50%;
transform: translateX(-50%);
z-index: 9999;
pointer-events: none;
animation: goal-slide 3.5s ease forwards;
opacity: 1;
transition: opacity 0.6s ease;
animation: goal-enter 0.35s ease forwards;
}
@keyframes goal-slide {
.goal-overlay.fading {
opacity: 0;
}
@keyframes goal-enter {
0% { transform: translateX(-50%) translateY(-20px); opacity: 0; }
10% { transform: translateX(-50%) translateY(0); opacity: 1; }
75% { opacity: 1; }
100% { transform: translateX(-50%) translateY(-10px); opacity: 0; }
100% { transform: translateX(-50%) translateY(0); opacity: 1; }
}
.goal-badge {
@@ -158,6 +294,11 @@
.goal-overlay {
animation: none;
opacity: 1;
transition: none;
}
.celebration-overlay.fading,
.goal-overlay.fading {
opacity: 0;
}
.confetti-particle {
display: none;

View File

@@ -112,6 +112,7 @@
// Natural break notification
let showNaturalBreakToast = $state(false);
let toastHovering = $state(false);
let naturalBreakToastTimeout: ReturnType<typeof setTimeout> | null = null;
// Watch for natural break detection
@@ -120,7 +121,9 @@
showNaturalBreakToast = true;
if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout);
naturalBreakToastTimeout = setTimeout(() => {
showNaturalBreakToast = false;
if (!toastHovering) {
showNaturalBreakToast = false;
}
}, 5000);
}
});
@@ -177,7 +180,7 @@
<!-- Status label -->
<span
class="block text-center text-[11px] font-medium tracking-[0.25em]"
class:text-[#8a8a8a]={!$timer.prebreakWarning && !$timer.deferredBreakPending}
class:text-text-sec={!$timer.prebreakWarning && !$timer.deferredBreakPending}
class:text-warning={$timer.prebreakWarning}
class:text-[#fca311]={$timer.deferredBreakPending}
>
@@ -205,15 +208,16 @@
></div>
{/each}
</div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
<span class="text-[9px] text-text-sec tabular-nums">
{$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle}
</span>
</div>
<span class="sr-only">Pomodoro cycle: session {$timer.pomodoroCyclePosition + 1} of {$timer.pomodoroTotalInCycle}</span>
{/if}
<!-- Microbreak countdown -->
{#if $timer.microbreakEnabled && !$timer.microbreakActive && $timer.state === "running"}
<div class="flex items-center gap-1 text-[9px] text-[#8a8a8a]">
<div class="flex items-center gap-1 text-[9px] text-text-sec">
<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<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"/>
@@ -231,14 +235,22 @@
</svg>
<span class="text-[9px] text-[#3fb950]">Goal met</span>
{:else}
<span class="text-[9px] text-[#8a8a8a]">Goal</span>
<div class="w-16 h-[2px] rounded-full overflow-hidden" style="background: #161616;">
<span class="text-[9px] text-text-sec">Goal</span>
<div
class="w-16 h-[2px] rounded-full overflow-hidden"
style="background: #161616;"
role="progressbar"
aria-label="Daily goal progress"
aria-valuemin={0}
aria-valuemax={$config.daily_goal_breaks}
aria-valuenow={dailyGoalProgress}
>
<div
class="h-full rounded-full transition-[width] duration-500"
style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};"
></div>
</div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums">
<span class="text-[9px] text-text-sec tabular-nums">
{dailyGoalProgress}/{$config.daily_goal_breaks}
</span>
{/if}
@@ -253,7 +265,7 @@
<!-- Last break info -->
<div use:fadeIn={{ delay: 0.4, y: 10 }}>
{#if $timer.hasHadBreak}
<p style="margin-bottom: {ringMargin}px;" class="text-[12px] text-[#8a8a8a]">
<p style="margin-bottom: {ringMargin}px;" class="text-[12px] text-text-sec">
Last break {formatDurationAgo($timer.secondsSinceLastBreak)}
</p>
{:else}
@@ -263,17 +275,24 @@
<!-- Natural break notification toast -->
{#if showNaturalBreakToast}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
role="alert"
class="absolute top-6 left-1/2 -translate-x-1/2 rounded-2xl border px-5 py-3 text-center backdrop-blur-xl transition-all duration-500"
style="border-color: rgba(63, 185, 80, 0.3); background: rgba(63, 185, 80, 0.1);"
use:scaleIn={{ duration: 0.3, delay: 0 }}
onmouseenter={() => toastHovering = true}
onmouseleave={() => { toastHovering = false; showNaturalBreakToast = false; }}
onfocusin={() => toastHovering = true}
onfocusout={() => { toastHovering = false; showNaturalBreakToast = false; }}
onkeydown={(e) => { if (e.key === 'Escape') showNaturalBreakToast = false; }}
>
<div class="flex items-center gap-2">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3fb950" stroke-width="2">
<path d="M20 6L9 17l-5-5"/>
</svg>
<span class="text-[13px] text-[#3fb950]">Natural break detected - timer reset</span>
<button onclick={() => showNaturalBreakToast = false} class="ml-2 text-text-sec hover:text-white" aria-label="Dismiss notification">&times;</button>
</div>
</div>
{/if}
@@ -292,94 +311,97 @@
{toggleBtnText}
</button>
<!-- Bottom left: start break now -->
<div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}>
<button
aria-label="Start break now"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a]
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={startBreakNow}
>
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
<!-- Bottom navigation buttons -->
<nav aria-label="Main actions" class="contents">
<!-- Bottom left: start break now -->
<div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}>
<button
aria-label="Start break now"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-border text-text-sec
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={startBreakNow}
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</button>
</div>
<svg
aria-hidden="true"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</button>
</div>
<!-- Bottom center: stats -->
<div class="absolute bottom-5 left-1/2 -translate-x-1/2" use:fadeIn={{ delay: 0.52, y: 8 }}>
<button
aria-label="Statistics"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a]
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={() => {
invoke("set_view", { view: "stats" });
currentView.set("stats");
}}
>
<svg
aria-hidden="true"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
<!-- Bottom center: stats -->
<div class="absolute bottom-5 left-1/2 -translate-x-1/2" use:fadeIn={{ delay: 0.52, y: 8 }}>
<button
aria-label="Statistics"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-border text-text-sec
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={() => {
invoke("set_view", { view: "stats" });
currentView.set("stats");
}}
>
<rect x="3" y="12" width="4" height="9" rx="1" />
<rect x="10" y="7" width="4" height="14" rx="1" />
<rect x="17" y="3" width="4" height="18" rx="1" />
</svg>
</button>
</div>
<svg
aria-hidden="true"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="12" width="4" height="9" rx="1" />
<rect x="10" y="7" width="4" height="14" rx="1" />
<rect x="17" y="3" width="4" height="18" rx="1" />
</svg>
</button>
</div>
<!-- Bottom right: settings -->
<div class="absolute bottom-5 right-5" use:fadeIn={{ delay: 0.55, y: 8 }}>
<button
aria-label="Settings"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a]
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={openSettings}
>
<svg
aria-hidden="true"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
<!-- Bottom right: settings -->
<div class="absolute bottom-5 right-5" use:fadeIn={{ delay: 0.55, y: 8 }}>
<button
aria-label="Settings"
use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full
border border-border text-text-sec
transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]"
onclick={openSettings}
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
<svg
aria-hidden="true"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</button>
</div>
</nav>
</div>
<style>

View File

@@ -159,10 +159,10 @@
<div class="space-y-3">
<!-- 1. Timer -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Timer
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -214,10 +214,10 @@
</section>
<!-- 2. Pomodoro Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.03 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Pomodoro Mode
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -276,10 +276,10 @@
</section>
<!-- 3. Microbreaks -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Microbreaks
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -342,10 +342,10 @@
</section>
<!-- 4. Break Screen (stripped down) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.09 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Break Screen
</h3>
</h2>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] text-white" for="break-title">
@@ -431,19 +431,19 @@
<!-- 5. Break Activities (own card, conditional) -->
{#if $config.show_break_activities}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Break Activities
</h3>
</h2>
<ActivityManager />
</section>
{/if}
<!-- 6. Breathing Guide (own card) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.15 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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" class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
Breathing Guide
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -494,10 +494,10 @@
</section>
<!-- 7. Behavior -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.18 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Behavior
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -586,10 +586,10 @@
</section>
<!-- 8. Alerts (MERGED: Notifications + Pre-Break Nudge) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.21 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Alerts
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -664,10 +664,10 @@
</section>
<!-- 9. Sound -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.24 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Sound
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -728,10 +728,10 @@
</section>
<!-- 10. Idle & Smart Breaks (MERGED) -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.27 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Idle & Smart Breaks
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -821,10 +821,10 @@
</section>
<!-- 11. Presentation Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.30 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Presentation Mode
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -858,10 +858,10 @@
</section>
<!-- 12. Goals & Streaks -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.33 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Goals & Streaks
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -902,10 +902,10 @@
</section>
<!-- 13. Appearance -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.36 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Appearance
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -972,7 +972,11 @@
</section>
<!-- 14. Working Hours -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.39 }}>
<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-[#8a8a8a] uppercase">
Working Hours
</h2>
<div class="flex items-center">
<div class="flex-1">
<div class="text-[13px] text-white">Working hours</div>
@@ -1074,10 +1078,10 @@
</section>
<!-- 15. Mini Mode -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.42 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Mini Mode
</h3>
</h2>
<div class="flex items-center justify-between">
<div>
@@ -1115,10 +1119,10 @@
</section>
<!-- 16. General -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.45 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
General
</h3>
</h2>
<div class="flex items-center">
<div class="flex-1">
@@ -1134,10 +1138,10 @@
</section>
<!-- 17. Keyboard Shortcuts -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.48 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<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-[#8a8a8a] uppercase">
Keyboard Shortcuts
</h3>
</h2>
<div class="space-y-3">
<div class="flex items-center justify-between">
@@ -1156,7 +1160,8 @@
</section>
<!-- 18. Reset -->
<div class="pt-2 pb-6" use:inView={{ delay: 0.51 }}>
<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
class="w-full rounded-full border py-3 text-[12px]
@@ -1169,7 +1174,7 @@
>
{resetConfirming ? "Tap again to confirm reset" : "Reset to defaults"}
</button>
</div>
</section>
</div>
</div>
</div>

View File

@@ -266,8 +266,8 @@
<button
aria-label="Back to dashboard"
use:pressable
class="mr-3 flex h-8 w-8 items-center justify-center rounded-full
text-[#8a8a8a] transition-colors hover:text-white"
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
@@ -294,14 +294,18 @@
</div>
<!-- Tab navigation -->
<div class="flex gap-1 px-5 mb-3" use:fadeIn={{ duration: 0.3, y: 6 }}>
<div class="flex gap-1 px-5 mb-3" role="tablist" aria-label="Statistics time range" use:fadeIn={{ duration: 0.3, y: 6 }}>
{#each [["today", "Today"], ["weekly", "Weekly"], ["monthly", "Monthly"]] as [tab, label]}
<button
use:pressable
class="rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
role="tab"
id="tab-{tab}"
aria-selected={activeTab === tab}
aria-controls="tabpanel-{tab}"
class="min-h-[44px] rounded-lg px-4 py-1.5 text-[11px] tracking-wider uppercase transition-all duration-200
{activeTab === tab
? 'bg-[#1a1a1a] text-white'
: 'text-[#8a8a8a] hover:text-white'}"
: 'text-text-sec hover:text-white'}"
onclick={() => activeTab = tab as any}
>
{label}
@@ -314,18 +318,19 @@
<div class="space-y-3">
{#if activeTab === "today"}
<div role="tabpanel" id="tabpanel-today" aria-labelledby="tab-today">
<!-- Today's summary -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Today
</h3>
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{stats?.todayCompleted ?? 0}
</div>
<div class="text-[11px] text-[#8a8a8a]">Breaks taken</div>
<div class="text-[11px] text-text-sec">Breaks taken</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold tabular-nums"
@@ -333,19 +338,19 @@
>
{compliancePercent}%
</div>
<div class="text-[11px] text-[#8a8a8a]">Compliance</div>
<div class="text-[11px] text-text-sec">Compliance</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{breakTimeFormatted()}
</div>
<div class="text-[11px] text-[#8a8a8a]">Break time</div>
<div class="text-[11px] text-text-sec">Break time</div>
</div>
<div class="text-center">
<div class="text-[28px] font-semibold text-white tabular-nums">
{stats?.todaySkipped ?? 0}
</div>
<div class="text-[11px] text-[#8a8a8a]">Skipped</div>
<div class="text-[11px] text-text-sec">Skipped</div>
</div>
</div>
</section>
@@ -353,9 +358,9 @@
<!-- F10: Daily goal -->
{#if $config.daily_goal_enabled}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.04 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Daily Goal
</h3>
</h2>
<div class="flex items-center gap-4">
<div class="relative w-16 h-16">
<svg width="64" height="64" viewBox="0 0 64 64" style="transform: rotate(-90deg);">
@@ -375,7 +380,7 @@
<div class="text-[14px] text-white font-medium">
{stats?.dailyGoalProgress ?? 0} / {$config.daily_goal_breaks} breaks
</div>
<div class="text-[11px] text-[#8a8a8a]">
<div class="text-[11px] text-text-sec">
{stats?.dailyGoalMet ? "Goal reached!" : `${$config.daily_goal_breaks - (stats?.dailyGoalProgress ?? 0)} more to go`}
</div>
</div>
@@ -385,26 +390,26 @@
<!-- Streak -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Streak
</h3>
</h2>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Current streak</div>
<div class="text-[11px] text-[#8a8a8a]">Consecutive days with breaks</div>
<div class="text-[11px] text-text-sec">Consecutive days with breaks</div>
</div>
<div class="text-[24px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{stats?.currentStreak ?? 0}
</div>
</div>
<div class="my-4 h-px bg-[#161616]"></div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Best streak</div>
<div class="text-[11px] text-[#8a8a8a]">All-time record</div>
<div class="text-[11px] text-text-sec">All-time record</div>
</div>
<div class="text-[24px] font-semibold text-white tabular-nums">
{stats?.bestStreak ?? 0}
@@ -413,13 +418,13 @@
<!-- F10: Next milestone -->
{#if nextMilestone()}
<div class="my-4 h-px bg-[#161616]"></div>
<div class="my-4 h-px bg-border"></div>
<div class="flex items-center justify-between">
<div>
<div class="text-[13px] text-white">Next milestone</div>
<div class="text-[11px] text-[#8a8a8a]">{nextMilestone()} day streak</div>
<div class="text-[11px] text-text-sec">{nextMilestone()} day streak</div>
</div>
<div class="text-[13px] text-[#8a8a8a] tabular-nums">
<div class="text-[13px] text-text-sec tabular-nums">
{nextMilestone()! - (stats?.currentStreak ?? 0)} days away
</div>
</div>
@@ -428,9 +433,9 @@
<!-- Weekly chart -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Last 7 Days
</h3>
</h2>
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
<canvas
@@ -458,7 +463,7 @@
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
Completed
@@ -470,35 +475,37 @@
</div>
</section>
</div>
{:else if activeTab === "weekly"}
<div role="tabpanel" id="tabpanel-weekly" aria-labelledby="tab-weekly">
<!-- Weekly summaries -->
{#each weeklySummaries as week, i}
{@const prevWeek = weeklySummaries[i + 1]}
{@const trend = prevWeek ? week.complianceRate - prevWeek.complianceRate : 0}
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: i * 0.06 }}>
<h3 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-3 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Week of {week.weekStart}
</h3>
</h2>
<div class="grid grid-cols-3 gap-3 mb-3">
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalCompleted}</div>
<div class="text-[10px] text-[#8a8a8a]">Completed</div>
<div class="text-[10px] text-text-sec">Completed</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{week.totalSkipped}</div>
<div class="text-[10px] text-[#8a8a8a]">Skipped</div>
<div class="text-[10px] text-text-sec">Skipped</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{Math.round(week.complianceRate * 100)}%
</div>
<div class="text-[10px] text-[#8a8a8a]">Compliance</div>
<div class="text-[10px] text-text-sec">Compliance</div>
</div>
</div>
<div class="flex items-center justify-between text-[11px]">
<span class="text-[#8a8a8a]">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
<span class="text-text-sec">Avg {week.avgDailyCompleted.toFixed(1)} breaks/day</span>
{#if prevWeek}
<span class="flex items-center gap-1"
style="color: {trend > 0 ? '#3fb950' : trend < 0 ? '#f85149' : '#8a8a8a'};"
@@ -517,12 +524,14 @@
</section>
{/each}
</div>
{:else}
<div role="tabpanel" id="tabpanel-monthly" aria-labelledby="tab-monthly">
<!-- Monthly: 30-day chart -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Last 30 Days
</h3>
</h2>
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
<canvas
@@ -532,7 +541,21 @@
aria-label="30-day break history chart"
></canvas>
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-[#8a8a8a]">
{#if monthHistory.length > 0}
<table class="sr-only">
<caption>Break history for the last {monthHistory.length} days</caption>
<thead>
<tr><th>Date</th><th>Completed</th><th>Skipped</th></tr>
</thead>
<tbody>
{#each monthHistory as day}
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td><td>{day.breaksSkipped}</td></tr>
{/each}
</tbody>
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-4 text-[10px] text-text-sec">
<div class="flex items-center gap-1.5">
<div class="h-2 w-2 rounded-sm" style="background: {$config.accent_color}"></div>
Completed
@@ -546,9 +569,9 @@
<!-- Heatmap -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.06 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Activity Heatmap
</h3>
</h2>
<div class="flex justify-center">
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
@@ -559,7 +582,21 @@
></canvas>
</div>
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-[#8a8a8a]">
{#if monthHistory.length > 0}
<table class="sr-only">
<caption>Activity heatmap for the last {monthHistory.length} days</caption>
<thead>
<tr><th>Date</th><th>Breaks completed</th></tr>
</thead>
<tbody>
{#each monthHistory as day}
<tr><td>{day.date}</td><td>{day.breaksCompleted}</td></tr>
{/each}
</tbody>
</table>
{/if}
<div class="mt-3 flex items-center justify-center gap-2 text-[10px] text-text-sec">
<span>Less</span>
<div class="flex gap-1">
<div class="w-3 h-3 rounded-sm" style="background: #161616;"></div>
@@ -574,35 +611,36 @@
<!-- Monthly totals -->
<section class="rounded-2xl p-5 backdrop-blur-xl" style="background: rgba(17,17,17,0.7);" use:inView={{ delay: 0.12 }}>
<h3 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-[#8a8a8a] uppercase">
<h2 class="mb-4 text-[11px] font-medium tracking-[0.15em] text-text-sec uppercase">
Monthly Summary
</h3>
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">{monthTotalCompleted}</div>
<div class="text-[10px] text-[#8a8a8a]">Total breaks</div>
<div class="text-[10px] text-text-sec">Total breaks</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold tabular-nums" style="color: {$config.accent_color}">
{monthAvgCompliance()}%
</div>
<div class="text-[10px] text-[#8a8a8a]">Avg compliance</div>
<div class="text-[10px] text-text-sec">Avg compliance</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">
{Math.floor(monthTotalTime / 60)} min
</div>
<div class="text-[10px] text-[#8a8a8a]">Total break time</div>
<div class="text-[10px] text-text-sec">Total break time</div>
</div>
<div class="text-center">
<div class="text-[22px] font-semibold text-white tabular-nums">
{(monthTotalCompleted / 30).toFixed(1)}
</div>
<div class="text-[10px] text-[#8a8a8a]">Avg daily breaks</div>
<div class="text-[10px] text-text-sec">Avg daily breaks</div>
</div>
</div>
</section>
</div>
{/if}
</div>

View File

@@ -6,7 +6,6 @@
<!-- Invisible drag region traffic lights on the right -->
<header
role="banner"
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"
>