feat(fitness/body-parts): "Same as last" button + larger Copy L→R pill

- New "Same as last" pill below each step's stepper. Clicking fills
  the input(s) with the prior recorded value(s) — for paired steps
  in split mode, both L and R — and advances to the next step.
  Only rendered when a previous measurement exists; the placeholder
  already surfaces the exact number so the button text stays terse.
- Copy L→R button resized to match the same-as-last pill (0.88 rem
  text, 0.55 × 1.1 rem padding) and given top margin. Unicode →
  swapped for a proper ArrowRight icon between L and R.
- i18n: added `same_as_last` and split `copy_l_to_r` into
  `copy_l_to_r_before` / `copy_l_to_r_after` so each language keeps
  its natural wrapping around the arrow (EN "Copy L / R",
  DE "L / R übernehmen").
This commit is contained in:
2026-04-23 13:45:16 +02:00
parent 91e1efda6f
commit 8611275bca
4 changed files with 65 additions and 8 deletions
+2 -1
View File
@@ -4,9 +4,10 @@
[x] on /fitness/measure, fill "Past measurements" in SSR only for the last 10 measurements. anything further should be fetched client side on mount to decreae initial page load time. use a "show more" button and paginate measurments.
[x] on /fitness/measure (resp. their associated logging API routes), consolidate measurements by day. If we want to log another measurement, overwriting an old one, show a warning to indicate this. disparate measurements (e.g., weight and bodyfat) should not show this warning but simply be merged into one log entry for that day.
[x] on /fitness/measure in the past measurments tab, show more than "Body measurements only" if we don't have Bodyweight logged. we can be a bit more elaborate in our syntax here tbh.
[ ] add a button on /fitness/measure/body-parts for each measurement directly below to say "Same value", instead of having to hit +, then - to lock in same number
[x] add a button on /fitness/measure/body-parts for each measurement directly below to say "Same value", instead of having to hit +, then - to lock in same number
[ ] BF graph (with trend line like weight graph) on /fitness/stats page. Emphasize relative changes, not absolute numbers in design (as we cannot trust those) (e.g., use start day of overview as 0% and then show +/- x % on the graph)
[ ] Workshop better names than "Measure" for the /fitness/measure route. It's about body data points (i.e., non-food related). What's a better, short name than "Measure" to capture the logging of weight, body composition, body part measurements, and period tracking?
[ ] on /fitness/stats/histoy/<part> for body measurement graphs, make the range reasonable. e.g., if we have 1 cm change, do not fill the entire y-height with 1 cm. Use reasonable padding for low ranges (i think we do something like htis already on the weight graph?)
## Refactor Recipe Search Component
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.46.5",
"version": "1.46.6",
"private": true,
"type": "module",
"scripts": {
+3
View File
@@ -292,6 +292,8 @@ const translations: Translations = {
exit: { en: 'Exit', de: 'Schlie\u00dfen' },
same_both_sides: { en: 'Same on both sides', de: 'Auf beiden Seiten gleich' },
copy_l_to_r: { en: 'Copy L \u2192 R', de: 'L \u2192 R \u00fcbernehmen' },
copy_l_to_r_before: { en: 'Copy L', de: 'L' },
copy_l_to_r_after: { en: 'R', de: 'R \u00fcbernehmen' },
kbd_nav: { en: 'nav', de: 'Navigation' },
kbd_next: { en: 'next', de: 'weiter' },
kbd_skip: { en: 'skip', de: 'auslassen' },
@@ -319,6 +321,7 @@ const translations: Translations = {
de: 'Für dieses Datum sind bereits Werte erfasst: {fields}. Überschreiben?'
},
overwrite_confirm: { en: 'Overwrite', de: 'Überschreiben' },
same_as_last: { en: 'Same as last', de: 'Wie zuletzt' },
// SetTable
set_header: { en: 'SET', de: 'SATZ' },
@@ -1,7 +1,7 @@
<script>
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Minus, Plus, X, ArrowLeft, ArrowRight, Check, Ruler, CopyPlus, TrendingUp } from '@lucide/svelte';
import { Minus, Plus, X, ArrowLeft, ArrowRight, Check, Ruler, CopyPlus, TrendingUp, History } from '@lucide/svelte';
import { fly, fade } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
@@ -119,6 +119,20 @@
}
/** @param {string} key */
function copyLtoR(key) { values[key].right = values[key].left; }
/** @param {string} key */
function useLastValue(key) {
const s = steps.find((x) => x.key === key);
if (!s) return;
const last = historyFor(s).at(-1);
if (!last) return;
if (s.paired) {
if (last.left != null) values[key].left = String(last.left);
if (!values[key].same && last.right != null) values[key].right = String(last.right);
} else if (last.value != null) {
values[key] = String(last.value);
}
}
/** @param {number} i */
function jumpTo(i) {
direction = i > idx ? 1 : -1;
@@ -463,10 +477,19 @@
</button>
</div>
<button type="button" class="copy-btn" onclick={() => copyLtoR(step.key)} disabled={!pv.left}>
<CopyPlus size={13} /> {t('copy_l_to_r', lang)}
<CopyPlus size={15} />
<span>{t('copy_l_to_r_before', lang)}</span>
<ArrowRight size={14} />
<span>{t('copy_l_to_r_after', lang)}</span>
</button>
</div>
{/if}
{#if lastForStep?.left != null || lastForStep?.right != null}
<button type="button" class="same-value-btn" onclick={() => { useLastValue(step.key); next(); }}>
<History size={15} />
<span>{t('same_as_last', lang)}</span>
</button>
{/if}
<div class="same-toggle">
<Toggle bind:checked={pv.same} label={t('same_both_sides', lang)} />
</div>
@@ -483,6 +506,12 @@
<Plus size={20} />
</button>
</div>
{#if lastForStep?.value != null}
<button type="button" class="same-value-btn" onclick={() => { useLastValue(step.key); next(); }}>
<History size={15} />
<span>{t('same_as_last', lang)}</span>
</button>
{/if}
{/if}
</section>
{/key}
@@ -876,13 +905,15 @@
align-self: center;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.7rem;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.55rem 1.1rem;
border: 1px dashed var(--color-border);
border-radius: var(--radius-pill);
background: transparent;
font-size: 0.7rem;
color: var(--color-text-tertiary);
font-size: 0.88rem;
font-weight: 600;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms;
}
@@ -893,6 +924,28 @@
}
.copy-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.same-value-btn {
align-self: center;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 1.1rem;
border: 1px dashed color-mix(in oklab, var(--color-primary) 40%, transparent);
border-radius: var(--radius-pill);
background: color-mix(in oklab, var(--color-primary) 5%, transparent);
font-size: 0.88rem;
font-weight: 600;
color: var(--color-text-secondary);
cursor: pointer;
transition: border-color var(--transition-fast, 120ms), background var(--transition-fast, 120ms), color var(--transition-fast, 120ms);
}
.same-value-btn:hover {
border-style: solid;
border-color: var(--color-primary);
background: color-mix(in oklab, var(--color-primary) 12%, transparent);
color: var(--color-primary);
}
.same-toggle {
display: inline-flex;
}