From 386fa4218972a13b26ce36dad0e84746bf21cce0 Mon Sep 17 00:00:00 2001 From: Dylan Trotter Date: Sat, 26 Sep 2026 23:33:18 +0000 Subject: [PATCH] Let the brief panel scroll and fit a phone screen The popover had a fixed 20rem width and no height cap, so a five-field brief ran past the bottom of a phone viewport with no way to reach the rest. Cap the height at the room Radix measures (--radix-popover-content-available-height, with a vh fallback for when collision detection is skipped) and scroll inside it, with overscroll-contain so the thread behind does not move. Use near-full width on a compact viewport and keep the fixed column on a wide one. The max height is an inline style rather than a Tailwind arbitrary value so it does not depend on what the plugin's Tailwind pass emits. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/thread-briefs/app.test.tsx | 42 +++++++++++++++++++++++++++++- plugins/thread-briefs/app.tsx | 15 ++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/plugins/thread-briefs/app.test.tsx b/plugins/thread-briefs/app.test.tsx index dfe4c6e..64fb8f5 100644 --- a/plugins/thread-briefs/app.test.tsx +++ b/plugins/thread-briefs/app.test.tsx @@ -62,11 +62,16 @@ describe("the header popover", () => { getBrief?: () => BriefState; setStageOverride?: (input: unknown) => BriefState; refresh?: () => { queued: boolean }; + isCompactViewport?: boolean; }) => { const captured = await loadApp(); return renderSlot( captured.threadHeaderActions[0]!, - { threadId: "thr_1", projectId: "proj_1", isCompactViewport: false }, + { + threadId: "thr_1", + projectId: "proj_1", + isCompactViewport: options.isCompactViewport ?? false, + }, { rpc: { getBrief: options.getBrief ?? (() => READY), @@ -142,6 +147,41 @@ describe("the header popover", () => { slot.lifecycle.unmount(); }); + it("caps its height and scrolls, so a long brief is reachable on a phone", async () => { + const slot = await render({ isCompactViewport: true }); + fireEvent.click(await slot.findByRole("button", { name: "Thread brief" })); + + const goal = await slot.findByText("Ship the thread-briefs plugin"); + const panel = goal.closest("[style*='max-height']") as HTMLElement | null; + expect(panel).not.toBeNull(); + // Without both of these the panel grows past the viewport with no way to + // reach the fields below the fold. + expect(panel?.style.maxHeight).toContain( + "--radix-popover-content-available-height", + ); + expect(panel?.className).toContain("overflow-y-auto"); + + slot.lifecycle.unmount(); + }); + + it("goes near-full-width on a compact viewport and a fixed column otherwise", async () => { + const compact = await render({ isCompactViewport: true }); + fireEvent.click(await compact.findByRole("button", { name: "Thread brief" })); + const compactPanel = ( + await compact.findByText("Ship the thread-briefs plugin") + ).closest("[style*='max-height']") as HTMLElement | null; + expect(compactPanel?.className).toContain("w-[calc(100vw-1rem)]"); + compact.lifecycle.unmount(); + + const wide = await render({ isCompactViewport: false }); + fireEvent.click(await wide.findByRole("button", { name: "Thread brief" })); + const widePanel = ( + await wide.findByText("Ship the thread-briefs plugin") + ).closest("[style*='max-height']") as HTMLElement | null; + expect(widePanel?.className).toContain("w-80"); + wide.lifecycle.unmount(); + }); + it("sets a manual stage", async () => { const slot = await render({}); fireEvent.click(await slot.findByRole("button", { name: "Thread brief" })); diff --git a/plugins/thread-briefs/app.tsx b/plugins/thread-briefs/app.tsx index d0518a9..9632f89 100644 --- a/plugins/thread-briefs/app.tsx +++ b/plugins/thread-briefs/app.tsx @@ -307,7 +307,20 @@ function BriefHeaderAction({