From 43667ec1fda89fd3aadd14d4956b1b49b1b3cc3d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 19 Jul 2026 00:11:50 +0100 Subject: [PATCH] feat(reporting): skeleton the KPI band on first scenario entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching from Current stock into a scenario has no previous overlay to keep, so the scenario KPIs were momentarily empty — an empty-bar flash before the figures loaded. That's the "no data yet" case, so it now shows the same KpiBandSkeleton the initial page load uses (extracted + shared), and drops the now-redundant "Loading scenario…" line there. The dim + "Updating figures…" path is unchanged and stays for the cases where data is already on screen (scenario→scenario switch, filter changes), where the previous figures are deliberately kept via keepPreviousData. Net: skeleton = no data yet; dim = refreshing existing data — one consistent convention. Verified in a headless browser on #796: first entry shows the KPI skeleton + aria-busy and no empty flash; a filter toggle still dims the figures with "Updating figures…". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../reporting/ReportingClientArea.tsx | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx index 8a6054b5..a9fb94c8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx @@ -485,17 +485,25 @@ function MetricsBody({ : null; const scenarioBusy = isScenario && isFetching; + // First entry into a scenario from Current stock: there's no previous overlay + // to keep, so the scenario KPIs are momentarily empty. Skeleton that band + // (the "no data yet" case → same pattern as first page load) rather than + // flashing an empty bar. A scenario→scenario switch keeps the old figures, so + // it stays on the dim+spinner path below instead. + const scenarioLoadingFirst = scenarioBusy && !scenarioData; return (
- {scenarioBusy && ( + {/* Refetch-with-data only: the figures stay on screen and dim, so a label + clarifies the dim means "updating". First load speaks via its skeleton. */} + {scenarioBusy && scenarioData && (
- {scenarioData ? "Updating figures…" : "Loading scenario…"} + Updating figures…
)} @@ -509,7 +517,11 @@ function MetricsBody({ aria-busy={scenarioBusy} > {/* KPI band */} - + {scenarioLoadingFirst ? ( + + ) : ( + + )} {/* Charts + ledger */}
@@ -610,21 +622,34 @@ function ScenarioRowSkeleton() { ); } +/** + * The five-cell KPI band in its loading shape. Shared by the initial page-load + * skeleton and the first Current-stock → scenario switch, so both "no data yet" + * moments read identically. + */ +function KpiBandSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
0 ? "border-l border-gray-100" : ""}`} + > +
+
+
+ ))} +
+ ); +} + function MetricsSkeleton() { return (
- {/* KPI band */} -
- {Array.from({ length: 5 }).map((_, i) => ( -
0 ? "border-l border-gray-100" : ""}`} - > -
-
-
- ))} -
+ {/* Charts + side panel */}