From 5ed15e27b78208512263e5e8f89fe91e3ff55466 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 20:32:56 -0700 Subject: [PATCH] FN-7830: fix Insights view header wrapping on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the Insights view header its own dedicated mobile layout so the title no longer collapses to an ellipsis and action buttons no longer crowd together at narrow widths/short heights. - Extend the mobile header media query to also trigger on short viewports (max-height: 480px), not just narrow widths - Force the Insights title onto its own full-width row (flex: 1 0 100%) and let it wrap actions below instead of shrinking - Stop truncating the title span (overflow: visible; text-overflow: clip) so "Insights" is never cut to "I…" - Let the actions cluster wrap to full width, left-aligned, with no leading margin so touch targets stay usable - Add regression tests covering the maximal header action cluster and asserting the new mobile/tablet/desktop CSS media-query contracts Files changed: packages/dashboard/app/components/InsightsView.css | 29 ++++++- .../app/components/__tests__/InsightsView.test.tsx | 88 ++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7830 Fusion-Task-Lineage: 4c92cb43-5c08-4b76-9f51-805ffa5deec9 Co-authored-by: Fusion (runfusion.ai) --- .../dashboard/app/components/InsightsView.css | 29 +++++- .../__tests__/InsightsView.test.tsx | 88 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/packages/dashboard/app/components/InsightsView.css b/packages/dashboard/app/components/InsightsView.css index 9f70b853b4..2a1140e020 100644 --- a/packages/dashboard/app/components/InsightsView.css +++ b/packages/dashboard/app/components/InsightsView.css @@ -488,7 +488,34 @@ insight row is hovered, keeping the title as the dominant element. } /* Mobile responsive: stack panes vertically; sidebar becomes a horizontal scroller */ -@media (max-width: 768px) { +@media (max-width: 768px), (max-height: 480px) { + /* + FNXC:Insights 2026-07-11-20:27: + FN-7830: Insights has the widest ViewHeader actions cluster, so mobile must give the title its own full-width row and wrap actions below it instead of shrinking the title to an `I…` ellipsis or crowding touch targets. + */ + .insights-view .view-header { + align-items: flex-start; + flex-wrap: wrap; + gap: var(--space-sm) var(--space-md); + } + + .insights-view .view-header__title { + flex: 1 0 100%; + min-width: 100%; + } + + .insights-view .view-header__title span { + overflow: visible; + text-overflow: clip; + } + + .insights-view .view-header__actions { + justify-content: flex-start; + width: 100%; + max-width: 100%; + margin-left: 0; + } + .insights-body { flex-direction: column; } diff --git a/packages/dashboard/app/components/__tests__/InsightsView.test.tsx b/packages/dashboard/app/components/__tests__/InsightsView.test.tsx index 7c54aa5ea4..d63da493d2 100644 --- a/packages/dashboard/app/components/__tests__/InsightsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/InsightsView.test.tsx @@ -1354,6 +1354,94 @@ describe("InsightsView", () => { }); describe("responsive CSS contracts", () => { + it("FN-7830: renders the maximal header action cluster without dropping the Insights title or controls", () => { + const maximalSections = [ + { + category: "workflow" as const, + label: "Workflow", + items: [ + { + id: "INS-BACKLOG", + projectId: "test", + title: "Backlog pressure detected 2026-07-11", + content: "Backlog health content", + category: "workflow" as const, + status: "generated" as const, + fingerprint: "fp-backlog", + provenance: { trigger: "manual" as const }, + lastRunId: null, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }, + { + id: "INS-ARCHIVED", + projectId: "test", + title: "Archived mobile header insight", + content: "Archived content", + category: "workflow" as const, + status: "archived" as const, + fingerprint: "fp-archived", + provenance: { trigger: "manual" as const }, + lastRunId: null, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }, + ], + isLoading: false, + error: null, + }, + ...mockSections, + ]; + + mockUseInsights.mockReturnValue({ + sections: maximalSections, + loading: false, + error: null, + latestRun: null, + isRunInFlight: false, + runError: null, + refresh: vi.fn(), + runInsights: vi.fn(), + dismiss: vi.fn(), + createTask: vi.fn(), + archive: vi.fn(), + unarchive: vi.fn(), + toggleShowArchived: vi.fn(), + dismissStates: new Map(), + createTaskStates: new Map(), + archiveStates: new Map(), + unarchiveStates: new Map(), + totalCount: 2, + dismissedCount: 0, + archivedCount: 1, + showArchived: false, + }); + + render(); + + expect(screen.getByText("Insights").closest("h2")).toHaveClass("view-header__title"); + expect(screen.getByText("2 total")).toHaveClass("insights-view-count"); + expect(screen.getByTestId("toggle-backlog-health")).toHaveTextContent("Backlog (1)"); + expect(screen.getByLabelText("Close insights view")).toHaveClass("insights-view-close"); + expect(screen.getByTestId("toggle-archived-insights")).toHaveTextContent("Archived (1)"); + expect(screen.getByTestId("refresh-insights")).toHaveClass("insights-refresh-btn"); + expect(screen.getByTestId("toggle-model-config")).toHaveClass("insights-model-toggle"); + expect(screen.getByTestId("run-insights")).toHaveTextContent("Generate Insights"); + }); + + it("FN-7830: stacks the Insights title above wrapped actions only in the mobile header tier", () => { + const css = loadAllAppCss(); + + expect(css).toMatch(/@media[^{}]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.insights-view\s+\.view-header\s*\{[^}]*flex-wrap:\s*wrap;[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.insights-view\s+\.view-header__title\s*\{[^}]*flex:\s*1\s+0\s+100%;[^}]*min-width:\s*100%;[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.insights-view\s+\.view-header__title\s+span\s*\{[^}]*overflow:\s*visible;[^}]*text-overflow:\s*clip;[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.insights-view\s+\.view-header__actions\s*\{[^}]*justify-content:\s*flex-start;[^}]*width:\s*100%;[^}]*margin-left:\s*0;[^}]*\}/); + + expect(css).toMatch(/@media[^{}]*\(min-width:\s*769px\)\s*and\s*\(min-height:\s*481px\)[^{]*\{[\s\S]*?\.view-header\s*\{[^}]*height:\s*var\(--view-header-min-height\);[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(min-width:\s*769px\)\s*and\s*\(min-height:\s*481px\)[^{]*\{[\s\S]*?\.view-header__actions\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*height:\s*var\(--view-header-content-row\);[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(min-width:\s*769px\)\s*and\s*\(max-width:\s*1024px\)[^{]*\{[\s\S]*?\.insights-body\s*\{[^}]*flex-direction:\s*column;[^}]*\}/); + }); + it("FN-6764: adds a tablet full-width reflow without regressing desktop or mobile tiers", () => { const baseCss = loadAllAppCssBaseOnly(); const css = loadAllAppCss();