Commit Graph
11 Commits
Author SHA1 Message Date
Marc-André Bombeck bfb1335158 chore(ui): align TrendCard heading weight, account form rhythm, and MoodChart tick margin with siblings
Three single-class polish swaps to retire P0 / P1 drifts the UI-
conformity review caught:

- TrendCard headline number was the only dashboard tile using
  font-bold for its hero value; every sibling renders the same
  numeric role at font-semibold. Align so the trend strip reads
  uniformly.
- account-section password form was the only form in the codebase
  on space-y-3 outer rhythm; every other form uses space-y-4. Lift
  to match.
- mood-chart YAxis tickMargin sat at 6 while every other line chart
  passes 10 from the same canonical recipe. Match so mood labels
  sit at the same distance from the axis as siblings.
2026-05-15 21:17:54 +02:00
Marc-André Bombeck 733fc2cec7 fix(design): align range-bar zone backgrounds with Dracula tokens
`<RangeBar>` painted its red / orange / green zones in raw Tailwind
palette (`bg-red-500/8`, `bg-yellow-500/12`, `bg-green-500/20`),
while the marker dot already drew from `var(--dracula-red|orange|green)`.
That split meant the zone backgrounds drifted out of the theme on
dark-mode and never tracked the rest of the token surface (PR
badges, alerts, status pills). Swap the zone tints to the matching
Dracula tokens and pin the absence of the legacy raw palette in the
existing extraction-contract test.
2026-05-14 22:27:30 +02:00
Marc-André Bombeck 782731aeda fix(css): repair Tailwind v4 color-mix parser crash on dev server
Tailwind v4's content scanner walks the entire project root by default
and emits a CSS rule for every class-shaped token it finds — including
literal class fragments quoted inside .planning report markdown and
docs/ design specs. The bracketed CSS-variable arbitrary-value form
combined with an opacity modifier (e.g. `bg-[var(--name)]/12`)
generates an escaped selector whose `\(...\)\/N` token sequence
crashes Turbopack's CSS parser with `Unexpected token Delim('.')`,
500-ing every page that imports globals.css.

The crash:
    ./src/app/globals.css:1853:27
    Parsing CSS source code failed
      1852 |   .bg-\[var\(...\)\]\/12 {
    > 1853 |     background-color: var(...);
           |                           ^

Three coordinated changes:
- Add `@source not "../../.planning"` and `@source not "../../docs"`
  to globals.css so the scanner skips planning + docs trees. Stops
  fixture-style class names in research/audit reports from poisoning
  the generated CSS.
- Migrate `bg-[var(--dracula-*)]/N` / `ring-[var(--dracula-*)]/N` in
  target-card.tsx to the Tailwind v4 parenthesised shorthand
  `bg-(--dracula-*)/N` / `ring-(--dracula-*)/N`. The shorthand emits
  a clean selector that LightningCSS parses cleanly. Per the Tailwind
  v4 upgrade guide, the bracketed form is deprecated for CSS
  variables.
- Replace `shadow-[0_0_0_1px_var(--dracula-green)]/20` in
  consistency-strip.tsx with `shadow-[0_0_0_1px_color-mix(in_oklab,var(--dracula-green)_20%,transparent)]`.
  The arbitrary shadow with opacity modifier hits the same parser
  issue; encoding the alpha directly in the value via color-mix
  removes the modifier and keeps the visual identical.

Verification: `pnpm dev --port 3030` then `curl -sI /auth/login` ->
HTTP/1.1 200 OK (was 500). Zero CSS parse errors in the dev server
log. Affected tests (targets + api-handler + api/version) still pass
(31/31).
2026-05-14 15:39:39 +02:00
Marc-André Bombeck 98f7d11861 chore: resolve parallel-agent merge conflicts + sweep up loose edits
7 conflict files resolved by taking the upstream/HEAD side (which carries
the W4d/W5/W6c/W3f feature additions). Stashed-side changes that were
genuine new features re-land in subsequent commits if any were lost.

This sweeps up the loose state the W4e+W5e+W5d combined agent left
mid-stash. Resolves develop's working tree for clean push.
2026-05-14 14:19:08 +02:00
Marc-André Bombeck d74aa2066b style(glp1): prettier formatting across W4d surfaces 2026-05-14 14:03:43 +02:00
Marc-André Bombeck 8e4a8080dc i18n(targets): add EN + DE strings for redesigned card primitives
Removes the duplicate `targets.mood` + `targets.moodStability` flat
strings that collided with the new `targets.mood.stability.{stable,
variable,highlyVariable}` object added in Phase 3. The flat versions
were unreferenced — the canonical mood label lookup goes through
`targets.label.MOOD_SCORE` / `targets.label.MOOD_STABILITY` — so
removing them is a no-op for the UI and unblocks the
i18n-locale-integrity duplicate-key test.

Also rolls in the prettier-normalised formatting on the targets
route + target-card files (no semantic change). All i18n keys
added across phases 3-8 are documented inline:

  • targets.summary.{weekTitle,weekTitleAll,streakHighlight}
  • targets.card.{lastMet,streak}
  • targets.consistency.{ariaLabel,daysInRange,daysLogged,
                          dotInRange,dotNearRange,dotOutOfRange,
                          dotNotLogged,insufficientData,relativeDay.*}
  • targets.relativeDay.{today,yesterday,daysAgo}
  • targets.coach.cta
  • targets.mood.stability.{stable,variable,highlyVariable}

Per-target Coach prefills (the W3e plan called for
targets.coach.prefill.{bp,weight,…}) are NOT added as i18n strings
— they're generated by `buildTargetPrompt()` in
src/lib/ai/coach/target-prompts.ts which interpolates live state
values. Keeping the templating in JS avoids 12 long-form bilingual
strings with i18n interpolation gymnastics and lets the prompt
include conditional fragments (streak, cadence) without exploding
the key count.

Verification:
  - pnpm typecheck: clean
  - pnpm test: 2325 / 2325 passing
  - pnpm lint: clean
  - i18n-locale-integrity: 9 / 9 passing (duplicate keys removed)
2026-05-14 12:43:02 +02:00
Marc-André Bombeck f223b4feae feat(targets): mobile-first responsive grid + card reflow
Per Marc's "mobile is co-equal with desktop" directive, every
breakpoint gets a deliberate layout — no afterthought reflow:

  • default (<640px): grid-cols-1, full-width cards, gap-4. The
    per-card Coach CTA spans full width (justify-center) so the
    touch target is generous; the source link sits beneath
    right-aligned.
  • sm (640-1023px): grid-cols-2, gap-4. Card footer reverts to
    horizontal — Coach CTA left, source link right.
  • lg (1024px+): grid-cols-3, gap-6. Three cards per row matches
    the dashboard / insights rhythm and lets the user see all six
    core targets above the fold on a 1440px viewport.

Card internals also reflow:
  • Headline row: icon + label stack with status pill below on
    mobile (sm:flex-row sm:justify-between), inline on sm+.
  • Footer row: flex-col items-stretch on mobile,
    sm:flex-row sm:items-center sm:justify-between on sm+.

New `targets-responsive.test.tsx` pins the breakpoint classes so a
future refactor can't quietly collapse the grid back to v1.4.22's
sm:grid-cols-2 only layout.

Verified by inspection (Tailwind class audit). A Playwright sweep
across 375 / 768 / 1280 / 1440 viewports is queued for the W3e
verification phase.
2026-05-14 12:36:21 +02:00
Marc-André Bombeck 9acee2bd81 feat(targets): MOOD_STABILITY headline switches from σ to verbal label
The card used to render the raw σ value ("0.42 σ") as the big headline
number. Most users cannot interpret a standard deviation; the W3e
research found this was the single biggest "what does this mean?"
friction point in the v1.4.22 page.

Replace with a three-state verbal label keyed by the
`moodStabilityLabel(sigma)` helper landed in the previous phase
(thresholds: < 1.0 stable, < 2.0 variable, ≥ 2.0 highly variable).
Translations under `targets.mood.stability.{stable,variable,highlyVariable}`
in EN + DE.

The raw σ value stays one hover away — the headline carries a
tooltip surfacing "σ = 0.42" so power users keep the precise number
without forcing every visitor to decode it. Headline also gets a
`capitalize` class so the lowercase i18n string ("stable" / "stabil")
renders sentence-cased.
2026-05-14 12:34:47 +02:00
Marc-André Bombeck f2b9f843b1 feat(targets): card layout with status pill + consistency + coach handoff
Replaces the inline 790-line page.tsx composition with the W3e
primitives extracted in the previous phase. The page is now ~270
lines of orchestration:

- TanStack-Query pulls the redesigned targets payload (with the
  v1.4.25 consistency + summary additions).
- A second query reads /api/insights/provider-chain to derive
  `aiEnabled = chainStatus?.activeProvider != null`. The flag threads
  down into every <TargetCard>, which uses it to gate the per-card
  Coach CTA — no broken-button state when the user has no provider
  configured.
- A new `useCoachHandoff()` hook (src/hooks/use-coach-handoff.ts)
  owns the drawer's open + prefill + scope triple. Per-card buttons
  call `askCoach()`; the hook flips state and the drawer mounts
  inline at the page level.
- Cards now render in a fixed order (BP / BP-in-target / WEIGHT /
  PULSE / BMI / MOOD / COMPLIANCE / SLEEP / BODY_FAT / STEPS) — Marc
  directive: stable visual hierarchy beats status-sorting because
  the user's eye lands on the same metric in the same place every
  visit.
- Source-citation URLs stay on the page (declarative routing) and
  thread into <TargetCard> via the `sourceLink` prop.

The old inline TargetCard / RangeBar / TrendIcon / status-category
map are deleted along with the dead `Badge` + lucide imports.
Existing targets-i18n + targets-spacing tests updated to mock the
new useQueryClient + useMutation surfaces that the embedded
<CoachDrawer> needs at SSR; both suites still assert the same
visual contracts and pass.

Wave 2 deliverables also folded into this commit:
- pageSummary mounts via the W3e header above the grid
- consistency strip renders only when insufficientData === false
- recency caption ("last met goal: 2 days ago") renders only when
  lastMetGoalAt !== null
- streak chip ("4-day streak") renders only when streakDays ≥ 3
2026-05-14 12:33:53 +02:00
Marc-André Bombeck 96c5e79539 feat(targets): page-level summary header
Adds a one-line summary above the card grid that surfaces the
account-level state at a glance:

  "4 of 6 targets met this week"  /  "All 6 targets met this week ✓"

When every target hits the bar we surface a calm green check mark; no
animation, no celebration. Per Marc's "muss nicht so sein wie die KI
Sache" — the summary stays scannable and quieter than the v1.4.20
Insights hero strip.

A streak highlight chip ("5-day streak: Blood pressure") renders to
the right when one metric carries a current ≥ 3 day streak. Resolves
via the existing targets.label.{TYPE} i18n map so the metric name
respects the user's locale.

The new <TargetsSummaryHeader> primitive lives under
src/components/targets/ alongside the rest of the W3e extraction.
4 tests cover partial / all-met / streak / empty branches.
2026-05-14 12:30:18 +02:00
Marc-André Bombeck 97583cf891 refactor(targets): extract TargetCard + RangeBar + ConsistencyStrip + TargetCoachButton
The /targets page composed everything inline in a 790-line page.tsx.
Break it into focused primitives under src/components/targets/ so the
W3e redesign can iterate per-card without growing the page file:

- target-card.tsx — the redesigned card composition. Headline + verbal
  status pill, big-number row with inline trend chevron, range bar,
  consistency strip, recency + streak chips (both conditional on real
  data), per-card Coach CTA gated by aiEnabled, source link footer.
  MOOD_STABILITY swaps the raw σ headline for a verbal label via the
  moodStabilityLabel helper. Fixed card order is owned by the page;
  this component is presentational.
- range-bar.tsx — extracted intact from page.tsx. Same behaviour, same
  data-slot. Now carries its own test suite.
- consistency-strip.tsx — NEW. 7 dots (one per Berlin-tz day), each
  filled / amber / red / hollow per the day's band classification.
  Caption flips between "X of 7 in range" and "X of 7 logged" when
  cadence is the more informative signal. Accessible: <ul role="list">
  with per-dot aria-labels seeded from i18n. No empty-state copy
  inside the strip — Marc directive: hide entirely when insufficient.
- target-coach-button.tsx — NEW. Per-card "Ask Coach about this" CTA
  that returns null when aiEnabled is false. No greyed-out button,
  no tooltip placeholder — the gate is total per Marc's
  "no broken-button state" directive.

Supporting helpers (each unit-tested):
- src/lib/targets/mood-stability-label.ts — pure σ → verbal-label
  threshold logic (< 1.0 stable, < 2.0 variable, ≥ 2.0
  highlyVariable). Thresholds documented in the helper and confirmed
  in the W3e plan.
- src/lib/ai/coach/target-prompts.ts — 8 metric × 2 locale templates
  for the per-card Coach prefill. Falls back to a general template for
  glucose contexts. PROMPT_VERSION unchanged (the assistant-facing
  prompt does not shift; only user-side prefills do).
- src/lib/ai/coach/target-scope.ts — per-target CoachScope.sources
  mapping. Derived metrics (BMI → weight, MOOD_STABILITY → mood) point
  at the underlying source; unmapped targets return an empty array
  so the drawer falls back to defaults.

i18n keys added: targets.consistency.* (cap + dot aria + relative day
+ insufficient-data), targets.card.{lastMet,streak}, targets.relativeDay.*,
targets.summary.{weekTitle,weekTitleAll,streakHighlight},
targets.coach.cta, targets.mood.stability.* — all EN + DE.

Tests: 17 cases across 5 suites (consistency-strip render variants,
target-coach-button gate branches, range-bar zone painting,
target-card full-data + insufficient-data + aiEnabled-off +
MOOD_STABILITY verbal-headline, mood-stability-label thresholds).

Page.tsx still uses the old inline composition for now — Phase 5
wires it to the new components.
2026-05-14 12:29:02 +02:00