Skip to content

feat(home): wire Real-world progress stats section with synced counters [INTORG-670]#301

Merged
Infi-Knight merged 8 commits into
stagingfrom
ravi/intorg-670
May 21, 2026
Merged

feat(home): wire Real-world progress stats section with synced counters [INTORG-670]#301
Infi-Knight merged 8 commits into
stagingfrom
ravi/intorg-670

Conversation

@Infi-Knight
Copy link
Copy Markdown
Contributor

@Infi-Knight Infi-Knight commented May 19, 2026

Summary

Adds the fifth home-page section: Real-world progress — four stats (21M+ grants, 300+ projects, 130+ countries, 1000+ students skilled) on a blush-100 background, with counters that animate from 0 to their target the first time the section enters the viewport.

Layout (per Figma desktop 466:58427, tablet 475:60244, mobile 372:49380):

  • Mobile: single column stack, natural height
  • Tablet: 2×2 grid, min-h-screen vertical center
  • Desktop: text + 2×2 data split, min-h-screen vertical center

Counter (AnimatedNumber.astro) — vanilla Astro + JS, no React/framer dependency:

  • The final value is baked into the static HTML at build time (output: 'static'), so no-JS users, screen readers, and search bots see the real number with no JS required; the client script then swaps it to 0 and animates up
  • 1200 ms linear default, fixed across all instances
  • Group trigger via [data-animated-number-trigger] ancestor — counters in the same group all fire together when any one of them crosses the viewport threshold, so the four stacked mobile cards animate in lockstep instead of one-at-a-time (matches counters-mobile.mov)
  • tabular-nums locks digit column width so the row doesn't jitter horizontally as digits change
  • Skip-no-op DOM writes: textContent only updated when the displayed integer actually changes
  • a11y: animating digit <span> is aria-hidden so screen readers don't get spammed every frame; parent <li> carries the human-readable aria-label (e.g. "21 million dollars in grants")
  • Replaces Radu's prototype Framer component — see commit body for the issue-by-issue diff

Related Issue

This PR closes all three of INTORG-670's sub-issues, INTORG-671, INTORG-672, INTORG-673

Manual Test

  1. Open the home page on desktop. Scroll until Real-world progress enters view. All four counters should ramp from 0 to their targets simultaneously, finishing in ~1.2 s.
  2. Resize to tablet (≥810px): section should be min-h-screen with a 2×2 stat grid.
  3. Resize to mobile (≤809px): cards stack into a single column. Scroll the section into view — all four counters should still finish in sync, even the ones still below the fold.
  4. macOS: System Settings → Accessibility → Display → enable Reduce motion. Reload. The final values should appear instantly with no animation.
  5. With VoiceOver/NVDA on, tab/arrow to the stats: each card should be announced once (e.g. "21 million dollars in grants"), not as a stream of intermediate values.
  6. Disable JS and reload — final values are still rendered (they're in the built HTML, no client JS required).

Checks

  • pnpm run format
  • pnpm run lint

PR Checklist

  • PR title follows Conventional Commits
  • Linked issue included
  • Scope is focused (5 files)
  • Screenshots for UI changes (Figma frames linked above; video evidence in docs/plans/counters.mov + counters-mobile.mov)

…rs [INTORG-670]

Add the fifth home-page section rendering the four stats from Figma
(21M+ in grants, 300+ projects, 130+ countries, 1000+ students). Mobile-
first stacked layout, 2x2 grid from tablet up, vertically centered at
min-h-screen on tablet & desktop per Radu's layout rule.

AnimatedNumber:
- SSR renders the final value so no-JS users, screen readers, and search
  bots see the real number; JS swaps to 0 and animates only when motion
  is allowed
- 1200ms linear, fixed default duration across all instances — matches
  the Framer prototype (verified by frame-stepping counters.mov at 60fps:
  the "300" counter advances ~25/100ms = 250/sec, extrapolating to 1.2s
  total; all four counters share the same progress curve)
- tabular-nums kills width jitter as digits change
- Group-trigger: counters that share a [data-animated-number-trigger]
  ancestor all fire together when any one enters the viewport. Lets the
  stacked mobile cards animate in lockstep instead of one-card-at-a-time,
  matching the prototype's "wave" feel (verified in counters-mobile.mov)
- a11y: animating digits are aria-hidden so SR doesn't announce every
  frame; the parent <li> carries an aria-label with the human-readable
  full value (e.g. "21 million dollars in grants")

StatsSection:
- Heading and body as slots (mirroring TextMediaSection); stats as a
  typed StatItem[] prop
- bg-blush-100 background, Poppins typography pinned to design tokens
  (text-h1/h2/h3/h5 + their -md/-lg responsive variants)
@netlify
Copy link
Copy Markdown

netlify Bot commented May 19, 2026

Deploy Preview for interledger-org-v5 ready!

Name Link
🔨 Latest commit f5a2207
🔍 Latest deploy log https://app.netlify.com/projects/interledger-org-v5/deploys/6a0ef8d2d006a50008a65801
😎 Deploy Preview https://deploy-preview-301--interledger-org-v5.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

…ing [INTORG-672]

INTORG-672 ("[Astro] Create individual stat component") asks for a single-
stat component as its own file, separate from the section container. Move
the per-card markup, the StatItem type, and the AnimatedNumber wiring into
StatCard.astro and have StatsSection map over `stats` rendering
`<StatCard {...stat} />`. StatsSection re-exports StatItem so consumers
(HomePage) keep their existing import path unchanged.

Also drop `whitespace-nowrap` from the description <p> and let it wrap
inside the card's fixed width — required by INTORG-671's i18n note that
the component must "render correctly when locale strings are significantly
longer or shorter than the default". The English labels fit on one line
at 227/251/290px, so the visual matches Figma; longer translations now
wrap to multiple lines centered (mobile/tablet) or left-aligned (desktop)
instead of overflowing. Suffix keeps nowrap since "M+" / "+" must never
break.
@Infi-Knight Infi-Knight self-assigned this May 19, 2026
The project is `output: 'static'` (no SSR runtime), so the comments
describing how the final value gets into the markup were inaccurate. The
behavior is unchanged — Astro renders the frontmatter at build time and
the final value is baked into the .html file shipped to the CDN — but
calling that "SSR" is misleading. Switch to "build time" / "static HTML"
wording so a future reader doesn't conclude the page needs a server.
@Infi-Knight Infi-Knight marked this pull request as draft May 19, 2026 09:30
…-670]

