From ee72c942f19c81851622ec748718ec5445f7eec3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 19 Jun 2026 08:18:12 -0700 Subject: [PATCH] FN-6715: move Overview funnel below charts Move the Command Center Overview funnel to the bottom while expanding chart palette coverage. - Render the SDLC throughput funnel after loading, error, empty, and populated Overview content. - Broaden hand-rolled chart primitive colors across existing semantic theme tokens. - Add regression coverage for Overview ordering, palette cycling, and safe chart geometry. - Document the Overview funnel placement and add a patch changeset. Files changed: .../fn-6715-command-center-overview-funnel.md | 5 + docs/dashboard-guide.md | 4 +- .../components/command-center/CommandCenter.tsx | 13 +- .../__tests__/CommandCenter.test.tsx | 20 +-- .../command-center/__tests__/charts.test.tsx | 103 ++++++++++++++- .../components/command-center/charts/charts.css | 141 +++++++++++++++++++++ 6 files changed, 263 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-6715 Fusion-Task-Lineage: 27fafdff-6cd8-4497-ab52-6f28f3100df1 --- .../fn-6715-command-center-overview-funnel.md | 5 + docs/dashboard-guide.md | 4 +- .../command-center/CommandCenter.tsx | 13 +- .../__tests__/CommandCenter.test.tsx | 20 +-- .../command-center/__tests__/charts.test.tsx | 103 ++++++++++++- .../command-center/charts/charts.css | 141 ++++++++++++++++++ 6 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 .changeset/fn-6715-command-center-overview-funnel.md diff --git a/.changeset/fn-6715-command-center-overview-funnel.md b/.changeset/fn-6715-command-center-overview-funnel.md new file mode 100644 index 0000000000..5fbe174be1 --- /dev/null +++ b/.changeset/fn-6715-command-center-overview-funnel.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Move the Command Center Overview SDLC throughput funnel to the bottom of the tab and broaden hand-rolled chart primitive colors to cycle through existing semantic theme tokens. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 06930ea8ea..5d7dee952c 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -663,7 +663,7 @@ Navigation: Features: - Global date-range picker in the header scopes the analytics tabs; **Mission Control** remains live rather than historical. -- **Overview** summarizes token usage/cost, autonomy, active nodes, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range. +- **Overview** summarizes token usage/cost, autonomy, active nodes, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range. - **Tokens** breaks down token totals, estimated cost, tasks, and per-model usage. Per-model and per-provider breakdowns use the task's analytics-only actually-used model snapshot when available, so usage from settings-resolved runs appears under the real runtime model instead of `(unknown)` without changing future model resolution; estimated cost uses the same snapshot-first, legacy-fallback model identity so those resolved runs price normally when the model is in the pricing table. It includes the existing token-usage-over-time chart, an additive recharts multi-series line graph, and a token-share pie backed by the same grouped token analytics; use the granularity control to switch the time-series request between hourly, daily, and weekly buckets. The token total and charts poll on a bounded cadence, keep the previous data visible during refresh, animate decorative count/bar transitions, and disable those animations for reduced-motion users. - **Tools** shows autonomy ratio, tool-call volume, intervention counts, sessions, and tool categories. The area keeps the existing category bar and adds a recharts category-share pie from `ToolAnalytics.byCategory`. There is intentionally no tools line chart yet because `ToolAnalytics` does not expose a per-day tool trend; the dashboard does not fabricate one or call a new endpoint. - **Activity** tracks sessions, messages, active nodes, active agents, agent heartbeat runs, and stickiness. Agent-run sheets show total, active, completed, and failed runs for the selected range, and the Agent runs/day sparkline trends runs by `agentRuns.startedAt`. The area keeps the existing live animated line charts for messages/day, active agents/day, active nodes/day, and combined throughput/day (`messages + active agents + active nodes`), and adds a recharts multi-series line graph for messages, active agents, and agent runs plus an agent-run outcome pie from the existing `agentRuns` split. These charts reuse the existing activity analytics endpoint, refresh on a bounded 15-second cadence while mounted, keep the previous data visible during refreshes, and disable decorative draw-on motion for reduced-motion users. @@ -1256,7 +1256,7 @@ 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 canonical `--accent` / `--text` for generic accent and primary text styling, use `--duration-*` for animation durations, and encode mobile chart invariants with shared classes rather than one-off area styles. The undefined `--color-accent` / `--text-primary` aliases are forbidden under `components/command-center/**` and guarded by `command-center-css-token-canonicalization.test.ts`. +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 canonical `--accent` / `--text` for generic accent and primary text styling, use `--duration-*` for animation durations, and encode mobile chart invariants with shared classes rather than one-off area styles. Hand-rolled chart primitives cycle through the existing semantic palette (`--accent`, workflow status tokens, and success/warning/error tokens) rather than adding one-off chart color aliases. The undefined `--color-accent` / `--text-primary` aliases are forbidden under `components/command-center/**` and guarded by `command-center-css-token-canonicalization.test.ts`. Non-Command-Center dashboard CSS uses `--text` as the canonical primary text token. The undefined `--text-primary` alias is forbidden outside `components/command-center/**` and guarded by `packages/dashboard/app/__tests__/text-token-canonicalization.test.ts`. diff --git a/packages/dashboard/app/components/command-center/CommandCenter.tsx b/packages/dashboard/app/components/command-center/CommandCenter.tsx index bc90d7ab7c..08d905c8fe 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenter.tsx @@ -217,9 +217,8 @@ function OverviewTab({ range }: { range: DateRange }) { }, ]; - // FNXC:CommandCenter 2026-06-19-00:00: - // Throughput funnel now renders first in every overview branch (loading/error/empty/populated) - // so the SDLC throughput card is top-of-page; mobile scroll owner and data-testid anchors unchanged. + // FNXC:CommandCenter 2026-06-19-07:56: + // The SDLC throughput funnel must sit at the bottom of the Overview first page in every branch (loading/error/empty/populated) while keeping mobile scroll owner and data-testid anchors unchanged. // The throughput funnel reads its own data (activityLog transitions) and shows // its own empty state, so it renders even when the stat-card aggregates have no // data yet. @@ -232,11 +231,11 @@ function OverviewTab({ range }: { range: DateRange }) { if (isInitialLoading) { return (
- {throughputSection}

