diff --git a/package.json b/package.json
index 18f020de..e109adfe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homepage",
- "version": "1.47.4",
+ "version": "1.48.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/src/lib/components/fitness/ExerciseName.svelte b/src/lib/components/fitness/ExerciseName.svelte
index 6bc526f9..f1bc0d59 100644
--- a/src/lib/components/fitness/ExerciseName.svelte
+++ b/src/lib/components/fitness/ExerciseName.svelte
@@ -3,7 +3,7 @@
import { getEnrichedExerciseById } from '$lib/data/exercisedb';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
- let { exerciseId } = $props();
+ let { exerciseId, plain = false } = $props();
const lang = $derived(detectFitnessLang($page.url.pathname));
const exercise = $derived(getEnrichedExerciseById(exerciseId, lang));
@@ -11,7 +11,11 @@
{#if exercise}
- {exercise.localName}
+ {#if plain}
+ {exercise.localName}
+ {:else}
+ {exercise.localName}
+ {/if}
{:else}
Unknown Exercise
{/if}
@@ -25,6 +29,10 @@
.exercise-link:hover {
text-decoration: underline;
}
+ .exercise-plain {
+ color: inherit;
+ font: inherit;
+ }
.exercise-unknown {
color: var(--nord11);
font-style: italic;
diff --git a/src/lib/components/fitness/WorkoutFocusCard.svelte b/src/lib/components/fitness/WorkoutFocusCard.svelte
new file mode 100644
index 00000000..6db0b7c3
--- /dev/null
+++ b/src/lib/components/fitness/WorkoutFocusCard.svelte
@@ -0,0 +1,194 @@
+
+
+
+
+ {labels.exerciseOf(exerciseIndex + 1, totalExercises)}
+ {#if bodyPart}
+ ·
+ {bodyPart}
+ {/if}
+ {#if equipment}
+ ·
+ {equipment}
+ {/if}
+
+
+
+
+ {#if detailsHref}
+
+
+
+ {/if}
+
+
+
+
+ {allDone ? labels.done(totalSets) : labels.setOf(activeSetIdx + 1, totalSets)}
+
+
+ {#each sets as s, si (si)}
+
+ {/each}
+
+
+
+
+
diff --git a/src/lib/components/fitness/WorkoutRail.svelte b/src/lib/components/fitness/WorkoutRail.svelte
new file mode 100644
index 00000000..da3df68f
--- /dev/null
+++ b/src/lib/components/fitness/WorkoutRail.svelte
@@ -0,0 +1,687 @@
+
+
+
+
+
diff --git a/src/routes/fitness/+layout.svelte b/src/routes/fitness/+layout.svelte
index d00bf3da..d9a710d7 100644
--- a/src/routes/fitness/+layout.svelte
+++ b/src/routes/fitness/+layout.svelte
@@ -111,7 +111,7 @@
{/snippet}
-
+
{@render children()}
diff --git a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte
index f8410b17..10e9d92e 100644
--- a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte
+++ b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte
@@ -25,6 +25,7 @@
import { toast } from '$lib/js/toast.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
+ const isEn = $derived(lang === 'en');
const sl = $derived(fitnessSlugs(lang));
import { getWorkout } from '$lib/js/workout.svelte';
import { getWorkoutSync } from '$lib/js/workoutSync.svelte';
@@ -35,7 +36,8 @@
import { queueSession } from '$lib/offline/fitnessQueue';
import SetTable from '$lib/components/fitness/SetTable.svelte';
import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte';
- import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte';
+ import WorkoutRail from '$lib/components/fitness/WorkoutRail.svelte';
+ import WorkoutFocusCard from '$lib/components/fitness/WorkoutFocusCard.svelte';
import Toggle from '$lib/components/Toggle.svelte';
import { onMount } from 'svelte';
@@ -50,6 +52,82 @@
/** @type {Record
>>} */
let previousData = $state({});
+ /** User-pinned exercise index for the focus pane (null = auto-follow first incomplete) */
+ /** @type {number | null} */
+ let focusedIdx = $state(null);
+
+ const autoCurrentIdx = $derived.by(() => {
+ const exs = workout.exercises;
+ for (let i = 0; i < exs.length; i++) {
+ if (exs[i].sets.some((/** @type {any} */ s) => !s.completed)) return i;
+ }
+ return Math.max(0, exs.length - 1);
+ });
+
+ const activeIdx = $derived.by(() => {
+ const exs = workout.exercises;
+ if (exs.length === 0) return -1;
+ if (focusedIdx != null && focusedIdx >= 0 && focusedIdx < exs.length) return focusedIdx;
+ return autoCurrentIdx;
+ });
+
+ const activeExercise = $derived(activeIdx >= 0 ? workout.exercises[activeIdx] : null);
+ const activeExerciseMeta = $derived(activeExercise ? getExerciseById(activeExercise.exerciseId, lang) : null);
+
+ const activeSetIdx = $derived.by(() => {
+ const ex = activeExercise;
+ if (!ex || !ex.sets.length) return 0;
+ for (let i = 0; i < ex.sets.length; i++) {
+ if (!ex.sets[i].completed) return i;
+ }
+ return ex.sets.length;
+ });
+
+ const activeExDoneCount = $derived(activeExercise ? activeExercise.sets.filter((/** @type {any} */ s) => s.completed).length : 0);
+
+ const workoutSetsDone = $derived(
+ workout.exercises.reduce(
+ (/** @type {number} */ n, /** @type {any} */ ex) => n + ex.sets.filter((/** @type {any} */ s) => s.completed).length,
+ 0
+ )
+ );
+ const workoutSetsTotal = $derived(
+ workout.exercises.reduce((/** @type {number} */ n, /** @type {any} */ ex) => n + ex.sets.length, 0)
+ );
+
+ /** @param {number} idx */
+ function setFocus(idx) { focusedIdx = idx; }
+
+ /**
+ * Reorder an exercise from one index to another, adjusting focus to follow.
+ * @param {number} fromIdx
+ * @param {number} toIdx
+ */
+ function reorderExercise(fromIdx, toIdx) {
+ if (fromIdx === toIdx) return;
+ const dir = fromIdx < toIdx ? 1 : -1;
+ let i = fromIdx;
+ while (i !== toIdx) {
+ workout.moveExercise(i, dir);
+ i += dir;
+ }
+ // Track the moved exercise if it was focused
+ if (focusedIdx === fromIdx) {
+ focusedIdx = toIdx;
+ } else if (focusedIdx != null) {
+ if (fromIdx < focusedIdx && toIdx >= focusedIdx) focusedIdx = focusedIdx - 1;
+ else if (fromIdx > focusedIdx && toIdx <= focusedIdx) focusedIdx = focusedIdx + 1;
+ }
+ }
+
+ /** @param {number} idx */
+ function removeExerciseFromRail(idx) {
+ workout.removeExercise(idx);
+ // Unpin focus so auto-current takes over (handles removing the focused one)
+ if (focusedIdx === idx) focusedIdx = null;
+ else if (focusedIdx != null && idx < focusedIdx) focusedIdx = focusedIdx - 1;
+ }
+
/** @type {any} */
let completionData = $state(null);
@@ -1436,15 +1514,17 @@
{:else if workout.active}
-
{ nameEditing = true; }}
- onblur={() => { nameEditing = false; workout.name = nameInput; }}
- onkeydown={(e) => { if (e.key === 'Enter' && e.target instanceof HTMLElement) e.target.blur(); }}
- placeholder={t('workout_name_placeholder', lang)}
- />
+ {#snippet workoutTitle()}
+
{ nameEditing = true; }}
+ onblur={() => { nameEditing = false; workout.name = nameInput; }}
+ onkeydown={(e) => { if (e.key === 'Enter' && e.target instanceof HTMLElement) e.target.blur(); }}
+ placeholder={t('workout_name_placeholder', lang)}
+ />
+ {/snippet}
{#if gps.available && hasCardioExercise()}
@@ -1555,89 +1635,105 @@
{/if}
- {#each workout.exercises as ex, exIdx (exIdx)}
- {@const exMetrics = getExerciseMetrics(getExerciseById(ex.exerciseId))}
- {@const isDurationOnly = exMetrics.includes('duration') && !exMetrics.includes('weight') && !exMetrics.includes('reps')}
-
-