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. */} +
    +
  • + + {t("medications.injectionSiteLegendRecommended")} +
  • +
  • + + {t("medications.injectionSiteLegendLastUsed")} +
  • +
+ )}