fix(fitness): hoist rest timer above set table, persist across exercise switches

Rest timer was inlined as a row inside SetTable, tied to a specific set.
Switching to another exercise hid it from view. Now lives below the
exercise focus card, renders whenever a rest is active regardless of
focused exercise, and labels which set/exercise it belongs to when
looking at a different one.
This commit is contained in:
2026-05-10 14:17:36 +02:00
parent e59e9679da
commit eeed31aaf4
4 changed files with 105 additions and 30 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.69.2", "version": "1.69.3",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -0,0 +1,89 @@
<script>
import RestTimer from './RestTimer.svelte';
import ExerciseName from './ExerciseName.svelte';
import { page } from '$app/state';
import { detectFitnessLang } from '$lib/js/fitnessI18n';
/**
* @type {{
* active: boolean,
* seconds: number,
* total: number,
* exerciseId?: string | null,
* setIdx: number,
* activeExerciseIdx: number,
* restExerciseIdx: number,
* onAdjust?: ((delta: number) => void) | null,
* onSkip?: (() => void) | null
* }}
*/
let {
active,
seconds,
total,
exerciseId = null,
setIdx,
activeExerciseIdx,
restExerciseIdx,
onAdjust = null,
onSkip = null
} = $props();
const lang = $derived(detectFitnessLang(page.url.pathname));
const isEn = $derived(lang === 'en');
const isOtherExercise = $derived(restExerciseIdx >= 0 && restExerciseIdx !== activeExerciseIdx);
const setLabel = $derived(setIdx >= 0
? (isEn ? `Rest · Set ${setIdx + 1}` : `Pause · Satz ${setIdx + 1}`)
: (isEn ? 'Rest' : 'Pause'));
</script>
{#if active && total > 0}
<section class="active-rest" aria-live="polite">
<header class="rest-context">
<span class="rest-label">{setLabel}</span>
{#if isOtherExercise && exerciseId}
<span class="rest-sep" aria-hidden="true">·</span>
<span class="rest-exercise"><ExerciseName {exerciseId} plain /></span>
{/if}
</header>
<RestTimer
{seconds}
{total}
onComplete={onSkip}
{onAdjust}
{onSkip}
/>
</section>
{/if}
<style>
.active-rest {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.rest-context {
display: flex;
align-items: baseline;
gap: 0.4rem;
flex-wrap: wrap;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-tertiary);
}
.rest-label {
color: var(--blue);
}
.rest-sep {
color: var(--color-text-tertiary);
}
.rest-exercise {
color: var(--color-text-secondary);
text-transform: none;
letter-spacing: 0;
font-weight: 600;
font-size: 0.78rem;
}
</style>
@@ -4,7 +4,6 @@
import Play from '@lucide/svelte/icons/play'; import Play from '@lucide/svelte/icons/play';
import Square from '@lucide/svelte/icons/square'; import Square from '@lucide/svelte/icons/square';
import { METRIC_LABELS } from '$lib/data/exercises'; import { METRIC_LABELS } from '$lib/data/exercises';
import RestTimer from './RestTimer.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { detectFitnessLang, m } from '$lib/js/fitnessI18n'; import { detectFitnessLang, m } from '$lib/js/fitnessI18n';
@@ -17,14 +16,9 @@
* previousSets?: Array<Record<string, any>> | null, * previousSets?: Array<Record<string, any>> | null,
* metrics?: Array<'weight' | 'reps' | 'rpe' | 'distance' | 'duration'>, * metrics?: Array<'weight' | 'reps' | 'rpe' | 'distance' | 'duration'>,
* editable?: boolean, * editable?: boolean,
* restAfterSet?: number,
* restSeconds?: number,
* restTotal?: number,
* holdAfterSet?: number, * holdAfterSet?: number,
* holdSeconds?: number, * holdSeconds?: number,
* holdTotal?: number, * holdTotal?: number,
* onRestAdjust?: ((delta: number) => void) | null,
* onRestSkip?: (() => void) | null,
* timedHold?: boolean, * timedHold?: boolean,
* onHoldSkip?: (() => void) | null, * onHoldSkip?: (() => void) | null,
* onUpdate?: ((setIndex: number, data: Record<string, number | null>) => void) | null, * onUpdate?: ((setIndex: number, data: Record<string, number | null>) => void) | null,
@@ -37,15 +31,10 @@
previousSets = null, previousSets = null,
metrics = ['weight', 'reps', 'rpe'], metrics = ['weight', 'reps', 'rpe'],
editable = false, editable = false,
restAfterSet = -1,
restSeconds = 0,
restTotal = 0,
timedHold = false, timedHold = false,
holdAfterSet = -1, holdAfterSet = -1,
holdSeconds = 0, holdSeconds = 0,
holdTotal = 0, holdTotal = 0,
onRestAdjust = null,
onRestSkip = null,
onHoldSkip = null, onHoldSkip = null,
onUpdate = null, onUpdate = null,
onToggleComplete = null, onToggleComplete = null,
@@ -215,19 +204,6 @@
</td> </td>
</tr> </tr>
{/if} {/if}
{#if restAfterSet === i && restTotal > 0}
<tr class="rest-row">
<td colspan={totalCols} class="rest-cell">
<RestTimer
seconds={restSeconds}
total={restTotal}
onComplete={onRestSkip}
onAdjust={onRestAdjust}
onSkip={onRestSkip}
/>
</td>
</tr>
{/if}
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -40,6 +40,7 @@
import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte'; import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte';
import WorkoutRail from '$lib/components/fitness/WorkoutRail.svelte'; import WorkoutRail from '$lib/components/fitness/WorkoutRail.svelte';
import WorkoutFocusCard from '$lib/components/fitness/WorkoutFocusCard.svelte'; import WorkoutFocusCard from '$lib/components/fitness/WorkoutFocusCard.svelte';
import ActiveRestTimer from '$lib/components/fitness/ActiveRestTimer.svelte';
import Toggle from '$lib/components/Toggle.svelte'; import Toggle from '$lib/components/Toggle.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -1681,6 +1682,20 @@
}} }}
/> />
<ActiveRestTimer
active={workout.restTimerActive}
seconds={workout.restTimerSeconds}
total={workout.restTimerTotal}
exerciseId={workout.restExerciseIdx >= 0 && workout.exercises[workout.restExerciseIdx]
? workout.exercises[workout.restExerciseIdx].exerciseId
: null}
setIdx={workout.restSetIdx}
activeExerciseIdx={activeIdx}
restExerciseIdx={workout.restExerciseIdx}
onAdjust={(delta) => workout.adjustRestTimer(delta)}
onSkip={cancelRest}
/>
<div class="exercise-block focused"> <div class="exercise-block focused">
<SetTable <SetTable
sets={activeExercise.sets} sets={activeExercise.sets}
@@ -1688,14 +1703,9 @@
metrics={exMetrics} metrics={exMetrics}
editable={true} editable={true}
timedHold={isDurationOnly} timedHold={isDurationOnly}
restAfterSet={workout.restTimerActive && workout.restExerciseIdx === activeIdx ? workout.restSetIdx : -1}
restSeconds={workout.restTimerSeconds}
restTotal={workout.restTimerTotal}
holdAfterSet={workout.holdTimerActive && workout.holdExerciseIdx === activeIdx ? workout.holdSetIdx : -1} holdAfterSet={workout.holdTimerActive && workout.holdExerciseIdx === activeIdx ? workout.holdSetIdx : -1}
holdSeconds={workout.holdTimerSeconds} holdSeconds={workout.holdTimerSeconds}
holdTotal={workout.holdTimerTotal} holdTotal={workout.holdTimerTotal}
onRestAdjust={(delta) => workout.adjustRestTimer(delta)}
onRestSkip={cancelRest}
onHoldSkip={() => workout.cancelHoldTimer()} onHoldSkip={() => workout.cancelHoldTimer()}
onUpdate={(setIdx, d) => workout.updateSet(activeIdx, setIdx, d)} onUpdate={(setIdx, d) => workout.updateSet(activeIdx, setIdx, d)}
onToggleComplete={(setIdx) => { onToggleComplete={(setIdx) => {