FN-7843: keep task-detail per-model token tables horizontally scrollable on mobile
Removes the mobile stacked-card override for the Summary tab's per-model token/cost table so it stays a real, horizontally-scrollable table matching the Command Center pattern, and updates the covering tests/changeset accordingly. - Drop the @media (max-width: 768px) block in TaskDetailModal.css that converted .task-summary-token-table rows into stacked cards (display:block, thead hidden, td::before labels), so the wrapper's overflow-x: auto and table min-width now govern mobile layout. - Update FNXC:TaskDetailSummaryTokenCost comments to document the new horizontal-scroll contract instead of the removed stacked-card behavior, and refresh the TaskCostTab.css FNXC comment to match. - Add a new regression test (token-table-mobile-scroll.test.ts) asserting the task-detail token table wrapper keeps overflow-x: auto, the mobile media block no longer stacks rows into cards, and the Command Center .cc-table-wrap stays scrollable. - Update the existing TaskDetailModal summary-tab test to assert the stacked-card CSS is absent from the mobile block instead of asserting its presence. - Add a patch changeset describing the fix for end users. Files changed: .changeset/mobile-token-table-scroll.md | 7 +++ .../__tests__/token-table-mobile-scroll.test.ts | 70 +++++++++++++++++++++ packages/dashboard/app/components/TaskCostTab.css | 2 +- .../dashboard/app/components/TaskDetailModal.css | 73 ++-------------------- .../__tests__/TaskDetailModal.summary-tab.test.tsx | 13 ++-- 5 files changed, 90 insertions(+), 75 deletions(-) Fusion-Task-Id: FN-7843 Fusion-Task-Lineage: bb6dde7c-f205-45f3-984a-0fc52be5d547 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/mobile-token-table-scroll.md
Normal file
7
.changeset/mobile-token-table-scroll.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep task detail per-model cost tables horizontally scrollable on mobile.
|
||||
category: fix
|
||||
dev: Removes the Task Detail stacked-card mobile override and guards the shared token table scroll contract.
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
|
||||
function readCss(relativePath: string): string {
|
||||
return readFileSync(resolve(__dirname, relativePath), "utf-8");
|
||||
}
|
||||
|
||||
function extractFirstMediaBlock(css: string, pattern: RegExp): string {
|
||||
const match = pattern.exec(css);
|
||||
expect(match, `missing media block for ${pattern}`).toBeTruthy();
|
||||
|
||||
const start = match!.index + match![0].length;
|
||||
let index = start;
|
||||
let depth = 1;
|
||||
while (index < css.length && depth > 0) {
|
||||
if (css[index] === "{") depth += 1;
|
||||
if (css[index] === "}") depth -= 1;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
expect(depth).toBe(0);
|
||||
return css.slice(start, index - 1);
|
||||
}
|
||||
|
||||
function ruleBlock(css: string, selector: string): string {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = css.match(new RegExp(`${escaped}\\s*\\{[^}]*\\}`, "s"));
|
||||
expect(match, `missing CSS rule for ${selector}`).toBeTruthy();
|
||||
return match![0];
|
||||
}
|
||||
|
||||
describe("FN-7843 token table mobile scroll contract", () => {
|
||||
const taskDetailCss = readCss("../components/TaskDetailModal.css");
|
||||
const commandCenterAreasCss = readCss("../components/command-center/areas/areas.css");
|
||||
const mobileTaskDetailCss = extractFirstMediaBlock(
|
||||
taskDetailCss,
|
||||
/@media\s*\(max-width:\s*768px\)\s*\{/,
|
||||
);
|
||||
|
||||
it("keeps the task-detail token table overflow owned by its wrapper", () => {
|
||||
const wrapperRule = ruleBlock(taskDetailCss, ".task-summary-token-table-wrap");
|
||||
const tableRule = ruleBlock(taskDetailCss, ".task-summary-token-table");
|
||||
|
||||
expect(wrapperRule).toContain("overflow-x: auto;");
|
||||
expect(tableRule).toMatch(/min-width:\s*[^;]+;/);
|
||||
});
|
||||
|
||||
it("does not convert task-detail token tables into stacked mobile cards", () => {
|
||||
expect(mobileTaskDetailCss).not.toMatch(
|
||||
/\.task-summary-token-table-wrap\s*\{[^}]*overflow-x:\s*visible\s*;/s,
|
||||
);
|
||||
expect(mobileTaskDetailCss).not.toMatch(
|
||||
/\.task-summary-token-table(?:\s|,|\{|[^{}]*\{[^}]*)display:\s*block\s*;/s,
|
||||
);
|
||||
expect(mobileTaskDetailCss).not.toMatch(
|
||||
/\.task-summary-token-table\s+(?:thead|tbody|tfoot|tr|th|td)(?:\s|,|\{|[^{}]*\{[^}]*)display:\s*block\s*;/s,
|
||||
);
|
||||
expect(mobileTaskDetailCss).not.toMatch(
|
||||
/\.task-summary-token-table\s+thead\s*\{[^}]*display:\s*none\s*;/s,
|
||||
);
|
||||
expect(mobileTaskDetailCss).not.toContain(".task-summary-token-table td::before");
|
||||
});
|
||||
|
||||
it("keeps the Command Center per-model table wrapper scrollable", () => {
|
||||
const commandCenterTableWrapRule = ruleBlock(commandCenterAreasCss, ".cc-table-wrap");
|
||||
|
||||
expect(commandCenterTableWrapRule).toContain("overflow-x: auto;");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
FNXC:TaskDetailCost 2026-07-11-12:05:
|
||||
The always-available Cost tab reuses the Summary token-table visual system so desktop, right-dock, and mobile task-detail surfaces inherit the same responsive stacked-table behavior without a second cost-table design.
|
||||
The always-available Cost tab reuses the Summary token-table visual system so desktop, right-dock, and mobile task-detail surfaces share one bounded horizontal-scroll table pattern without a second cost-table design.
|
||||
*/
|
||||
.task-cost-tab {
|
||||
display: flex;
|
||||
|
||||
@@ -2408,10 +2408,13 @@ The done-task Summary tab is a scrollable overview composed from existing detail
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailSummaryTokenCost 2026-06-27-00:00:
|
||||
The done-task Summary token-cost section must reuse task-summary card hierarchy while showing per-model counts and cost without hardcoded colors or spacing. Mobile converts table rows to stacked cards so the modal and right dock do not trap horizontal scrolling.
|
||||
The done-task Summary token-cost section must reuse task-summary card hierarchy while showing per-model counts and cost without hardcoded colors or spacing.
|
||||
|
||||
FNXC:TaskDetailSummaryTokenCost 2026-06-28-00:00:
|
||||
The desktop table needs a token-scale min-width and non-anywhere model-name wrapping so narrow right-dock containers scroll horizontally instead of collapsing MODEL values to one character per line.
|
||||
|
||||
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.
|
||||
*/
|
||||
.task-summary-token-table-wrap {
|
||||
width: 100%;
|
||||
@@ -2609,74 +2612,6 @@ has history; no shell appears otherwise.
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-summary-token-table-wrap {
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.task-summary-token-table,
|
||||
.task-summary-token-table thead,
|
||||
.task-summary-token-table tbody,
|
||||
.task-summary-token-table tfoot,
|
||||
.task-summary-token-table tr,
|
||||
.task-summary-token-table th,
|
||||
.task-summary-token-table td {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-summary-token-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-summary-token-table tbody tr,
|
||||
.task-summary-token-table tfoot tr {
|
||||
padding: var(--space-sm);
|
||||
background: var(--surface);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.task-summary-token-table tbody,
|
||||
.task-summary-token-table tfoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.task-summary-token-table td,
|
||||
.task-summary-token-table tfoot th {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xs) 0;
|
||||
border-bottom: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.task-summary-token-table td::before {
|
||||
content: attr(data-label);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.task-summary-token-table tfoot td::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.task-summary-token-table tfoot th {
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.task-summary-token-table tfoot th::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
.task-summary-work-list li {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -436,7 +436,10 @@ describe("TaskDetailModal Summary tab", () => {
|
||||
const css = readDashboardStylesSource();
|
||||
const tokenTableRule = css.match(/\.task-summary-token-table\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
const modelNameRule = css.match(/\.task-summary-model-label span:last-child\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
const mobileTokenBlock = css.slice(css.indexOf("@media (max-width: 768px)"), css.indexOf("/* Spec tab layout"));
|
||||
const tokenTableSectionStart = css.lastIndexOf("FNXC:TaskDetailSummaryTokenCost");
|
||||
const tokenTableSectionEnd = css.indexOf("/* Spec tab layout", tokenTableSectionStart);
|
||||
const tokenTableSection = css.slice(tokenTableSectionStart, tokenTableSectionEnd);
|
||||
const mobileTokenBlock = tokenTableSection.slice(tokenTableSection.indexOf("@media (max-width: 768px)"));
|
||||
|
||||
expect(css).toContain(".task-summary-token-table");
|
||||
expect(tokenTableRule).toMatch(/min-width:\s*calc\(var\(--space-2xl\)\s*\*\s*16\)/);
|
||||
@@ -444,10 +447,10 @@ describe("TaskDetailModal Summary tab", () => {
|
||||
expect(modelNameRule).toContain("word-break: normal");
|
||||
expect(modelNameRule).not.toContain("overflow-wrap: anywhere");
|
||||
expect(css).toContain("@media (max-width: 768px)");
|
||||
expect(mobileTokenBlock).toContain(".task-summary-token-table-wrap");
|
||||
expect(mobileTokenBlock).toContain("overflow-x: visible");
|
||||
expect(mobileTokenBlock).toContain(".task-summary-token-table td::before");
|
||||
expect(mobileTokenBlock).toContain("min-width: 0");
|
||||
expect(mobileTokenBlock).not.toContain(".task-summary-token-table-wrap");
|
||||
expect(mobileTokenBlock).not.toContain("overflow-x: visible");
|
||||
expect(mobileTokenBlock).not.toContain(".task-summary-token-table td::before");
|
||||
expect(mobileTokenBlock).not.toContain("min-width: 0");
|
||||
expect(css).toContain("var(--color-warning)");
|
||||
expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user