Commit Graph
395 Commits
Author SHA1 Message Date
Marc-André Bombeck c443e2db21 Merge remote-tracking branches 'origin/wave/v1214-b' and 'origin/wave/v1214-ac' into release/v1.21.4 2026-06-27 12:09:32 +02:00
Marc-André Bombeck 5117bc603c feat(coach): settings in composer menu, dismissable seeded opener
Add a Settings entry to the composer's "+" actions menu and remove the
page-toolbar gear; the drawer header keeps its own gear. Let the seeded
"worth a look" opener fill the composer column and give it a per-day
dismiss control. Route the Conversations action to /coach/conversations
and drop the in-page history drawer.
2026-06-27 11:23:47 +02:00
Marc-André Bombeck 31b7fa6fc6 feat(coach): move conversation history to a dedicated page
Replace the left slide-in conversations drawer with a standalone
/coach/conversations page: a search field above a recency-grouped list
(Today / Yesterday / This week / Earlier) built on the existing
conversations query. Selecting a row routes to /coach?c=<id>, reusing the
deep-link open mechanism — no new API, no new state. The drawer's
Conversations affordance now navigates to the new route, and the Coach
page drops its ?view=conversations in-page drawer handling.
2026-06-27 11:21:32 +02:00
Marc-André Bombeck 61e21ea28b fix(coach): show only the animated dots while the Coach is thinking
The pre-stream thinking bubble dropped the visible "Thinking…" word and now
renders just the three bouncing dots. The label stays in an sr-only span so
the thinking state is still announced to screen readers.
2026-06-27 10:33:11 +02:00
Marc-André Bombeck 2c33faa648 chore(insights): align the ambient-Coach file comments to the shipped version
The A1–A3 source files carried v1.22.0 docstrings from before the work was
re-cut as the v1.21.2 point release. Comment-only sweep so blame and the
in-file version tags read consistently with the shipped version.
2026-06-27 00:48:40 +02:00
Marc-André Bombeck 1de72d045e fix(insights): localise the Coach scope pill and clarify the return-to-baseline copy
The "the Coach is already on …" pill resolved its metric label through the
brand-free English domain vocabulary, so it read in English on every
non-English locale. It now prefers the existing localised measurements.type*
name for each measurement-backed source, keeping the English phrase only for
the gait/audio long tail that rarely becomes a scoped launch. Also rewords the
English return-to-baseline line so the day count reads as the in-range run
length rather than the time since.
2026-06-27 00:48:29 +02:00
Marc-André Bombeck 4d05bcf8d9 feat(insights): wire the score-card tension + return-to-baseline and the briefing recall
The hero score card and the daily briefing accept tension, returnToBand, and
memory props, but nothing populated them server-side, so the lines never rendered.

Resolve all three server-authoritatively on the dashboard snapshot DTO (iOS rides
the same payload):

- tension — fires when the readiness composite's contributors disagree (a strongly
  favourable one alongside a strongly unfavourable one), reusing the readiness
  components the wellness ring grades and the shared disagreement detector.
  Suppressed under a clinical red-flag (a coincident-deviation that no illness
  explains) so the red-flag path is never reconciled into a calm verdict.
- returnToBand — runs the streak detector across the salient vitals and surfaces
  at most one metric that has come back inside its own personal range after a
  genuine prior out-of-band run.
- memory — maps the machine-derived coach memory block to the briefing's recall +
  forward-look.

The blocks are locale-agnostic (contributor keys + a metric type); the insights
hero localises them through the existing readiness-contributor and metric-name
labels before handing the cards their resolved strings. Each value omits rather
than guesses. The snapshot cache key now carries the locale for the recall prose.
2026-06-27 00:28:56 +02:00
Marc-André Bombeck 6c1a4b7f45 merge: wave/v1220-narrative into release/v1.21.2 2026-06-26 23:58:51 +02:00
Marc-André Bombeck f5195cfcda merge: wave/v1220-coachui into release/v1.21.2 2026-06-26 23:58:51 +02:00
Marc-André Bombeck fe6c2e281b feat(insights): ambient narrative presence — callbacks, tension verdict, streaks, MI moves
Add a deterministic narrative layer the Coach narrates rather than invents:

- Briefing callbacks + forward-looks: the daily briefing recalls the prior
  period and points ahead, anchored to a figure, one callback per surface.
- Tension Verdict: when the readiness composite's contributors disagree
  (good sleep but a rising resting pulse), surface a brief honest internal
  read on the health-score card. A real clinical red-flag is never reconciled
  away into a calm verdict.
