From 33ca595405cdd230223d72daf263764b3069d9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 21:18:53 +0200 Subject: [PATCH] feat(medications): replace the injection-site body map with a refined illustration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the hand-authored body silhouette in the injection-site picker for a clean, gender-neutral medical line-art figure with correct proportions (arms to mid-thigh, head-to-body ≈1:7.5, separated legs, feet). The figure ships as a small transparent raster (~8 KB) and is painted theme-aware: its alpha drives an SVG mask over a currentColor rect, so the strokes take the picker's text colour and tint correctly in light and dark. Keeping the figure inside the SVG (rather than a CSS-masked sibling) means it shares the dots' 100×200 viewBox, so alignment stays responsive by construction. Recalibrate SITE_COORDS so the upper-arm, abdomen-quadrant and thigh anchors land on the new figure's anatomy; the pointer overlay, keyboard targets, recommendation ring, last-used marker and disabled-set logic are unchanged. --- public/injection-body-front.png | Bin 0 -> 8038 bytes .../medications/injection-site-picker.tsx | 217 +++++------------- src/lib/medications/injection-sites.ts | 35 ++- 3 files changed, 75 insertions(+), 177 deletions(-) create mode 100644 public/injection-body-front.png diff --git a/public/injection-body-front.png b/public/injection-body-front.png new file mode 100644 index 0000000000000000000000000000000000000000..2464532b2534a238af2cdf64608ac404c259ff97 GIT binary patch literal 8038 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 literal 0 HcmV?d00001 diff --git a/src/components/medications/injection-site-picker.tsx b/src/components/medications/injection-site-picker.tsx index 2f7d90364..1c6acdc00 100644 --- a/src/components/medications/injection-site-picker.tsx +++ b/src/components/medications/injection-site-picker.tsx @@ -13,22 +13,24 @@ import { } from "@/lib/medications/injection-sites"; /** - * Injection-site picker — anatomical front-of-body silhouette with 8 + * Injection-site picker — anatomical front-of-body figure with 8 * tappable sites. * - * 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 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. + * The body is a clean, gender-neutral medical line-art figure shipped as + * a small transparent raster (`/injection-body-front.png`, ~8 KB). It is + * painted THEME-AWARE without baking in a colour: the figure's alpha is + * used as an SVG mask (`mask-type="alpha"`) over a `` filled with + * `currentColor`, so the strokes take the picker's text colour and tint + * correctly in light/dark and across the zinc/Dracula palettes. Keeping + * the figure inside the SVG (rather than a CSS-masked sibling element) + * means it shares the exact `viewBox` coordinate space as the dots, so + * alignment is responsive by construction. * - * `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 - * the selected fill all read directly from those coordinates. + * `viewBox="0 0 100 200"` (aspect ≈0.5). The figure is drawn height-fit + * and horizontally centred on that box; `SITE_COORDS` is expressed on the + * same viewBox and measured to land each anchor on the figure's anatomy. + * The recommended-next ring, the last-used marker and the selected fill + * all read directly from those coordinates. * * Per the long-standing "nichts brickt" directive the picker is opt-in — * mounted from the GLP-1 dashboard tile and the optional log dialog. @@ -91,10 +93,10 @@ export function InjectionSitePicker({ // map (WCAG 2.4.7). const [focusedSite, setFocusedSite] = useState(null); - // Stable, instance-unique ids for the SVG paint servers so two pickers - // on one page can't collide their . + // Stable, instance-unique id for the SVG mask so two pickers on one + // page can't collide their . const uid = useId().replace(/[:]/g, ""); - const bodyFillId = `inj-body-${uid}`; + const bodyMaskId = `inj-body-mask-${uid}`; // The overlay maps a pointer event to viewBox space through the live // CTM, then resolves the nearest allowed site centre and selects it. @@ -136,148 +138,51 @@ export function InjectionSitePicker({ aria-label={t("medications.injectionSiteBodyOutlineAriaLabel")} > - {/* A whisper-soft vertical wash gives the flat silhouette a - little depth without fighting the theme — both stops are the - current text colour at low alpha, so it tints with the body. */} - - - - + {/* The body figure's alpha drives an SVG mask. `mask-type="alpha"` + keys on the PNG's transparency (strokes opaque, surround + transparent) rather than luminance, so a `` painted through it renders the figure in the + picker's text colour — theme-aware in light/dark and across + the zinc/Dracula palettes with no baked-in tint. The image is + height-fit + horizontally centred on the 100×200 box so it + shares the dots' coordinate space exactly. */} + + + - {/* Anatomical front-of-body silhouette — traced to the iOS - reference proportions: a plain oval head on a short neck, - naturally sloped shoulders, a torso that narrows to the waist - and flares to the hips, arms held slightly away from the body - reaching to mid-thigh, and separated legs ending in feet. The - figure is composed as two mirror halves about x=50 so it stays - perfectly symmetric, with the upper-arm mass sitting under the - calibrated arm anchors (≈x26/74 @ y72) and the thigh mass under - the thigh anchors (≈x43/57 @ y140) — the dots land on real - anatomy on both platforms. Filled with the soft wash + a - hairline stroke so it reads as a calm medical illustration. */} - - {/* 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. */} - - - + {/* Anatomical front-of-body figure — a clean, gender-neutral + medical line-art body painted in `currentColor` through the + alpha mask above. The figure is calibrated so the upper-arm + anchors (x23/77 @ y78) sit on the arm mass clear of the torso, + the abdomen quadrants (x43.5/56.5 @ y81/95) sit on the torso + either side of the midline, and the thigh anchors (x40/60 @ + y140) sit on the centre of each thigh — the dots land on real + anatomy. Rendered at a soft opacity so it reads as a calm + backdrop the dots sit on rather than competing with them. */} + {/* Centre + waist references — barely-there guides that help the eye read the abdomen quadrants. Kept low-alpha so they never diff --git a/src/lib/medications/injection-sites.ts b/src/lib/medications/injection-sites.ts index 2b2a06a89..3cbdcd5ad 100644 --- a/src/lib/medications/injection-sites.ts +++ b/src/lib/medications/injection-sites.ts @@ -22,38 +22,31 @@ export const INJECTION_SITE_KEYS = [ export type InjectionSiteKey = (typeof INJECTION_SITE_KEYS)[number]; /** - * SVG position for each site on the anatomical front-of-body silhouette + * SVG position for each site on the anatomical front-of-body figure * (`100 x 200` viewBox, aspect ≈0.5). Used by `injection-site-picker.tsx` * to render the clickable dots AND by {@link nextInjectionSite} as the * Euclidean-distance basis for the rotation recommendation, so the two * always agree on where a site "is". * - * Calibrated against the iOS client's hands-on tune (see - * `.planning/ios-coord/v0.12-ios-to-server-injection-bodymap-coords.md`): - * fractional coords (0–1, x across width / y down height) multiplied by - * the viewBox `width=100` / `height=200`, so both apps land their dots on - * the same anatomy. Screen-left (`*_LEFT`) sits at the viewer's left, - * matching the long-standing "links == screen-left" convention. - * - * site (iOS naming) fraction (x, y) → viewBox (x, y) - * armLeft 0.262, 0.360 26.2, 72 - * armRight 0.738, 0.360 73.8, 72 - * abdomenLeftUpper 0.435, 0.405 43.5, 81 - * abdomenRightUpper 0.565, 0.405 56.5, 81 - * abdomenLeftLower 0.435, 0.475 43.5, 95 - * abdomenRightLower 0.565, 0.475 56.5, 95 - * thighLeft 0.430, 0.700 43, 140 - * thighRight 0.570, 0.700 57, 140 + * Calibrated against the body figure the picker masks onto the viewBox + * (`/injection-body-front.png`, rendered height-fit + horizontally + * centred on the 100×200 box): the anchors are measured to land on the + * figure's anatomy — the abdomen quadrants on the torso either side of + * the midline, the thigh anchors on the centre of each thigh, and the + * arm anchors on the upper-arm mass clear of the torso edge. Screen-left + * (`*_LEFT`) sits at the viewer's left, matching the long-standing + * "links == screen-left" convention. The iOS client carries its own + * coordinate set; these are the web picker's only consumer. */ export const SITE_COORDS: Record = { ABDOMEN_UPPER_LEFT: { x: 43.5, y: 81 }, ABDOMEN_UPPER_RIGHT: { x: 56.5, y: 81 }, ABDOMEN_LEFT: { x: 43.5, y: 95 }, ABDOMEN_RIGHT: { x: 56.5, y: 95 }, - THIGH_LEFT: { x: 43, y: 140 }, - THIGH_RIGHT: { x: 57, y: 140 }, - UPPER_ARM_LEFT: { x: 26.2, y: 72 }, - UPPER_ARM_RIGHT: { x: 73.8, y: 72 }, + THIGH_LEFT: { x: 40, y: 140 }, + THIGH_RIGHT: { x: 60, y: 140 }, + UPPER_ARM_LEFT: { x: 23, y: 78 }, + UPPER_ARM_RIGHT: { x: 77, y: 78 }, }; /**