From be1950b79c5dea6d79076f67fead9ead2ae2c5a4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 22:00:05 -0700 Subject: [PATCH] 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) --- .changeset/mobile-token-table-scroll.md | 7 ++ .../token-table-mobile-scroll.test.ts | 70 ++++++++++++++++++ .../dashboard/app/components/TaskCostTab.css | 2 +- .../app/components/TaskDetailModal.css | 73 +------------------ .../TaskDetailModal.summary-tab.test.tsx | 13 ++-- 5 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 .changeset/mobile-token-table-scroll.md create mode 100644 packages/dashboard/app/__tests__/token-table-mobile-scroll.test.ts diff --git a/.changeset/mobile-token-table-scroll.md b/.changeset/mobile-token-table-scroll.md new file mode 100644 index 0000000000..638b3255ab --- /dev/null +++ b/.changeset/mobile-token-table-scroll.md @@ -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. diff --git a/packages/dashboard/app/__tests__/token-table-mobile-scroll.test.ts b/packages/dashboard/app/__tests__/token-table-mobile-scroll.test.ts new file mode 100644 index 0000000000..7ecbfefa72 --- /dev/null +++ b/packages/dashboard/app/__tests__/token-table-mobile-scroll.test.ts @@ -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;"); + }); +}); diff --git a/packages/dashboard/app/components/TaskCostTab.css b/packages/dashboard/app/components/TaskCostTab.css index 911216ce67..feb30de725 100644 --- a/packages/dashboard/app/components/TaskCostTab.css +++ b/packages/dashboard/app/components/TaskCostTab.css @@ -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; diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 37eb305c10..8ac29b1ad2 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -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; } diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx index 5e90646a34..8b502b4a6b 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx @@ -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}/); });