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 breathCountdown = $state(4);
let breathScale = $state(0.6); 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 // 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)); const textScale = $derived(0.9 + (breathScale - 0.6) * (0.7 / 0.4));
@@ -169,11 +181,11 @@
<span <span
class="block mt-1.5 text-[9px] tracking-wider uppercase text-center font-medium" 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;" 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-hidden="true"
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
> >
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""} {breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
</span> </span>
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
{/if} {/if}
</div> </div>
</TimerRing> </TimerRing>
@@ -185,16 +197,16 @@
<h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1"> <h2 class="text-[17px] font-medium text-white mb-1.5" tabindex="-1">
{$timer.breakTitle} {$timer.breakTitle}
</h2> </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} {$timer.breakMessage}
</p> </p>
{#if $config.show_break_activities} {#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="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)} {getCategoryLabel(currentActivity.category)}
</div> </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} {currentActivity.text}
</p> </p>
</div> </div>
@@ -205,7 +217,7 @@
<button <button
use:pressable use:pressable
class="rounded-full border border-[#333] px-5 py-2 text-[11px] 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 transition-colors duration-200
hover:border-[#444] hover:text-[#ccc]" hover:border-[#444] hover:text-[#ccc]"
onclick={cancelBreak} onclick={cancelBreak}
@@ -226,10 +238,15 @@
{/if} {/if}
</div> </div>
{#if $config.snooze_limit > 0} {#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 {$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
</p> </p>
{/if} {/if}
{:else}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span tabindex="0" class="sr-only" aria-live="polite">
Break in progress, please wait
</span>
{/if} {/if}
</div> </div>
@@ -302,11 +319,11 @@
class:text-[10px]={!isModal} class:text-[10px]={!isModal}
class:text-[9px]={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;" 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-hidden="true"
aria-label="Breathing guide: {breathPhase} {breathCountdown > 0 ? breathCountdown + ' seconds' : ''}"
> >
{breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""} {breathPhase}{breathCountdown > 0 ? ` ${breathCountdown}s` : ""}
</span> </span>
<span class="sr-only" aria-live="polite" aria-atomic="true">{breathAnnouncement}</span>
{/if} {/if}
</div> </div>
</TimerRing> </TimerRing>
@@ -328,7 +345,7 @@
</h2> </h2>
<p <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-4={$config.show_break_activities}
class:mb-8={!$config.show_break_activities} class:mb-8={!$config.show_break_activities}
use:fadeIn={{ delay: 0.35, y: 10 }} 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" 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 }} 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)} {getCategoryLabel(currentActivity.category)}
</div> </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} {currentActivity.text}
</p> </p>
</div> </div>
@@ -354,8 +371,8 @@
<div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}> <div class="flex items-center gap-3" use:fadeIn={{ delay: 0.45, y: 10 }}>
<button <button
use:pressable use:pressable
class="rounded-full border border-[#222] px-6 py-2.5 text-[12px] class="rounded-full border border-border px-6 py-2.5 text-[12px]
tracking-wider text-[#8a8a8a] uppercase tracking-wider text-text-sec uppercase
transition-colors duration-200 transition-colors duration-200
hover:border-[#333] hover:text-[#ccc]" hover:border-[#333] hover:text-[#ccc]"
onclick={cancelBreak} onclick={cancelBreak}
@@ -376,10 +393,15 @@
{/if} {/if}
</div> </div>
{#if $config.snooze_limit > 0} {#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 {$timer.snoozesUsed}/{$config.snooze_limit} snoozes used
</p> </p>
{/if} {/if}
{:else}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span tabindex="0" class="sr-only" aria-live="polite">
Break in progress, please wait
</span>
{/if} {/if}
<!-- Bottom progress bar for modal --> <!-- Bottom progress bar for modal -->

View File

@@ -2,10 +2,99 @@
import { milestoneEvent, dailyGoalEvent } from "../stores/timer"; import { milestoneEvent, dailyGoalEvent } from "../stores/timer";
import { config } from "../stores/config"; import { config } from "../stores/config";
const showMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations); const storeMilestone = $derived($milestoneEvent !== null && $config.milestone_celebrations);
const showGoal = $derived($dailyGoalEvent && $config.milestone_celebrations); const storeGoal = $derived($dailyGoalEvent && $config.milestone_celebrations);
const streakDays = $derived($milestoneEvent ?? 0); 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 // Generate confetti particles on milestone
const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"]; const confettiColors = ["#ff4d00", "#7c6aef", "#3fb950", "#fca311", "#f72585", "#4361ee"];
const confettiParticles = $derived( const confettiParticles = $derived(
@@ -23,7 +112,27 @@
</script> </script>
{#if showMilestone} {#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 --> <!-- Confetti burst -->
<div class="confetti-container"> <div class="confetti-container">
{#each confettiParticles as p (p.id)} {#each confettiParticles as p (p.id)}
@@ -51,12 +160,31 @@
{/if} {/if}
{#if showGoal && !showMilestone} {#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"> <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"> <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"/> <path d="M20 6L9 17l-5-5"/>
</svg> </svg>
<span class="text-[14px] font-medium text-[#3fb950] ml-2">Daily goal reached!</span> <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>
</div> </div>
{/if} {/if}
@@ -69,13 +197,18 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 9999; z-index: 9999;
pointer-events: none; opacity: 1;
animation: celebration-fade 3.5s ease forwards; transition: opacity 0.6s ease;
animation: celebration-enter 0.3s ease forwards;
} }
@keyframes celebration-fade { .celebration-overlay.fading {
0%, 70% { opacity: 1; } opacity: 0;
100% { opacity: 0; } }
@keyframes celebration-enter {
0% { opacity: 0; }
100% { opacity: 1; }
} }
.confetti-container { .confetti-container {
@@ -130,15 +263,18 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 9999; z-index: 9999;
pointer-events: none; opacity: 1;
animation: goal-slide 3.5s ease forwards; 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; } 0% { transform: translateX(-50%) translateY(-20px); opacity: 0; }
10% { transform: translateX(-50%) translateY(0); opacity: 1; } 100% { transform: translateX(-50%) translateY(0); opacity: 1; }
75% { opacity: 1; }
100% { transform: translateX(-50%) translateY(-10px); opacity: 0; }
} }
.goal-badge { .goal-badge {
@@ -158,6 +294,11 @@
.goal-overlay { .goal-overlay {
animation: none; animation: none;
opacity: 1; opacity: 1;
transition: none;
}
.celebration-overlay.fading,
.goal-overlay.fading {
opacity: 0;
} }
.confetti-particle { .confetti-particle {
display: none; display: none;

View File

@@ -112,6 +112,7 @@
// Natural break notification // Natural break notification
let showNaturalBreakToast = $state(false); let showNaturalBreakToast = $state(false);
let toastHovering = $state(false);
let naturalBreakToastTimeout: ReturnType<typeof setTimeout> | null = null; let naturalBreakToastTimeout: ReturnType<typeof setTimeout> | null = null;
// Watch for natural break detection // Watch for natural break detection
@@ -120,7 +121,9 @@
showNaturalBreakToast = true; showNaturalBreakToast = true;
if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout); if (naturalBreakToastTimeout) clearTimeout(naturalBreakToastTimeout);
naturalBreakToastTimeout = setTimeout(() => { naturalBreakToastTimeout = setTimeout(() => {
if (!toastHovering) {
showNaturalBreakToast = false; showNaturalBreakToast = false;
}
}, 5000); }, 5000);
} }
}); });
@@ -177,7 +180,7 @@
<!-- Status label --> <!-- Status label -->
<span <span
class="block text-center text-[11px] font-medium tracking-[0.25em]" 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-warning={$timer.prebreakWarning}
class:text-[#fca311]={$timer.deferredBreakPending} class:text-[#fca311]={$timer.deferredBreakPending}
> >
@@ -205,15 +208,16 @@
></div> ></div>
{/each} {/each}
</div> </div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums"> <span class="text-[9px] text-text-sec tabular-nums">
{$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle} {$timer.pomodoroCyclePosition + 1}/{$timer.pomodoroTotalInCycle}
</span> </span>
</div> </div>
<span class="sr-only">Pomodoro cycle: session {$timer.pomodoroCyclePosition + 1} of {$timer.pomodoroTotalInCycle}</span>
{/if} {/if}
<!-- Microbreak countdown --> <!-- Microbreak countdown -->
{#if $timer.microbreakEnabled && !$timer.microbreakActive && $timer.state === "running"} {#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"> <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"/> <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"/> <circle cx="12" cy="12" r="3"/>
@@ -231,14 +235,22 @@
</svg> </svg>
<span class="text-[9px] text-[#3fb950]">Goal met</span> <span class="text-[9px] text-[#3fb950]">Goal met</span>
{:else} {:else}
<span class="text-[9px] text-[#8a8a8a]">Goal</span> <span class="text-[9px] text-text-sec">Goal</span>
<div class="w-16 h-[2px] rounded-full overflow-hidden" style="background: #161616;"> <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 <div
class="h-full rounded-full transition-[width] duration-500" class="h-full rounded-full transition-[width] duration-500"
style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};" style="width: {Math.min(100, (dailyGoalProgress / $config.daily_goal_breaks) * 100)}%; background: {$config.accent_color};"
></div> ></div>
</div> </div>
<span class="text-[9px] text-[#8a8a8a] tabular-nums"> <span class="text-[9px] text-text-sec tabular-nums">
{dailyGoalProgress}/{$config.daily_goal_breaks} {dailyGoalProgress}/{$config.daily_goal_breaks}
</span> </span>
{/if} {/if}
@@ -253,7 +265,7 @@
<!-- Last break info --> <!-- Last break info -->
<div use:fadeIn={{ delay: 0.4, y: 10 }}> <div use:fadeIn={{ delay: 0.4, y: 10 }}>
{#if $timer.hasHadBreak} {#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)} Last break {formatDurationAgo($timer.secondsSinceLastBreak)}
</p> </p>
{:else} {:else}
@@ -263,17 +275,24 @@
<!-- Natural break notification toast --> <!-- Natural break notification toast -->
{#if showNaturalBreakToast} {#if showNaturalBreakToast}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div <div
role="alert" 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" 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);" style="border-color: rgba(63, 185, 80, 0.3); background: rgba(63, 185, 80, 0.1);"
use:scaleIn={{ duration: 0.3, delay: 0 }} 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"> <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"> <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"/> <path d="M20 6L9 17l-5-5"/>
</svg> </svg>
<span class="text-[13px] text-[#3fb950]">Natural break detected - timer reset</span> <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>
</div> </div>
{/if} {/if}
@@ -292,13 +311,15 @@
{toggleBtnText} {toggleBtnText}
</button> </button>
<!-- Bottom navigation buttons -->
<nav aria-label="Main actions" class="contents">
<!-- Bottom left: start break now --> <!-- Bottom left: start break now -->
<div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}> <div class="absolute bottom-5 left-5" use:fadeIn={{ delay: 0.5, y: 8 }}>
<button <button
aria-label="Start break now" aria-label="Start break now"
use:pressable use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a] border border-border text-text-sec
transition-colors duration-200 transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]" hover:border-[#333] hover:text-[#aaa]"
onclick={startBreakNow} onclick={startBreakNow}
@@ -325,7 +346,7 @@
aria-label="Statistics" aria-label="Statistics"
use:pressable use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a] border border-border text-text-sec
transition-colors duration-200 transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]" hover:border-[#333] hover:text-[#aaa]"
onclick={() => { onclick={() => {
@@ -357,7 +378,7 @@
aria-label="Settings" aria-label="Settings"
use:pressable use:pressable
class="flex h-11 w-11 items-center justify-center rounded-full class="flex h-11 w-11 items-center justify-center rounded-full
border border-[#222] text-[#8a8a8a] border border-border text-text-sec
transition-colors duration-200 transition-colors duration-200
hover:border-[#333] hover:text-[#aaa]" hover:border-[#333] hover:text-[#aaa]"
onclick={openSettings} onclick={openSettings}
@@ -380,6 +401,7 @@
</svg> </svg>
</button> </button>
</div> </div>
</nav>
</div> </div>
<style> <style>

View File

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

View File

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

View File

@@ -6,7 +6,6 @@
<!-- Invisible drag region traffic lights on the right --> <!-- Invisible drag region traffic lights on the right -->
<header <header
role="banner"
data-tauri-drag-region 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" class="group absolute top-0 left-0 right-0 z-50 flex h-10 items-center justify-end pr-3.5 select-none"
> >