The <ul> had tablet:w-full max-w-[600px] from the tablet 2x2 layout but
no desktop reset. On desktop the inner is a flex row, so the ul's
inherited w-full forced it to claim the entire row, squeezing the text
column down to its min-content (~252px). With desktop:flex-1 still on
the text column, the heading "Real-world progress" wrapped to three
lines and the 120px gap between text and stats read as enormous next to
the collapsed text — Sarah's "spacing is not right between the text and
stats" comment on PR #301.

Add desktop:w-auto so the ul shrinks to the grid's intrinsic 2×290 +
12px gap = 592px on desktop. Text's flex-1 then fills the remaining
1280 − 120 − 592 = 568px at a 1440 viewport, where the heading fits on
one line at 56/64 SemiBold and the body wraps to two lines, matching
Figma node 466:58427.
# Conflicts:
#	src/components/pages/HomePage.astro
#	src/data/ui.ts
…s on desktop

Both home-page TextMediaSection CTAs shipped with
`w-full tablet:w-auto tablet:mx-auto`. Tailwind's mobile-first cascade
means `tablet:mx-auto` keeps applying at the desktop breakpoint, so the
auto margins center the button on desktop too — but the reviewer on
PR #290 asked for it to be centered on tablet only and left-aligned on
desktop. Same regression on Independent by Design from the PR #296
merge.

Add `desktop:mx-0` to both buttons. Final cascade:

- mobile: `w-full` → full-width button
- tablet: `w-auto` + `mx-auto` → auto-width, horizontally centered
- desktop: `w-auto` + `mx-0` → auto-width, left-aligned (picked up by
  TextMediaSection's text column `desktop:items-start`)
@Infi-Knight Infi-Knight marked this pull request as ready for review May 20, 2026 09:48
@Infi-Knight Infi-Knight requested review from Anca2022 and JonathanMatthey and removed request for Anca2022 May 20, 2026 09:48
@JonathanMatthey
Copy link
Copy Markdown
Collaborator

figma doesnt have a comma in 1000, not 1,000

image image

Copy link
Copy Markdown
Collaborator

@JonathanMatthey JonathanMatthey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM -

couple of feedback notes

Comment thread src/data/ui.ts Outdated
Co-authored-by: Jonathan Matthey <mattheyj@gmail.com>
Figma shows the stat numbers without a comma (1000, not 1,000).
AnimatedNumber was using `toLocaleString('en-US')` at build time and
`Intl.NumberFormat('en-US')` during the tick animation, both of which
insert a comma at the thousands break. Switched both to `String(...)`.
@Infi-Knight Infi-Knight merged commit c808b08 into staging May 21, 2026
6 of 7 checks passed
@Infi-Knight Infi-Knight deleted the ravi/intorg-670 branch May 21, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants