insights(mood): order classification first, route every tile through TileHeader

Reorder the mood subpage spine so the classification tiles (in-target
share + stability band) lead, then the better-days assessment, then the
descriptive breakdown sections. The classification answers where the user
stands; the better-days board, directly beneath, answers what associates
with their good days.

Every mood card header now routes through the shared TileHeader (icon +
white heading) so the subpage speaks one card language. The in-target and
stability tiles gain proper headings (new inTargetTitle / stability.title
keys across all six locales) and drop the icon-in-a-circle headline glyph;
the narrative feed and the discovered-relations board move into headed
cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-05 12:36:12 +02:00
1 parent 85c9f3ff3f
commit fcd8eccf6e
10 files changed
+167 -113

No files matched your search

@@ -1,7 +1,10 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Compass } from "lucide-react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
import { useTranslations } from "@/lib/i18n/context";
@@ -81,26 +84,32 @@ export function MoodDiscoveredRelations() {
if (moodPairs.length === 0) return null;
return (
<div className="space-y-2" data-slot="mood-discovered-relations">
<div className="flex items-center gap-1.5">
<h3 className="text-base font-semibold">
{t("insights.mood.discovery.title")}
</h3>
<Card data-slot="mood-discovered-relations">
<CardHeader className="pb-2">
{/* v1.12.4 (C3) — the false-discovery footer + observational
disclaimer were two full-width footnote rows. Fold both into a
single explainer icon so the explanation is one hover/focus away
instead of a low-density block under the list. */}
<MoodExplainerIcon
label={t("insights.mood.discovery.explainerLabel")}
detail={`${t("insights.mood.discovery.footer", {
tested: data.pairsTested,
})} ${t("insights.mood.discovery.disclaimer")}`}
instead of a low-density block under the list. v1.12.6 — the
heading now rides the canonical `TileHeader`, with the explainer
icon pinned to its trailing slot. */}
<TileHeader
icon={Compass}
title={t("insights.mood.discovery.title")}
right={
<MoodExplainerIcon
label={t("insights.mood.discovery.explainerLabel")}
detail={`${t("insights.mood.discovery.footer", {
tested: data.pairsTested,
})} ${t("insights.mood.discovery.disclaimer")}`}
/>
}
/>
</div>
<p className="text-muted-foreground text-sm">
{t("insights.mood.discovery.description")}
</p>
<ul className="divide-border divide-y">
</CardHeader>
<CardContent>
<p className="text-muted-foreground mb-2 text-sm">
{t("insights.mood.discovery.description")}
</p>
<ul className="divide-border divide-y">
{moodPairs.map((pair) => {
const moodIsOutcome = pair.outcome === "MOOD";
const channelKey = moodIsOutcome ? pair.behaviour : pair.outcome;
@@ -139,7 +148,8 @@ export function MoodDiscoveredRelations() {
</li>
);
})}
</ul>
</div>
</ul>
</CardContent>
</Card>
);
}
@@ -2,7 +2,8 @@
import { Target } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import { useTranslations } from "@/lib/i18n/context";
/**
@@ -12,6 +13,10 @@ import { useTranslations } from "@/lib/i18n/context";
* the LLM snapshot) to a visible stat: the share of recent days that
* landed in the good-mood band. Renders nothing when the number is
* unavailable (no recent data) so the page degrades gracefully.
*
* v1.12.6 — the tile now leads with the canonical `TileHeader`
* (icon + white heading) so it speaks the same card language as every
* other Insights tile; the icon-in-a-circle headline glyph is dropped.
*/
export function MoodInTargetTile({ pct }: { pct: number | null }) {
const { t } = useTranslations();
@@ -22,18 +27,16 @@ export function MoodInTargetTile({ pct }: { pct: number | null }) {
return (
<Card>
<CardContent className="flex items-center gap-4 py-4">
<div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
<Target className="size-5" />
</div>
<div className="min-w-0">
<p className="text-foreground text-2xl font-semibold tabular-nums">
{t("insights.mood.inTargetValue", { pct: String(rounded) })}
</p>
<p className="text-muted-foreground text-sm">
{t("insights.mood.inTargetCaption")}
</p>
</div>
<CardHeader className="pb-2">
<TileHeader icon={Target} title={t("insights.mood.inTargetTitle")} />
</CardHeader>
<CardContent>
<p className="text-foreground text-2xl font-semibold tabular-nums">
{t("insights.mood.inTargetValue", { pct: String(rounded) })}
</p>
<p className="text-muted-foreground text-sm">
{t("insights.mood.inTargetCaption")}
</p>
</CardContent>
</Card>
);
@@ -3,7 +3,22 @@
import dynamic from "next/dynamic";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ComponentType } from "react";
import {
BarChart3,
CalendarDays,
CalendarRange,
Clock,
Grid3x3,
Link2,
Sparkles,
Tag,
Tags,
TrendingUp,
} from "lucide-react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
@@ -136,15 +151,19 @@ interface MoodInsightsResponse {
function SectionCard({
title,
icon,
children,
}: {
title: string;
// v1.12.6 — every mood section header now routes through `TileHeader`
// (icon + white heading) so the subpage reads as one card language.
icon: ComponentType<{ className?: string }>;
children: React.ReactNode;
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">{title}</CardTitle>
<TileHeader icon={icon} title={title} />
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
@@ -210,7 +229,14 @@ export function MoodInsightsSections() {
const hasCrosstab = crosstabRows.length > 0;
return (
// v1.12.6 — the mood spine is ordered as: Einordnung (the classification
// tiles: in-target share + stability band) FIRST, then the "what goes with
// your better days" assessment read, then the descriptive breakdown
// sections. The classification answers "where do I stand", the better-days
// board answers "what associates with my good days", and the rest is the
// supporting detail.
<div className="space-y-4">
{/* Einordnung — the classification tiles. */}
{(hasInTargetTile || hasStabilityTile) && (
// Two-up only when BOTH tiles render; a lone tile spans full width so
// it never leaves a half-width orphan with dead space beside it.
@@ -229,15 +255,25 @@ export function MoodInsightsSections() {
</div>
)}
<MoodNarrativeFeed items={narratives} />
{/* "Was zu deinen besseren Tagen passt" — the better-days assessment,
ranked by effect size. The substance of the Einschätzung sits here,
directly under the classification. */}
{hasBetterDays && (
<SectionCard title={t("insights.mood.betterDays.title")}>
<SectionCard
title={t("insights.mood.betterDays.title")}
icon={Sparkles}
>
<MoodBetterDays factors={betterDays} />
</SectionCard>
)}
<SectionCard title={t("insights.mood.heatmapTitle")}>
{narratives.length > 0 && (
<SectionCard title={t("insights.mood.narrative.title")} icon={TrendingUp}>
<MoodNarrativeFeed items={narratives} />
</SectionCard>
)}
<SectionCard title={t("insights.mood.heatmapTitle")} icon={CalendarDays}>
<MoodHeatmap
cells={heatmapCells}
days={data.heatmap.windowDays}
@@ -246,49 +282,58 @@ export function MoodInsightsSections() {
</SectionCard>
<div className="grid gap-4 lg:grid-cols-2">
<SectionCard title={t("insights.mood.distributionTitle")}>
<SectionCard
title={t("insights.mood.distributionTitle")}
icon={BarChart3}
>
<MoodDistributionChart distribution={data.distribution} />
</SectionCard>
<SectionCard title={t("insights.mood.weekdayTitle")}>
<SectionCard
title={t("insights.mood.weekdayTitle")}
icon={CalendarRange}
>
<MoodWeekdayChart weekday={data.weekday} />
</SectionCard>
</div>
{data.timeOfDay.reliable && (
<SectionCard title={t("insights.mood.timeOfDay.title")}>
<SectionCard title={t("insights.mood.timeOfDay.title")} icon={Clock}>
<MoodTimeOfDayChart pattern={data.timeOfDay} />
</SectionCard>
)}
{hasStructuredTags && (
<SectionCard title={t("insights.mood.structuredTagsTitle")}>
<SectionCard
title={t("insights.mood.structuredTagsTitle")}
icon={Tags}
>
<MoodStructuredTagBreakdown tags={data.structuredTags} />
</SectionCard>
)}
{hasTags && (
<SectionCard title={t("insights.mood.tagsTitle")}>
<SectionCard title={t("insights.mood.tagsTitle")} icon={Tag}>
<MoodTagBreakdown tags={data.tags} />
</SectionCard>
)}
{hasInfluence && (
<SectionCard title={t("insights.mood.influence.title")}>
<SectionCard title={t("insights.mood.influence.title")} icon={TrendingUp}>
<MoodTagInfluence rows={influenceRows} />
</SectionCard>
)}
{hasCrosstab && (
<SectionCard title={t("insights.mood.crosstab.title")}>
<SectionCard title={t("insights.mood.crosstab.title")} icon={Grid3x3}>
<MoodTagMetricCrosstab rows={crosstabRows} />
</SectionCard>
)}
<div className="space-y-2">
<h3 className="text-base font-semibold">
{t("insights.mood.correlationsTitle")}
</h3>
<p className="text-muted-foreground text-sm">
<SectionCard
title={t("insights.mood.correlationsTitle")}
icon={Link2}
>
<p className="text-muted-foreground mb-2 text-sm">
{t("insights.mood.correlationsDescription")}
</p>
<MoodCorrelationCards
@@ -298,7 +343,7 @@ export function MoodInsightsSections() {
weight={data.correlations.weight}
bloodPressureSystolic={data.correlations.bloodPressureSystolic}
/>
</div>
</SectionCard>
{/* F3 — the FDR-controlled discovered mood relations. Self-fetches the
correlation-discovery surface and renders nothing when the operator
@@ -2,7 +2,8 @@
import { Activity } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import { useTranslations } from "@/lib/i18n/context";
/**
@@ -46,20 +47,21 @@ export function MoodStabilityTile({
return (
<Card>
<CardContent className="flex items-center gap-4 py-4">
<div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
<Activity className="size-5" />
</div>
<div className="min-w-0">
<p className="text-foreground text-2xl font-semibold tabular-nums">
{t("insights.mood.stability.value", {
score: String(stability.score),
})}
</p>
<p className="text-muted-foreground text-sm">
{t(BAND_KEY[stability.band])}
</p>
</div>
<CardHeader className="pb-2">
<TileHeader
icon={Activity}
title={t("insights.mood.stability.title")}
/>
</CardHeader>
<CardContent>
<p className="text-foreground text-2xl font-semibold tabular-nums">
{t("insights.mood.stability.value", {
score: String(stability.score),
})}
</p>
<p className="text-muted-foreground text-sm">
{t(BAND_KEY[stability.band])}
</p>
</CardContent>
</Card>
);