Commit Graph
7 Commits
Author SHA1 Message Date
Marc-André Bombeck 8f3a95463d fix(design): Dracula tokens + Romance BP abbreviations + therapy-timeline polish
- GLP-1 medication card streak flame: `text-orange-400` →
  `text-dracula-orange`. The hardcoded Tailwind shade was skewing
  warm against the rest of the card.
- /insights/medikamente compliance counters: `text-green-500` /
  `text-orange-500` / `text-red-500` → `text-dracula-green` /
  `-orange` / `-red`. Same drift in three sibling spans on one
  surface; replace as a set.
- Side-effect affordance: `Smile` icon read as a positive
  affordance for a *negative* logging surface. Switch to
  `Stethoscope` so the icon matches the clinical context.
- Health Score componentBp: FR `BP` → `TA`, IT `BP` → `PA`,
  ES `BP` → `PA`, PL `BP` → `RR` — the native compact forms
  used in clinical practice. DE "Blutdruck" + EN "BP" unchanged.
- Therapy-timeline `<li>` key: array index → composite
  (`kind-date-medicationName`). Prevents DOM-reuse bugs when a
  dose change is logged mid-window and reorders the list.
- Therapy-timeline inline icons: `aria-hidden="true"` on the
  `<Pill>` injection glyph and the dose-arrow `<ArrowUp>` /
  `<ArrowDown>` so screen readers stop announcing "image"
  mid-sentence.
- Medication-form Dialog tap targets: the schedule-row dropdown
  trigger (`h-7 w-7`) and the New-schedule button + the four
  schedule inputs (`h-8`) promoted to `h-11` so the dense
  schedule grid honours the 44px floor. The labels stay compact;
  the controls grow.
2026-05-14 16:57:56 +02:00
Marc-André Bombeck 72fcdc656f fix(a11y): WCAG touch-target floor + ARIA pairing across W4–W9 surfaces
Extends the W8.3 44px touch-target rule and the W8e accordion ARIA
contract to surfaces that shipped without them:

- Insights tab-strip pills: pills are primary navigation; promote to
  `min-h-11` so a thumb tap on "Stimmung" cannot bleed into the
  neighbour two pixels below.
- Injection-site picker: raise the invisible hit-target radius from
  11 to 12 SVG units so the rendered diameter (\~25.6 CSS px)
  clears the WCAG 2.5.8 Level AA 24×24 floor. The abdomen left/right
  pair at \Δx=24 caps the upper bound — larger circles would
  collide.
- Source-priority reorder arrows (per-metric + device-type axes):
  promote four `h-7 w-7` icon buttons to `h-11 w-11`.
- Sub-page heading: replace the bare `focus-visible:outline-none`
  with the same ring vocabulary used by insights pills + the Coach
  drawer so sighted-keyboard users get a visible indicator on
  programmatic focus.
- Source-priority per-metric + device-type accordions: pair
  `aria-expanded` with `aria-controls` + matching `id` on the
  panel, mirroring the W8e Provenance contract.
- Maintainership banner dismiss button: 22×22 hit zone replaced by a
  44×44 inline-flex button that keeps the same icon size.
- Injection-site picker group: `aria-label` was leaking the
  cadence-preset string; introduce
  `medications.injectionSitePickerAriaLabel` and translate it
  across all six locales.
- Body-outline SVG: hardcoded English "Body outline" replaced by
  `medications.injectionSiteBodyOutlineAriaLabel` with native
  translations in DE/FR/IT/ES/PL.
2026-05-14 16:55:10 +02:00
Marc-André Bombeck e14466e238 fix(i18n): sleep headline suffix translates across all six locales
The 'per night' caption beneath the sleep headline was branching on
`locale === "de"` and falling through to English for FR/IT/ES/PL.
Move the string into `insights.sleep.headlineCaptionSuffix` with
native translations (de: pro Nacht, fr: par nuit, it: per notte,
es: por noche, pl: na noc) and resolve it through `t()`.
2026-05-14 16:47:06 +02:00
Marc-André Bombeck b48d05a8be feat(health-score): inline provenance accordion with source attribution
Adds a tap-to-expand 'Driven by' panel under the four component
sub-bars inside <HealthScoreCard>. Each row pairs the existing
value bar with a second weight-share bar tinted in the Coach
provenance accent plus a source pill (manual / withings /
appleHealth / mixed / none). Rows sort by effective weight
descending so the biggest contributor leads; null components sink
to the bottom and dim to 50 % opacity.

A 'Provisional' header badge fires when fewer than half of the
configured inputs carry data, and a role=status 'mixed sources'
banner surfaces above the rows whenever at least one component
blends multiple ingest paths.

i18n keys land under insights.healthScore.provenance.* across all
six locales (en / de / fr / es / it / pl with formal register).
The locale-integrity drift-guard auto-discovers the new keys.
14 new RTL-style SSR tests pin the contract: aria-expanded toggle,
aria-controls/id pairing, sort order, source-pill localisation,
mixed banner gating, empty-state dimming, provisional threshold,
asOf tooltip forwarding, and footnote rendering.
2026-05-14 16:21:32 +02:00
Marc-André Bombeck b324e6c41d feat(dashboard): VO2 max trend tile (opt-in, secondary-metric pattern)
Adds the VO2 max strip tile to /, matching the existing
secondary-metric convention (sleep / steps / glucose / oxygen
saturation are all default-invisible + opt-in via Settings →
Dashboard).

