Activity stats: fixed-position pairs so optional values don't shift layout
This commit is contained in:
@@ -234,25 +234,47 @@
|
|||||||
return { label: MODE_LABEL[colorMode], min: fmt(minV), max: fmt(maxV) };
|
return { label: MODE_LABEL[colorMode], min: fmt(minV), max: fmt(maxV) };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const stat = (label: string, value: string, key?: string) => ({ label, value, key });
|
type Stat = { label: string; value: string; key?: string };
|
||||||
|
const stat = (label: string, value: string, key?: string): Stat => ({ label, value, key });
|
||||||
$: hiddenStats = new Set<string>((detail?.custom as any)?.hide_stats ?? []);
|
$: hiddenStats = new Set<string>((detail?.custom as any)?.hide_stats ?? []);
|
||||||
$: stats = [
|
|
||||||
stat('Distance', formatDistance(activity.distance_m)),
|
// Fixed-position pairs: null = empty slot. Pairing is always preserved regardless
|
||||||
stat('Moving time', formatDuration(activity.moving_time_s ?? activity.duration_s)),
|
// of which optional values (VAM, power) are available.
|
||||||
stat('Elevation ↑', formatElevation(activity.elevation_gain_m), 'elevation'),
|
$: statRows = (() => {
|
||||||
...(detail?.climbing_vam_mh != null ? [
|
const h = hiddenStats;
|
||||||
stat('Climbing VAM', `${detail.climbing_vam_mh.toLocaleString()} m/h`, 'elevation'),
|
const vis = (s: Stat | null): Stat | null => s && (!s.key || !h.has(s.key)) ? s : null;
|
||||||
] : []),
|
const rows: [Stat | null, Stat | null][] = [
|
||||||
stat('Avg speed', formatSpeed(activity.avg_speed_kmh), 'speed'),
|
[
|
||||||
stat('Max speed', formatSpeed(activity.max_speed_kmh), 'speed'),
|
stat('Distance', formatDistance(activity.distance_m)),
|
||||||
stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—', 'heart_rate'),
|
stat('Moving time', formatDuration(activity.moving_time_s ?? activity.duration_s)),
|
||||||
stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'),
|
],
|
||||||
...(activity.avg_power_w != null ? [
|
[
|
||||||
stat('Avg power', `${activity.avg_power_w} W`, 'power'),
|
stat('Elevation ↑', formatElevation(activity.elevation_gain_m), 'elevation'),
|
||||||
stat('NP', npPower != null ? `${npPower} W` : '—', 'power'),
|
detail?.climbing_vam_mh != null
|
||||||
] : []),
|
? stat('Climbing VAM', `${detail.climbing_vam_mh.toLocaleString()} m/h`, 'elevation')
|
||||||
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
|
: null,
|
||||||
].filter(s => !s.key || !hiddenStats.has(s.key));
|
],
|
||||||
|
[
|
||||||
|
stat('Avg speed', formatSpeed(activity.avg_speed_kmh), 'speed'),
|
||||||
|
stat('Max speed', formatSpeed(activity.max_speed_kmh), 'speed'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—', 'heart_rate'),
|
||||||
|
stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'),
|
||||||
|
],
|
||||||
|
...(activity.avg_power_w != null ? [[
|
||||||
|
stat('Avg power', `${activity.avg_power_w} W`, 'power'),
|
||||||
|
stat('NP', npPower != null ? `${npPower} W` : '—', 'power'),
|
||||||
|
] as [Stat, Stat]] : []),
|
||||||
|
[
|
||||||
|
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return rows
|
||||||
|
.map(([a, b]) => [vis(a), vis(b)] as [Stat | null, Stat | null])
|
||||||
|
.filter(([a, b]) => a !== null || b !== null);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={onKeydown} />
|
<svelte:window on:keydown={onKeydown} />
|
||||||
@@ -431,25 +453,31 @@
|
|||||||
|
|
||||||
<!-- Right column: stats summary -->
|
<!-- Right column: stats summary -->
|
||||||
<div class="grid grid-cols-2 gap-px bg-zinc-800 rounded-xl overflow-hidden">
|
<div class="grid grid-cols-2 gap-px bg-zinc-800 rounded-xl overflow-hidden">
|
||||||
{#each stats as s}
|
{#each statRows as [left, right]}
|
||||||
{@const cm =
|
{#each [left, right] as s}
|
||||||
s.key === 'speed' && hasSpeedTrack ? 'speed' :
|
{@const cm =
|
||||||
s.key === 'heart_rate' && hasHrTrack ? 'hr' :
|
s?.key === 'speed' && hasSpeedTrack ? 'speed' :
|
||||||
s.key === 'power' && hasPowerTrack ? 'power' :
|
s?.key === 'heart_rate' && hasHrTrack ? 'hr' :
|
||||||
s.key === 'elevation' && hasElevTrack ? 'elevation' :
|
s?.key === 'power' && hasPowerTrack ? 'power' :
|
||||||
s.key === 'cadence' && hasCadenceTrack ? 'cadence' : null}
|
s?.key === 'elevation' && hasElevTrack ? 'elevation' :
|
||||||
<div
|
s?.key === 'cadence' && hasCadenceTrack ? 'cadence' : null}
|
||||||
class="bg-zinc-900 px-4 py-3 transition-colors {cm ? 'hover:bg-zinc-800 cursor-pointer' : ''} {cm && stickyMode === cm ? 'ring-1 ring-inset ring-white/25' : ''}"
|
{#if s}
|
||||||
role={cm ? 'button' : 'none'}
|
<div
|
||||||
tabindex={cm ? 0 : -1}
|
class="bg-zinc-900 px-4 py-3 transition-colors {cm ? 'hover:bg-zinc-800 cursor-pointer' : ''} {cm && stickyMode === cm ? 'ring-1 ring-inset ring-white/25' : ''}"
|
||||||
on:mouseenter={() => { if (cm) colorMode = cm; }}
|
role={cm ? 'button' : 'none'}
|
||||||
on:mouseleave={() => { colorMode = stickyMode ?? 'default'; }}
|
tabindex={cm ? 0 : -1}
|
||||||
on:click={() => handleStatClick(cm)}
|
on:mouseenter={() => { if (cm) colorMode = cm; }}
|
||||||
on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleStatClick(cm); } }}
|
on:mouseleave={() => { colorMode = stickyMode ?? 'default'; }}
|
||||||
>
|
on:click={() => handleStatClick(cm)}
|
||||||
<p class="text-2xl font-bold text-white">{s.value}</p>
|
on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleStatClick(cm); } }}
|
||||||
<p class="text-xs text-zinc-500">{s.label}</p>
|
>
|
||||||
</div>
|
<p class="text-2xl font-bold text-white">{s.value}</p>
|
||||||
|
<p class="text-xs text-zinc-500">{s.label}</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="bg-zinc-900 px-4 py-3"></div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
{#if detail?.gear}
|
{#if detail?.gear}
|
||||||
<div class="bg-zinc-900 px-4 py-3 col-span-2">
|
<div class="bg-zinc-900 px-4 py-3 col-span-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user