- Streaks + return-to-baseline: a pure detector over a DAY-bucket series
  closes a worry once a metric returns inside the personal MAD band after a
  genuine out-of-band run; a gap day breaks a streak.
- Coach prompt: add develop-discrepancy, roll-with-resistance, and an
  anti-persuasion clause; ratchet PROMPT_VERSION.

The tension flag and streak event are computed server-side; the card lines
render resolved fields and the model narrates them.
2026-06-26 23:57:37 +02:00
Marc-André Bombeck 9136a9bdba feat(coach): make launch scope visible and seed the blank state
Replace the hidden composer prefill with a visible scope affordance: when
the Coach is launched narrowed to a metric, the hero shows a scope pill
naming that metric plus a tappable seed question, and the drawer sources
rail surfaces the same scope line. When the Coach opens unscoped, resolve
today's most notable derived signal server-side and offer it as a tappable
opener; fall back to the neutral greeting when no signal crosses the gate,
so the blank box is never cold and never fabricated.

Add an internal GET /api/insights/coach/seeded-question that reuses the
briefing's derived-signal detector to select the opener server-side; the
client renders the resolved DTO.
2026-06-26 23:56:56 +02:00
Marc-André Bombeck cbf903841c feat(insights): add Coach-read strip above metric charts
Render a compact two-line "Coach read" above each metric sub-page chart:
an own-baseline line ("Your usual range is X–Y; today's Z sits within /
above / below") and one top lagged-association line from discovered
correlations, framed descriptively. It makes a metric read seen-in-context
rather than isolated.

The strip is server-authoritative: a new GET /api/insights/coach-read
resolves the band via the median ± k·MAD baseline engine (with its 7-day
history floor — below it the strip says "still learning your range" rather
than inventing a range) and the strongest on-metric driver from the
existing FDR-controlled, effect-size-floored correlation discovery (no
lowered floor; the line is omitted when nothing clears it). The client
component only renders the resolved DTO, so web and iOS read identical
numbers. DTO shapes and the pure selection logic live in a client-safe
sibling shared by the component and the unit test.

Wired through the HealthKit metric scaffold (every generic metric page) and
the bespoke weight, blood-pressure, and pulse pages.
2026-06-26 23:56:28 +02:00
Marc-André Bombeck 03e1935c8a merge(LEARNUI): Learn-links registry, component, and surface wiring 2026-06-26 13:23:28 +02:00
Marc-André Bombeck 8c58b5b1c7 Wire Learn pointers into vitals, glucose, resilience, and lab surfaces
Add a single discreet LearnMoreLink to the per-vital baseline tiles
(via a new footer slot on the shared tile), the glucose clinical panel,
the resilience tile, and the lab biomarker detail. Each pointer is
registry-resolved and renders nothing when the concept has no mapped
guide, so only surfaces with a real article show one.
2026-06-26 13:22:33 +02:00
Marc-André Bombeck 174c26afc9 refactor(coach): streamline the new-chat page composer
Reduce the full-page Coach new-chat surface to a calmer layout. Drop the
toolbar Conversations and New chat buttons and place the settings gear in
the top-right corner; both removed actions remain in the composer's + menu.
Collapse the greeting to one line and remove the gear from the composer so
its front is only the + actions menu, leaving a single baseline row of
[+] textarea [mic] [send].
2026-06-26 13:15:58 +02:00
Marc-André Bombeck 9a3049a6cf fix(coach): phrase the daily-limit reset as midnight UTC
The budget-exceeded copy named "00:00 UTC", which reads as a wrong local
clock for any non-UTC user (Berlin midnight UTC is 01:00/02:00 local) and
drove confused "the limit resets at the wrong time" reports. Reword
dailyLimitBody across all six locales to "midnight UTC", matching the
existing errorBudget wording, so the figure is honest regardless of the
reader's timezone.
2026-06-26 12:52:52 +02:00
Marc-André Bombeck 93433c3197 feat(coach): add "ask the Coach about this" to insight cards
Add a discreet, on-brand action to the high-value cards (briefing,
recommendation, correlation, status/assessment, health score, period
narrative) that opens the Coach pre-scoped to the card topic with a
seeded question. The action self-gates on the operator flag and the
per-user opt-out, never tints the card, and surfaces once per card.
2026-06-26 12:38:03 +02:00
Marc-André Bombeck 44796d1c0e feat(coach): carry metric scope into the Coach launch
Make the launch context's metric scope live instead of discarding it.
A conversation opened from a metric surface now narrows its snapshot to
that metric with a data-aware seed question, and the global FAB inherits
the page's ambient scope so drilling into a metric and tapping it no
longer opens a blank chat. The scope threads through the drawer into the
first turn of a fresh conversation.
2026-06-26 12:37:54 +02:00
Marc-André Bombeck 559db472fc Merge Oura resilience tile into release/v1.19.2
Surface the captured Oura resilience band (limited…exceptional) as a calm
tile on the recovery surface, with a quiet above/in-line/below-average cue
and a learning state while the series is sparse. Server-authoritative read
of the existing summaries slice; neutral card chrome.
2026-06-21 20:58:42 +02:00
Marc-André Bombeck 7f40860891 feat(insights): surface the Oura resilience band on the recovery page
v1.19.0 ingests Oura's daily resilience level end-to-end (RESILIENCE,
ordinal-encoded limited=1 … exceptional=5) but no surface rendered it.
Add a calm resilience tile to the recovery page: it names the latest
band, gives a one-line description, and a quiet "vs the recent average"
cue once a few days exist, with a learning note while the series is
still sparse. Resilience is a categorical band, not a continuous score,
so it reads as a dedicated readout rather than a misleading line chart.