Wire-up:
  - src/lib/dashboard-layout.ts: vo2Max appended to
    DASHBOARD_WIDGET_IDS and DEFAULT_DASHBOARD_LAYOUT.widgets with
    visible=false / tileVisible=false. The /api/dashboard/widgets
    Zod enum is derived from the same array so the toggle is wired
    automatically.
  - src/app/page.tsx: vo2Summary from data?.summaries?.VO2_MAX
    (auto-populated by /api/analytics; the route iterates the full
    measurementTypeEnum.options list, no backend change needed),
    hasVo2 data-floor gate, showVo2Tile combined gate, and the
    TrendCard push next to showStepsTile. directionSentiment is
    up-good (higher VO2 max is better) and the unit string runs
    through the t() helper so the iOS app session can localise it.
  - src/components/settings/dashboard-layout-section.tsx: vo2Max
    label key added to WIDGET_LABEL_KEYS so the Settings → Dashboard
    toggle row renders the localized label.
  - messages/{en,de,fr,es,it,pl}.json: dashboard.vo2Max + vo2MaxUnit
    keys. "VO₂ max" is universal across all six locales (scientific
    abbreviation, no translation needed); the unit string is
    consistent SI notation.

No chart-row card — that's an explicit v1.5 deferral per the W8d
outline §6.4. The tile lights up the moment the user accumulates any
VO2 sample (Withings ingest or the upcoming iOS Health passthrough).
2026-05-14 16:04:46 +02:00
Marc-André Bombeck 6a7da4b2cb fix(i18n): backfill audio-exposure + time-in-daylight + comparison hint keys
W8d.1 follow-up — the first commit landed the schema + code surface but
the messages/*.json edits did not persist through the editor sync. This
commit completes the locale parity:

  - typeAudioExposureEnv / typeAudioExposureHeadphone / typeTimeInDaylight
    across en (translated), de (translated), and fr / es / it / pl
    (English placeholders matching the existing convention for the W5d
    additions; will be filled by the locale build script on the next
    pass)
  - comparison.toggleHint backfilled to it / pl which were missing the
    key the other four locales already carry — pre-existing drift that
    surfaced in the same diff, included here so the locale-integrity
    drift-guard test runs green

No code change. The drift-guard tests
(measurement-list-meta + i18n-locale-integrity) now pass.
2026-05-14 15:52:44 +02:00
Marc-André Bombeck 3c4bbdef4f feat(i18n): MaintainershipBanner notice + foundation for 4 new locales
Add the four AI-initial locale bundles (FR / ES / IT / PL) and a
small dismissible MaintainershipBanner strip that paints at the top
of the auth shell whenever the active locale falls outside the
maintainer-curated set (currently DE + EN). The banner names the
AI-initial provenance and links to a GitHub translation-feedback
template so users can contribute fixes back. Dismissal persists
per-locale in localStorage so dismissing the FR banner does not
hide the IT banner once the user switches language.

The bundles are generated by scripts/i18n/build-locale.py — a layered
walk of messages/en.json that applies a vocabulary lookup keyed on
the lowercase EN value, preserves every {interpolation} placeholder
and <tag> marker verbatim, and falls through to the English value
for long-tail prose the script can't translate. The banner advertises
this status so users know what they are looking at. All four locale
files pass the i18n-locale-integrity test verbatim — full key
parity to en.json, no empty values, no TODO/FIXME placeholders,
no value-equals-last-segment matches.

Foundation work bundled with the banner so the build never lands in
a half-wired state:
- locales array extended to ["de","en","fr","es","it","pl"] with
  matching localeLabels + MAINTAINED_LOCALES set + isMaintainedLocale
  helper in src/lib/i18n/config.ts.
- INTL_LOCALE_MAP and parseLocaleFromAcceptLanguage in
  format-locale.ts gained the four new tags so SSR locale resolution
  and Intl number/date formatting work end-to-end.
- i18n/context.tsx and i18n/server-translator.ts import the four new
  JSON bundles alongside DE + EN.
- Coach (src/lib/ai/coach/system-prompt.ts) and the strict insights
  generator (src/lib/ai/prompts/insight-generator.ts) ship the EN
  system prompt with a one-line REPLY LANGUAGE footer for the four
  AI-initial locales. The DE branch keeps its hand-curated body
  verbatim. Routing through the EN system prompt preserves every
  safety contract (zero hallucinations, no dose-prescription,
  evidence-block sentinel format) without re-validating four new
  language bodies.
- Five locale-parameter signatures (no-key-fallbacks, glp1-plateau,
  time-window-format, target-prompts, medication-form) widened
  from "de" | "en" to the canonical Locale type. Helpers that
  ship DE+EN copy route non-DE locales to the EN body, mirroring
  the same fallback chain the JSON bundles use.
- OpenGraph alternateLocale on the root layout lists the four new
  BCP-47 tags so social-link previews advertise the broader
  language coverage.

Tests:
- 4 new SSR tests pin the locale gate (EN/DE hidden, FR/ES/IT/PL
  visible by default, dismissed flag honoured) and the re-exported
  isMaintainedLocale helper.
- parseLocaleFromAcceptLanguage gains a FR/ES/IT/PL coverage block
  alongside the existing DE / EN / unknown branches.
- The existing i18n-locale-integrity suite auto-discovers each new
  messages/<locale>.json and runs the 25-sub-test parity + value-
  quality battery against it — all six locales pass.

LocalePicker is data-driven (settings and onboarding both iterate
the locales array) so promoting the array to 6 entries grows both
dropdowns without per-call-site changes. The PDF doctor-report
renderer is t()-driven for every label, so the four new JSON
bundles give that renderer FR/ES/IT/PL output without touching
doctor-report-pdf-core.ts.
2026-05-14 15:40:34 +02:00