FN-7830: fix Insights view header wrapping on mobile
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) <noreply@runfusion.ai>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(<InsightsView {...defaultProps} />);
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user