Server-authoritative — the tile reads the stored summaries slice (latest
+ mean, both computed server-side) and never re-derives the value. Calm
posture: one neutral card, no green-when-good / red-when-low tint. The
band label and trend cue carry the read in muted text only.

Self-gating: renders nothing without a reading, and counts toward the
recovery-page data gate so an Oura-only account no longer sees the empty
note. Copy lands in all six locales.
2026-06-21 20:57:30 +02:00
Marc-André Bombeck 1bf8f3d661 refactor(coach): drop the unread streaming startedAt and reasoning state
The thinking disclosure was removed, so the coach streaming bubble no
longer reads startedAt or reasoning. Both fields stayed populated by the
send hook but nothing consumed them. Remove them from
CoachStreamingMessage and every writer, accept the additive reasoning
SSE frame as an explicit no-op so the wire type stays recognised, and
update the thread-test fixture. Streaming is otherwise unchanged.
2026-06-21 20:54:39 +02:00
Marc-André Bombeck 62a4dfdf7e Merge S1/S2 sleep average + mood saturation into release/v1.19.1
S1: add an average-per-night sleep card as a third peer beside debt and
chronotype, computed by a shared helper over the same scorable-night set
so the three cannot disagree; grid widens to three columns. S2: restore
full saturation on the two missed mood surfaces (calendar heatmap and the
time-of-day chart), keeping the quiet tint only for no-data cells.
2026-06-21 14:02:09 +02:00
Marc-André Bombeck bbff6ab143 fix(mood): full-saturate the time-of-day chart and mood calendar
The v1.19.0 saturation fix missed two surfaces: the mood-by-time-of-day
bars and the mood calendar still rendered populated values at 0.55
opacity, reading as a dimmed glitch. Drop the dimming on populated bars
and cells (and the legend swatches); keep the quiet tint only on the
no-data empty state.
2026-06-21 14:01:37 +02:00
Marc-André Bombeck c67bd942a2 feat(sleep): add average-per-night card beside sleep debt and chronotype
Compute the mean asleep minutes per night from the same canonical
per-night data the sleep-debt and chronotype cards read, exposed as a
new averagePerNight field on the sleep-rhythm DTO. Surface it as a third
peer card with identical chrome, so the three tiles sit side by side.
Calm partial state under a four-night floor; localised in six locales.
2026-06-21 14:01:30 +02:00
Marc-André Bombeck c6f4772606 fix(coach): default to new chat and never dead-end on conversations
Reverse the resume-most-recent entry default: opening the Coach now lands
on the new-chat hero. The only exception is an unread coach-initiated
message (nudge-status `unread`), which auto-opens the most-recent thread so
the user sees what the Coach wrote. The `?c=<id>` deep-link is unchanged.

Add a dedicated, always-visible Conversations button (plus a New chat
button) to the page surface so reaching past conversations no longer hides
in the composer menu. The drawer's Conversations handoff now routes to
`/coach?view=conversations`, which opens the history drawer on arrival, so
the side-view to full-view path always shows the list instead of a blank
pane.
2026-06-21 13:59:05 +02:00
Marc-André Bombeck a86abd3662 fix(coach): restore the typing animation for the live turn
Replace the elapsed-time thinking disclosure with the classic typing
indicator: while a turn is still thinking with no prose, the assistant
bubble shows the pulsing typing dots, then swaps to the streamed
word-by-word prose once tokens land. Drop the now-unused reasoning/
startedAt bubble props and the orphaned disclosure component.
2026-06-21 13:58:57 +02:00
Marc-André Bombeck 26230542ce fix(sleep): make the chronotype tile uniform with the sleep-debt tile
The chronotype card carried its own section heading on top of the title it
already renders, so 'Chronotype' appeared twice and the tile did not match
its sibling. Drop the standalone heading and bring the card to the same
shape as the sleep-debt tile: identical chrome and a bold primary value with
one secondary line. The chronotype band is the primary value at the same
scale the debt tile uses for its hours figure, with the natural sleep-
midpoint clock as the companion line.
2026-06-21 12:31:26 +02:00
Marc-André Bombeck c9b1631da7 fix(insights): paint mood bars at full saturation
The muted mood treatment (opacity-55 on the tag bars, fillOpacity 0.55 on the
distribution and weekday charts) read as a rendering glitch rather than a
deliberate calm tint. Drop the dimming so every mood bar paints at full
saturation in its level hue; the guard test now asserts the dimming stays gone.
A separate mood colour concept can be revisited later.
2026-06-21 10:16:06 +02:00
Marc-André Bombeck ead44e79d9 feat(coach): deep-link the Coach page into a conversation
The Coach page opened a blank new chat regardless of where the user came
from. Make it a deep-link target: a `?c=<id>` query param opens that
thread on mount, and absent it the page auto-opens the user's most-recent
conversation. "Most recent" is the `updatedAt desc` head of the rail
list, so the same thread opens on web and mobile.

The shared conversation surface grows two additive props
(`initialConversationId`, `autoOpenMostRecent`) that feed its existing
selection state; the history drawer's selection stays the single source of
truth. The drawer surface is unchanged — both props default off and the
rail list is only fetched when auto-open is requested.
2026-06-21 00:25:54 +02:00
Marc-André Bombeck 6975849bec Merge w11/perf into release/v1.18.11
Serve achievements via SWR and precompute Berlin day-keys to kill the per-row
Intl cost; switch dashboard/summary to cachedSwr; lazy-load three charts that
bypassed the dynamic boundary.
2026-06-20 19:05:27 +02:00
Marc-André Bombeck f3af5d361a perf(gamification,dashboard,insights): cut achievements cold rebuilds and lazy-load three charts
Achievements: the bucket was 60 s hard-TTL while the app-wide unlock
notifier polls every 2 minutes, so every poll landed on an expired
entry and fired a cold ~400 ms rebuild in the foreground. Add a
10-minute stale-while-revalidate window and read the route through
cachedSwr; the route already persists pendingUnlocks outside the cached
factory, so a stale serve never skips an unlock write.

Achievements builder: the vitals array ran through several full-array
passes, each re-deriving a Berlin day-key (and hour) per row via
Intl.DateTimeFormat. Compute each row's day-key once and thread the
parallel array into the green-day, weekly-consistency, engagement, and
hidden passes; the hidden pass reuses a single precomputed hour array.
Results are byte-identical to the per-row derivation (parity test added).

dashboard/summary: read through cachedSwr so the iOS-polled endpoint
serves its existing 1 h stale window instead of re-paying the cold
rebuild every 60 s on the plain cached path.

Charts: route the three recharts consumers that bypassed the shared
next/dynamic boundary (lab biomarker chart, sleep stage stacked bar,
recommendation rationale charts) through dynamic imports with matching
loading skeletons, taking recharts off the first-load JS for
/labs/[id], /insights/sleep, and the insights recommendations surface.
2026-06-20 19:04:31 +02:00
Marc-André Bombeck 1469336cfe feat(coach): rebuild the Coach page around a ChatGPT-style composer hub
Drop the dedicated page's top header bar and the conversation-history
strip. The composer now carries the controls: a leading actions menu
(new chat + open conversations) and a settings deep-link beside the mic
and send. Conversation history opens as a left slide-in drawer that works
on phone and desktop.

