diff --git a/.changeset/fn-6664-command-center-mobile-chart-polish.md b/.changeset/fn-6664-command-center-mobile-chart-polish.md new file mode 100644 index 0000000000..df0bd15cb8 --- /dev/null +++ b/.changeset/fn-6664-command-center-mobile-chart-polish.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix Command Center mobile chart rendering so chart primitives shrink inside the tabpanel without scroll-stealing overflow, zero-height collapse, or stretch artifacts, and normalize chart/card border and spacing rhythm across the combined analytics surfaces. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 23eccd2e88..f9b752b759 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -676,6 +676,10 @@ Features: - **Mission Control** shows live active sessions/runs/nodes, current sessions and nodes, an animated live activity snapshot, and a live SDLC funnel; when idle it reports that live updates resume when work starts. Motion-heavy accents respect reduced-motion preferences. - CSV exports are available from the analytics endpoints with `?format=csv`. The Activity CSV includes daily `agentRuns` values plus summary rows for `(agentRuns.total)`, `(agentRuns.active)`, `(agentRuns.completed)`, and `(agentRuns.failed)`. +Rendering invariants: +- On mobile (`max-width: 768px`), `.cc-tabpanel` remains the sole vertical scroll owner for every chart-bearing tab. Shared chart primitives (`Bar`, `StackedBar`, `Sparkline`, `LineChart`, `RadialGauge`, `Funnel`, and `TokenSeriesChart`) must shrink within the tabpanel, keep non-zero usable height, avoid stretch/clipping artifacts, and never introduce a competing vertical overflow container. +- Command Center stat cards, overview chart cards, live strips, table wrappers, Team chart panels, token-series plots, and gauge/chart cards share the same tokenized rhythm: `--border-width` borders, `--radius-md` radii, and `--space-*` gaps/padding. Area-specific accents may use `color-mix(...)`, but layout, border, radius, text color, and motion must stay on design tokens. + Data states: - Overview shows a loading state while core analytics settle, then shows `No usage data yet. Run some agents to populate the Command Center.` only after the selected range has settled with no core usage data. - GitHub issue analytics is local and additive: empty filed/fixed totals render the GitHub area's empty state; malformed historical `githubTracking` JSON is skipped instead of breaking the Command Center. @@ -1249,6 +1253,8 @@ The `index.html` shell is templated server-side: the server injects a per-user ` **Always reference tokens. Never hardcode pixels, hex, or `rgba()` in component CSS** — global/theme token CSS is also covered by `global-theme-css-no-raw-rgba.test.ts`, so raw `rgba()` belongs only in explicit `var(--token, rgba(...))` fallbacks. For translucent backgrounds use `color-mix(in srgb, var(--color) X%, transparent)`, not `rgba()`. +Command Center chart surfaces are a stricter token-only zone: `CommandCenter.css`, `areas/areas.css`, and `charts/charts.css` should avoid raw color fallbacks and hardcoded dimensions in component rules, keep secondary copy on `--text-muted`, use `--duration-*` for animation durations, and encode mobile chart invariants with shared classes rather than one-off area styles. + ### Theme system Dark/light modes via `data-theme`; 54 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`). diff --git a/packages/dashboard/app/components/command-center/CommandCenter.css b/packages/dashboard/app/components/command-center/CommandCenter.css index f64bdce583..d26030c737 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.css +++ b/packages/dashboard/app/components/command-center/CommandCenter.css @@ -11,10 +11,10 @@ The FN-4286 dashboard text-token guard requires command-center secondary copy to display: flex; flex: 1; flex-direction: column; - gap: var(--space-4, 1rem); + gap: var(--space-4); min-height: 0; - width: 100%; - padding: var(--space-4, 1rem); + inline-size: 100%; + padding: var(--space-4); } /* @@ -26,15 +26,15 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . flex-shrink: 0; align-items: center; justify-content: space-between; - gap: var(--space-3, 0.75rem); + gap: var(--space-3); } .cc-title { display: flex; align-items: center; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); margin: 0; - font-size: var(--font-size-lg, 1.1rem); + font-size: var(--font-size-lg); } /* ---- Tabs ---- */ @@ -42,34 +42,36 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . display: flex; flex-shrink: 0; flex-wrap: wrap; - gap: var(--space-1, 0.25rem); - border-bottom: 1px solid var(--border-subtle, rgba(127, 127, 127, 0.25)); + gap: var(--space-1); + border-bottom: var(--border-width) solid var(--border-subtle); } .cc-tab { appearance: none; background: none; border: none; - border-bottom: 2px solid transparent; + border-bottom: var(--border-width-thick) solid transparent; color: var(--text-muted); - padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem); + padding: var(--space-2) var(--space-3); cursor: pointer; - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); transition: color var(--transition-fast), border-color var(--transition-fast); } .cc-tab:hover { - color: var(--text-primary, #ddd); + color: var(--text-primary); } .cc-tab.active { - color: var(--text-primary, #ddd); - border-bottom-color: var(--color-accent, #4f8cff); + color: var(--text-primary); + border-bottom-color: var(--color-accent); } .cc-tabpanel { flex: 1; min-height: 0; + min-inline-size: 0; + overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; outline: none; @@ -78,39 +80,48 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . @media (max-width: 768px) { .cc-tabpanel { - padding-bottom: calc(var(--space-4, 1rem) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap)); + padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0) + var(--standalone-bottom-gap)); } } /* ---- Overview ---- */ .cc-overview { + min-inline-size: 0; display: flex; flex-direction: column; - gap: var(--space-4, 1rem); + gap: var(--space-4); } .cc-stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); - gap: var(--space-3, 0.75rem); + gap: var(--space-3); } +/* +FNXC:CommandCenterStyling 2026-06-18-00:00: +Command Center chart/stat surfaces share one tokenized card rhythm so new chart areas do not drift in border, radius, or spacing when Team/System/agent-runs render together (FN-6664). +*/ .cc-stat-card { display: flex; flex-direction: column; - gap: var(--space-1, 0.25rem); - padding: var(--space-3, 0.75rem); + gap: var(--space-2); + min-inline-size: 0; + padding: var(--space-3); + border: var(--border-width) solid var(--border-subtle); + border-radius: var(--radius-md); + background: var(--surface-1); } .cc-stat-label { - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); color: var(--text-muted); } .cc-stat-value { - font-size: var(--font-size-xl, 1.5rem); + font-size: var(--font-size-xl); font-variant-numeric: tabular-nums; - color: var(--text-primary, #ddd); + color: var(--text-primary); } .cc-stat-card--gauge { @@ -118,21 +129,25 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . text-align: center; } +/* +FNXC:CommandCenterStyling 2026-06-18-00:00: +Command Center live/chart containers must shrink inside the mobile tabpanel without creating a second scroll owner or clipping chart height (FN-6664). +*/ .cc-live-strip { position: relative; display: grid; grid-template-columns: minmax(10rem, 1fr) minmax(16rem, 2fr) minmax(10rem, 1fr); align-items: center; - gap: var(--space-3, 0.75rem); - padding: var(--space-3, 0.75rem); - border: var(--border-width, 0.0625rem) solid color-mix(in srgb, var(--color-accent) 35%, var(--border-subtle)); + gap: var(--space-3); + padding: var(--space-3); + border: var(--border-width) solid var(--border-subtle); border-radius: var(--radius-md); background: linear-gradient(135deg, color-mix(in srgb, var(--color-accent) 14%, transparent), transparent), var(--surface-1); box-shadow: 0 0 var(--space-4) color-mix(in srgb, var(--color-accent) 18%, transparent); overflow: hidden; - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); } .cc-live-strip::before { @@ -149,6 +164,7 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . .cc-live-strip-heading, .cc-live-strip-metrics, .cc-live-trend { + min-inline-size: 0; position: relative; z-index: 1; } @@ -156,7 +172,7 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . .cc-live-strip-heading { display: flex; align-items: center; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-live-strip-label { @@ -165,17 +181,19 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . } .cc-live-strip-metrics { + min-inline-size: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-live-metric { + min-inline-size: 0; display: flex; flex-direction: column; - gap: var(--space-1, 0.25rem); - min-width: 0; - padding: var(--space-2, 0.5rem); + gap: var(--space-1); + min-inline-size: 0; + padding: var(--space-2); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--surface-2) 70%, transparent); animation: cc-live-signal-pulse calc(var(--duration-slow) * 6) ease-in-out infinite; @@ -183,7 +201,7 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden . .cc-live-metric-value { color: var(--text-primary); - font-size: var(--font-size-lg, 1.1rem); + font-size: var(--font-size-lg); font-weight: 700; font-variant-numeric: tabular-nums; } @@ -210,19 +228,20 @@ Overview token totals now live-poll and should visibly count up on change in bot .cc-live-metric-label, .cc-live-trend-label { color: var(--text-muted); - font-size: var(--font-size-xs, 0.75rem); + font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.04em; } .cc-live-trend { + min-inline-size: 0; display: flex; flex-direction: column; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-live-trend .cc-sparkline { - height: 2.5rem; + block-size: var(--space-10); } .cc-live-trend .cc-sparkline-bar { @@ -263,6 +282,7 @@ FNXC:CommandCenterStyling 2026-06-18-00:00: Overview charts must use dashboard tokens only and keep motion decorative; animations use --duration-* values and are disabled for reduced-motion users so the graph-rich snapshot does not violate accessibility or the mobile scroll contract. */ .cc-overview-charts { + min-inline-size: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); @@ -270,13 +290,15 @@ Overview charts must use dashboard tokens only and keep motion decorative; anima } .cc-overview-chart-card { + min-inline-size: 0; position: relative; display: flex; flex-direction: column; gap: var(--space-3); - min-width: 0; + min-inline-size: 0; padding: var(--space-3); - border-color: color-mix(in srgb, var(--color-accent) 22%, var(--border-subtle)); + border: var(--border-width) solid var(--border-subtle); + border-radius: var(--radius-md); background: linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent), var(--surface-1); @@ -320,7 +342,7 @@ Overview charts must use dashboard tokens only and keep motion decorative; anima } .cc-overview-chart-card .cc-sparkline { - height: var(--space-16); + block-size: var(--space-16); } .cc-overview-chart-card .cc-bar-fill, @@ -367,10 +389,12 @@ Overview charts must use dashboard tokens only and keep motion decorative; anima } .cc-live-strip-metrics { + min-inline-size: 0; grid-template-columns: 1fr; } .cc-overview-charts { + min-inline-size: 0; grid-template-columns: 1fr; } @@ -387,20 +411,20 @@ Overview charts must use dashboard tokens only and keep motion decorative; anima flex-direction: column; align-items: center; justify-content: center; - gap: var(--space-2, 0.5rem); - padding: var(--space-6, 2rem); + gap: var(--space-2); + padding: var(--space-6); color: var(--text-muted); text-align: center; } .cc-error { - color: var(--color-error, #e5484d); + color: var(--color-error); } .cc-loading .cc-chart-skeleton { - height: 1rem; - border-radius: var(--radius-sm, 4px); - background: var(--surface-2, rgba(127, 127, 127, 0.12)); + block-size: var(--space-4); + border-radius: var(--radius-sm); + background: var(--surface-2); animation: cc-shell-pulse var(--duration-slow) ease-in-out infinite; } diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsx index a48bc26f7b..6cbd7cfd0d 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsx @@ -78,6 +78,10 @@ function populatedTokenFixture() { cost: { usd: 9, unavailable: false, stale: false }, }, ], + series: [ + { bucket: "2026-06-17T00:00:00.000Z", totalTokens: 250 }, + { bucket: "2026-06-18T00:00:00.000Z", totalTokens: 750 }, + ], }; } @@ -92,6 +96,65 @@ function populatedToolsFixture() { }; } +function populatedProductivityFixture() { + return { + modifiedFiles: 6, + commits: 2, + pullRequests: 1, + loc: { value: 42, unavailable: false }, + byLanguage: [{ language: "TypeScript", count: 6 }], + }; +} + +function emptyProductivityFixture() { + return { + modifiedFiles: 0, + commits: 0, + pullRequests: 0, + loc: { value: null, unavailable: true }, + byLanguage: [], + }; +} + +function populatedTeamFixture() { + return { + ...emptyTeamFixture(), + totals: { + tokens: { inputTokens: 900, outputTokens: 450, cachedTokens: 150, cacheWriteTokens: 0, totalTokens: 1500, nTasks: 2 }, + cost: { usd: 4.25, unavailable: false, stale: false }, + filesChanged: 7, + tasksCompleted: 3, + tasksInProgress: 1, + tasksInReview: 0, + }, + agents: [ + { + agentId: "agent-alpha", + agentName: "Alpha Agent", + role: "executor", + state: "running", + tokens: { inputTokens: 900, outputTokens: 450, cachedTokens: 150, cacheWriteTokens: 0, totalTokens: 1500, nTasks: 2 }, + cost: { usd: 4.25, unavailable: false, stale: false }, + filesChanged: 7, + tasksCompleted: 3, + tasksInProgress: 1, + tasksInReview: 0, + }, + ], + }; +} + +function populatedSignalsFixture() { + return { + totalSignals: 3, + open: 2, + resolved: 1, + mttr: { value: 30, unavailable: false }, + bySource: [{ source: "sentry", count: 2 }], + bySeverity: [{ severity: "high", count: 1 }], + }; +} + function emptyGithubFixture() { return { filed: 0, fixed: 0, net: 0, daily: [], byRepo: [] }; } @@ -171,9 +234,10 @@ function mockOverviewApi({ populated = false }: { populated?: boolean } = {}) { if (path.startsWith("/command-center/tokens")) return Promise.resolve(populated ? populatedTokenFixture() : emptyTokenFixture()); if (path.startsWith("/command-center/tools")) return Promise.resolve(populated ? populatedToolsFixture() : emptyToolsFixture()); if (path.startsWith("/command-center/activity")) return Promise.resolve(populated ? populatedActivityFixture() : emptyActivityFixture()); - if (path.startsWith("/command-center/github")) return Promise.resolve(emptyGithubFixture()); - if (path.startsWith("/command-center/team")) return Promise.resolve(emptyTeamFixture()); - if (path.startsWith("/command-center/signals")) return Promise.resolve({ totalSignals: 0, open: 0, resolved: 0, mttr: { value: null, unavailable: true }, bySource: [], bySeverity: [] }); + if (path.startsWith("/command-center/productivity")) return Promise.resolve(populated ? populatedProductivityFixture() : emptyProductivityFixture()); + if (path.startsWith("/command-center/github")) return Promise.resolve(populated ? { filed: 3, fixed: 1, net: 2, daily: [{ date: "2026-06-18", filed: 3, fixed: 1 }], byRepo: [{ repo: "acme/repo", filed: 3, fixed: 1 }] } : emptyGithubFixture()); + if (path.startsWith("/command-center/team")) return Promise.resolve(populated ? populatedTeamFixture() : emptyTeamFixture()); + if (path.startsWith("/command-center/signals")) return Promise.resolve(populated ? populatedSignalsFixture() : { totalSignals: 0, open: 0, resolved: 0, mttr: { value: null, unavailable: true }, bySource: [], bySeverity: [] }); if (path === "/system-stats") return Promise.resolve(systemStatsFixture()); if (path === "/settings/global") return Promise.resolve({ vitestAutoKillEnabled: true, vitestKillThresholdPct: 90 }); if (path === "/command-center/live") { @@ -198,6 +262,9 @@ function injectCommandCenterCss() { style.textContent = [ loadStylesCss(), readFileSync(join(__dirname, "..", "CommandCenter.css"), "utf-8"), + readFileSync(join(__dirname, "..", "charts", "charts.css"), "utf-8"), + readFileSync(join(__dirname, "..", "areas", "areas.css"), "utf-8"), + readFileSync(join(__dirname, "..", "areas", "SystemStatsArea.css"), "utf-8"), ].join("\n"); document.head.appendChild(style); } @@ -235,6 +302,28 @@ function assertScrollOwnerContract(panel: HTMLElement) { expect(window.getComputedStyle(tablist).flexShrink).toBe("0"); } +function assertNoChartScrollSteal(panel: HTMLElement) { + const chartContainers = panel.querySelectorAll( + ".cc-bar-chart, .cc-bar-row, .cc-sparkline, .cc-line-chart, .cc-radial-gauge, .cc-funnel, .cc-token-series, .cc-token-series-plot, .cc-overview-chart-card, .cc-team-chart-panel, .cc-stat-card", + ); + expect(chartContainers.length).toBeGreaterThan(0); + for (const container of chartContainers) { + const style = window.getComputedStyle(container); + expect(style.overflowY === "auto" || style.overflowY === "scroll").toBe(false); + expect(style.maxInlineSize === "100%" || style.maxWidth === "100%" || style.overflowX === "hidden" || style.display.length > 0).toBe(true); + } +} + +async function openChartTab(tab: string) { + fireEvent.click(screen.getByTestId(`command-center-tab-${tab}`)); + const panel = screen.getByTestId(`command-center-panel-${tab}`); + expect(panel).toBe(screen.getByRole("tabpanel")); + await vi.waitFor(() => { + expect(screen.queryByTestId(`cc-area-${tab}-loading`)).toBeNull(); + }); + return panel; +} + describe("CommandCenter mobile scroll regression (FN-6595)", () => { beforeEach(() => { apiMock.mockReset(); @@ -282,6 +371,39 @@ describe("CommandCenter mobile scroll regression (FN-6595)", () => { assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview")); }); + it("keeps every populated chart-bearing tab inside the mobile tabpanel scroll owner", async () => { + mockOverviewApi({ populated: true }); + render(); + + const overviewPanel = screen.getByTestId("command-center-panel-overview"); + await screen.findByTestId("command-center-overview-charts"); + assertScrollOwnerContract(overviewPanel); + assertNoChartScrollSteal(overviewPanel); + + for (const tab of ["tokens", "tools", "activity", "productivity", "team", "ecosystem", "github", "signals", "system"]) { + const panel = await openChartTab(tab); + if (tab === "system") await screen.findByTestId("cc-area-system"); + assertScrollOwnerContract(panel); + assertNoChartScrollSteal(panel); + expect(panel.textContent).not.toContain("NaN"); + } + }); + + it("encodes the mobile chart CSS fixes for the discovered overflow primitives", () => { + const styles = Array.from(document.head.querySelectorAll("style")) + .map((style) => style.textContent ?? "") + .join("\n"); + + expect(styles).toContain(".cc-tabpanel"); + expect(styles).toContain("overflow-x: hidden"); + expect(styles).toContain("grid-template-columns: minmax(0, 1fr) minmax(var(--space-12), 2fr)"); + expect(styles).toContain(".cc-line-chart"); + expect(styles).toContain("aspect-ratio: auto"); + expect(styles).toContain(".cc-radial-gauge-ring"); + expect(styles).toContain("inline-size: clamp(var(--space-20), 44vw, var(--space-32))"); + expect(styles).toContain("min-inline-size: 0"); + }); + it("keeps the same flex-fill scroll-owner contract outside the mobile breakpoint", () => { mockMobileMatchMedia(false); render(); diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx index 1395235650..b8b803f36d 100644 --- a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx +++ b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx @@ -17,6 +17,7 @@ import { ProductivityArea } from "../ProductivityArea"; import { GithubArea } from "../GithubArea"; import { SignalsArea } from "../SignalsArea"; import { ActivityArea } from "../ActivityArea"; +import { EcosystemArea } from "../EcosystemArea"; import { useAnalyticsArea } from "../useAnalyticsArea"; import type { DateRange } from "../DateRangePicker"; @@ -492,7 +493,7 @@ describe("ToolsArea", () => { }); describe("ProductivityArea", () => { - it("renders unavailable LOC as the dash sentinel, never 0", async () => { + it("renders unavailable LOC as the dash sentinel, never 0 and keeps chart geometry finite", async () => { apiMock.mockResolvedValue({ from: "2026-06-08", to: null, @@ -509,6 +510,50 @@ describe("ProductivityArea", () => { expect(loc.getAttribute("title")).toBeTruthy(); // The commits outcome counter still shows a real number. expect(screen.getByTestId("cc-productivity-commits").textContent).toContain("4"); + expect(screen.getByRole("list", { name: "Files by language" })).toBeTruthy(); + expect(screen.getByTestId("cc-area-productivity").textContent).not.toContain("NaN"); + }); + + it("renders empty, loading, and error states without empty chart shells", async () => { + apiMock.mockResolvedValueOnce({ + from: null, + to: null, + modifiedFiles: 0, + byLanguage: [], + commits: 0, + pullRequests: 0, + loc: { value: null, unavailable: true }, + }); + const { unmount } = render(); + await screen.findByTestId("cc-area-productivity-empty"); + expect(screen.queryByRole("list", { name: "Files by language" })).toBeNull(); + unmount(); + + apiMock.mockImplementationOnce(() => new Promise(() => undefined)); + const pending = render(); + expect(screen.getByTestId("cc-area-productivity-loading")).toBeTruthy(); + pending.unmount(); + + apiMock.mockRejectedValueOnce(new Error("productivity failed")); + render(); + await screen.findByTestId("cc-area-productivity-error"); + expect(screen.getByTestId("cc-area-productivity-error").textContent).toContain("productivity failed"); + }); +}); + +describe("EcosystemArea", () => { + it("renders populated and empty model chart states without NaN or empty chart shells", async () => { + apiMock.mockResolvedValueOnce(tokenFixture()); + const { unmount } = render(); + await screen.findByTestId("cc-area-ecosystem"); + expect(screen.getByRole("list", { name: "Tasks per model" })).toBeTruthy(); + expect(screen.getByTestId("cc-area-ecosystem").textContent).not.toContain("NaN"); + unmount(); + + apiMock.mockResolvedValueOnce({ ...tokenFixture(), groups: [], totals: { ...tokenFixture().totals, totalTokens: 0, nTasks: 0 } }); + render(); + await screen.findByTestId("cc-area-ecosystem-empty"); + expect(screen.queryByRole("list", { name: "Tasks per model" })).toBeNull(); }); }); diff --git a/packages/dashboard/app/components/command-center/areas/areas.css b/packages/dashboard/app/components/command-center/areas/areas.css index f4039b4f91..f288c78536 100644 --- a/packages/dashboard/app/components/command-center/areas/areas.css +++ b/packages/dashboard/app/components/command-center/areas/areas.css @@ -10,13 +10,19 @@ Area headings, table metadata, and empty states use --text-muted so the analytic .cc-area { display: flex; flex-direction: column; - gap: var(--space-4, 1rem); + gap: var(--space-4); + min-inline-size: 0; } +/* +FNXC:CommandCenterStyling 2026-06-18-00:00: +Chart-bearing Command Center areas use one tokenized section rhythm and logical shrink bounds so mobile charts stay inside .cc-tabpanel without scroll-steal (FN-6664). +*/ .cc-area-section { display: flex; flex-direction: column; - gap: var(--space-2, 0.5rem); + gap: var(--space-3); + min-inline-size: 0; } .cc-area-section-header { @@ -28,7 +34,7 @@ Area headings, table metadata, and empty states use --text-muted so the analytic .cc-area-section-title { margin: 0; - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); font-weight: 600; color: var(--text-muted); text-transform: uppercase; @@ -39,11 +45,11 @@ Area headings, table metadata, and empty states use --text-muted so the analytic .cc-area .cc-stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); - gap: var(--space-3, 0.75rem); + gap: var(--space-3); } .cc-stat-sub { - font-size: var(--font-size-xs, 0.75rem); + font-size: var(--font-size-xs); color: var(--text-muted); } @@ -98,22 +104,31 @@ The Tokens area needs a real hour/day/week control and live token-number motion. } /* ---- Tables ---- */ +/* +FNXC:CommandCenterStyling 2026-06-18-00:00: +Command Center table-heavy chart areas share the same border/radius rhythm as chart cards while keeping horizontal table overflow scoped to the table wrapper, not the mobile tabpanel vertical scroll owner (FN-6664). +*/ .cc-table-wrap { + max-inline-size: 100%; + border: var(--border-width) solid var(--border-subtle); + border-radius: var(--radius-md); + background: var(--surface-1); overflow-x: auto; + overflow-y: hidden; } .cc-table { - width: 100%; + inline-size: 100%; border-collapse: collapse; - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); font-variant-numeric: tabular-nums; } .cc-table th, .cc-table td { - padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem); + padding: var(--space-2) var(--space-3); text-align: right; - border-bottom: 1px solid var(--border-subtle, rgba(127, 127, 127, 0.2)); + border-bottom: var(--border-width) solid var(--border-subtle); white-space: nowrap; } @@ -133,7 +148,7 @@ The Tokens area needs a real hour/day/week control and live token-number motion. } .cc-table th.cc-sortable:hover { - color: var(--text-primary, #ddd); + color: var(--text-primary); } .cc-table tbody tr { @@ -141,31 +156,31 @@ The Tokens area needs a real hour/day/week control and live token-number motion. } .cc-table tbody tr.cc-row-selected { - background: var(--surface-2, rgba(127, 127, 127, 0.12)); + background: var(--surface-2); } .cc-table tbody tr.cc-row-selected td { - color: var(--text-primary, #ddd); + color: var(--text-primary); } .cc-sort-caret { - margin-left: var(--space-1, 0.25rem); + margin-inline-start: var(--space-1); font-size: 0.7em; - color: var(--color-accent, #4f8cff); + color: var(--color-accent); } /* Unavailable sentinel ("—") with a help cursor for its tooltip. */ .cc-unavailable { color: var(--text-muted); cursor: help; - border-bottom: 1px dotted var(--border-subtle, rgba(127, 127, 127, 0.4)); + border-bottom: var(--border-width) dotted var(--border-subtle); } .cc-loading-inline { display: flex; align-items: center; - gap: var(--space-2, 0.5rem); - padding: var(--space-4, 1rem); + gap: var(--space-2); + padding: var(--space-4); color: var(--text-muted); } @@ -174,18 +189,18 @@ The Tokens area needs a real hour/day/week control and live token-number motion. display: flex; flex-direction: column; align-items: center; - gap: var(--space-2, 0.5rem); - padding: var(--space-6, 2rem); + gap: var(--space-2); + padding: var(--space-6); color: var(--text-muted); text-align: center; } .cc-area-error { - color: var(--color-error, #e5484d); + color: var(--color-error); } .cc-pricing-note { - font-size: var(--font-size-xs, 0.75rem); + font-size: var(--font-size-xs); color: var(--text-muted); } @@ -202,8 +217,8 @@ The Team view must use dashboard design tokens only, preserve .cc-tabpanel as th .cc-team-chart-panel { display: flex; flex-direction: column; - gap: var(--space-2); - min-width: 0; + gap: var(--space-3); + min-inline-size: 0; padding: var(--space-3); border: var(--border-width) solid var(--border-subtle); border-radius: var(--radius-md); @@ -229,7 +244,7 @@ The Team view must use dashboard design tokens only, preserve .cc-tabpanel as th display: inline-flex; align-items: center; gap: var(--space-2); - min-width: 0; + min-inline-size: 0; } .cc-team-agent-name, diff --git a/packages/dashboard/app/components/command-center/charts/charts.css b/packages/dashboard/app/components/command-center/charts/charts.css index 273768d5e4..af128daec4 100644 --- a/packages/dashboard/app/components/command-center/charts/charts.css +++ b/packages/dashboard/app/components/command-center/charts/charts.css @@ -10,6 +10,22 @@ Chart labels and legends must use --text-muted so command-center CSS stays align * invalidate animation declarations (see animation-duration-tokens.css.test.ts). */ +/* +FNXC:CommandCenterStyling 2026-06-18-00:00: +Command Center charts must render within the mobile tabpanel without overflow scroll-steal, zero-height collapse, or stretch; every primitive opts into logical shrink bounds before area layouts compose it (FN-6664). +*/ +.cc-bar-chart, +.cc-stacked-bar, +.cc-sparkline, +.cc-token-series, +.cc-token-series-plot, +.cc-line-chart, +.cc-radial-gauge, +.cc-funnel { + min-inline-size: 0; + max-inline-size: 100%; +} + /* ---- Bar ---- */ .cc-bar-chart { list-style: none; @@ -17,18 +33,18 @@ Chart labels and legends must use --text-muted so command-center CSS stays align padding: 0; display: flex; flex-direction: column; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-bar-row { display: grid; - grid-template-columns: minmax(6rem, 12rem) 1fr auto; + grid-template-columns: minmax(var(--space-20), 32%) minmax(0, 1fr) auto; align-items: center; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-bar-label { - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; @@ -37,43 +53,43 @@ Chart labels and legends must use --text-muted so command-center CSS stays align .cc-bar-track { position: relative; - height: 0.75rem; - background: var(--surface-2, rgba(127, 127, 127, 0.12)); - border-radius: var(--radius-sm, 4px); + block-size: var(--space-3); + background: var(--surface-2); + border-radius: var(--radius-sm); overflow: hidden; } .cc-bar-fill { - height: 100%; - background: var(--color-accent, #4f8cff); - border-radius: var(--radius-sm, 4px); + block-size: 100%; + background: var(--color-accent); + border-radius: var(--radius-sm); transition: width var(--transition-normal); } .cc-bar-value { - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); font-variant-numeric: tabular-nums; - color: var(--text-primary, #ddd); + color: var(--text-primary); } /* ---- StackedBar ---- */ .cc-stacked-bar { display: flex; flex-direction: column; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-stacked-track { display: flex; - height: 0.75rem; - background: var(--surface-2, rgba(127, 127, 127, 0.12)); - border-radius: var(--radius-sm, 4px); + block-size: var(--space-3); + background: var(--surface-2); + border-radius: var(--radius-sm); overflow: hidden; } .cc-stacked-segment { - height: 100%; - background: var(--color-accent, #4f8cff); + block-size: 100%; + background: var(--color-accent); transition: width var(--transition-normal); } @@ -83,22 +99,22 @@ Chart labels and legends must use --text-muted so command-center CSS stays align padding: 0; display: flex; flex-wrap: wrap; - gap: var(--space-3, 0.75rem); + gap: var(--space-3); } .cc-stacked-legend-item { display: flex; align-items: center; - gap: var(--space-1, 0.25rem); - font-size: var(--font-size-sm, 0.85rem); + gap: var(--space-1); + font-size: var(--font-size-sm); color: var(--text-muted); } .cc-stacked-swatch { - width: 0.6rem; - height: 0.6rem; - border-radius: 2px; - background: var(--color-accent, #4f8cff); + inline-size: var(--space-2); + block-size: var(--space-2); + border-radius: var(--radius-sm); + background: var(--color-accent); display: inline-block; } @@ -106,15 +122,15 @@ Chart labels and legends must use --text-muted so command-center CSS stays align .cc-sparkline { display: flex; align-items: flex-end; - gap: 1px; - height: 2rem; + gap: var(--border-width); + block-size: var(--space-8); } .cc-sparkline-bar { flex: 1 1 0; - min-width: 1px; - background: var(--color-accent, #4f8cff); - border-radius: 1px; + min-inline-size: var(--border-width); + background: var(--color-accent); + border-radius: var(--border-width); transition: height var(--transition-normal); } @@ -205,7 +221,7 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us block-size: clamp(var(--space-16), 22vw, calc(var(--space-20) * 2)); aspect-ratio: 5 / 2; color: var(--color-accent); - overflow: visible; + overflow: hidden; } .cc-line-chart-series { @@ -252,7 +268,8 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us @media (max-width: 768px) { .cc-line-chart { - block-size: clamp(var(--space-14), 34vw, calc(var(--space-20) + var(--space-12))); + block-size: clamp(var(--space-16), 44vw, calc(var(--space-20) + var(--space-12))); + aspect-ratio: auto; } } @@ -268,7 +285,7 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us position: relative; display: grid; place-items: center; - width: clamp(6rem, 32vw, 9rem); + inline-size: clamp(var(--space-24), 32vw, var(--space-36)); aspect-ratio: 1; border-radius: 50%; background: @@ -284,7 +301,7 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us position: absolute; inset: var(--space-2); border-radius: inherit; - border: var(--border-width, 0.0625rem) solid color-mix(in srgb, var(--color-accent) 45%, transparent); + border: var(--border-width) solid color-mix(in srgb, var(--color-accent) 45%, transparent); box-shadow: inset 0 0 var(--space-3) color-mix(in srgb, var(--color-accent) 22%, transparent); animation: cc-radial-gauge-sweep calc(var(--duration-slow) * 8) linear infinite; } @@ -294,7 +311,7 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us z-index: 1; display: grid; place-items: center; - width: 58%; + inline-size: 58%; aspect-ratio: 1; background: var(--surface-1); border-radius: 50%; @@ -346,19 +363,19 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us padding: 0; display: flex; flex-direction: column; - gap: var(--space-2, 0.5rem); + gap: var(--space-2); } .cc-funnel-stage { display: flex; flex-direction: column; - gap: var(--space-1, 0.25rem); + gap: var(--space-1); } .cc-funnel-header { display: flex; justify-content: space-between; - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); color: var(--text-muted); } @@ -367,30 +384,30 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us } .cc-funnel-track { - height: 1rem; - background: var(--surface-2, rgba(127, 127, 127, 0.12)); - border-radius: var(--radius-sm, 4px); + block-size: var(--space-4); + background: var(--surface-2); + border-radius: var(--radius-sm); overflow: hidden; } .cc-funnel-fill { - height: 100%; - background: var(--color-accent, #4f8cff); - border-radius: var(--radius-sm, 4px); + block-size: 100%; + background: var(--color-accent); + border-radius: var(--radius-sm); transition: width var(--transition-normal); } .cc-funnel-value { - font-size: var(--font-size-sm, 0.85rem); + font-size: var(--font-size-sm); font-variant-numeric: tabular-nums; - color: var(--text-primary, #ddd); + color: var(--text-primary); } /* ---- Loading shimmer (used by chart skeletons) ---- */ .cc-chart-skeleton { - height: 0.75rem; - border-radius: var(--radius-sm, 4px); - background: var(--surface-2, rgba(127, 127, 127, 0.12)); + block-size: var(--space-3); + border-radius: var(--radius-sm); + background: var(--surface-2); animation: cc-chart-pulse var(--duration-slow) ease-in-out infinite; } @@ -403,3 +420,23 @@ Line-chart motion is decorative, token-timed, and disabled for reduced-motion us opacity: 0.9; } } + + +@media (max-width: 768px) { + .cc-bar-row { + grid-template-columns: minmax(0, 1fr) minmax(var(--space-12), 2fr); + } + + .cc-bar-value { + grid-column: 1 / -1; + justify-self: end; + } + + .cc-radial-gauge-ring { + inline-size: clamp(var(--space-20), 44vw, var(--space-32)); + } + + .cc-sparkline { + block-size: clamp(var(--space-8), 18vw, var(--space-14)); + } +}