feat(fitness): tinted body-part icons with semantic accents

Unifies PNG and SVG body-part images behind a single CSS-mask
render path, so both now colorize with a --accent CSS variable.
Accent splits by measurement type: --blue for proportion parts
(chest, shoulders, waist, hips) and --nord8 for muscle parts
(neck, biceps, forearms, thighs, calves). Stats cards gain a
matching 8%-tint fill and accent-colored hover border. History
page header image enlarged. Thigh SVG stroke-width bumped to 11
for better mask legibility.
This commit is contained in:
2026-04-21 12:37:46 +02:00
parent 5915fd323d
commit 56d438631b
6 changed files with 57 additions and 72 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.40.5",
"version": "1.40.6",
"private": true,
"type": "module",
"scripts": {
+6
View File
@@ -36,3 +36,9 @@ export function findBodyPart(slug: string): BodyPartCard | null {
export function bodyPartSlug(card: BodyPartCard, lang: string): string {
return lang === 'de' ? card.slugDe : card.key;
}
const PROPORTION_KEYS: ReadonlySet<string> = new Set(['chest', 'shoulders', 'waist', 'hips']);
export function bodyPartAccent(key: string): string {
return PROPORTION_KEYS.has(key) ? 'var(--blue)' : 'var(--nord8)';
}
@@ -8,6 +8,7 @@
import { toast } from '$lib/js/toast.svelte';
import DatePicker from '$lib/components/DatePicker.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
import { bodyPartAccent } from '$lib/js/fitnessBodyParts';
let { data } = $props();
@@ -360,18 +361,14 @@
{#if !done}
{#key step.key}
<section class="card" in:fly={flyOpts}>
<div class="hero">
<div class="hero" style="--accent: {bodyPartAccent(step.key)}">
{#if step.img}
{#if step.img.endsWith('.svg')}
<div
class="hero-pic hero-svg"
style="--hero-svg-src: url(/fitness/measure/{step.img})"
role="img"
aria-label={stepLabel(step)}
></div>
{:else}
<img src="/fitness/measure/{step.img}" alt="" class="hero-pic" />
{/if}
<div
class="hero-pic"
style="--hero-src: url(/fitness/measure/{step.img})"
role="img"
aria-label={stepLabel(step)}
></div>
{:else}
<div class="hero-placeholder" aria-hidden="true">
<Ruler size={72} strokeWidth={1.4} />
@@ -682,33 +679,21 @@
display: grid;
place-items: center;
border-radius: 50%;
background:
radial-gradient(closest-side, var(--color-surface), transparent 70%),
var(--color-bg-secondary);
box-shadow: var(--shadow-md);
position: relative;
background: color-mix(in oklab, var(--accent, var(--color-primary)) 15%, transparent);
}
.hero-pic {
width: 150px;
height: 150px;
object-fit: contain;
}
.hero-svg {
mask-image: var(--hero-svg-src);
-webkit-mask-image: var(--hero-svg-src);
mask-image: var(--hero-src);
-webkit-mask-image: var(--hero-src);
mask-size: contain;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-position: center;
background-color: var(--color-text-primary);
background-color: var(--accent, var(--color-primary));
}
@media (prefers-color-scheme: dark) {
img.hero-pic { filter: invert(1); }
}
:global(:root[data-theme="dark"]) img.hero-pic { filter: invert(1); }
:global(:root[data-theme="light"]) img.hero-pic { filter: none; }
.hero-placeholder {
display: flex;
@@ -9,7 +9,7 @@
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
import StatsRingGraph from '$lib/components/fitness/StatsRingGraph.svelte';
import { BODY_PART_CARDS, bodyPartSlug } from '$lib/js/fitnessBodyParts';
import { BODY_PART_CARDS, bodyPartSlug, bodyPartAccent } from '$lib/js/fitnessBodyParts';
const lang = $derived(detectFitnessLang($page.url.pathname));
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
@@ -390,16 +390,15 @@
{@const cv = currentValue(card)}
<a
class="bp-card"
style="--accent: {bodyPartAccent(card.key)}"
href="/fitness/{statsSlug}/{historySlug}/{bodyPartSlug(card, lang)}"
>
<div class="bp-img-wrap" aria-hidden="true">
{#if card.img && card.img.endsWith('.svg')}
{#if card.img}
<div
class="bp-img bp-img-svg"
style="--bp-svg-src: url(/fitness/measure/{card.img})"
class="bp-img"
style="--bp-src: url(/fitness/measure/{card.img})"
></div>
{:else if card.img}
<img src="/fitness/measure/{card.img}" alt="" class="bp-img" />
{:else}
<Ruler size={36} strokeWidth={1.5} />
{/if}
@@ -989,10 +988,22 @@
font: inherit;
text-decoration: none;
position: relative;
overflow: hidden;
isolation: isolate;
transition: border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal);
}
.bp-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: var(--accent, var(--color-primary));
opacity: 0.08;
z-index: -1;
pointer-events: none;
}
.bp-card:hover {
border-color: var(--color-primary);
border-color: var(--accent, var(--color-primary));
box-shadow: var(--shadow-sm);
}
.bp-img-wrap {
@@ -1002,30 +1013,22 @@
height: 3.25rem;
flex-shrink: 0;
border-radius: 50%;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
background: color-mix(in oklab, var(--accent, var(--color-primary)) 18%, transparent);
color: var(--accent, var(--color-primary));
}
.bp-img {
width: 2.4rem;
height: 2.4rem;
object-fit: contain;
}
.bp-img-svg {
mask-image: var(--bp-svg-src);
-webkit-mask-image: var(--bp-svg-src);
mask-image: var(--bp-src);
-webkit-mask-image: var(--bp-src);
mask-size: contain;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-position: center;
background-color: var(--color-text-primary);
background-color: var(--accent, var(--color-primary));
}
@media (prefers-color-scheme: dark) {
img.bp-img { filter: invert(1); }
}
:global(:root[data-theme="dark"]) img.bp-img { filter: invert(1); }
:global(:root[data-theme="light"]) img.bp-img { filter: none; }
.bp-meta {
display: flex;
flex-direction: column;
@@ -3,6 +3,7 @@
import { ArrowLeft, Ruler, TrendingUp, TrendingDown, Minus as MinusIcon } from '@lucide/svelte';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
import { bodyPartAccent } from '$lib/js/fitnessBodyParts';
let { data } = $props();
@@ -112,7 +113,7 @@
<svelte:head><title>{t(card.labelKey, lang)} · {lang === 'en' ? 'History' : 'Verlauf'} - Bocken</title></svelte:head>
<div class="detail-page">
<header class="detail-header">
<header class="detail-header" style="--accent: {bodyPartAccent(card.key)}">
<a class="back-link" href="/fitness/{statsSlug}" aria-label={t('back', lang)}>
<ArrowLeft size={18} />
</a>
@@ -121,13 +122,11 @@
<h1>{t(card.labelKey, lang)}</h1>
</div>
<div class="head-img" aria-hidden="true">
{#if card.img && card.img.endsWith('.svg')}
{#if card.img}
<div
class="head-pic head-pic-svg"
style="--bp-svg-src: url(/fitness/measure/{card.img})"
class="head-pic"
style="--bp-src: url(/fitness/measure/{card.img})"
></div>
{:else if card.img}
<img src="/fitness/measure/{card.img}" alt="" class="head-pic" />
{:else}
<Ruler size={40} strokeWidth={1.5} />
{/if}
@@ -274,34 +273,26 @@
.head-img {
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
width: 5.5rem;
height: 5.5rem;
flex-shrink: 0;
border-radius: 50%;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
background: color-mix(in oklab, var(--accent, var(--color-primary)) 15%, transparent);
color: var(--accent, var(--color-primary));
}
.head-pic {
width: 2.6rem;
height: 2.6rem;
object-fit: contain;
}
.head-pic-svg {
mask-image: var(--bp-svg-src);
-webkit-mask-image: var(--bp-svg-src);
width: 4.1rem;
height: 4.1rem;
mask-image: var(--bp-src);
-webkit-mask-image: var(--bp-src);
mask-size: contain;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-position: center;
background-color: var(--color-text-primary);
background-color: var(--accent, var(--color-primary));
}
@media (prefers-color-scheme: dark) {
img.head-pic { filter: invert(1); }
}
:global(:root[data-theme="dark"]) img.head-pic { filter: invert(1); }
:global(:root[data-theme="light"]) img.head-pic { filter: none; }
.summary .stat-grid {
display: grid;
+1 -1
View File
@@ -5,7 +5,7 @@
viewBox="0 0 230.97847 161.57938"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<g transform="translate(10.583333,-67.733332)" fill="none" stroke="currentColor" stroke-width="7.9375" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(10.583333,-67.733332)" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round">
<path d="m -5.2938776,75.349546 c 0,0 37.0210826,-4.638254 65.9220826,-3.451251 16.254828,0.667607 16.716356,0.857996 37.092959,6.092661 20.583856,5.287909 30.187556,12.829711 52.828226,19.244569 22.64067,6.414855 47.35673,7.358215 58.11105,22.263325 10.75432,14.90511 4.33946,48.11142 4.33946,48.11142 0,0 4.7168,25.47075 3.01875,35.47038 -1.69804,9.99963 -4.7168,22.26332 -4.7168,22.26332" />
<path d="m -6.6145834,170.25168 c 0,0 19.8105854,15.09378 43.2059404,17.73519 23.395358,2.64141 79.431013,-10.1883 95.090803,-13.39573 15.6598,-3.20743 19.81059,1.88672 19.81059,1.88672 0,0 -6.7922,14.52776 -6.60353,28.86686 0.18867,14.33909 2.45274,19.62191 2.45274,19.62191" />
<path d="m 30.275732,127.2863 c 0,0 18.901817,1.34096 48.200862,10.32508 27.805246,8.52607 33.634316,12.94496 33.634316,12.94496" />

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB