mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 12:22:03 +00:00
feat(insights): add <HeroStrip> + Dracula gradient utilities
New `src/components/insights/hero-strip.tsx` — replaces the v1.4.16
`<InsightsPageHero>` band with the wider strip from the v1.4.20
redesign:
- locale-aware time-of-day greeting (morning / afternoon / evening
/ night → 4 i18n keys, with "night" mapping to "Good evening" in
both locales since 03:00 + Guten Morgen reads odd)
- briefing-paragraph subtitle (falls back to a generic line when
no briefing is cached)
- personal-baseline + 'Generated <relative-time>' meta band
- 3-button action row: weekly-report (disabled, B4), ask-the-coach
(disabled, B2), re-run analysis (wired through onRegenerate).
Disabled buttons surface a 'Coming soon' tooltip via title= so
we don't drag in a Radix tooltip provider for static labels.
- <SuggestedPrompts> chip strip below the action band; onPickPrompt
is a TODO until B2 lands the Coach drawer (parent passes a no-op)
New CSS utilities in `globals.css`:
- .hero-gradient — Dracula purple → pink linear gradient over card
- .glow-purple — soft purple aura via box-shadow
- @keyframes pulseDot + .pulse-dot — for the future "live" dot
affordance; reduced-motion users get a static dot
Old `<InsightsPageHero>` stays in place (used by the dashboard
preview) and gains a JSDoc @deprecated note pointing to the new
component. EN + DE translations added in lockstep.
Test suite: +22 specs covering greeting buckets per locale + per
hour, paragraph fallback, disabled-button affordance, regenerate
wiring, suggested-prompts mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
92f332efe6
commit
59e63a0641
4 files changed
+512
No files matched your search
@@ -243,3 +243,62 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── v1.4.20 phase B1 — Insights redesign hero strip ─────────────── */
|
||||
|
||||
/*
|
||||
* `.hero-gradient` — purple → pink linear gradient at low opacity, for
|
||||
* the new <HeroStrip> band on /insights. Sits above `var(--card)` so
|
||||
* the band reads as a richer surface than the card it overlays.
|
||||
*
|
||||
* Matches the design handoff (`prototype/artboard-fullpage.jsx →
|
||||
* BriefingHero` background) — purple/pink at ~12% opacity over the
|
||||
* card colour, with a subtle border-tint to anchor it on the dark bg.
|
||||
*/
|
||||
.hero-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--dracula-purple) 18%, var(--card)) 0%,
|
||||
color-mix(in srgb, var(--dracula-pink) 12%, var(--card)) 100%
|
||||
);
|
||||
border: 1px solid color-mix(in srgb, var(--dracula-purple) 24%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* `.glow-purple` — soft purple aura behind the hero card, painted via
|
||||
* `box-shadow` so it stays GPU-cheap and respects the existing card
|
||||
* border radius without an extra wrapper element.
|
||||
*/
|
||||
.glow-purple {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--dracula-purple) 16%, transparent),
|
||||
0 18px 48px -28px color-mix(in srgb, var(--dracula-purple) 50%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* `pulseDot` keyframes — the "live, refreshed N min ago" green dot in
|
||||
* the hero meta row. Used as `.pulse-dot { animation: pulseDot ... }`.
|
||||
* Reduced-motion users get a static dot via the prefers-reduced-motion
|
||||
* guard below.
|
||||
*/
|
||||
@keyframes pulseDot {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
animation: pulseDot 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pulse-dot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { HeroStrip } from "../hero-strip";
|
||||
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B1 — Insights hero strip.
|
||||
*
|
||||
* Replaces v1.4.16 `<InsightsPageHero>`. Pinning the slots so future
|
||||
* polish can't silently drop the greeting / subtitle / action row /
|
||||
* suggested-prompt strip.
|
||||
*/
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const sampleBriefing: DailyBriefingPayload = {
|
||||
paragraph:
|
||||
"You're trending well this week. BP is in target band on 9 of 10 readings.",
|
||||
keyFindings: [],
|
||||
};
|
||||
|
||||
// `getHours()` reads from the local representation. Force a known
|
||||
// local hour by constructing from a local-time fields tuple.
|
||||
const morningLocal = new Date(2026, 4, 10, 9, 0, 0); // May 10, 09:00 local
|
||||
const afternoonLocal = new Date(2026, 4, 10, 14, 0, 0); // May 10, 14:00 local
|
||||
const eveningLocal = new Date(2026, 4, 10, 20, 0, 0); // May 10, 20:00 local
|
||||
|
||||
describe("<HeroStrip>", () => {
|
||||
it("renders the slot wrapper + Dracula gradient utility", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip"/);
|
||||
expect(html).toContain("hero-gradient");
|
||||
expect(html).toContain("glow-purple");
|
||||
});
|
||||
|
||||
it("renders the morning greeting in English", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-greeting"/);
|
||||
expect(html).toContain("Good morning");
|
||||
});
|
||||
|
||||
it("renders the morning greeting in German", () => {
|
||||
const html = render(
|
||||
<HeroStrip briefing={null} now={morningLocal} />,
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("Guten Morgen");
|
||||
});
|
||||
|
||||
it("renders the afternoon greeting in English at 14:00 local", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={afternoonLocal} />);
|
||||
expect(html).toContain("Good afternoon");
|
||||
});
|
||||
|
||||
it("renders the evening greeting in English at 20:00 local", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={eveningLocal} />);
|
||||
expect(html).toContain("Good evening");
|
||||
});
|
||||
|
||||
it("appends the user name to the greeting when supplied", () => {
|
||||
const html = render(
|
||||
<HeroStrip briefing={null} now={morningLocal} userName="Marc" />,
|
||||
);
|
||||
expect(html).toContain("Good morning, Marc");
|
||||
});
|
||||
|
||||
it("does NOT append a comma when userName is missing", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
// Greeting renders without trailing comma + name.
|
||||
expect(html).not.toMatch(/Good morning,\s*</);
|
||||
});
|
||||
|
||||
it("uses the briefing paragraph as the subtitle when one is supplied", () => {
|
||||
const html = render(
|
||||
<HeroStrip briefing={sampleBriefing} now={morningLocal} />,
|
||||
);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-subtitle"/);
|
||||
expect(html).toContain(
|
||||
"You're trending well this week. BP is in target band on 9 of 10 readings.",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the heroFallbackSubtitle when briefing is null", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toContain(
|
||||
"A daily read of your trends, drawn straight from the numbers you've logged.",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the personal-baseline meta line", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-baseline"/);
|
||||
expect(html).toContain("Based on your last 90 days");
|
||||
});
|
||||
|
||||
it("renders the generated-time caption when updatedAt is supplied", () => {
|
||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000).toISOString();
|
||||
const html = render(
|
||||
<HeroStrip
|
||||
briefing={null}
|
||||
updatedAt={fiveMinutesAgo}
|
||||
now={morningLocal}
|
||||
/>,
|
||||
);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-generated"/);
|
||||
expect(html).toContain("Generated");
|
||||
});
|
||||
|
||||
it("does NOT render the generated caption when updatedAt is missing", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).not.toContain('data-slot="insights-hero-strip-generated"');
|
||||
});
|
||||
|
||||
// ── Action row ─────────────────────────────────────────────────────
|
||||
it("renders the weekly-report action button as disabled with a 'Coming soon' title", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(
|
||||
/data-slot="insights-hero-strip-action-weekly-report"[^>]*disabled[^>]*title="Coming soon"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the ask-the-coach action button as disabled with a 'Coming soon' title", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(
|
||||
/data-slot="insights-hero-strip-action-coach"[^>]*disabled[^>]*title="Coming soon"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the regenerate button when onRegenerate is supplied", () => {
|
||||
const html = render(
|
||||
<HeroStrip briefing={null} now={morningLocal} onRegenerate={() => {}} />,
|
||||
);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-action-rerun"/);
|
||||
expect(html).toContain("Re-run analysis");
|
||||
});
|
||||
|
||||
it("hides the regenerate button when no handler is supplied", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).not.toContain('data-slot="insights-hero-strip-action-rerun"');
|
||||
});
|
||||
|
||||
it("disables the regenerate button while regenerating", () => {
|
||||
const html = render(
|
||||
<HeroStrip
|
||||
briefing={null}
|
||||
now={morningLocal}
|
||||
onRegenerate={() => {}}
|
||||
regenerating
|
||||
/>,
|
||||
);
|
||||
expect(html).toMatch(
|
||||
/data-slot="insights-hero-strip-action-rerun"[^>]*disabled/,
|
||||
);
|
||||
expect(html).toContain("Regenerating");
|
||||
});
|
||||
|
||||
// ── Suggested prompts strip ────────────────────────────────────────
|
||||
it("mounts the SuggestedPrompts strip below the action band", () => {
|
||||
const html = render(<HeroStrip briefing={null} now={morningLocal} />);
|
||||
expect(html).toMatch(/data-slot="insights-hero-strip-prompts"/);
|
||||
expect(html).toMatch(/data-slot="insights-suggested-prompts"/);
|
||||
expect(html).toContain("Try asking");
|
||||
expect(html).toContain("Why was BP higher on Monday?");
|
||||
});
|
||||
|
||||
it("forwards onPickPrompt down to the prompt strip without throwing", () => {
|
||||
const html = render(
|
||||
<HeroStrip
|
||||
briefing={null}
|
||||
now={morningLocal}
|
||||
onPickPrompt={() => {}}
|
||||
/>,
|
||||
);
|
||||
// The chips render verbatim; SSR doesn't run click handlers.
|
||||
expect(html).toMatch(/data-slot="insights-suggested-prompts-chip"/);
|
||||
});
|
||||
|
||||
// ── Misc ───────────────────────────────────────────────────────────
|
||||
it("supports the German locale fallback subtitle", () => {
|
||||
const html = render(
|
||||
<HeroStrip briefing={null} now={morningLocal} />,
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("Täglicher Blick auf deine Trends");
|
||||
});
|
||||
|
||||
it("uses the now override deterministically (smoke for greeting-bucket logic)", () => {
|
||||
// Five different hours map to four different greeting buckets:
|
||||
// 09:00 → morning, 14:00 → afternoon, 20:00 → evening, 02:00 → night.
|
||||
const earlyMorning = new Date(2026, 4, 10, 2, 0, 0);
|
||||
const html = render(<HeroStrip briefing={null} now={earlyMorning} />);
|
||||
// Night maps to "Good evening" in EN per the resolver mapping.
|
||||
expect(html).toContain("Good evening");
|
||||
// Sanity: midnight-bucket sample is NOT "Good morning".
|
||||
expect(html).not.toContain("Good morning");
|
||||
});
|
||||
|
||||
it("uses the morning bucket at noon-1 (11:59 boundary)", () => {
|
||||
const justBeforeNoon = new Date(2026, 4, 10, 11, 59, 0);
|
||||
const html = render(<HeroStrip briefing={null} now={justBeforeNoon} />);
|
||||
expect(html).toContain("Good morning");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
FileText,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SuggestedPrompts } from "./suggested-prompts";
|
||||
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B1 — Insights redesign hero strip.
|
||||
*
|
||||
* Replaces the v1.4.16 `<InsightsPageHero>` with the wider band from
|
||||
* the design handoff (`prototype/artboard-fullpage.jsx → BriefingHero`):
|
||||
*
|
||||
* - locale-aware time-of-day greeting line ("Good morning, …" /
|
||||
* "Guten Morgen, …") + a narrative subtitle (the briefing
|
||||
* paragraph when one is cached, else a fallback)
|
||||
* - personal-baseline + "Generated <relative-time>" meta row
|
||||
* - 3-button action row: "Generate weekly report" (disabled, B4),
|
||||
* "Ask the coach" (disabled, B2), "Re-run analysis" (wired to
|
||||
* the existing regenerate handler from v1.4.16 D-reconcile)
|
||||
* - <SuggestedPrompts> chip strip below the action band
|
||||
* - Dracula gradient + soft purple glow via the new `.hero-gradient`
|
||||
* + `.glow-purple` utilities in `globals.css`
|
||||
*
|
||||
* Mobile-first: the action row wraps + the suggested-prompt chips
|
||||
* wrap. Right-side Health Score panel is *not* part of B1 — that
|
||||
* lands in B5; for B1 the right side just keeps its meta band.
|
||||
*
|
||||
* Pure presentational — the page owns the briefing data + the
|
||||
* regenerate handler.
|
||||
*/
|
||||
|
||||
interface HeroStripProps {
|
||||
/** Briefing payload — its paragraph drives the hero subtitle. */
|
||||
briefing: DailyBriefingPayload | null;
|
||||
/** ISO timestamp of the freshest cached payload. */
|
||||
updatedAt?: string | null;
|
||||
/**
|
||||
* Display name for the greeting; defaults to no-name greeting
|
||||
* ("Good morning,") when omitted so the hero never paints "Good
|
||||
* morning, undefined".
|
||||
*/
|
||||
userName?: string | null;
|
||||
/** Click handler for the regenerate button — wired to the advisor query. */
|
||||
onRegenerate?: () => void;
|
||||
/** Disables the regenerate button + flips its icon to a spinner. */
|
||||
regenerating?: boolean;
|
||||
/**
|
||||
* Click handler for the suggested prompts. The Coach drawer (B2)
|
||||
* will own the routing; for B1 the parent passes a no-op.
|
||||
*/
|
||||
onPickPrompt?: (prompt: string) => void;
|
||||
/**
|
||||
* Now() override for tests so the greeting bucket is deterministic.
|
||||
* Defaults to `new Date()`. Production callers omit this.
|
||||
*/
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucketed time-of-day greeting:
|
||||
* 05:00–11:59 → morning
|
||||
* 12:00–17:59 → afternoon
|
||||
* 18:00–22:59 → evening
|
||||
* 23:00–04:59 → night (mapped to "evening" for both locales — it's
|
||||
* rare to see /insights at 03:00 and "Guten
|
||||
* Morgen" pre-5am reads odd)
|
||||
*/
|
||||
function resolveGreetingKey(now: Date): string {
|
||||
const hour = now.getHours();
|
||||
if (hour >= 5 && hour < 12) return "insights.heroGreetingMorning";
|
||||
if (hour >= 12 && hour < 18) return "insights.heroGreetingAfternoon";
|
||||
if (hour >= 18 && hour < 23) return "insights.heroGreetingEvening";
|
||||
return "insights.heroGreetingNight";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucketed relative-time using i18n translation keys so every surface
|
||||
* shares vocabulary. Mirrors the helper in `<InsightsPageHero>`.
|
||||
*/
|
||||
function formatRelativeTime(
|
||||
iso: string,
|
||||
t: (key: string, params?: Record<string, string | number>) => string,
|
||||
): string {
|
||||
const target = new Date(iso).getTime();
|
||||
if (Number.isNaN(target)) return "";
|
||||
const diffMs = Date.now() - target;
|
||||
if (diffMs < 60_000) return t("insights.relativeJustNow");
|
||||
const minutes = Math.floor(diffMs / 60_000);
|
||||
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
|
||||
const days = Math.floor(hours / 24);
|
||||
return t("insights.relativeDaysAgo", { count: days });
|
||||
}
|
||||
|
||||
export function HeroStrip({
|
||||
briefing,
|
||||
updatedAt,
|
||||
userName,
|
||||
onRegenerate,
|
||||
regenerating = false,
|
||||
onPickPrompt,
|
||||
now,
|
||||
}: HeroStripProps) {
|
||||
const { t } = useTranslations();
|
||||
const greetingKey = resolveGreetingKey(now ?? new Date());
|
||||
const greetingBase = t(greetingKey);
|
||||
const greeting = userName ? `${greetingBase}, ${userName}` : greetingBase;
|
||||
const subtitle = briefing?.paragraph ?? t("insights.heroFallbackSubtitle");
|
||||
const generatedLine = updatedAt
|
||||
? t("insights.heroGenerated", { time: formatRelativeTime(updatedAt, t) })
|
||||
: null;
|
||||
const comingSoon = t("insights.heroComingSoonTooltip");
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="insights-hero-strip"
|
||||
className={cn(
|
||||
"hero-gradient glow-purple animate-insight-in",
|
||||
"relative overflow-hidden rounded-xl px-4 py-5 sm:px-6 sm:py-6",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles
|
||||
className="text-dracula-purple h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h1
|
||||
data-slot="insights-hero-strip-greeting"
|
||||
className="text-2xl leading-tight font-semibold tracking-tight sm:text-[28px]"
|
||||
>
|
||||
{greeting}
|
||||
</h1>
|
||||
</div>
|
||||
<p
|
||||
data-slot="insights-hero-strip-subtitle"
|
||||
className="text-muted-foreground max-w-3xl text-sm leading-relaxed"
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
<div className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span data-slot="insights-hero-strip-baseline">
|
||||
{t("insights.heroPersonalBaseline")}
|
||||
</span>
|
||||
{generatedLine && (
|
||||
<>
|
||||
<span aria-hidden="true" className="opacity-50">
|
||||
·
|
||||
</span>
|
||||
<span data-slot="insights-hero-strip-generated">
|
||||
{generatedLine}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/*
|
||||
* v1.4.20 phase B4 ships the weekly-report route. Disabled
|
||||
* here so the affordance lives in the hero from B1 onwards
|
||||
* without dead links — title= surfaces the "Coming soon"
|
||||
* caption on hover/focus without dragging in a Radix
|
||||
* tooltip provider for a single static label.
|
||||
*/}
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="sm"
|
||||
disabled
|
||||
title={comingSoon}
|
||||
data-slot="insights-hero-strip-action-weekly-report"
|
||||
className="gap-1.5"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>{t("insights.heroActionWeeklyReport")}</span>
|
||||
</Button>
|
||||
{/* B2 wires this into the Coach drawer. */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
title={comingSoon}
|
||||
data-slot="insights-hero-strip-action-coach"
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>{t("insights.heroActionAskCoach")}</span>
|
||||
</Button>
|
||||
{onRegenerate && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onRegenerate}
|
||||
disabled={regenerating}
|
||||
data-slot="insights-hero-strip-action-rerun"
|
||||
className="gap-1.5"
|
||||
>
|
||||
{regenerating ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>
|
||||
{regenerating
|
||||
? t("insights.heroRegenerating")
|
||||
: t("insights.heroActionRerun")}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-slot="insights-hero-strip-prompts"
|
||||
className="border-border/50 border-t pt-4"
|
||||
>
|
||||
{/*
|
||||
* TODO(B2): wire onPickPrompt into the Coach drawer so a
|
||||
* chip click pre-fills the input + opens the drawer. For
|
||||
* B1 the parent passes a no-op.
|
||||
*/}
|
||||
<SuggestedPrompts
|
||||
onPick={onPickPrompt ?? (() => undefined)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
/**
|
||||
* v1.4.16 phase B1b — Apple-Health-style page hero for `/insights`.
|
||||
*
|
||||
* @deprecated v1.4.20 phase B1 — superseded by `<HeroStrip>` in
|
||||
* `./hero-strip.tsx`. Kept here for the dashboard preview's continued
|
||||
* use; will be removed once that surface migrates.
|
||||
*
|
||||
* Polished header band that frames the page below. Pure presentational —
|
||||
* the page passes timestamps + the regenerate handler in. Stays a thin
|
||||
* shell around the existing translation keys so a future redesign only
|
||||
|
||||
Reference in new issue
Block a user