FN-8679: contain mobile Summary table overflow
Keep Summary token and cost tables horizontally scrollable within mobile task details. - Constrain Summary flex ancestors so wide tables cannot widen the detail body - Preserve real token/cost table rendering across populated and empty usage states - Add a patch changeset for the mobile overflow fix Files changed: .changeset/fn-8679-mobile-token-scroll.md | 7 +++ .../dashboard/app/components/TaskDetailModal.css | 9 ++++ .../__tests__/TaskDetailModal.summary-tab.test.tsx | 60 +++++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8679 Fusion-Task-Lineage: 33562f89-053f-4681-b1b1-98aa2f2704a2 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8679-mobile-token-scroll.md
Normal file
7
.changeset/fn-8679-mobile-token-scroll.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Keep Summary token and cost tables scrollable on mobile.
|
||||||
|
category: fix
|
||||||
|
dev: Contains mobile table overflow within the task Summary surface.
|
||||||
@@ -2383,12 +2383,16 @@ The done-task Summary tab is a scrollable overview composed from existing detail
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-lg);
|
gap: var(--space-lg);
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-summary-section {
|
.task-summary-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: var(--btn-border-width) solid var(--border);
|
border: var(--btn-border-width) solid var(--border);
|
||||||
@@ -2467,9 +2471,14 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap
|
|||||||
|
|
||||||
FNXC:TaskDetailSummaryTokenCost 2026-07-11-00:00:
|
FNXC:TaskDetailSummaryTokenCost 2026-07-11-00:00:
|
||||||
Mobile per-model breakdowns must stay real tables and scroll horizontally inside .task-summary-token-table-wrap, matching the Command Center .cc-table-wrap pattern. Do not stack rows into cards; side-by-side model token/cost columns are the user-facing scan path.
|
Mobile per-model breakdowns must stay real tables and scroll horizontally inside .task-summary-token-table-wrap, matching the Command Center .cc-table-wrap pattern. Do not stack rows into cards; side-by-side model token/cost columns are the user-facing scan path.
|
||||||
|
|
||||||
|
FNXC:TaskDetailSummaryTab 2026-08-01-07:15:
|
||||||
|
Mobile token/cost scrolling requires min-width: 0 containment through the Summary flex ancestors. Overflow-x alone cannot shrink an intrinsically wide table's flex/grid child, so the wrapper must own horizontal overflow rather than widening the detail body.
|
||||||
*/
|
*/
|
||||||
.task-summary-token-table-wrap {
|
.task-summary-token-table-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,64 @@ describe("TaskDetailModal Summary tab", () => {
|
|||||||
expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull();
|
expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps populated token costs in a locally scrollable real table across data states", () => {
|
||||||
|
const twoModelUsage = tokenUsage({
|
||||||
|
inputTokens: 300,
|
||||||
|
outputTokens: 150,
|
||||||
|
totalTokens: 450,
|
||||||
|
perModel: [
|
||||||
|
{
|
||||||
|
modelProvider: "anthropic",
|
||||||
|
modelId: "claude-sonnet-4-6",
|
||||||
|
inputTokens: 200,
|
||||||
|
outputTokens: 100,
|
||||||
|
cachedTokens: 0,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
totalTokens: 300,
|
||||||
|
firstUsedAt: "2026-01-01T00:00:00Z",
|
||||||
|
lastUsedAt: "2026-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelProvider: "openai",
|
||||||
|
modelId: "gpt-4o-mini",
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 50,
|
||||||
|
cachedTokens: 0,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
totalTokens: 150,
|
||||||
|
firstUsedAt: "2026-01-01T00:00:00Z",
|
||||||
|
lastUsedAt: "2026-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const view = render(<TaskSummaryTab task={doneTask({ tokenUsage: twoModelUsage })} />);
|
||||||
|
const section = screen.getByTestId("task-summary-token-cost-section");
|
||||||
|
const wrapper = section.querySelector<HTMLElement>(".task-summary-token-table-wrap");
|
||||||
|
const table = wrapper?.querySelector<HTMLTableElement>("table.task-summary-token-table");
|
||||||
|
|
||||||
|
expect(wrapper).toBeTruthy();
|
||||||
|
expect(table).toBeTruthy();
|
||||||
|
expect(table?.tagName).toBe("TABLE");
|
||||||
|
expect(wrapper?.contains(table ?? null)).toBe(true);
|
||||||
|
for (const heading of ["Model", "Input", "Output", "Cached", "Total", "Cost"]) {
|
||||||
|
expect(within(table!).getByRole("columnheader", { name: heading })).toBeTruthy();
|
||||||
|
}
|
||||||
|
expect(table?.querySelector("tfoot")).toBeTruthy();
|
||||||
|
expect(screen.getAllByTestId("task-summary-token-row")).toHaveLength(2);
|
||||||
|
|
||||||
|
const css = readDashboardStylesSource();
|
||||||
|
expect(css).toMatch(/\.task-summary-tab\s*\{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/s);
|
||||||
|
expect(css).toMatch(/\.task-summary-section\s*\{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/s);
|
||||||
|
expect(css).toMatch(/\.task-summary-token-table-wrap\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow-x:\s*auto;/s);
|
||||||
|
|
||||||
|
view.rerender(<TaskSummaryTab task={doneTask({ tokenUsage: { ...twoModelUsage, perModel: [twoModelUsage.perModel![0]] } })} />);
|
||||||
|
expect(screen.getAllByTestId("task-summary-token-row")).toHaveLength(1);
|
||||||
|
expect(screen.getByTestId("task-summary-token-cost-section").querySelector("table.task-summary-token-table")).toBeTruthy();
|
||||||
|
|
||||||
|
view.rerender(<TaskSummaryTab task={doneTask({ tokenUsage: undefined })} />);
|
||||||
|
expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders multi-model token counts with priced, unpriced, and unavailable total cost states", () => {
|
it("renders multi-model token counts with priced, unpriced, and unavailable total cost states", () => {
|
||||||
render(
|
render(
|
||||||
<TaskSummaryTab
|
<TaskSummaryTab
|
||||||
@@ -450,7 +508,7 @@ describe("TaskDetailModal Summary tab", () => {
|
|||||||
expect(mobileTokenBlock).not.toContain(".task-summary-token-table-wrap");
|
expect(mobileTokenBlock).not.toContain(".task-summary-token-table-wrap");
|
||||||
expect(mobileTokenBlock).not.toContain("overflow-x: visible");
|
expect(mobileTokenBlock).not.toContain("overflow-x: visible");
|
||||||
expect(mobileTokenBlock).not.toContain(".task-summary-token-table td::before");
|
expect(mobileTokenBlock).not.toContain(".task-summary-token-table td::before");
|
||||||
expect(mobileTokenBlock).not.toContain("min-width: 0");
|
expect(tokenTableRule).not.toContain("min-width: 0");
|
||||||
expect(css).toContain("var(--color-warning)");
|
expect(css).toContain("var(--color-warning)");
|
||||||
expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/);
|
expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user