FN-6664: polish Command Center mobile charts
Polish Command Center chart surfaces and mobile scrolling behavior. - Keep mobile Command Center chart tabs constrained to the tabpanel scroll owner without clipping or stretch artifacts. - Normalize chart, stat, table, gauge, and team panel card styling around shared design tokens. - Expand regression coverage for mobile chart sizing, overflow ownership, and tokenized area styling. - Document the mobile rendering and token-only invariants and add a patch changeset. Files changed: .../fn-6664-command-center-mobile-chart-polish.md | 5 + docs/dashboard-guide.md | 6 + .../components/command-center/CommandCenter.css | 112 ++++++++++------- .../__tests__/CommandCenter.mobile-scroll.test.tsx | 128 ++++++++++++++++++- .../command-center/areas/__tests__/areas.test.tsx | 47 ++++++- .../app/components/command-center/areas/areas.css | 63 ++++++---- .../components/command-center/charts/charts.css | 135 +++++++++++++-------- 7 files changed, 375 insertions(+), 121 deletions(-) Fusion-Task-Id: FN-6664 Fusion-Task-Lineage: 14afa9c5-7287-4db1-beea-3bf7cbb70e7b
This commit is contained in:
5
.changeset/fn-6664-command-center-mobile-chart-polish.md
Normal file
5
.changeset/fn-6664-command-center-mobile-chart-polish.md
Normal file
@@ -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.
|
||||
@@ -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`).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HTMLElement>(
|
||||
".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(<CommandCenter />);
|
||||
|
||||
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(<CommandCenter />);
|
||||
|
||||
@@ -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(<ProductivityArea range={range7d} />);
|
||||
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(<ProductivityArea range={range7d} />);
|
||||
expect(screen.getByTestId("cc-area-productivity-loading")).toBeTruthy();
|
||||
pending.unmount();
|
||||
|
||||
apiMock.mockRejectedValueOnce(new Error("productivity failed"));
|
||||
render(<ProductivityArea range={range7d} />);
|
||||
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(<EcosystemArea range={range7d} />);
|
||||
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(<EcosystemArea range={range7d} />);
|
||||
await screen.findByTestId("cc-area-ecosystem-empty");
|
||||
expect(screen.queryByRole("list", { name: "Tasks per model" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user