Pin the composer to one centred, max-width-capped column shared with the
thread so its width stays constant from the new-chat hero into an active
conversation. The drawer surface keeps its own header and body chrome
unchanged.
2026-06-20 18:57:18 +02:00
Marc-André Bombeck 649bfb3ea8 fix(dashboard): refresh briefing and layout on the Startseite without reload
The dashboard hero reads the briefing headline and the layout from the
server snapshot. Regenerating a briefing or saving the dashboard layout
only touched their own caches, so the snapshot kept stale data until its
own window lapsed and the Startseite looked unchanged until a manual
reload. Invalidate the dashboard snapshot on briefing regenerate and on
layout save/reset.

Harmonise the daily-briefing card body rhythm with its sibling insights
cards (space-y-3, matching correlation-card) so it no longer reads taller.
2026-06-20 16:51:18 +02:00
Marc-André Bombeck 398f5c3b6b fix(coach): make the composer mic reliable and never a dead tap
Always render the dictation mic so the affordance stays discoverable. On
a browser without the Web Speech API (or during SSR) render it disabled
with an explanatory tooltip instead of hiding it or leaving a control
that silently does nothing on tap. When a dictation start fails — denied
microphone permission, an insecure-context self-host, or no network for
the recogniser — surface a quiet toast rather than resetting in silence.
Add voiceUnsupported + dictateError strings across all locales.
2026-06-20 16:42:10 +02:00
Marc-André Bombeck 01b73f6379 feat(coach): make the full Coach page a single full-bleed surface
Drop the inner bordered card on the dedicated /coach page so the chat
reads as one continuous conversation rather than a window inside a
window, and remove the top "back to the drawer" minimize control (it is
pointless on the full page; the bottom-right FAB drawer remains the
separate compact surface). Also remove the two starter-question
suggestion chips below the new-chat composer, leaving a greeting +
composer hero that matches the reference new-chat layout. History stays
reachable through the existing conversation rail.
2026-06-20 16:41:58 +02:00
Marc-André Bombeck 0798373877 Merge v189/recon-a into release/v1.18.9
Hard-evict the dashboard snapshot on batch and Withings background sync so the
Startseite reflects synced readings on focus; withdraw the Oura daily_stress
mapping pending source-priority wiring; make OCR in-batch dedup independent of DB
visibility; widen the vision allowlist to Claude 3; null-guard the reasoning
frame; correct OpenAPI and cache-TTL drift.
2026-06-20 13:02:04 +02:00
Marc-André Bombeck cbc2187705 fix(coach): guard empty reasoning SSE frame against literal undefined
A reasoning frame with no text appended the string "undefined" to the
thinking disclosure. Coalesce evt.text to an empty string on both the
streaming and the trailing-flush paths.
2026-06-20 12:59:29 +02:00
Marc-André Bombeck a1f5ed625c fix(coach): cap starter-question chips to a single-line pill 2026-06-20 12:53:41 +02:00
Marc-André Bombeck 3505ffde24 Merge v189/coachui into release/v1.18.9
Rebuild the /coach surface as a centred-hero chat with a word-by-word fade, a
single thinking disclosure, and a per-message token footer; emit token usage on
the done frame and persist it via migration 0182; plumb an additive reasoning
frame with an elapsed-time fallback for non-reasoning providers.
2026-06-20 12:14:42 +02:00
Marc-André Bombeck 144bfbe553 feat(coach): centred new-chat hero on the page surface
Replace the cramped empty thread on the /coach page with a centred hero:
a greeting, the live composer beneath it, and HealthLog's own starter
questions as chips, over a faint purple atmospheric backdrop with one
staggered entrance reveal. The hero re-parents the existing composer
rather than forking it, so dictation, auto-grow, and the send/stop
control behave identically; the first send docks the composer to the
bottom and the conversation takes over. Gated to the page surface and
the fresh new-chat state, so the drawer, guided-question, and adopt
flows are untouched.
2026-06-20 12:12:54 +02:00
Marc-André Bombeck 88ee5bb4f8 feat(coach): word-fade stream, thinking disclosure, token footer
Stream the assistant turn word-by-word with a soft motion-safe fade
(StreamedProse), reduced-motion collapses to instant. Replace the
dots-plus-word placeholder with a single thinking indicator: a live
elapsed-time line that freezes into an auto-collapsed past-tense
disclosure, which surfaces a reasoning summary when one is present and a
factual fallback otherwise — never both a spinner and a Thinking word at
once. Add a quiet per-message token footer (count plus model, no cost)
fed by the live done.usage frame and persisted tokensUsed on reload.
Add the localised strings across all six bundles.
2026-06-20 12:05:31 +02:00
Marc-André Bombeck b0e6079858 Merge v189/fresh into release/v1.18.9
Invalidate the dashboard snapshot on measurement writes and refetch on focus so
fresh and background-synced readings surface without a hard reload; expose the
no-provider state on the briefing read with a connect-a-provider hint; align the
briefing footer caption.
2026-06-20 12:01:20 +02:00
Marc-André Bombeck 5cebc168df fix(insights): flag a stale briefing that has no provider to refresh it
An account with no AI provider configured still reads the last cached
briefing — the read path serves it without ever touching a provider — so
the days-old report rendered as current. The pregenerate cron runs fine;
the cache simply can never refill without a provider. Report provider
availability from the read GET and pair the briefing's relative age with a
discreet 'this can't refresh — connect a provider' hint linking to
Settings -> AI, on both the overview hero meta row and the briefing card
footer. The hint only shows when a briefing is actually rendered; the
empty-state already owns its own no-provider branch.

