diff --git a/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx b/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx new file mode 100644 index 000000000..ea7859d47 --- /dev/null +++ b/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx @@ -0,0 +1,103 @@ +import { describe, it, expect } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { CoachDrawerBody } from "../coach-drawer-body"; + +/** + * v1.4.20 phase B4 — Coach drawer mobile rail trays. + * + * The drawer hides the history + sources rails on `` so the SSR test can pin their `lg:hidden` class + * + slot markers without rendering the outer Radix `` portal + * (which is client-only). + */ + +function render(node: React.ReactNode, locale: "en" | "de" = "en") { + return renderToStaticMarkup( + {node}, + ); +} + +const baseProps = { + historyRail: history, + sourcesRail: sources, + thread: thread, + composer: composer, + onOpenHistoryTray: () => undefined, + onOpenSourcesTray: () => undefined, +}; + +describe(" — mobile rail trays", () => { + it("renders both tray triggers", () => { + const html = render(); + expect(html).toMatch(/data-slot="coach-drawer-history-tray-trigger"/); + expect(html).toMatch(/data-slot="coach-drawer-sources-tray-trigger"/); + }); + + it("hides both triggers on >=lg via the lg:hidden class", () => { + const html = render(); + const historyTrigger = html.match( + /]*data-slot="coach-drawer-history-tray-trigger"[^>]*>/, + ); + expect(historyTrigger).not.toBeNull(); + expect(historyTrigger?.[0]).toContain("lg:hidden"); + + const sourcesTrigger = html.match( + /]*data-slot="coach-drawer-sources-tray-trigger"[^>]*>/, + ); + expect(sourcesTrigger).not.toBeNull(); + expect(sourcesTrigger?.[0]).toContain("lg:hidden"); + }); + + it("renders the desktop history + sources rails (visible on >=lg)", () => { + const html = render(); + expect(html).toMatch( + /]*data-slot="coach-drawer-history"[^>]*lg:flex/, + ); + expect(html).toMatch( + /]*data-slot="coach-drawer-sources"[^>]*lg:flex/, + ); + }); + + it("renders the message thread + composer slots", () => { + const html = render(); + expect(html).toMatch(/data-slot="coach-drawer-thread"/); + expect(html).toMatch(/data-slot="coach-drawer-composer"/); + // Slot content forwarded verbatim. + expect(html).toContain("thread"); + expect(html).toContain("composer"); + }); + + it("uses chevron-style trigger labels — History on the left, Sources on the right", () => { + const html = render(); + const historyTrigger = html.match( + /]*data-slot="coach-drawer-history-tray-trigger"[^>]*>[\s\S]*?<\/button>/, + ); + expect(historyTrigger).not.toBeNull(); + expect(historyTrigger?.[0]).toContain("Conversations"); + + const sourcesTrigger = html.match( + /]*data-slot="coach-drawer-sources-tray-trigger"[^>]*>[\s\S]*?<\/button>/, + ); + expect(sourcesTrigger).not.toBeNull(); + expect(sourcesTrigger?.[0]).toContain("What I can see"); + }); + + it("renders the German trigger labels in the de locale", () => { + const html = render(, "de"); + expect(html).toContain("Unterhaltungen"); + expect(html).toContain("Worauf ich zugreife"); + }); + + it("the thread region wraps the rail triggers in a relative container", () => { + // The triggers use absolute positioning; the thread region must + // be `relative` for them to anchor inside it. + const html = render(); + expect(html).toMatch( + /]*data-slot="coach-drawer-thread"[^>]*relative/, + ); + }); +}); diff --git a/src/components/insights/coach-panel/coach-drawer-body.tsx b/src/components/insights/coach-panel/coach-drawer-body.tsx new file mode 100644 index 000000000..3ba0952da --- /dev/null +++ b/src/components/insights/coach-panel/coach-drawer-body.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { ChevronLeft, ChevronRight } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { useTranslations } from "@/lib/i18n/context"; + +/** + * v1.4.20 phase B4 — pure presentational body for ``. + * + * The drawer's outer Radix `` does not surface in SSR (its + * portal is client-only), so the unit suite couldn't pin the + * mobile-only rail-tray triggers via the `` itself. + * Lifting the body grid + chevron triggers + composer slot up here + * gives the test a node-renderable surface; the `` + * itself stays the integrating shell that owns state + the trays. + * + * The component is stateless: the parent owns `historyTrayOpen` / + * `sourcesTrayOpen` and passes the open-toggles + slot content. This + * keeps the trigger logic + the desktop rail layout in one place + * while delegating the actual side-sheet to the parent. + */ +export interface CoachDrawerBodyProps { + historyRail: React.ReactNode; + sourcesRail: React.ReactNode; + thread: React.ReactNode; + composer: React.ReactNode; + onOpenHistoryTray: () => void; + onOpenSourcesTray: () => void; +} + +export function CoachDrawerBody({ + historyRail, + sourcesRail, + thread, + composer, + onOpenHistoryTray, + onOpenSourcesTray, +}: CoachDrawerBodyProps) { + const { t } = useTranslations(); + return ( +
+ {/* History rail — desktop only. Mobile users summon it via the + tray trigger inside the thread column. */} + + + {/* Centre — message thread. */} +
+ {/* v1.4.20 phase B4 — mobile rail tray triggers. Hidden on + `>=lg` because the rails are inline there. */} + + + +
{thread}
+ {/* Composer pinned to the bottom. */} +
+ {composer} +
+
+ + {/* Sources rail — desktop only. */} + +
+ ); +} diff --git a/src/components/insights/coach-panel/coach-drawer.tsx b/src/components/insights/coach-panel/coach-drawer.tsx index f79824d8a..942930d1e 100644 --- a/src/components/insights/coach-panel/coach-drawer.tsx +++ b/src/components/insights/coach-panel/coach-drawer.tsx @@ -15,6 +15,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; +import { CoachDrawerBody } from "./coach-drawer-body"; import { CoachInput } from "./coach-input"; import { HistoryRail } from "./history-rail"; import { MessageThread } from "./message-thread"; @@ -74,6 +75,12 @@ export function CoachDrawer({ const [currentConversationId, setCurrentConversationId] = useState< string | null >(null); + // v1.4.20 phase B4 — mobile rail trays. The history + sources rails + // are hidden on ` - {/* Body — three columns on lg+, single column on smaller. */} -
- {/* History rail — desktop only. Mobile collapses; v1.4.21 will - ship a tray for it. */} - - - {/* Centre — message thread. */} -
-
- } + thread={ + + } + composer={ + composer ?? ( + handleSubmit(inputValue)} + disabled={send.isStreaming} + isStreaming={send.isStreaming} /> -
- {/* Composer pinned to the bottom. */} -
- {composer ?? ( - handleSubmit(inputValue)} - disabled={send.isStreaming} - isStreaming={send.isStreaming} + ) + } + onOpenHistoryTray={() => setHistoryTrayOpen(true)} + 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 ` + + + + {t("insights.coach.historyTitle")} + + +
+ {historyRail ?? ( + { + setCurrentConversationId(id); + setHistoryTrayOpen(false); + }} /> )}
-
- - {/* Sources rail — desktop only. */} - -
+ + + {t("insights.coach.sourcesTitle")} + + +
+ {sourcesRail ?? } +
+ +
);