{t("commandCenter.loading", "Loading command center...")}

+ {throughputSection}
); } @@ -244,11 +243,11 @@ function OverviewTab({ range }: { range: DateRange }) { if (coreError !== null && !hasData) { return (
- {throughputSection}

{coreError}

+ {throughputSection}
); } @@ -256,18 +255,17 @@ function OverviewTab({ range }: { range: DateRange }) { if (!hasData) { return (
- {throughputSection}

{t("commandCenter.empty", "No usage data yet. Run some agents to populate the Command Center.")}

+ {throughputSection}
); } return (
- {throughputSection}
{cards.map((card) => (
@@ -378,6 +376,7 @@ function OverviewTab({ range }: { range: DateRange }) { ) : null} ) : null} + {throughputSection}
); } diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx index bf8c37efde..03b193a5d1 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsx @@ -291,14 +291,14 @@ function liveMetricValue(testId = "command-center-live-tasks-in-progress") { return screen.getByTestId(testId).querySelector(".cc-live-metric-value")?.textContent ?? null; } -function expectThroughputFirstBefore(...followingTestIds: string[]) { +function expectThroughputLastAfter(...precedingTestIds: string[]) { const throughput = screen.getByTestId("command-center-throughput"); expect(throughput.parentElement?.classList.contains("cc-overview")).toBe(true); - expect(throughput.parentElement?.firstElementChild).toBe(throughput); + expect(throughput.parentElement?.lastElementChild).toBe(throughput); - for (const testId of followingTestIds) { - const followingNode = screen.getByTestId(testId); - expect(Boolean(throughput.compareDocumentPosition(followingNode) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true); + for (const testId of precedingTestIds) { + const precedingNode = screen.getByTestId(testId); + expect(Boolean(precedingNode.compareDocumentPosition(throughput) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true); } } @@ -325,11 +325,11 @@ describe("CommandCenter shell", () => { expect(screen.getByTestId("command-center-panel-overview")).toBeTruthy(); }); - it("renders throughput first while the Overview branch is loading", () => { + it("renders throughput last while the Overview branch is loading", () => { mockEmptyOverviewApi(); render(); expect(screen.getByTestId("command-center-overview-loading")).toBeTruthy(); - expectThroughputFirstBefore("command-center-overview-loading"); + expectThroughputLastAfter("command-center-overview-loading"); }); it("renders the documented empty state when there is no data (no crash)", async () => { @@ -342,7 +342,7 @@ describe("CommandCenter shell", () => { expect(screen.queryByTestId("cc-overview-line")).toBeNull(); expect(screen.queryByTestId("command-center-overview-chart-activity")).toBeNull(); await screen.findByTestId("command-center-empty"); - expectThroughputFirstBefore("command-center-empty"); + expectThroughputLastAfter("command-center-empty"); expect(screen.queryByTestId("command-center-overview-charts")).toBeNull(); expect(screen.queryByTestId("cc-overview-pie")).toBeNull(); expect(screen.queryByTestId("cc-overview-line")).toBeNull(); @@ -379,7 +379,6 @@ describe("CommandCenter shell", () => { expect(statValue("command-center-stat-models")).toBe("2"); expect(statValue("command-center-stat-signals")).toBe("2"); expect(screen.getByTestId("command-center-live-strip")).toBeTruthy(); - expectThroughputFirstBefore("command-center-stat-tokens", "command-center-live-strip"); expect(screen.getByTestId("command-center-live-snapshot")).toBeTruthy(); await waitFor(() => expect(liveMetricValue()).toBe("3")); expect(screen.getByTestId("command-center-live-agents-working").textContent).toContain("2"); @@ -399,6 +398,7 @@ describe("CommandCenter shell", () => { expect(screen.getByRole("img", { name: "Token share by model" })).toBeTruthy(); expect(screen.getByRole("img", { name: "Daily activity line" })).toBeTruthy(); expect(screen.getByRole("img", { name: "Daily activity trend" })).toBeTruthy(); + expectThroughputLastAfter("command-center-stat-tokens", "command-center-live-strip", "command-center-overview-charts"); }); it("live-polls token totals for the Overview card and live strip", async () => { @@ -590,7 +590,7 @@ describe("CommandCenter shell", () => { render(); await screen.findByTestId("command-center-overview-error"); - expectThroughputFirstBefore("command-center-overview-error"); + expectThroughputLastAfter("command-center-overview-error"); expect(screen.getByTestId("command-center-overview-error").textContent).toContain("tokens failed"); expect(screen.queryByTestId("command-center-overview-loading")).toBeNull(); expect(screen.queryByTestId("command-center-empty")).toBeNull(); diff --git a/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx b/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx index 263b6fa43c..65cdb4aca1 100644 --- a/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/charts.test.tsx @@ -18,6 +18,48 @@ function heightOf(el: HTMLElement): string { return el.style.height; } +const chartCssPath = resolve(__dirname, "../charts/charts.css"); +const chartCss = readFileSync(chartCssPath, "utf8"); +const chartPaletteTokens = [ + "--accent", + "--todo", + "--in-progress", + "--in-review", + "--triage", + "--color-success", + "--color-warning", + "--color-error", +]; + +function expectPaletteTokenRule(selector: string, token: string): void { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + expect(chartCss).toMatch(new RegExp(`${escapedSelector}\\s*\\{[^}]*background:\\s*var\\(${token}\\)`, "s")); +} + +describe("chart primitive palette CSS", () => { + it("cycles Bar fills through the canonical palette and wraps after eight rows", () => { + chartPaletteTokens.forEach((token, index) => { + const nth = index === chartPaletteTokens.length - 1 ? "8n" : `8n + ${index + 1}`; + expectPaletteTokenRule(`.cc-bar-row:nth-child(${nth}) .cc-bar-fill`, token); + }); + }); + + it("cycles StackedBar segments and legend swatches through the canonical palette", () => { + chartPaletteTokens.forEach((token, index) => { + const nth = index === chartPaletteTokens.length - 1 ? "8n" : `8n + ${index + 1}`; + expectPaletteTokenRule(`.cc-stacked-segment:nth-child(${nth}),\n.cc-stacked-legend-item:nth-child(${nth}) .cc-stacked-swatch`, token); + }); + }); + + it("cycles Sparkline and Funnel fills through the canonical palette", () => { + chartPaletteTokens.forEach((token, index) => { + const nth = index === chartPaletteTokens.length - 1 ? "8n" : `8n + ${index + 1}`; + expectPaletteTokenRule(`.cc-sparkline-bar:nth-child(${nth})`, token); + expectPaletteTokenRule(`.cc-funnel-stage:nth-child(${nth}) .cc-funnel-fill`, token); + }); + }); +}); + describe("Bar", () => { it("renders a fill per datum and an accessible label", () => { render( @@ -58,6 +100,20 @@ describe("Bar", () => { const el = screen.getByLabelText("nan: 0"); expect(widthOf(el)).toBe("0%"); }); + + it("renders empty, single, and more-than-palette datasets without NaN geometry", () => { + const { rerender } = render(); + expect(screen.getByRole("list", { name: "empty bars" }).querySelectorAll(".cc-bar-fill")).toHaveLength(0); + + rerender(); + expect(widthOf(screen.getByLabelText("single: 1"))).toBe("100%"); + + const many = Array.from({ length: 9 }, (_, index) => ({ label: `item-${index + 1}`, value: index === 8 ? 0 : index + 1 })); + rerender(); + expect(screen.getByRole("list", { name: "many bars" }).querySelectorAll(".cc-bar-fill")).toHaveLength(9); + expect(widthOf(screen.getByLabelText("item-9: 0"))).toBe("0%"); + expect(screen.getByRole("list", { name: "many bars" }).innerHTML).not.toMatch(/NaN|Infinity/); + }); }); describe("StackedBar", () => { @@ -81,6 +137,20 @@ describe("StackedBar", () => { expect(widthOf(screen.getByLabelText("a: 0"))).toBe("0%"); expect(widthOf(screen.getByLabelText("b: 0"))).toBe("0%"); }); + + it("renders empty, single, and more-than-palette segment sets without NaN geometry", () => { + const { rerender } = render(); + expect(screen.getByRole("img", { name: "empty stack" }).querySelectorAll(".cc-stacked-segment")).toHaveLength(0); + + rerender(); + expect(widthOf(screen.getByLabelText("single: 3"))).toBe("100%"); + + const many = Array.from({ length: 9 }, (_, index) => ({ label: `segment-${index + 1}`, value: index === 8 ? 0 : 1 })); + rerender(); + expect(screen.getByRole("img", { name: "many stack" }).querySelectorAll(".cc-stacked-segment")).toHaveLength(9); + expect(widthOf(screen.getByLabelText("segment-9: 0"))).toBe("0%"); + expect(screen.getByRole("img", { name: "many stack" }).innerHTML).not.toMatch(/NaN|Infinity/); + }); }); describe("Sparkline", () => { @@ -100,6 +170,20 @@ describe("Sparkline", () => { expect(heightOf(bars[0])).toBe("0%"); expect(heightOf(bars[1])).toBe("0%"); }); + + it("renders empty, single, and more-than-palette value sets without NaN geometry", () => { + const { rerender } = render(); + expect(screen.getByRole("img", { name: "empty spark" }).querySelectorAll(".cc-sparkline-bar")).toHaveLength(0); + + rerender(); + expect(heightOf(screen.getByRole("img", { name: "single spark" }).querySelector(".cc-sparkline-bar")!)).toBe("100%"); + + rerender(); + const bars = screen.getByRole("img", { name: "many spark" }).querySelectorAll(".cc-sparkline-bar"); + expect(bars).toHaveLength(9); + expect(heightOf(bars[8])).toBe("0%"); + expect(screen.getByRole("img", { name: "many spark" }).innerHTML).not.toMatch(/NaN|Infinity/); + }); }); describe("TokenSeriesChart", () => { @@ -260,6 +344,20 @@ describe("Funnel", () => { expect(screen.getByText("—")).toBeTruthy(); expect(widthOf(screen.getByLabelText("a: 0"))).toBe("0%"); }); + + it("renders empty, single, and more-than-palette stages without NaN geometry", () => { + const { rerender } = render(); + expect(screen.getByLabelText("empty funnel").querySelectorAll(".cc-funnel-fill")).toHaveLength(0); + + rerender(); + expect(widthOf(screen.getByLabelText("single: 2"))).toBe("100%"); + + const many = Array.from({ length: 9 }, (_, index) => ({ label: `stage-${index + 1}`, value: index === 8 ? 0 : index + 1 })); + rerender(); + expect(screen.getByLabelText("many funnel").querySelectorAll(".cc-funnel-fill")).toHaveLength(9); + expect(widthOf(screen.getByLabelText("stage-9: 0"))).toBe("0%"); + expect(screen.getByLabelText("many funnel").innerHTML).not.toMatch(/NaN|Infinity/); + }); }); /** @@ -268,11 +366,8 @@ describe("Funnel", () => { * duration+easing pair (which silently resolves to animation: none). */ describe("chart CSS animation tokens", () => { - const cssPath = resolve(__dirname, "../charts/charts.css"); - const css = readFileSync(cssPath, "utf8"); - it("uses a --duration-* token in the loader animation, not --transition-*", () => { - const animationLines = css.split("\n").filter((line) => /animation\s*:/.test(line) && !/animation\s*:\s*none/.test(line)); + const animationLines = chartCss.split("\n").filter((line) => /animation\s*:/.test(line) && !/animation\s*:\s*none/.test(line)); expect(animationLines.length).toBeGreaterThan(0); for (const line of animationLines) { expect(line).not.toMatch(/var\(--transition-/); diff --git a/packages/dashboard/app/components/command-center/charts/charts.css b/packages/dashboard/app/components/command-center/charts/charts.css index 699438c22e..252919e709 100644 --- a/packages/dashboard/app/components/command-center/charts/charts.css +++ b/packages/dashboard/app/components/command-center/charts/charts.css @@ -15,6 +15,11 @@ 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-19-07:58: +The Overview and area tabs should feel like a richer software-factory surface. Cycle hand-rolled chart marks through the same canonical semantic palette as the recharts wrappers, using only existing theme tokens so dark/light variants stay aligned without hardcoded colors. +*/ + /* FNXC:CommandCenterStyling 2026-06-18-22:31: FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not compute grid/flex min-content sizes. Chart primitives and their text-bearing children need explicit logical shrink bounds so long labels cannot crush tracks or create hidden horizontal overflow inside the mobile tabpanel. @@ -75,6 +80,38 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not transition: width var(--transition-normal); } +.cc-bar-row:nth-child(8n + 1) .cc-bar-fill { + background: var(--accent); +} + +.cc-bar-row:nth-child(8n + 2) .cc-bar-fill { + background: var(--todo); +} + +.cc-bar-row:nth-child(8n + 3) .cc-bar-fill { + background: var(--in-progress); +} + +.cc-bar-row:nth-child(8n + 4) .cc-bar-fill { + background: var(--in-review); +} + +.cc-bar-row:nth-child(8n + 5) .cc-bar-fill { + background: var(--triage); +} + +.cc-bar-row:nth-child(8n + 6) .cc-bar-fill { + background: var(--color-success); +} + +.cc-bar-row:nth-child(8n + 7) .cc-bar-fill { + background: var(--color-warning); +} + +.cc-bar-row:nth-child(8n) .cc-bar-fill { + background: var(--color-error); +} + .cc-bar-value { min-inline-size: 0; font-size: 0.8125rem; @@ -132,6 +169,46 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not display: inline-block; } +.cc-stacked-segment:nth-child(8n + 1), +.cc-stacked-legend-item:nth-child(8n + 1) .cc-stacked-swatch { + background: var(--accent); +} + +.cc-stacked-segment:nth-child(8n + 2), +.cc-stacked-legend-item:nth-child(8n + 2) .cc-stacked-swatch { + background: var(--todo); +} + +.cc-stacked-segment:nth-child(8n + 3), +.cc-stacked-legend-item:nth-child(8n + 3) .cc-stacked-swatch { + background: var(--in-progress); +} + +.cc-stacked-segment:nth-child(8n + 4), +.cc-stacked-legend-item:nth-child(8n + 4) .cc-stacked-swatch { + background: var(--in-review); +} + +.cc-stacked-segment:nth-child(8n + 5), +.cc-stacked-legend-item:nth-child(8n + 5) .cc-stacked-swatch { + background: var(--triage); +} + +.cc-stacked-segment:nth-child(8n + 6), +.cc-stacked-legend-item:nth-child(8n + 6) .cc-stacked-swatch { + background: var(--color-success); +} + +.cc-stacked-segment:nth-child(8n + 7), +.cc-stacked-legend-item:nth-child(8n + 7) .cc-stacked-swatch { + background: var(--color-warning); +} + +.cc-stacked-segment:nth-child(8n), +.cc-stacked-legend-item:nth-child(8n) .cc-stacked-swatch { + background: var(--color-error); +} + /* ---- Sparkline ---- */ .cc-sparkline { display: flex; @@ -148,6 +225,38 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not transition: height var(--transition-normal); } +.cc-sparkline-bar:nth-child(8n + 1) { + background: var(--accent); +} + +.cc-sparkline-bar:nth-child(8n + 2) { + background: var(--todo); +} + +.cc-sparkline-bar:nth-child(8n + 3) { + background: var(--in-progress); +} + +.cc-sparkline-bar:nth-child(8n + 4) { + background: var(--in-review); +} + +.cc-sparkline-bar:nth-child(8n + 5) { + background: var(--triage); +} + +.cc-sparkline-bar:nth-child(8n + 6) { + background: var(--color-success); +} + +.cc-sparkline-bar:nth-child(8n + 7) { + background: var(--color-warning); +} + +.cc-sparkline-bar:nth-child(8n) { + background: var(--color-error); +} + /* ---- TokenSeriesChart ---- */ /* FNXC:CommandCenterStyling 2026-06-18-15:14: @@ -451,6 +560,38 @@ ResponsiveContainer measures its direct parent. Keep the shared recharts wrapper transition: width var(--transition-normal); } +.cc-funnel-stage:nth-child(8n + 1) .cc-funnel-fill { + background: var(--accent); +} + +.cc-funnel-stage:nth-child(8n + 2) .cc-funnel-fill { + background: var(--todo); +} + +.cc-funnel-stage:nth-child(8n + 3) .cc-funnel-fill { + background: var(--in-progress); +} + +.cc-funnel-stage:nth-child(8n + 4) .cc-funnel-fill { + background: var(--in-review); +} + +.cc-funnel-stage:nth-child(8n + 5) .cc-funnel-fill { + background: var(--triage); +} + +.cc-funnel-stage:nth-child(8n + 6) .cc-funnel-fill { + background: var(--color-success); +} + +.cc-funnel-stage:nth-child(8n + 7) .cc-funnel-fill { + background: var(--color-warning); +} + +.cc-funnel-stage:nth-child(8n) .cc-funnel-fill { + background: var(--color-error); +} + .cc-funnel-value { font-size: 0.8125rem; font-variant-numeric: tabular-nums;