Also align the briefing card footer caption to the 11px scale the
adjacent period-in-review card uses, so the two overview footers read at
one size.
2026-06-20 12:00:21 +02:00
Marc-André Bombeck f46e9d3bb8 feat(coach): surface per-turn token usage on the chat stream
Carry an additive usage envelope on the SSE done frame
({ totalTokens, model }) and persist tokensUsed + model onto
coach_messages (migration 0182, nullable, no backfill) so the
per-message token footer survives a conversation reload. Add an
optional reasoning frame the client can render inside the thinking
disclosure when a reasoning-capable provider emits a summary; providers
without reasoning emit none. Extend CoachMessageDTO and regenerate the
OpenAPI contract. Server-authoritative: clients display the count, never
recompute it.
2026-06-20 11:58:58 +02:00
Marc-André Bombeck 979d1ced8d fix(insights): soften Coach FAB glyph to the on-accent token
Drop the pure-white Coach floating-action-button icon for the button's own
text-background on-accent token, so the glyph reads as a calmer mark on the
purple-to-pink gradient instead of a harsh white. Update the colocated
assertion to pin the glyph token.
2026-06-20 11:49:33 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck a371678460 test(coach): guard composer controls against the sub-44px floor
Assert the send and stop controls render the `size-11 sm:size-9` tap-
target pattern so a future restyle cannot quietly drop them back to the
36px size the audit flagged. Renders the real component and matches the
emitted class string, the same model as the bottom-nav size test.
2026-06-19 21:18:27 +02:00
Marc-André Bombeck e3071d4f0a fix(mobile): raise primary touch controls to the 44px floor
Sweep the primary touch controls that sat below the 44px tap-target
floor on phones onto the canonical `min-h-11 sm:min-h-9` (text) /
`size-11 sm:size-9` (icon) pattern, condensing back at `sm:` upward:

- Coach composer mic / stop / send icon buttons (were `size-9`).
- Dashboard hero CTA, link and action variants (were `size="sm"`).
- Vorsorge list-view "mark done" button (was `min-h-9`) and its kebab,
  which now uses `size="icon-lg"` so the rendered control is 44px
  rather than the icon variant's 40px.
- Illness list-view "log day" button (was `min-h-9`).
- Medication wizard Step 3 injection-sites disclosure (was `h-9`).

The Vorsorge dashboard-card capture action was raised alongside the
ListRow migration.
2026-06-19 21:18:19 +02:00
Marc-André Bombeck 54a2d19833 feat(ui): add shared ListRow primitive and adopt it for mini-cards
The bordered list-row "mini-card" appeared three times with three
shapes: the Vorsorge dashboard row (`rounded-md p-2.5`), the daily-
briefing rows (`rounded-md p-3`), and the measurement-list mobile row
(bespoke `rounded-lg p-3` div). Introduce `ListRow` (`rounded-lg border
p-3`, `asChild` per the shadcn convention) and migrate all three to it.
The primitive owns only the box shape; each caller keeps its layout,
colour, and interaction classes, so this is structural normalization
with no content or behaviour change.
2026-06-19 21:18:11 +02:00
Marc-André Bombeck d2be72d57a feat(mobile): add scroll-touch momentum utility for horizontal strips
Add a `.scroll-touch` utility that restores iOS WebKit momentum
scrolling (`-webkit-overflow-scrolling: touch`) and contains the
overscroll chain so a flick at the end of a horizontal row does not
rubber-band the whole page. Apply it to the insights tab strip and the
cycle tab list — the two in-app horizontal scrollers that lost inertial
flick under the default `overflow-x-auto`.
2026-06-19 21:18:05 +02:00