mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
refactor(coach): consolidate launch button to inline + layout-FAB shape
Lift the mobile FAB out of <CoachLaunchButton> into a new <LayoutCoachFab> mounted once next to <CoachLaunchProvider> in the Insights layout. Each sub-page previously mounted <CoachLaunchButton> which rendered BOTH the FAB and the inline pill, so a phone user ended up with 2-3 duplicate FAB nodes in the a11y tree (collapsed visually by fixed positioning). The inline pill stays on the per-page action rows where copy + position matter; the FAB now paints exactly once per Insights surface.
This commit is contained in:
1 parent
155b529def
commit
4c6d877921
3 files changed
+87
-49
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { InsightsLayoutShell } from "@/components/insights/insights-layout-shell";
|
||||
import { LayoutCoachFab } from "@/components/insights/layout-coach-fab";
|
||||
import { LayoutCoachMount } from "@/components/insights/layout-coach-mount";
|
||||
import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context";
|
||||
|
||||
@@ -20,11 +21,18 @@ import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context";
|
||||
* strip + suggested-prompt chips. Decision F (audit MA3) drove the
|
||||
* promotion: a mobile user who navigates from `/insights` to
|
||||
* `/insights/blutdruck` keeps a one-tap path back into the Coach.
|
||||
*
|
||||
* v1.4.28 R3c — the mobile FAB now also lives here via
|
||||
* `<LayoutCoachFab>` (carved out of `<CoachLaunchButton>` so each sub-
|
||||
* page mount no longer duplicates the FAB into the a11y tree). The
|
||||
* inline `<CoachLaunchButton>` pill stays on the per-page action rows
|
||||
* where copy + position matter.
|
||||
*/
|
||||
export default function InsightsLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<CoachLaunchProvider>
|
||||
<InsightsLayoutShell>{children}</InsightsLayoutShell>
|
||||
<LayoutCoachFab />
|
||||
<LayoutCoachMount />
|
||||
</CoachLaunchProvider>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,17 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
|
||||
/**
|
||||
* v1.4.27 R3d MB4 — Coach launch button.
|
||||
* Inline desktop Coach launch button.
|
||||
*
|
||||
* Decision F: every routed Insights sub-page mounts this button so the
|
||||
* Coach drawer (mounted at the layout root) is always reachable. The
|
||||
* button renders as a sticky-bottom FAB on `<lg` viewports (so a phone
|
||||
* user always sees it within thumb range) and as an inline header
|
||||
* action on `lg+` (where the desktop drawer slides in from the right
|
||||
* edge and the FAB would overlap the chart legend).
|
||||
* 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
|
||||
@@ -46,48 +49,24 @@ export function CoachLaunchButton({
|
||||
|
||||
const accessibleLabel = label ?? t("insights.heroActionAskCoach");
|
||||
|
||||
// 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
|
||||
// of the user.
|
||||
return (
|
||||
<>
|
||||
{/* Sticky FAB on `<lg` viewports — pinned to the bottom-right so
|
||||
the user's thumb always finds it. `bottom-20` clears the
|
||||
mobile bottom navigation; `safe-area-inset-bottom` plus the
|
||||
existing app shell padding keep it clear of the iOS home bar.
|
||||
Hidden on `lg+` where the inline button below takes over. */}
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
data-slot="coach-launch-fab"
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
aria-label={accessibleLabel}
|
||||
title={accessibleLabel}
|
||||
className={cn(
|
||||
"fixed right-4 bottom-20 z-40 h-12 rounded-full px-4 shadow-lg",
|
||||
"from-dracula-purple to-dracula-pink bg-gradient-to-br text-white",
|
||||
"hover:from-dracula-purple/90 hover:to-dracula-pink/90",
|
||||
"lg:hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
<span>{accessibleLabel}</span>
|
||||
</Button>
|
||||
{/* Inline header-style action on `lg+`. Lives in the page body so
|
||||
it picks up the sub-page layout's spacing instead of fighting
|
||||
a sticky header overlay. */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="coach-launch-inline"
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
className={cn(
|
||||
"hidden h-10 gap-2 self-end lg:inline-flex",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
<span>{accessibleLabel}</span>
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="coach-launch-inline"
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
className={cn(
|
||||
"hidden h-10 gap-2 self-end lg:inline-flex",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
<span>{accessibleLabel}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"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";
|
||||
|
||||
/**
|
||||
* Layout-level mobile Coach launch FAB.
|
||||
*
|
||||
* v1.4.28 R3c — promoted out of `<CoachLaunchButton>` (which previously
|
||||
* rendered both the FAB and the inline pill from a single component;
|
||||
* each sub-page mount therefore painted a duplicate FAB into the DOM
|
||||
* before `fixed` positioning collapsed them visually). Mounting the
|
||||
* FAB once at the layout level keeps the affordance reachable from
|
||||
* every routed Insights surface while leaving the a11y tree with a
|
||||
* single button.
|
||||
*
|
||||
* Hidden on `lg+` viewports — desktop users reach the Coach via the
|
||||
* inline `<CoachLaunchButton>` pill that each page mounts in its
|
||||
* action row.
|
||||
*/
|
||||
export function LayoutCoachFab() {
|
||||
const { t } = useTranslations();
|
||||
const launch = useCoachLaunch();
|
||||
if (!launch) return null;
|
||||
|
||||
const accessibleLabel = t("insights.heroActionAskCoach");
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
data-slot="coach-launch-fab"
|
||||
onClick={() => launch.askCoach(null)}
|
||||
aria-label={accessibleLabel}
|
||||
title={accessibleLabel}
|
||||
className={cn(
|
||||
"fixed right-4 bottom-20 z-40 h-12 rounded-full px-4 shadow-lg",
|
||||
"from-dracula-purple to-dracula-pink bg-gradient-to-br text-white",
|
||||
"hover:from-dracula-purple/90 hover:to-dracula-pink/90",
|
||||
"lg:hidden",
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
<span>{accessibleLabel}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user