mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(insights): move coach-ask to the header and de-emphasise the assessment border
The Coach launch sat at the foot of every category page; move it into the
sub-page header as an icon-only button at heading height, top-right, so
"ask the coach" reads as a header action and the page no longer ends on a
stray button. The shared shell mounts it (gated on the Coach flag plus the
per-user opt-out), so every category page picks it up and the foot
instance is dropped. Also drop the purple left accent on the assessment
("Einschätzung") card — the coloured rule made it read as restless against
the calmer surrounding cards.
This commit is contained in:
1 parent
4935a9b000
commit
6e61713fc6
12 files changed
+68
-53
No files matched your search
@@ -8,13 +8,13 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
*
|
||||
* Two contracts under test:
|
||||
*
|
||||
* 1. Every routed Insights sub-page mounts a `<CoachLaunchButton>`.
|
||||
* The component renders BOTH a sticky-bottom FAB
|
||||
* (`data-slot="coach-launch-fab"`, visible on `<lg`) and an
|
||||
* inline action (`data-slot="coach-launch-inline"`, visible on
|
||||
* `lg+`). The DOM carries both nodes; CSS picks the visible one.
|
||||
* The spec asserts the FAB renders on Pixel-5 and the inline
|
||||
* action renders on Desktop Chrome.
|
||||
* 1. Every routed Insights sub-page mounts a Coach launch surface.
|
||||
* The layout renders a sticky-bottom FAB
|
||||
* (`data-slot="coach-launch-fab"`, visible on `<lg`); v1.8.6 moved
|
||||
* the per-page action into the sub-page header as an icon-only
|
||||
* button (`data-slot="coach-launch-icon"`, mounted by the shell at
|
||||
* heading height across breakpoints). The spec asserts the FAB
|
||||
* renders on Pixel-5 and the header icon renders on Desktop Chrome.
|
||||
*
|
||||
* 2. Opening the Coach drawer on `<sm` mounts it as a bottom-sheet
|
||||
* (`data-variant="bottom-sheet"`). On `>=sm` it mounts as a
|
||||
@@ -121,28 +121,23 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => {
|
||||
.locator('[data-slot="coach-launch-fab"]')
|
||||
.first();
|
||||
await expect(fab).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// The inline branch is in the DOM but hidden via `hidden lg:inline-flex`.
|
||||
const inline = page.locator('[data-slot="coach-launch-inline"]').first();
|
||||
await expect(inline).toBeHidden();
|
||||
});
|
||||
|
||||
test(`/insights/${slug} mounts the Coach launch button on Desktop Chrome (inline branch)`, async ({
|
||||
test(`/insights/${slug} mounts the header Coach icon on Desktop Chrome`, async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
// Desktop Chrome viewport is 1280 — `lg` (1024) is matched so the
|
||||
// inline branch wins; the FAB is hidden by `lg:hidden`.
|
||||
// layout FAB is hidden by `lg:hidden`; the header icon the shell
|
||||
// mounts is the Coach entry on the page itself.
|
||||
test.skip(
|
||||
testInfo.project.name !== "chromium-desktop",
|
||||
"desktop inline branch",
|
||||
"desktop header-icon branch",
|
||||
);
|
||||
|
||||
await page.goto(`/insights/${slug}`, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const inline = page
|
||||
.locator('[data-slot="coach-launch-inline"]')
|
||||
.first();
|
||||
await expect(inline).toBeVisible({ timeout: 10_000 });
|
||||
const icon = page.locator('[data-slot="coach-launch-icon"]').first();
|
||||
await expect(icon).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const fab = page.locator('[data-slot="coach-launch-fab"]').first();
|
||||
await expect(fab).toBeHidden();
|
||||
@@ -168,7 +163,7 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => {
|
||||
await expect(drawer).toHaveAttribute("data-variant", "bottom-sheet");
|
||||
});
|
||||
|
||||
test("clicking the inline action opens the Coach drawer as a side-sheet on Desktop Chrome", async ({
|
||||
test("clicking the header Coach icon opens the Coach drawer as a side-sheet on Desktop Chrome", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
@@ -178,11 +173,9 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => {
|
||||
|
||||
await page.goto("/insights/blutdruck", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const inline = page
|
||||
.locator('[data-slot="coach-launch-inline"]')
|
||||
.first();
|
||||
await expect(inline).toBeVisible({ timeout: 10_000 });
|
||||
await inline.click();
|
||||
const icon = page.locator('[data-slot="coach-launch-icon"]').first();
|
||||
await expect(icon).toBeVisible({ timeout: 10_000 });
|
||||
await icon.click();
|
||||
|
||||
const drawer = page.locator('[data-slot="coach-drawer"]');
|
||||
await expect(drawer).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
|
||||
@@ -101,6 +100,7 @@ export default function InsightsBlutdruckPage() {
|
||||
title={t("insights.bloodPressureSectionTitle")}
|
||||
description={t("insights.subPage.blutdruckDescription")}
|
||||
explainerMetric="bloodPressure"
|
||||
coachLaunch
|
||||
>
|
||||
<HealthChartDynamic
|
||||
chartKey="bp"
|
||||
@@ -126,8 +126,6 @@ export default function InsightsBlutdruckPage() {
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -110,6 +110,7 @@ export default function InsightsBmiPage() {
|
||||
title={t("insights.bmiSectionTitle")}
|
||||
description={t("insights.subPage.bmiDescription")}
|
||||
explainerMetric="bmi"
|
||||
coachLaunch
|
||||
>
|
||||
<HealthChartDynamic
|
||||
chartKey="bmi"
|
||||
@@ -135,8 +136,6 @@ export default function InsightsBmiPage() {
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -187,6 +187,7 @@ export default function InsightsMedikamentePage() {
|
||||
title={t("insights.medicationCompliance")}
|
||||
description={t("insights.subPage.medikamenteDescription")}
|
||||
explainerMetric="medications"
|
||||
coachLaunch
|
||||
>
|
||||
<div
|
||||
className={
|
||||
@@ -290,8 +291,6 @@ export default function InsightsMedikamentePage() {
|
||||
without an active GLP-1 medication, so the page collapses
|
||||
back to the compliance grid for everyone else. */}
|
||||
<TherapyTimeline />
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
|
||||
@@ -99,6 +98,7 @@ export default function InsightsStimmungPage() {
|
||||
title={t("insights.moodSectionTitle")}
|
||||
description={t("insights.subPage.stimmungDescription")}
|
||||
explainerMetric="mood"
|
||||
coachLaunch
|
||||
>
|
||||
<MoodChart
|
||||
chartKey="mood"
|
||||
@@ -120,8 +120,6 @@ export default function InsightsStimmungPage() {
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
|
||||
@@ -129,6 +128,7 @@ export default function InsightsPulsPage() {
|
||||
timeZone={user?.timezone ?? undefined}
|
||||
/>
|
||||
}
|
||||
coachLaunch
|
||||
showAllValuesType="PULSE"
|
||||
>
|
||||
<HealthChartDynamic
|
||||
@@ -166,8 +166,6 @@ export default function InsightsPulsPage() {
|
||||
compareBaseline={compareBaseline}
|
||||
userTimezone={user?.timezone}
|
||||
/>
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { Moon } from "lucide-react";
|
||||
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
|
||||
import { SleepOverview } from "@/components/insights/sleep-overview";
|
||||
@@ -62,6 +61,7 @@ export default function InsightsSchlafPage() {
|
||||
title={t("insights.sleep.title")}
|
||||
description={t("insights.sleep.description")}
|
||||
explainerMetric="sleep"
|
||||
coachLaunch
|
||||
>
|
||||
<SleepOverview />
|
||||
|
||||
@@ -78,8 +78,6 @@ export default function InsightsSchlafPage() {
|
||||
structural slot waits for v1.5 where the route + hook key will
|
||||
wire in.
|
||||
*/}
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
|
||||
@@ -86,6 +85,7 @@ export default function InsightsGewichtPage() {
|
||||
timeZone={user?.timezone ?? undefined}
|
||||
/>
|
||||
}
|
||||
coachLaunch
|
||||
showAllValuesType="WEIGHT"
|
||||
>
|
||||
<HealthChartDynamic
|
||||
@@ -111,8 +111,6 @@ export default function InsightsGewichtPage() {
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
/>
|
||||
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Activity } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useWorkouts } from "@/hooks/use-workouts";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { SubPageShell } from "@/components/insights/sub-page-shell";
|
||||
import { WorkoutList } from "@/components/insights/workout-list";
|
||||
@@ -36,6 +35,7 @@ export default function InsightsWorkoutsPage() {
|
||||
title={t("insights.workouts.title")}
|
||||
description={t("insights.workouts.description")}
|
||||
explainerMetric="workouts"
|
||||
coachLaunch
|
||||
>
|
||||
{isLoading ? (
|
||||
<div data-slot="workouts-loading" className="space-y-2">
|
||||
@@ -52,10 +52,7 @@ export default function InsightsWorkoutsPage() {
|
||||
coachPrefill="I haven't logged any workouts yet — why does tracking them matter, and what should I focus on first?"
|
||||
/>
|
||||
) : data ? (
|
||||
<>
|
||||
<WorkoutList workouts={data.workouts} />
|
||||
<CoachLaunchButton />
|
||||
</>
|
||||
<WorkoutList workouts={data.workouts} />
|
||||
) : null}
|
||||
</SubPageShell>
|
||||
);
|
||||
|
||||
@@ -33,12 +33,24 @@ export interface CoachLaunchButtonProps {
|
||||
prefill?: string;
|
||||
/** Optional className passthrough for inline overrides. */
|
||||
className?: string;
|
||||
/**
|
||||
* v1.8.6 — render the launch as an icon-only affordance instead of the
|
||||
* labelled pill. The sub-page shell mounts this variant at heading
|
||||
* height in the page header so "Coach fragen" sits top-right, aligned
|
||||
* with the title, rather than at the foot of the page. The accessible
|
||||
* label still resolves the shared CTA copy via `aria-label`. The icon
|
||||
* variant stays visible across breakpoints — it is the header's own
|
||||
* Coach entry, not the `lg+`-only inline pill the old foot placement
|
||||
* used (the mobile FAB covers `<lg` for that legacy surface).
|
||||
*/
|
||||
variant?: "inline" | "icon";
|
||||
}
|
||||
|
||||
export function CoachLaunchButton({
|
||||
label,
|
||||
prefill,
|
||||
className,
|
||||
variant = "inline",
|
||||
}: CoachLaunchButtonProps) {
|
||||
const { t } = useTranslations();
|
||||
const launch = useCoachLaunch();
|
||||
@@ -59,6 +71,26 @@ export function CoachLaunchButton({
|
||||
|
||||
const accessibleLabel = label ?? t("insights.heroActionAskCoach");
|
||||
|
||||
if (variant === "icon") {
|
||||
// v1.8.6 — heading-height icon button mounted by the sub-page shell.
|
||||
// Icon-only so it reads as a compact action beside the title; the
|
||||
// CTA copy moves to `aria-label` + a native tooltip.
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-slot="coach-launch-icon"
|
||||
aria-label={accessibleLabel}
|
||||
title={accessibleLabel}
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
className={cn("text-muted-foreground hover:text-foreground", className)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline header-style action on `lg+`. Hidden below `lg` because the
|
||||
// layout-level FAB (`<LayoutCoachFab>`) covers that breakpoint —
|
||||
// mounting both at the same time would put two affordances in front
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { InsightMetric } from "@/lib/insights/metric-availability";
|
||||
import type { ChartOverlayKey } from "@/lib/dashboard-layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { MetricEmptyState } from "@/components/insights/metric-empty-state";
|
||||
import { MetricStatStrip } from "@/components/insights/metric-stat-strip";
|
||||
@@ -33,8 +32,9 @@ import { SubPageShell } from "@/components/insights/sub-page-shell";
|
||||
* pass `null` to render an onboarding hint without a dead link.
|
||||
* 3. `<HealthChartDynamic>` mounted on the canonical
|
||||
* `ChartOverlayKey` so the chart-cog popover persists per metric.
|
||||
* 4. `<CoachLaunchButton>` at the foot — feature-flag gating runs
|
||||
* inside the button so this scaffold stays agnostic.
|
||||
* 4. A header-height Coach launch icon (`coachLaunch` on the shell) —
|
||||
* feature-flag gating runs inside the button so this scaffold stays
|
||||
* agnostic.
|
||||
*
|
||||
* Adding a new metric is a four-line page module that hands the right
|
||||
* Zod / MeasurementType + ChartOverlayKey + i18n-key prefix to this
|
||||
@@ -181,6 +181,7 @@ export function HealthKitMetricPage({
|
||||
timeZone={user?.timezone ?? undefined}
|
||||
/>
|
||||
}
|
||||
coachLaunch
|
||||
showAllValuesType={measurementType}
|
||||
>
|
||||
<HealthChartDynamic
|
||||
@@ -198,7 +199,6 @@ export function HealthKitMetricPage({
|
||||
{targetSummarySlug ? (
|
||||
<MetricTargetSummary slug={targetSummarySlug} />
|
||||
) : null}
|
||||
<CoachLaunchButton />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +163,12 @@ export function InsightStatusCard({
|
||||
// shell padding to `py-4 md:py-5` so the assessment block reads tight
|
||||
// in the denser sub-page rhythm. The same override lands on every
|
||||
// status variant below so the gap is consistent across states.
|
||||
className="animate-insight-in border-l-dracula-purple gap-2 border-l-2 py-4 md:gap-3 md:py-5"
|
||||
//
|
||||
// v1.8.6 — drop the purple left accent (`border-l-dracula-purple
|
||||
// border-l-2`). The coloured rule made the assessment card read as
|
||||
// restless against the calmer surrounding cards; the plain card
|
||||
// border carries enough separation on its own.
|
||||
className="animate-insight-in gap-2 py-4 md:gap-3 md:py-5"
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in new issue
Block a user