Files
HealthLog/src/components/insights/coach-launch-button.tsx
T
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00

112 lines
4.1 KiB
TypeScript

"use client";
import { Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
import { useDisableCoach } from "@/hooks/use-disable-coach";
/**
* Inline desktop Coach launch button.
*
* v1.4.28 R3c — the legacy implementation rendered BOTH a sticky FAB
* (visible `<lg`) and the inline pill (`lg+`) from the same component.
* Every sub-page that mounted `<CoachLaunchButton>` therefore painted a
* second FAB into the DOM, collapsing to one visible button only
* because of `fixed` positioning — the duplicates still landed in the
* a11y tree. The FAB now lives once at the layout level (see
* `<LayoutCoachFab>` in `src/app/insights/layout.tsx`); this component
* keeps only the inline `lg+` pill that sub-pages mount inside their
* action rows.
*
* The button is a thin wrapper around `useCoachLaunch()` — it only
* renders when the context provider is mounted (so dropping the button
* onto a non-Insights page is a no-op rather than a crash).
*/
export interface CoachLaunchButtonProps {
/** Optional override for the visual label. Defaults to the shared CTA. */
label?: string;
/** Optional prefill seed for the next Coach turn. */
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();
const flags = useFeatureFlags();
const disableCoach = useDisableCoach();
if (!launch) {
// The button only makes sense beneath the provider. Render nothing
// so the sub-page doesn't paint a dead control.
return null;
}
// v1.4.31 — operator can hide the Coach surface app-wide.
if (!flags.coach) return null;
// v1.4.47 W3 — per-user opt-out is a peer gate to the operator's
// flag; either being off hides the pill entirely. See
// `<LayoutCoachFab>` for the matching FAB gate.
if (disableCoach) return null;
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. v1.16.1 — visible on every viewport:
// the layout-level FAB is no longer a permanent launcher (it only
// surfaces an unseen proactive nudge), so the pill is the everyday
// Coach entry point on mobile too.
return (
<Button
type="button"
variant="outline"
size="sm"
data-slot="coach-launch-inline"
onClick={() => launch.askCoach(prefill ?? null)}
className={cn("inline-flex h-10 gap-2 self-end", className)}
>
<Sparkles className="size-4" aria-hidden="true" />
<span>{accessibleLabel}</span>
</Button>
);
}