feat(medications,coach): the take-now prompt joins the schedule grammar and the coach opens in place

An open take-window renders as the right-aligned value of the next-
intake row — same label/value columns as every other card line —
instead of a standalone line above the slot; the escalation tiers
(late, very late, taken-early) keep the top line. The coach launcher
opens the side drawer on the page you are on instead of navigating to
the chat page, and grows to the standard FAB size.
This commit is contained in:
Marc-André Bombeck committed 2026-06-12 20:27:59 +02:00
1 parent 8b0fbfb522
commit 4493eb2cac
3 files changed
+51 -12

No files matched your search

+7 -5
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Sparkles } from "lucide-react";
@@ -58,7 +58,6 @@ export function isNudgeUnread(
export function LayoutCoachFab() {
const { t } = useTranslations();
const router = useRouter();
const pathname = usePathname();
const launch = useCoachLaunch();
const flags = useFeatureFlags();
@@ -137,7 +136,10 @@ export function LayoutCoachFab() {
// Best effort — see above.
}
}
router.push("/insights/coach");
// v1.16.11 — open the side drawer in place (the launch context the
// layout mount consumes) instead of navigating to the chat page;
// the conversation arrives without losing the page underneath.
launch?.askCoach();
};
return (
@@ -157,7 +159,7 @@ export function LayoutCoachFab() {
// not `lg:` — keeps the FAB from floating mid-air 768-1023px).
// The right inset mirrors the desktop bottom inset so the
// corner gap is even.
"fixed right-6 z-40 size-12 rounded-full shadow-lg",
"fixed right-6 z-40 size-14 rounded-full shadow-lg",
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-6",
// Dark glyph on the purple/pink gradient — white sat at
// ≈2.3:1 against the gradient midpoint; the background token
@@ -185,7 +187,7 @@ export function LayoutCoachFab() {
"[body:has([data-testid=onboarding-tour])_&]:invisible",
)}
>
<Sparkles className="size-5" aria-hidden="true" />
<Sparkles className="size-6" aria-hidden="true" />
{unread ? (
<span
data-slot="coach-fab-unread"
@@ -154,6 +154,18 @@ export function MedicationCardBody({
? t("medications.overdue")
: null;
// v1.16.11 — the plain take-now prompt rides the "next intake" row as
// its right-aligned value ("Nächste Einnahme: ✓ Jetzt einnehmen —
// 20:00–22:00") instead of a standalone line above the slot, so the
// card keeps one label/value grammar whether a window is open or not.
// The escalation tiers (late / very-late / taken-early) stay on the
// top line — they are interruptions, not schedule values.
const takeNowOnNextLine =
windowStatus !== null &&
windowStatus.status === "in_window" &&
windowStatus.takenEarlyDaysAgo == null &&
overdueLabel === null;
return (
<Card
// The card surface is a constant neutral surface — dose status is never
@@ -195,7 +207,7 @@ export function MedicationCardBody({
<AlertTriangle className="size-3.5 shrink-0" aria-hidden="true" />
{overdueLabel}
</p>
) : windowStatus ? (
) : windowStatus && !takeNowOnNextLine ? (
<MedicationStatusPill
status={windowStatus.status}
windowStart={windowStatus.windowStart}
@@ -204,8 +216,23 @@ export function MedicationCardBody({
) : null}
</div>
{/* Next / last intake — the two decisive lines, each shown once. */}
<MedicationNextLastSlot next={nextLine} last={lastLine} />
{/* Next / last intake — the two decisive lines, each shown once.
An open take-window renders as the next-intake value. */}
<MedicationNextLastSlot
next={
takeNowOnNextLine ? (
<MedicationStatusPill
status={windowStatus.status}
windowStart={windowStatus.windowStart}
windowEnd={windowStatus.windowEnd}
inline
/>
) : (
nextLine
)
}
last={lastLine}
/>
{/* Compliance bars — always two rows; constant-height skeleton holds
the slot while the query is in flight so the grid row stays even.
@@ -26,6 +26,14 @@ interface MedicationStatusPillProps {
* with the card pill — only the suffix is dropped.
*/
compact?: boolean;
/**
* v1.16.11 — inline variant: renders a `<span>` root instead of the
* block `<p>` so the pill can sit as the right-aligned VALUE of the
* "next intake" row (the take-now prompt lives on that row now, not
* on its own line above the slot). A `<p>` inside the row's `<span>`
* would be invalid nesting.
*/
inline?: boolean;
}
/**
@@ -42,12 +50,14 @@ export function MedicationStatusPill({
windowEnd,
takenEarlyDaysAgo = null,
compact = false,
inline = false,
}: MedicationStatusPillProps) {
const { t, locale } = useTranslations();
const Root = inline ? "span" : "p";
if (takenEarlyDaysAgo != null) {
return (
<p className="text-sm">
<Root className="text-sm">
<span className="text-muted-foreground inline-flex items-center gap-1 font-medium">
<CircleCheck className="size-3.5 shrink-0" aria-hidden="true" />
{takenEarlyDaysAgo === 1
@@ -60,12 +70,12 @@ export function MedicationStatusPill({
— {formatTimeWindowRange(windowStart, windowEnd, locale)}
</span>
)}
</p>
</Root>
);
}
return (
<p className="text-sm">
<Root className="text-sm">
<span
className={
// v1.12.2 — converge the three tiers onto the semantic feedback
@@ -101,6 +111,6 @@ export function MedicationStatusPill({
— {formatTimeWindowRange(windowStart, windowEnd, locale)}
</span>
)}
</p>
</Root>
);
}