fitness: theme-reactive chart colors, bar outline fix, and stats label polish
All checks were successful
CI / update (push) Successful in 2m17s
All checks were successful
CI / update (push) Successful in 2m17s
- Stats and exercise pages: chart colors adapt to light/dark theme reactively - FitnessChart: remove bar outline (borderWidth 0 for bar type) - Stats: workouts icon/card use --color-primary, plural-aware label, rename labels
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
backgroundColor: ds.backgroundColor ?? (type === 'bar'
|
||||
? (nordColors[i % nordColors.length])
|
||||
: 'transparent'),
|
||||
borderWidth: ds.borderWidth ?? (type === 'line' ? 2 : 1),
|
||||
borderWidth: ds.borderWidth ?? (type === 'line' ? 2 : 0),
|
||||
pointRadius: ds.pointRadius ?? (type === 'line' ? 3 : 0),
|
||||
pointBackgroundColor: ds.pointBackgroundColor || ds.borderColor || nordColors[i % nordColors.length],
|
||||
tension: ds.tension ?? 0.3,
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
<script>
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
function checkDark() {
|
||||
if (typeof document === 'undefined') return false;
|
||||
const t = document.documentElement.dataset.theme;
|
||||
if (t === 'dark') return true;
|
||||
if (t === 'light') return false;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
let dark = $state(checkDark());
|
||||
onMount(() => {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const onMql = () => { dark = checkDark(); };
|
||||
mql.addEventListener('change', onMql);
|
||||
const obs = new MutationObserver(() => { dark = checkDark(); });
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
return () => { mql.removeEventListener('change', onMql); obs.disconnect(); };
|
||||
});
|
||||
|
||||
const primary = $derived(dark ? '#88C0D0' : '#5E81AC');
|
||||
|
||||
let activeTab = $state('about');
|
||||
|
||||
const exercise = $derived(data.exercise?.exercise ?? getExerciseById(data.exercise?.id));
|
||||
@@ -21,7 +42,7 @@
|
||||
const points = charts.est1rmOverTime ?? [];
|
||||
return withTrend({
|
||||
labels: points.map((/** @type {any} */ p) => new Date(p.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })),
|
||||
datasets: [{ label: 'Est. 1RM (kg)', data: points.map((/** @type {any} */ p) => p.value), borderColor: '#88C0D0' }]
|
||||
datasets: [{ label: 'Est. 1RM (kg)', data: points.map((/** @type {any} */ p) => p.value), borderColor: primary }]
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +100,7 @@
|
||||
* @param {{ labels: string[], datasets: Array<any> }} chartData
|
||||
* @param {string} trendColor
|
||||
*/
|
||||
function withTrend(chartData, trendColor = '#5E81AC') {
|
||||
function withTrend(chartData, trendColor = primary) {
|
||||
const values = chartData.datasets[0]?.data;
|
||||
if (!values || values.length < 3) return chartData;
|
||||
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
<script>
|
||||
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
||||
import { Dumbbell, Route, Flame } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
function checkDark() {
|
||||
if (typeof document === 'undefined') return false;
|
||||
const t = document.documentElement.dataset.theme;
|
||||
if (t === 'dark') return true;
|
||||
if (t === 'light') return false;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
let dark = $state(checkDark());
|
||||
onMount(() => {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const onMql = () => { dark = checkDark(); };
|
||||
mql.addEventListener('change', onMql);
|
||||
const obs = new MutationObserver(() => { dark = checkDark(); });
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
return () => { mql.removeEventListener('change', onMql); obs.disconnect(); };
|
||||
});
|
||||
|
||||
const primary = $derived(dark ? '#88C0D0' : '#5E81AC');
|
||||
const primaryFill = $derived(dark ? 'rgba(136, 192, 208, 0.15)' : 'rgba(94, 129, 172, 0.15)');
|
||||
|
||||
const stats = $derived(data.stats ?? {});
|
||||
|
||||
const workoutsChartData = $derived({
|
||||
@@ -11,7 +33,7 @@
|
||||
datasets: [{
|
||||
label: 'Workouts',
|
||||
data: stats.workoutsChart?.data ?? [],
|
||||
backgroundColor: '#88C0D0'
|
||||
backgroundColor: primary
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -25,7 +47,7 @@
|
||||
label: '± 1σ',
|
||||
data: stats.weightChart.upper,
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: 'rgba(94, 129, 172, 0.15)',
|
||||
backgroundColor: primaryFill,
|
||||
fill: '+1',
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
@@ -46,7 +68,7 @@
|
||||
{
|
||||
label: 'Trend',
|
||||
data: stats.weightChart.sma,
|
||||
borderColor: '#5E81AC',
|
||||
borderColor: primary,
|
||||
pointRadius: 0,
|
||||
borderWidth: 3,
|
||||
tension: 0.3,
|
||||
@@ -73,17 +95,17 @@
|
||||
<div class="lifetime-card workouts">
|
||||
<div class="card-icon"><Dumbbell size={24} /></div>
|
||||
<div class="card-value">{stats.totalWorkouts ?? 0}</div>
|
||||
<div class="card-label">Workouts</div>
|
||||
<div class="card-label">{(stats.totalWorkouts ?? 0) === 1 ? 'Workout' : 'Workouts'}</div>
|
||||
</div>
|
||||
<div class="lifetime-card tonnage">
|
||||
<div class="card-icon"><Flame size={24} /></div>
|
||||
<div class="card-value">{stats.totalTonnage ?? 0}<span class="card-unit">t</span></div>
|
||||
<div class="card-label">Tonnage Lifted</div>
|
||||
<div class="card-label">Lifted</div>
|
||||
</div>
|
||||
<div class="lifetime-card cardio">
|
||||
<div class="card-icon"><Route size={24} /></div>
|
||||
<div class="card-value">{stats.totalCardioKm ?? 0}<span class="card-unit">km</span></div>
|
||||
<div class="card-label">Cardio Distance</div>
|
||||
<div class="card-label">Distance Covered</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +167,7 @@
|
||||
opacity: 0.08;
|
||||
}
|
||||
.lifetime-card.workouts::before {
|
||||
background: var(--nord8);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
.lifetime-card.tonnage::before {
|
||||
background: var(--nord12);
|
||||
@@ -163,8 +185,8 @@
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
.workouts .card-icon {
|
||||
color: var(--nord8);
|
||||
background: color-mix(in srgb, var(--nord8) 15%, transparent);
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||
}
|
||||
.tonnage .card-icon {
|
||||
color: var(--nord12);
|
||||
@@ -187,10 +209,10 @@
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
.card-label {
|
||||
font-size: 0.65rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user