From 121a68255d9d04da685dd6593044231734f43d69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Thu, 4 Jun 2026 21:01:47 +0200
Subject: [PATCH] feat(medications): refine the injection-site body map and add
a marker legend
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Redraw the inline body-map silhouette to the proportions the iOS client
calibrated against: an oval head on a short neck, sloped shoulders, a
torso that tapers to the waist and flares to the hips, arms held slightly
out to mid-thigh with simple hands, and separated legs ending in feet.
The figure is composed as a torso-plus-legs outline with two separate arm
paths so there is a clean underarm gap and the arms read as arms rather
than a single blob. The eight site anchors keep their calibrated
coordinates on the 100x200 viewBox, so every tappable region and its
nearest-centre hit routing is preserved — the dots still land on real
anatomy (upper arms, the four abdomen quadrants, the thighs) and stay
selectable, theme-aware, and keyboard reachable.
Add a compact marker legend to the injection-site dialog: a dashed primary
ring marks the recommended next site and an amber ring marks the last-used
site. The last-used marker on the picker switches to the same amber tone so
the legend swatches mirror the body map exactly.
---
messages/de.json | 3 +
messages/en.json | 3 +
messages/es.json | 3 +
messages/fr.json | 3 +
messages/it.json | 3 +
messages/pl.json | 3 +
.../medications/injection-site-picker.tsx | 205 ++++++++++++------
.../medications/log-injection-site-dialog.tsx | 57 ++++-
8 files changed, 213 insertions(+), 67 deletions(-)
diff --git a/messages/de.json b/messages/de.json
index 10302180e..24ebef389 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Körperumriss",
"injectionSitePickerAriaLabel": "Injektionsstelle auswählen",
"injectionSiteRecommendedAriaLabel": "{site} – empfohlene nächste Stelle",
+ "injectionSiteLegendAriaLabel": "Legende der Markierungen",
+ "injectionSiteLegendRecommended": "Empfohlene nächste Stelle",
+ "injectionSiteLegendLastUsed": "Zuletzt verwendete Stelle",
"siteAbdomenLeft": "Bauch, unten links",
"siteAbdomenRight": "Bauch, unten rechts",
"siteAbdomenUpperLeft": "Bauch, oben links",
diff --git a/messages/en.json b/messages/en.json
index d8fe8ad56..e0832a629 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Body outline",
"injectionSitePickerAriaLabel": "Injection site picker",
"injectionSiteRecommendedAriaLabel": "{site} — recommended next site",
+ "injectionSiteLegendAriaLabel": "Marker legend",
+ "injectionSiteLegendRecommended": "Recommended next site",
+ "injectionSiteLegendLastUsed": "Last used site",
"siteAbdomenLeft": "Abdomen, lower left",
"siteAbdomenRight": "Abdomen, lower right",
"siteAbdomenUpperLeft": "Abdomen, upper left",
diff --git a/messages/es.json b/messages/es.json
index 0c78edc61..0537ec304 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Silueta del cuerpo",
"injectionSitePickerAriaLabel": "Selector de sitio de inyección",
"injectionSiteRecommendedAriaLabel": "{site}: sitio siguiente recomendado",
+ "injectionSiteLegendAriaLabel": "Leyenda de marcadores",
+ "injectionSiteLegendRecommended": "Próximo sitio recomendado",
+ "injectionSiteLegendLastUsed": "Último sitio usado",
"siteAbdomenLeft": "Abdomen, inferior izquierdo",
"siteAbdomenRight": "Abdomen, inferior derecho",
"siteAbdomenUpperLeft": "Abdomen, superior izquierdo",
diff --git a/messages/fr.json b/messages/fr.json
index 81d7b018b..ef6105d02 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Silhouette du corps",
"injectionSitePickerAriaLabel": "Sélecteur de site d'injection",
"injectionSiteRecommendedAriaLabel": "{site} — prochain site recommandé",
+ "injectionSiteLegendAriaLabel": "Légende des repères",
+ "injectionSiteLegendRecommended": "Prochain site recommandé",
+ "injectionSiteLegendLastUsed": "Dernier site utilisé",
"siteAbdomenLeft": "Abdomen, en bas à gauche",
"siteAbdomenRight": "Abdomen, en bas à droite",
"siteAbdomenUpperLeft": "Abdomen, en haut à gauche",
diff --git a/messages/it.json b/messages/it.json
index 61a22b2af..aaff084e5 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Sagoma del corpo",
"injectionSitePickerAriaLabel": "Selettore del sito di iniezione",
"injectionSiteRecommendedAriaLabel": "{site} — prossimo sito consigliato",
+ "injectionSiteLegendAriaLabel": "Legenda dei segnaposto",
+ "injectionSiteLegendRecommended": "Prossimo sito consigliato",
+ "injectionSiteLegendLastUsed": "Ultimo sito utilizzato",
"siteAbdomenLeft": "Addome, basso a sinistra",
"siteAbdomenRight": "Addome, basso a destra",
"siteAbdomenUpperLeft": "Addome, alto a sinistra",
diff --git a/messages/pl.json b/messages/pl.json
index 1c83dc2f0..7d2b48da2 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1035,6 +1035,9 @@
"injectionSiteBodyOutlineAriaLabel": "Sylwetka ciała",
"injectionSitePickerAriaLabel": "Wybór miejsca iniekcji",
"injectionSiteRecommendedAriaLabel": "{site} — zalecane następne miejsce",
+ "injectionSiteLegendAriaLabel": "Legenda oznaczeń",
+ "injectionSiteLegendRecommended": "Zalecane następne miejsce",
+ "injectionSiteLegendLastUsed": "Ostatnio użyte miejsce",
"siteAbdomenLeft": "Brzuch, dolny lewy",
"siteAbdomenRight": "Brzuch, dolny prawy",
"siteAbdomenUpperLeft": "Brzuch, górny lewy",
diff --git a/src/components/medications/injection-site-picker.tsx b/src/components/medications/injection-site-picker.tsx
index 423edccf4..2f7d90364 100644
--- a/src/components/medications/injection-site-picker.tsx
+++ b/src/components/medications/injection-site-picker.tsx
@@ -19,9 +19,12 @@ import {
* The silhouette is an INLINE SVG so it inherits the app's theme tokens
* (`currentColor` + CSS variables) and tints correctly in light/dark and
* across the zinc/Dracula palettes with no raster asset and no luminance
- * key. The body is traced to the proportions the iOS client calibrated
- * against (see `.planning/ios-coord/v0.12-…-injection-bodymap-coords.md`)
- * so dots land on the same anatomy on both platforms.
+ * key. The body is hand-authored to the proportions the iOS client
+ * calibrated against (see `.planning/ios-coord/v0.12-…-injection-bodymap-coords.md`):
+ * an oval head + short neck, sloped shoulders, a torso that tapers to the
+ * waist and flares to the hips, arms held slightly out to mid-thigh with
+ * simple hands, and separated legs ending in feet — so dots land on the
+ * same anatomy on both platforms.
*
* `viewBox="0 0 100 200"` (aspect ≈0.5). `SITE_COORDS` is expressed on
* the same viewBox; the recommended-next ring, the last-used marker and
@@ -142,61 +145,139 @@ export function InjectionSitePicker({
- {/* Anatomical front-of-body silhouette — one continuous outline
- traced to the iOS reference proportions (upright, arms slightly
- out, plain oval head). Filled with the soft wash + a hairline
- stroke so it reads as a calm medical illustration. */}
-
+ strokeLinecap="round"
+ >
+ {/* Head + neck — a clean oval head with small ears, a short
+ tapering neck blending into the trapezius line. */}
+
+ {/* Ears */}
+
+
+ {/* Torso + legs — one outline. From the neck the shoulders slope
+ out to the top of the arms, dive into the armpit notch, run
+ down the torso side (narrowing to the waist, flaring to the
+ hips), split at the crotch into two legs ending in feet, and
+ mirror back up the right side. The arms are drawn separately
+ (below) so there is a clean underarm gap and the arm anchors
+ sit on real arm mass, not on the torso edge. */}
+
+ {/* Left arm — held slightly out, tapering from the shoulder to a
+ rounded hand around hip height, with a clear underarm gap from
+ the torso. The arm anchor (≈x26 @ y72) sits squarely on the
+ upper-arm mass: at that height the arm spans ≈x21–31, so the
+ dot is centred on the muscle. */}
+
+ {/* Right arm — mirror of the left. */}
+
+ {/* Feet — small forward-pointing shapes anchoring each leg so the
+ figure reads as a full body rather than truncated calves. */}
+
+
+
{/* Centre + waist references — barely-there guides that help the
eye read the abdomen quadrants. Kept low-alpha so they never
@@ -211,9 +292,9 @@ export function InjectionSitePicker({
strokeOpacity="0.35"
/>
)}
- {/* Last-used marker — a soft solid ring so the user can see
+ {/* Last-used marker — a solid amber ring so the user can see
where they injected most recently at a glance, distinct
- from the dashed recommendation ring. Suppressed on the
- active selection (the filled dot already says "here"). */}
+ from the dashed primary recommendation ring. The amber tone
+ matches the dialog legend and reads in light + dark.
+ Suppressed on the active selection (the filled dot already
+ says "here"). */}
{isLastUsed && !disabled && (
)}
{isRecommended && !isActive && !disabled && (
diff --git a/src/components/medications/log-injection-site-dialog.tsx b/src/components/medications/log-injection-site-dialog.tsx
index 2b8ee7753..132983fc2 100644
--- a/src/components/medications/log-injection-site-dialog.tsx
+++ b/src/components/medications/log-injection-site-dialog.tsx
@@ -109,12 +109,57 @@ export function LogInjectionSiteDialog({
{t("medications.logInjectionSiteNoneAvailable")}
) : (
-
+ <>
+
+ {/* Marker legend — explains the two body-map annotations so the
+ dashed primary ring and the amber ring read at a glance. The
+ swatches mirror the picker's own marker strokes exactly. */}
+