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:
@@ -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.6–1.0 scale to 0.9–1.6 range for visible breathing text
|
// Map raw 0.6–1.0 scale to 0.9–1.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 -->
|
||||||
|
|||||||
@@ -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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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;
|
||||||
|
|||||||
@@ -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">×</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user