refactor(coach): unify launch glyph and propagate medication row shape

The per-target Coach button rode the MessageCircle glyph; hero strip, inline pill, layout FAB and daily-briefing CTA all paint Sparkles. The lone outlier swaps to Sparkles so the Coach-launch glyph reads as one icon vocabulary across the app. Separately, the insights/medikamente per-medication card painted its own header recipe (text-sm title with a Pill glyph and dose on line 2); routing the same row through MedicationCardHeader matches the canonical FB-G1 two-line shape used on the medications page. The streak chip rides the stateBadges slot.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 02:42:30 +02:00
1 parent e5cb74b459
commit 9768066354
3 files changed
+39 -31

No files matched your search

+25 -24
View File
@@ -8,7 +8,7 @@ import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { Progress } from "@/components/ui/progress";
import { ComplianceHeatmap } from "@/components/charts/compliance-heatmap";
@@ -16,6 +16,7 @@ import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
import { TherapyTimeline } from "@/components/insights/therapy-timeline";
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
/**
* v1.4.25 W4 — `/insights/medikamente`.
@@ -155,31 +156,31 @@ export default function InsightsMedikamentePage() {
>
{medications.map((med) => {
const medicationSummary = medicationSummaryById.get(med.id);
// UI-H3 — route every medication-list row (medications page
// + insights/medikamente) through `<MedicationCardHeader>`
// so the surface stays one shape: `{name} {dose}` on line 1
// + outline category badge on line 2. The streak chip rides
// the `stateBadges` slot so a strong streak still surfaces.
const categoryLabel =
med.category === "BLOOD_PRESSURE"
? t("medications.categoryBloodPressure")
: med.category === "VITAMIN"
? t("medications.categoryVitamin")
: t("medications.categoryOther");
const streakBadge =
med.streak > 0 ? (
<Badge variant="outline" className="shrink-0 text-xs">
{t("insights.dayStreak", { count: med.streak })}
</Badge>
) : null;
return (
<Card key={med.id}>
<CardHeader className="pb-2">
{/* v1.4.27 MB7 / MA2-F8 — `min-w-0` on the title
wrapper + `truncate` on the CardTitle keeps long
medication names (e.g. "Semaglutid Wegovy 0.5 mg
weekly") from pushing the streak badge off the
card on Galaxy Fold / Pixel 5. The Pill icon
+ CardTitle pair shrinks to fit; the badge stays
on the right at its natural width. */}
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<Pill className="text-dracula-orange h-4 w-4 shrink-0" />
<CardTitle className="truncate text-sm font-medium">
{med.name}
</CardTitle>
</div>
{med.streak > 0 && (
<Badge variant="outline" className="shrink-0 text-xs">
{t("insights.dayStreak", { count: med.streak })}
</Badge>
)}
</div>
<p className="text-muted-foreground text-xs">{med.dose}</p>
</CardHeader>
<MedicationCardHeader
name={med.name}
dose={med.dose}
categoryLabel={categoryLabel}
stateBadges={streakBadge}
/>
<CardContent className="space-y-3">
<div className="space-y-1.5">
<div className="flex justify-between text-xs">
@@ -6,8 +6,8 @@ import { TargetCoachButton } from "../target-coach-button";
/**
* v1.4.25 W3e — gate test for the per-card Coach CTA. The button
* MUST disappear when `aiEnabled` is false (Marc directive: no
* broken-button state for users with no AI provider configured).
* MUST disappear when `aiEnabled` is false (the maintainer's rule:
* no broken-button state for users with no provider configured).
*/
function render(props: Parameters<typeof TargetCoachButton>[0]) {
@@ -33,8 +33,12 @@ describe("<TargetCoachButton>", () => {
// still surface "Ask Coach" / "Coach fragen".
expect(html).toContain('aria-label="Ask Coach"');
expect(html).toContain('title="Ask Coach"');
// No visible text label — only the chat-bubble glyph.
// No visible text label — only the Sparkles glyph.
expect(html).not.toMatch(/>Ask Coach</);
// UI-H2 — the Coach-launch glyph is one Sparkles vocabulary
// across hero strip, inline pill, layout FAB and per-card icon.
expect(html).toMatch(/lucide-sparkles/i);
expect(html).not.toMatch(/lucide-message-circle/i);
});
it("lifts the tap target to the 44 px WCAG 2.5.5 floor", () => {
@@ -1,6 +1,6 @@
"use client";
import { MessageCircle } from "lucide-react";
import { Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -24,8 +24,11 @@ import type { CoachScope, CoachScopeSource } from "@/lib/ai/coach/types";
* a status pill, range bar, consistency strip, headline number, edit
* cog and a source link, the labelled pill dominated the visual
* hierarchy. Dropping the label retains the affordance via the
* chat-bubble glyph + `aria-label` while shrinking the optical weight
* to a per-card icon button.
* Sparkles glyph + `aria-label` while shrinking the optical weight
* to a per-card icon button. UI-H2 — the glyph is the same Sparkles
* the hero strip, the inline pill and the layout-level FAB use, so
* the per-card Coach launch reads on one icon vocabulary across the
* app.
*/
export interface TargetCoachButtonProps {
/**
@@ -85,7 +88,7 @@ export function TargetCoachButton({
// Coach drawer cluster). Glyph stays `size-4`.
className={cn("min-h-11 min-w-11", className)}
>
<MessageCircle className="size-4" aria-hidden="true" />
<Sparkles className="size-4" aria-hidden="true" />
</Button>
);
}