fix(ui): elevate the capture FAB and unify selectable-control and export-card styling

Lift the bottom-bar center capture action into a proper floating-action
button: a 56px target raised out of the bar with a card collar ring and
a stronger shadow, so it reads as the primary CTA rather than a fifth
flush tab. Desktop sidebar nav is unaffected.

Unify the selectable controls across the mood logging surface onto one
design language: the 5 mood faces, the tag tiles, the rated-factor steps
and the GLP-1 chips now share a single selected-state treatment
(border-primary, bg-primary/15, primary text, single border) and a
consistent rounding family. Match the More-hub row height to the capture
tiles so equivalent tappable rows share one min-height at the same tier.

Give the demoted doctor-report export card a primary-tinted icon to match
its peer cards — the demotion comes from position and size, not a muted
icon. Drop the doubled heading atop the mood sheet by demoting the hero
question to a field label so the sheet title is the single primary
heading. Reword the capture mood option description to describe the
action instead of echoing the hero question.

(cherry picked from commit 1296f151f1c9a7c95ff917e3d69a5c0d369ba210)
This commit is contained in:
Marc-André Bombeck committed 2026-06-05 01:05:47 +02:00
1 parent 61573301c3
commit 6657e3da6f
11 files changed
+54 -19

No files matched your search

+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Medikament",
"medicationDescription": "Eine Dosis als genommen markieren",
"mood": "Stimmung",
"moodDescription": "Wie geht es dir?"
"moodDescription": "Stimmung und Tags erfassen"
}
},
"auth": {
+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Medication",
"medicationDescription": "Mark a dose as taken",
"mood": "Mood",
"moodDescription": "How are you feeling?"
"moodDescription": "Log your mood and tags"
}
},
"auth": {
+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Medicamento",
"medicationDescription": "Marcar una dosis como tomada",
"mood": "Estado de ánimo",
"moodDescription": "¿Cómo te sientes?"
"moodDescription": "Registrar estado de ánimo y etiquetas"
}
},
"auth": {
+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Médicament",
"medicationDescription": "Marquer une dose comme prise",
"mood": "Humeur",
"moodDescription": "Comment vous sentez-vous ?"
"moodDescription": "Enregistrer l'humeur et les tags"
}
},
"auth": {
+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Farmaco",
"medicationDescription": "Segna una dose come assunta",
"mood": "Umore",
"moodDescription": "Come ti senti?"
"moodDescription": "Registra umore e tag"
}
},
"auth": {
+1 -1
View File
@@ -215,7 +215,7 @@
"medication": "Lek",
"medicationDescription": "Oznacz dawkę jako przyjętą",
"mood": "Nastrój",
"moodDescription": "Jak się czujesz?"
"moodDescription": "Zapisz nastrój i tagi"
}
},
"auth": {
@@ -75,11 +75,24 @@ describe("<BottomNav> iOS-parity layout", () => {
it("each strip entry meets the 44 px tap-target floor (min-h-11 min-w-11)", () => {
const html = render();
// Five flanking strip slots (Home, Meds, capture button, Insights,
// More) all carry `min-h-11 min-w-11`. We don't hard-pin the count
// because a future entry would also satisfy the contract.
// The four flanking strip slots (Home, Meds, Insights, More) carry
// `min-h-11 min-w-11`. We don't hard-pin the count because a future
// entry would also satisfy the contract.
const matches = html.match(/min-h-11 min-w-11/g) ?? [];
expect(matches.length).toBeGreaterThanOrEqual(5);
expect(matches.length).toBeGreaterThanOrEqual(4);
});
it("the center capture FAB is a larger, elevated tap target", () => {
const html = render();
// The FAB clears the 44px floor with a 56px (h-14 w-14) target and
// is lifted out of the bar (negative top margin + card ring + raised
// shadow) so it reads as the primary CTA, not a flush fifth tab.
const capture = html.match(/<button[^>]*bottom-nav-capture[^>]*>/);
expect(capture).not.toBeNull();
expect(capture![0]).toMatch(/h-14 w-14/);
expect(capture![0]).toMatch(/-mt-5/);
expect(capture![0]).toMatch(/ring-4/);
expect(capture![0]).toMatch(/shadow-lg/);
});
it("no longer lists the deprecated /targets entry (v1.8.6)", () => {
+13 -5
View File
@@ -128,8 +128,13 @@ export function BottomNav() {
{PRIMARY_LEFT.map(renderPrimary)}
{/* Center capture action — a labeled button (NOT a link) that
opens the capture picker. Visually elevated so it reads as
the primary CTA. */}
opens the capture picker. Elevated as a Floating-Action-
Button: it sits larger than the flanking tabs (h-14 = 56px,
well above the 44px tap-target floor), lifts above the bar
with a negative top margin, and carries a filled-primary
collar (a `bg-card` ring that punches it out of the bar) plus
a stronger shadow so it reads as the bar's primary CTA rather
than a fifth flush tab. */}
<div className="flex flex-1 items-center justify-center">
<button
type="button"
@@ -138,9 +143,9 @@ export function BottomNav() {
aria-expanded={captureOpen}
onClick={() => setCaptureOpen(true)}
data-testid="bottom-nav-capture"
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring/50 flex min-h-11 min-w-11 items-center justify-center rounded-full shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring/70 ring-card -mt-5 flex h-14 w-14 items-center justify-center rounded-full shadow-lg ring-4 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Plus className="h-6 w-6" />
<Plus className="h-7 w-7" />
</button>
</div>
@@ -193,8 +198,11 @@ export function BottomNav() {
href={item.href}
onClick={() => setMoreOpen(false)}
aria-current={isActive ? "page" : undefined}
// v1.12 — `min-h-14` matches the capture-picker tiles so
// the equivalent tappable rows share one height at this
// tier (and the larger row is a more comfortable target).
className={cn(
"border-border flex min-h-11 items-center gap-3 rounded-lg border px-4 py-3 transition-colors",
"border-border flex min-h-14 items-center gap-3 rounded-lg border px-4 py-3 transition-colors",
isActive
? "text-primary bg-primary/5"
: "text-foreground hover:bg-accent/40",
+13 -2
View File
@@ -275,7 +275,14 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
radiogroup (best-on-the-left order); tapping one picks the mood
and reveals the annotate panel below. */}
<div className="space-y-2" data-slot="mood-face-hero">
<Label id="mood-level-label" className="text-base">
{/* v1.12 — the sheet title ("Add") is the single primary heading;
the hero question stays as the face row's field label rather
than a second heading-weight line, so the sheet header isn't a
doubled heading. */}
<Label
id="mood-level-label"
className="text-muted-foreground text-sm font-normal"
>
{t("mood.heroQuestion")}
</Label>
<div
@@ -297,9 +304,13 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
data-mood={level.value}
onClick={() => setMood(level.value)}
{...getMoodRadioProps(index)}
// v1.12 — selected-state unified with the tag tiles and
// factor steps: `border-primary bg-primary/15 text-primary`
// on a single 1px border (no `border-2` jump) so the
// selectable controls read as one design language.
className={`flex flex-col items-center gap-1.5 rounded-xl border p-2 text-center transition-colors sm:p-3 ${
isSelected
? "border-primary bg-primary/10 text-primary border-2"
? "border-primary bg-primary/15 text-primary"
: "border-border text-muted-foreground hover:bg-accent hover:text-foreground"
}`}
>
+4 -1
View File
@@ -201,7 +201,10 @@ export function MoodTagPicker({
onClick={() =>
onRateFactor(tag.key, isActive ? null : value)
}
className={`flex h-8 items-center justify-center rounded-md border px-2 text-xs tabular-nums transition-colors ${
// v1.12 — `rounded-lg` aligns the step
// radius with the tag tiles and mood faces;
// selected-state matches them too.
className={`flex h-8 items-center justify-center rounded-lg border px-2 text-xs tabular-nums transition-colors ${
isBinaryScale ? "min-w-12" : "min-w-8"
} ${
isActive
@@ -112,7 +112,7 @@ export function ArztberichtHeroCard() {
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Stethoscope
className="text-muted-foreground h-5 w-5 shrink-0"
className="text-primary h-5 w-5 shrink-0"
aria-hidden="true"
/>
<h2