Refine swipe affordance and sleep bar

This commit is contained in:
2026-05-21 13:07:03 +02:00
parent a087eb508b
commit f5daff1a04
3 changed files with 77 additions and 34 deletions
+48 -6
View File
@@ -477,6 +477,7 @@ body.page-dashboard .content {
z-index: 0;
display: inline-flex;
align-items: center;
gap: 0.48rem;
min-height: 3rem;
max-width: min(12rem, 42vw);
padding: 0.8rem 1rem;
@@ -494,6 +495,22 @@ body.page-dashboard .content {
transition: opacity 120ms ease, transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.day-slide-hint__arrow {
width: 0.8rem;
height: 0.8rem;
border-top: 2px solid currentColor;
border-left: 2px solid currentColor;
opacity: 0.9;
}
.day-slide-hint--prev .day-slide-hint__arrow {
transform: rotate(-45deg);
}
.day-slide-hint--next .day-slide-hint__arrow {
transform: rotate(135deg);
}
.day-slide-hint--prev {
left: 0.75rem;
opacity: var(--day-prev-hint);
@@ -523,6 +540,7 @@ body.page-dashboard .content {
position: relative;
z-index: 1;
margin-bottom: 0;
touch-action: pan-y;
transform: translate3d(var(--day-slider-offset, 0), 0, 0) scale(var(--day-slider-scale, 1));
transition: transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform;
@@ -978,7 +996,7 @@ body.page-dashboard .content {
.sleep-phase-bar {
position: relative;
min-height: 2.35rem;
height: 2.35rem;
margin-top: 1.55rem;
overflow: visible;
border-radius: 999px;
@@ -986,6 +1004,17 @@ body.page-dashboard .content {
background: rgba(255, 255, 255, 0.08);
}
.sleep-phase-bar__fill {
position: absolute;
inset: 0 auto 0 0;
display: flex;
width: var(--sleep-actual-width, 0);
min-width: 0.18rem;
overflow: hidden;
border-radius: inherit;
box-shadow: 0 12px 28px rgba(6, 16, 28, 0.2);
}
.sleep-phase-bar__target {
position: absolute;
left: min(var(--sleep-optimal-left, 100%), calc(100% - 2px));
@@ -1012,11 +1041,10 @@ body.page-dashboard .content {
.sleep-phase-bar__segment {
box-sizing: border-box;
position: absolute;
left: var(--sleep-segment-left, 0);
top: 0;
bottom: 0;
position: relative;
width: var(--sleep-segment-width, auto);
height: 100%;
flex: 0 0 var(--sleep-segment-width, auto);
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1092,10 +1120,24 @@ body.page-dashboard .content {
background: rgba(255, 255, 255, 0.1);
}
.sleep-phase-bar__segment:last-of-type {
.sleep-phase-bar__fill .sleep-phase-bar__segment:last-child {
border-radius: 0 999px 999px 0;
}
.sleep-phase-bar__fill .sleep-phase-bar__segment:first-child {
border-radius: 999px 0 0 999px;
}
.sleep-phase-bar__rest-label {
position: absolute;
right: 0.65rem;
top: 50%;
color: rgba(255, 255, 255, 0.52);
font-size: 0.68rem;
transform: translateY(-50%);
pointer-events: none;
}
.media-lightbox[hidden] {
display: none;
}
+4 -2
View File
@@ -1512,7 +1512,9 @@
};
const handleSwipe = (deltaX, deltaY) => {
if (document.body.classList.contains("is-dashboard-overlay-open") || Math.abs(deltaX) < 70 || Math.abs(deltaY) > 50) {
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
if (document.body.classList.contains("is-dashboard-overlay-open") || absX < 55 || absY > Math.max(120, absX * 1.15)) {
resetStrip();
return;
}
@@ -1553,7 +1555,7 @@
const deltaX = event.clientX - pointerStartX;
const deltaY = event.clientY - pointerStartY;
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 20) {
if (Math.abs(deltaY) > Math.max(52, Math.abs(deltaX) * 1.35)) {
dragging = false;
activePointerId = null;
resetStrip();
+25 -26
View File
@@ -56,8 +56,8 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
<?php if ($dashboardView === 'day'): ?>
<div class="dashboard-day" data-day-swipe data-prev-date="<?= e($dashboardPrevDate) ?>" data-next-date="<?= e($dashboardNextDate) ?>">
<div class="dashboard-day-slider" data-day-slider-shell>
<span class="day-slide-hint day-slide-hint--prev" data-day-slide-prev-hint>Vorherigen Tag laden</span>
<span class="day-slide-hint day-slide-hint--next" data-day-slide-next-hint>Nächster Tag laden</span>
<span class="day-slide-hint day-slide-hint--prev" data-day-slide-prev-hint><span class="day-slide-hint__arrow" aria-hidden="true"></span>Vorherigen Tag laden</span>
<span class="day-slide-hint day-slide-hint--next" data-day-slide-next-hint>Nächster Tag laden<span class="day-slide-hint__arrow" aria-hidden="true"></span></span>
<div class="dashboard-day__hero" data-day-slider>
<p class="dashboard-day__eyebrow"><?= e((string) $dayWeekday) ?></p>
<h1><?= e(format_display_date((string) $dayEntry['date'], false)) ?></h1>
@@ -149,6 +149,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
$sleepUnclassified = max(0.0, $sleepActualTotal - $sleepPhaseTotal);
$sleepPhaseRemainder = max(0.0, $sleepBarTotal - $sleepActualTotal);
$sleepOptimalPercent = $sleepBarTotal > 0 ? max(0, min(100, ($optimalSleepHours / $sleepBarTotal) * 100)) : 0;
$sleepActualPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepActualTotal / $sleepBarTotal) * 100)) : 0;
$sleepPhaseLeft = 0.0;
?>
<?php $eventStats = array_values(array_filter([
@@ -219,32 +220,30 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
<?php endif; ?>
<?php if ($eventType === 'sleep' && $sleepActualTotal > 0): ?>
<div class="sleep-phase-bar" aria-label="Schlafphasen" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%">
<?php if ($sleepPhaseTotal > 0): ?>
<?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?>
<?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?>
<?php if ($phaseHours <= 0) { continue; } ?>
<?php $phasePercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepBarTotal) * 100)) : 0; ?>
<?php $phaseLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseLeft / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--<?= e($class) ?><?= $phasePercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-left: <?= e((string) $phaseLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $phasePercent) ?>%" title="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>" data-tooltip="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>">
<strong><?= e($label) ?></strong> <?= e(format_duration_hours($phaseHours)) ?>
<div class="sleep-phase-bar" aria-label="Schlafphasen" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%; --sleep-actual-width: <?= e((string) $sleepActualPercent) ?>%">
<div class="sleep-phase-bar__fill">
<?php if ($sleepPhaseTotal > 0): ?>
<?php foreach (['deep' => ['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?>
<?php $phaseHours = max(0.0, (float) ($sleepPhases[$phase] ?? 0)); ?>
<?php if ($phaseHours <= 0) { continue; } ?>
<?php $phasePercent = $sleepActualTotal > 0 ? max(0.5, min(100, ($phaseHours / $sleepActualTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--<?= e($class) ?><?= $phasePercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-width: <?= e((string) $phasePercent) ?>%" title="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>" data-tooltip="<?= e($label) ?>: <?= e(format_duration_hours($phaseHours)) ?>">
<strong><?= e($label) ?></strong> <?= e(format_duration_hours($phaseHours)) ?>
</span>
<?php $sleepPhaseLeft += $phaseHours; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($sleepUnclassified > 0): ?>
<?php $unclassifiedPercent = $sleepActualTotal > 0 ? max(0.5, min(100, ($sleepUnclassified / $sleepActualTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--total<?= $sleepPhaseTotal > 0 ? ' is-after-phase' : '' ?><?= $unclassifiedPercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-width: <?= e((string) $unclassifiedPercent) ?>%" title="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>" data-tooltip="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>">
<strong>Schlaf</strong> <?= e(format_duration_hours($sleepUnclassified)) ?>
</span>
<?php $sleepPhaseLeft += $phaseHours; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($sleepUnclassified > 0): ?>
<?php $unclassifiedPercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($sleepUnclassified / $sleepBarTotal) * 100)) : 0; ?>
<?php $unclassifiedLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepPhaseTotal / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--total<?= $sleepPhaseTotal > 0 ? ' is-after-phase' : '' ?><?= $unclassifiedPercent < 13 ? ' is-compact' : '' ?>" style="--sleep-segment-left: <?= e((string) $unclassifiedLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $unclassifiedPercent) ?>%" title="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>" data-tooltip="Schlaf: <?= e(format_duration_hours($sleepUnclassified)) ?>">
<strong>Schlaf</strong> <?= e(format_duration_hours($sleepUnclassified)) ?>
</span>
<?php endif; ?>
<?php if ($sleepPhaseRemainder > 0): ?>
<?php $remainderPercent = $sleepBarTotal > 0 ? max(0.5, min(100, ($sleepPhaseRemainder / $sleepBarTotal) * 100)) : 0; ?>
<?php $remainderLeftPercent = $sleepBarTotal > 0 ? max(0, min(100, ($sleepActualTotal / $sleepBarTotal) * 100)) : 0; ?>
<span class="sleep-phase-bar__segment sleep-phase-bar__segment--rest" style="--sleep-segment-left: <?= e((string) $remainderLeftPercent) ?>%; --sleep-segment-width: <?= e((string) $remainderPercent) ?>%" title="Bis Ziel-/Skalenende: <?= e(format_duration_hours($sleepPhaseRemainder)) ?>" data-tooltip="Bis Ziel-/Skalenende: <?= e(format_duration_hours($sleepPhaseRemainder)) ?>"></span>
<?php endif; ?>
<?php endif; ?>
</div>
<span class="sleep-phase-bar__target"><span><?= e(format_duration_hours($optimalSleepHours)) ?></span></span>
<?php if ($sleepPhaseRemainder > 0): ?>
<span class="sleep-phase-bar__rest-label">noch <?= e(format_duration_hours($sleepPhaseRemainder)) ?> bis Skalenende</span>
<?php endif; ?>
</div>
<?php endif; ?>