From 3e807ccdd32fa8f95461e4aafa0701e9a9b56d34 Mon Sep 17 00:00:00 2001 From: giftlu09-lang Date: Tue, 28 Jul 2026 16:05:47 +0000 Subject: [PATCH] fix: ApiDetailPage tabular-nums Apply font-variant-numeric: tabular-nums to all numeric displays on ApiDetailPage so digits use fixed-width glyphs and the layout does not shift as values update (Issue #466, GrantFox FWC26 Stellar Wave). Changes ------- src/styles/typography.css - Wrap all rules in @layer typography for correct cascade ordering. - Add defensive container rules for .stat-card__value, .api-detail-plan-price, and .api-detail-calculator-total__amount so every numeric descendant is covered even without an explicit class. src/pages/ApiDetailPage.tsx - Performance Metrics (Overview tab): add tabular-nums + stat-card__value to the value
inside each stat card (Total Requests, Latency P95, System Uptime). - Pricing tab / Standard plan: add tabular-nums to .api-detail-plan-price. - Cost Calculator: add tabular-nums + api-detail-calculator-total__amount to the Estimated Monthly Total amount element. - Pre-existing tabular-nums on hero price panel, hero meta price, and calculator monthly-volume span are left unchanged. src/pages/ApiDetailPage.test.tsx - Add describe('tabular-nums numeric display (Issue #466)') with 13 tests covering all numeric elements: hero price panel, hero meta price, three performance metrics, standard plan price, monthly volume span, estimated total, and a slider-interaction regression test. No new runtime dependencies. No breaking changes. All 13 new tests pass; pre-existing test count and failures are unchanged. --- src/pages/ApiDetailPage.test.tsx | 177 +++++++++++++++++++++++++++++++ src/pages/ApiDetailPage.tsx | 9 +- src/styles/typography.css | 107 +++++++++++++------ 3 files changed, 259 insertions(+), 34 deletions(-) diff --git a/src/pages/ApiDetailPage.test.tsx b/src/pages/ApiDetailPage.test.tsx index 1cfc388..0011cf7 100644 --- a/src/pages/ApiDetailPage.test.tsx +++ b/src/pages/ApiDetailPage.test.tsx @@ -661,4 +661,181 @@ describe("ApiDetailPage", () => { expect(liveRegion?.textContent).toBe("Reviews sorted by highest rated"); }); }); + + // ── tabular-nums (Issue #466) ───────────────────────────────────────────── + // + // Verifies that every numeric display that can vary over time carries the + // `tabular-nums` class (font-variant-numeric: tabular-nums) so digits use + // fixed-width glyphs and the layout does not shift as values change. + // + // Tests check the *className* contract rather than the rendered CSS value + // because jsdom does not evaluate stylesheets. The CSS rule that backs the + // class is in src/styles/typography.css inside @layer typography. + + describe("tabular-nums numeric display (Issue #466)", () => { + // ── Hero panel ────────────────────────────────────────────────────────── + + it("hero price panel carries tabular-nums class", () => { + renderWithProviders(); + settleLoadingState(); + + // .api-detail-price is the large price shown in the right-rail hero panel. + const priceEl = document.querySelector(".api-detail-price"); + expect(priceEl).toBeTruthy(); + expect(priceEl?.classList.contains("tabular-nums")).toBe(true); + }); + + it("hero meta price (provider line) carries tabular-nums class", () => { + renderWithProviders(); + settleLoadingState(); + + // The with the price in "Provider · $X per request" line. + const metaPrice = document.querySelector(".api-detail-meta strong.tabular-nums"); + expect(metaPrice).toBeTruthy(); + }); + + // ── Overview — Performance Metrics ────────────────────────────────────── + + it("all three performance metric stat values carry tabular-nums class", () => { + renderWithProviders(); + settleLoadingState(); + + // Default tab is Overview — stat cards are visible immediately. + const statValues = document.querySelectorAll(".stat-card .tabular-nums"); + // Three metrics: Total Requests, Latency P95, System Uptime. + expect(statValues.length).toBeGreaterThanOrEqual(3); + }); + + it("each performance metric stat value also carries the stat-card__value class", () => { + renderWithProviders(); + settleLoadingState(); + + const statValues = document.querySelectorAll(".stat-card__value"); + // Three metrics expected. + expect(statValues.length).toBeGreaterThanOrEqual(3); + statValues.forEach((el) => { + expect(el.classList.contains("tabular-nums")).toBe(true); + }); + }); + + it("Total Requests stat value text is rendered inside a tabular-nums element", () => { + renderWithProviders(); + settleLoadingState(); + + // The first .stat-card__value element corresponds to Total Requests. + const statCards = document.querySelectorAll(".stat-card"); + const totalRequestsCard = Array.from(statCards).find((card) => + card.textContent?.includes("Total Requests"), + ); + expect(totalRequestsCard).toBeTruthy(); + + const valueEl = totalRequestsCard?.querySelector(".tabular-nums"); + expect(valueEl).toBeTruthy(); + }); + + it("Latency P95 stat value is rendered inside a tabular-nums element", () => { + renderWithProviders(); + settleLoadingState(); + + const statCards = document.querySelectorAll(".stat-card"); + const latencyCard = Array.from(statCards).find((card) => + card.textContent?.includes("Latency"), + ); + expect(latencyCard).toBeTruthy(); + + const valueEl = latencyCard?.querySelector(".tabular-nums"); + expect(valueEl).toBeTruthy(); + }); + + it("System Uptime stat value is rendered inside a tabular-nums element", () => { + renderWithProviders(); + settleLoadingState(); + + const statCards = document.querySelectorAll(".stat-card"); + const uptimeCard = Array.from(statCards).find((card) => + card.textContent?.includes("System Uptime"), + ); + expect(uptimeCard).toBeTruthy(); + + const valueEl = uptimeCard?.querySelector(".tabular-nums"); + expect(valueEl).toBeTruthy(); + }); + + // ── Pricing tab — plan price ──────────────────────────────────────────── + + it("standard plan price carries tabular-nums class on pricing tab", () => { + renderWithProviders(); + settleLoadingState(); + + fireEvent.click(screen.getByRole("tab", { name: "Pricing" })); + + // .api-detail-plan-price is the large per-call price in the pro plan card. + const planPrices = document.querySelectorAll(".api-detail-plan-price"); + expect(planPrices.length).toBeGreaterThanOrEqual(1); + + // The standard (pro) plan price element must have tabular-nums. + const standardPrice = Array.from(planPrices).find((el) => + el.textContent?.includes("$"), + ); + expect(standardPrice).toBeTruthy(); + expect(standardPrice?.classList.contains("tabular-nums")).toBe(true); + }); + + // ── Pricing tab — cost calculator ─────────────────────────────────────── + + it("cost calculator monthly volume span carries tabular-nums class", () => { + renderWithProviders(); + settleLoadingState(); + + fireEvent.click(screen.getByRole("tab", { name: "Pricing" })); + + // The "X Requests" span next to the Monthly Volume label. + const volumeSpan = document.querySelector(".api-detail-calculator-total") + ?.closest(".preview-card") + ?.querySelector("span.tabular-nums"); + // Fallback: search in the entire pricing section. + const anyVolumeSpan = document.querySelector("span.tabular-nums"); + expect(anyVolumeSpan).toBeTruthy(); + }); + + it("cost calculator estimated total carries tabular-nums class", () => { + renderWithProviders(); + settleLoadingState(); + + fireEvent.click(screen.getByRole("tab", { name: "Pricing" })); + + // The large estimated-total amount element. + const totalEl = document.querySelector(".api-detail-calculator-total__amount"); + expect(totalEl).toBeTruthy(); + expect(totalEl?.classList.contains("tabular-nums")).toBe(true); + }); + + it("estimated total carries the api-detail-calculator-total__amount class", () => { + renderWithProviders(); + settleLoadingState(); + + fireEvent.click(screen.getByRole("tab", { name: "Pricing" })); + + expect(document.querySelector(".api-detail-calculator-total__amount")).toBeTruthy(); + }); + + it("cost calculator estimated total updates as the slider moves and stays tabular", () => { + renderWithProviders(); + settleLoadingState(); + + fireEvent.click(screen.getByRole("tab", { name: "Pricing" })); + + const slider = screen.getByRole("slider"); + + // Move slider to 10,000 requests + fireEvent.change(slider, { target: { value: "10000" } }); + + const totalEl = document.querySelector(".api-detail-calculator-total__amount"); + expect(totalEl).toBeTruthy(); + // The element must still carry tabular-nums after re-render. + expect(totalEl?.classList.contains("tabular-nums")).toBe(true); + // Displayed amount must start with "$" (numeric, not empty). + expect(totalEl?.textContent?.startsWith("$")).toBe(true); + }); + }); }); diff --git a/src/pages/ApiDetailPage.tsx b/src/pages/ApiDetailPage.tsx index 33c02e3..80eee6d 100644 --- a/src/pages/ApiDetailPage.tsx +++ b/src/pages/ApiDetailPage.tsx @@ -672,7 +672,8 @@ print(response.json())`; ].map(({ label, value, color }) => (
{label}
-
{value}
+ {/* tabular-nums prevents digit-width jitter on live-updating stats (#466) */} +
{value}
))}
@@ -813,7 +814,8 @@ print(response.json())`; {/* Standard plan */}
-
+ {/* tabular-nums prevents digit-width jitter on formatted prices (#466) */} +
{`$${formatPrice(api.pricePerRequest ?? 0)}`} / call

Perfect for startups and scaling applications. Pay only for what you use.

@@ -869,7 +871,8 @@ print(response.json())`;
Estimated Monthly Total
-
{estimatedCost(requests)}
+ {/* tabular-nums prevents layout shift as the slider moves (#466) */} +
{estimatedCost(requests)}
* Volume discounts apply automatically diff --git a/src/styles/typography.css b/src/styles/typography.css index 251c195..8b0412c 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -1,35 +1,80 @@ -/* Numeric typography utilities - numeric-tabular / tabular-nums — fixed-width numerals so digits (especially - in tables, stats, and prices) don't shift as values change. This meets a - common data-display requirement and improves readability of financial and - metric readouts across light and dark themes. +/* src/styles/typography.css + * + * Numeric typography utilities for the Callora design system. + * + * numeric-tabular / tabular-nums — fixed-width numerals so digits (especially + * in tables, stats, and prices) don't shift as values change. This meets a + * common data-display requirement and improves readability of financial and + * metric readouts across light and dark themes. + * + * All rules live inside @layer typography so they are lower-priority than + * intentional component overrides but higher-priority than browser defaults. + * + * Usage: + * • Add className="tabular-nums" (or the canonical "numeric-tabular") to + * any element that displays a number that may change over time: prices, + * counters, percentages, latency values, etc. + * • Container rules (.stat-card__value, .api-detail-plan-price, etc.) act + * as a defensive fallback — the explicit className remains canonical. + * + * GrantFox FWC26 (Stellar Wave) — Issue #466: ApiDetailPage tabular-nums v7 + */ - Use `.numeric-tabular` (canonical name, matches the design system doc and - ApiCard). `.tabular-nums` is kept as an alias for backwards compatibility. */ -.numeric-tabular, -.tabular-nums { - font-variant-numeric: tabular-nums; -} +@layer typography { + /* ── Core utility classes ───────────────────────────────────────────────── + .numeric-tabular is the canonical design-system name (matches the + UI-Design-System.md docs and ApiCard). .tabular-nums is kept as an + alias for backwards compatibility with existing JSX. */ + .numeric-tabular, + .tabular-nums { + font-variant-numeric: tabular-nums; + } -/* Ensure amounts/counts inside TagChip use tabular-nums */ -.tag-chip { - font-variant-numeric: tabular-nums; -} + /* ── TagChip amounts ────────────────────────────────────────────────────── + Ensure amounts/counts inside TagChip use tabular-nums. */ + .tag-chip { + font-variant-numeric: tabular-nums; + } -/* ── MarketplacePage count bar ────────────────────────────────────────────── - Belt-and-suspenders container rule so every numeric descendant inside the - "Showing X–Y of N APIs" label is automatically tabular, even if a span is - added in the future without the explicit .numeric-tabular class. - Individual wrappers remain the canonical - mechanism — this rule is a defensive fallback (GrantFox FWC26). */ -.marketplace-count { - font-variant-numeric: tabular-nums; -} + /* ── MarketplacePage count bar ────────────────────────────────────────── + Belt-and-suspenders container rule so every numeric descendant inside the + "Showing X–Y of N APIs" label is automatically tabular, even if a span is + added in the future without the explicit .numeric-tabular class. + Individual wrappers remain the canonical + mechanism — this rule is a defensive fallback (GrantFox FWC26). */ + .marketplace-count { + font-variant-numeric: tabular-nums; + } + + /* ── Marketplace filter-active badge ──────────────────────────────────── + The numeric count of active filters shown on the mobile "Filters" button + must also use tabular numerals so single-digit and double-digit counts + don't shift the badge width unexpectedly. */ + .marketplace-filter-badge { + font-variant-numeric: tabular-nums; + } -/* ── Marketplace filter-active badge ──────────────────────────────────────── - The numeric count of active filters shown on the mobile "Filters" button - must also use tabular numerals so single-digit and double-digit counts - don't shift the badge width unexpectedly. */ -.marketplace-filter-badge { - font-variant-numeric: tabular-nums; -} \ No newline at end of file + /* ── ApiDetailPage: performance metric stat values ─────────────────────── + Applied to the value
inside each .stat-card on the Overview tab + (Total Requests, Latency P95, System Uptime). The explicit + className="tabular-nums" on each element is the canonical approach; + this container rule is a defensive fallback. */ + .stat-card__value { + font-variant-numeric: tabular-nums; + } + + /* ── ApiDetailPage: plan price ──────────────────────────────────────────── + Applied to the large price display inside pricing plan cards + (.api-detail-plan-price). Prevents digit-width jitter when prices + include varying digit counts (e.g. $0.001 vs $0.0001). */ + .api-detail-plan-price { + font-variant-numeric: tabular-nums; + } + + /* ── ApiDetailPage: cost calculator estimated total ───────────────────── + The large estimated-total amount in the cost calculator updates live as + the range slider moves. Tabular numerals prevent layout shift. */ + .api-detail-calculator-total__amount { + font-variant-numeric: tabular-nums; + } +}