mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
refactor(charts): single HealthChartDynamic re-export
This commit is contained in:
1 parent
8808561581
commit
235e52cb95
3 files changed
+237
-63
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.4.28 R3c (BK-F-M6) — pin the carved-out mobile rail tray's
|
||||
* presentational contract. The carve-out is pure render: the parent
|
||||
* owns open/closed state + the rail content; this component just
|
||||
* paints two side-sheets with the right slots when open.
|
||||
*
|
||||
* The shadcn `<Sheet>` portals its content out of the React tree, so
|
||||
* we mock the primitives down to plain wrappers (same pattern as
|
||||
* `coach-settings-sheet.test.tsx`) to keep the slot assertions
|
||||
* reachable in static markup.
|
||||
*/
|
||||
vi.mock("@/components/ui/sheet", () => ({
|
||||
Sheet: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
|
||||
open ? <div data-slot="mock-sheet">{children}</div> : null,
|
||||
SheetContent: ({
|
||||
children,
|
||||
"data-slot": dataSlot,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
"data-slot"?: string;
|
||||
className?: string;
|
||||
}) => (
|
||||
<div data-slot={dataSlot} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SheetHeader: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
SheetTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<h2>{children}</h2>
|
||||
),
|
||||
}));
|
||||
|
||||
import { MobileRailTray } from "../mobile-rail-tray";
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
historyOpen: true,
|
||||
onHistoryOpenChange: () => undefined,
|
||||
historyRail: <span>history-content</span>,
|
||||
sourcesOpen: true,
|
||||
onSourcesOpenChange: () => undefined,
|
||||
sourcesRail: <span>sources-content</span>,
|
||||
};
|
||||
|
||||
describe("<MobileRailTray>", () => {
|
||||
it("renders both trays with their data-slot identifiers when open", () => {
|
||||
const html = render(<MobileRailTray {...baseProps} />);
|
||||
expect(html).toContain('data-slot="coach-drawer-history-tray"');
|
||||
expect(html).toContain('data-slot="coach-drawer-sources-tray"');
|
||||
});
|
||||
|
||||
it("forwards the rail content slots verbatim", () => {
|
||||
const html = render(<MobileRailTray {...baseProps} />);
|
||||
expect(html).toContain("history-content");
|
||||
expect(html).toContain("sources-content");
|
||||
});
|
||||
|
||||
it("hides the history tray on lg+ and the sources tray on xl+", () => {
|
||||
// Matches the breakpoints documented inline: the history rail
|
||||
// mounts inline at lg+, so the tray is `lg:hidden`. The sources
|
||||
// rail mounts inline at xl+ only, so the tray is `xl:hidden`.
|
||||
const html = render(<MobileRailTray {...baseProps} />);
|
||||
expect(html).toMatch(
|
||||
/data-slot="coach-drawer-history-tray"[^>]*class="[^"]*lg:hidden/,
|
||||
);
|
||||
expect(html).toMatch(
|
||||
/data-slot="coach-drawer-sources-tray"[^>]*class="[^"]*xl:hidden/,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders both header titles localised", () => {
|
||||
const enHtml = render(<MobileRailTray {...baseProps} />, "en");
|
||||
expect(enHtml).toContain("Conversations");
|
||||
expect(enHtml).toContain("What I can see");
|
||||
|
||||
const deHtml = render(<MobileRailTray {...baseProps} />, "de");
|
||||
expect(deHtml).toContain("Unterhaltungen");
|
||||
expect(deHtml).toContain("Worauf ich zugreife");
|
||||
});
|
||||
|
||||
it("does not render either tray when its open prop is false", () => {
|
||||
// The mock's `Sheet` returns `null` when `open === false`. Pin
|
||||
// that contract so the carve-out stays gated on `historyOpen` /
|
||||
// `sourcesOpen` and never paints orphan overlays.
|
||||
const html = render(
|
||||
<MobileRailTray
|
||||
{...baseProps}
|
||||
historyOpen={false}
|
||||
sourcesOpen={false}
|
||||
/>,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="coach-drawer-history-tray"');
|
||||
expect(html).not.toContain('data-slot="coach-drawer-sources-tray"');
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,7 @@ import { CoachInput } from "./coach-input";
|
||||
import { CoachSettingsSheet } from "./coach-settings-sheet";
|
||||
import { HistoryRail } from "./history-rail";
|
||||
import { MessageThread } from "./message-thread";
|
||||
import { MobileRailTray } from "./mobile-rail-tray";
|
||||
import { DEFAULT_COACH_SCOPE, SourcesRail } from "./sources-rail";
|
||||
import { useCoachConversation, useSendCoachMessage } from "./use-coach";
|
||||
import type {
|
||||
@@ -488,74 +489,52 @@ export function CoachDrawer({
|
||||
onOpenSourcesTray={() => setSourcesTrayOpen(true)}
|
||||
/>
|
||||
|
||||
{/* v1.4.20 phase B4 — mobile-only rail trays. Each renders a
|
||||
slide-in panel from the matching edge and surfaces the same
|
||||
HistoryRail / SourcesRail instance the desktop layout uses,
|
||||
so a `>=lg` viewport never shows them but a `<lg` viewport
|
||||
can summon either rail without leaving the message thread. */}
|
||||
<Sheet open={historyTrayOpen} onOpenChange={setHistoryTrayOpen}>
|
||||
<SheetContent
|
||||
side="left"
|
||||
data-slot="coach-drawer-history-tray"
|
||||
className="w-[88vw] max-w-[320px] p-0 lg:hidden"
|
||||
>
|
||||
<SheetHeader className="border-border/70 border-b p-3">
|
||||
<SheetTitle className="text-sm">
|
||||
{t("insights.coach.historyTitle")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-full min-h-0 overflow-y-auto">
|
||||
{historyRail ?? (
|
||||
<HistoryRail
|
||||
activeId={currentConversationId}
|
||||
onSelect={(id) => {
|
||||
setCurrentConversationId(id);
|
||||
setHistoryTrayOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{/* v1.4.23 H4 — Coach prompt-tuning sheet. Right-edge sheet so
|
||||
it doesn't conflict with the existing left/right rail
|
||||
trays (those are <lg / <xl only; the settings sheet works
|
||||
on every viewport). */}
|
||||
it doesn't conflict with the existing rail trays (those are
|
||||
<lg / <xl only; the settings sheet works on every
|
||||
viewport). */}
|
||||
<CoachSettingsSheet
|
||||
open={settingsOpen}
|
||||
onOpenChange={setSettingsOpen}
|
||||
/>
|
||||
<Sheet open={sourcesTrayOpen} onOpenChange={setSourcesTrayOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
data-slot="coach-drawer-sources-tray"
|
||||
// Sources rail is xl+ inline; on lg it's only available
|
||||
// via this tray to match the narrowed drawer cap.
|
||||
className="w-[88vw] max-w-[320px] p-0 xl:hidden"
|
||||
>
|
||||
<SheetHeader className="border-border/70 border-b p-3">
|
||||
<SheetTitle className="text-sm">
|
||||
{t("insights.coach.sourcesTitle")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-full min-h-0 overflow-y-auto">
|
||||
{sourcesRail ?? (
|
||||
<SourcesRail
|
||||
scope={{
|
||||
sources: scope.sources,
|
||||
window: effectiveWindow,
|
||||
}}
|
||||
onScopeChange={(next) => {
|
||||
setScope((prev) => ({ ...prev, sources: next.sources }));
|
||||
setWindowOverride(
|
||||
next.window === savedDefaultWindow ? null : next.window,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{/* v1.4.28 R3c (BK-F-M6) — mobile-only rail trays carved out of
|
||||
the drawer shell. Each tray surfaces the same HistoryRail /
|
||||
SourcesRail instance the desktop layout uses; the parent
|
||||
still owns the open/closed state + the rail callbacks so
|
||||
the carve-out is pure render. */}
|
||||
<MobileRailTray
|
||||
historyOpen={historyTrayOpen}
|
||||
onHistoryOpenChange={setHistoryTrayOpen}
|
||||
historyRail={
|
||||
historyRail ?? (
|
||||
<HistoryRail
|
||||
activeId={currentConversationId}
|
||||
onSelect={(id) => {
|
||||
setCurrentConversationId(id);
|
||||
setHistoryTrayOpen(false);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
sourcesOpen={sourcesTrayOpen}
|
||||
onSourcesOpenChange={setSourcesTrayOpen}
|
||||
sourcesRail={
|
||||
sourcesRail ?? (
|
||||
<SourcesRail
|
||||
scope={{
|
||||
sources: scope.sources,
|
||||
window: effectiveWindow,
|
||||
}}
|
||||
onScopeChange={(next) => {
|
||||
setScope((prev) => ({ ...prev, sources: next.sources }));
|
||||
setWindowOverride(
|
||||
next.window === savedDefaultWindow ? null : next.window,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B4 — mobile-only rail trays for the Coach drawer.
|
||||
*
|
||||
* The desktop layout pins the history rail (lg+) and the sources rail
|
||||
* (xl+) alongside the message thread. Below those breakpoints the
|
||||
* rails are reachable via chevron triggers in the thread's sub-header
|
||||
* strip; each trigger opens the matching side-sheet here.
|
||||
*
|
||||
* v1.4.28 R3c (BK-F-M6) — carved out of `<CoachDrawer>` (~560 LOC).
|
||||
* The parent still owns the rail content + the open/closed state for
|
||||
* each tray so this component stays a thin presentational shell that
|
||||
* can be unit-tested in isolation without standing up the drawer's
|
||||
* snapshot fetch + scope-picker hooks.
|
||||
*
|
||||
* The component takes pre-rendered rail nodes rather than the raw
|
||||
* rail components so the caller controls how each rail's
|
||||
* `onSelect` / `onScopeChange` callbacks close the tray (some flows
|
||||
* dismiss the tray after a selection; the contract for that lives in
|
||||
* the parent, not here).
|
||||
*/
|
||||
export interface MobileRailTrayProps {
|
||||
historyOpen: boolean;
|
||||
onHistoryOpenChange: (next: boolean) => void;
|
||||
historyRail: ReactNode;
|
||||
sourcesOpen: boolean;
|
||||
onSourcesOpenChange: (next: boolean) => void;
|
||||
sourcesRail: ReactNode;
|
||||
}
|
||||
|
||||
export function MobileRailTray({
|
||||
historyOpen,
|
||||
onHistoryOpenChange,
|
||||
historyRail,
|
||||
sourcesOpen,
|
||||
onSourcesOpenChange,
|
||||
sourcesRail,
|
||||
}: MobileRailTrayProps) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<>
|
||||
{/* History tray — slides in from the left edge. `lg:hidden`
|
||||
because the history rail is inline on lg+. */}
|
||||
<Sheet open={historyOpen} onOpenChange={onHistoryOpenChange}>
|
||||
<SheetContent
|
||||
side="left"
|
||||
data-slot="coach-drawer-history-tray"
|
||||
className="w-[88vw] max-w-[320px] p-0 lg:hidden"
|
||||
>
|
||||
<SheetHeader className="border-border/70 border-b p-3">
|
||||
<SheetTitle className="text-sm">
|
||||
{t("insights.coach.historyTitle")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-full min-h-0 overflow-y-auto">{historyRail}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{/* Sources tray — slides in from the right edge. `xl:hidden`
|
||||
because the sources rail is inline on xl+. */}
|
||||
<Sheet open={sourcesOpen} onOpenChange={onSourcesOpenChange}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
data-slot="coach-drawer-sources-tray"
|
||||
className="w-[88vw] max-w-[320px] p-0 xl:hidden"
|
||||
>
|
||||
<SheetHeader className="border-border/70 border-b p-3">
|
||||
<SheetTitle className="text-sm">
|
||||
{t("insights.coach.sourcesTitle")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="h-full min-h-0 overflow-y-auto">{sourcesRail}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user