SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%

Web polish: line-chart y-domain rule, currency prefixes, collapsible subtopics, home filter notes, headline debt indicator

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 days ago (Sep 11, 2026) parent 0ff8724

588 changed files +1,371 −99

modified apps/web/README.md +15 −4
@@ -33,6 +33,8 @@ node qa/screens.mjs [BASE_URL] # screenshots 320/360/375/390/414/430/1280/1440
33 33 # overflow (scrollWidth>clientWidth), tap targets <44px, tab-bar/footer overlap, CLS, console errors
34 34 node qa/segments.mjs [BASE_URL] # viewport-sized segments (top + scroll offsets) → qa/screens/seg/ for visual review
35 35 node qa/favicons.mjs # re-rasterise src/app/icon.svg → icon.png (512) + apple-icon.png (180)
36 +node qa/polish-sweep.mjs [BASE] [--quick] # 25 routes × 320…1440 × light/dark → qa/screens/polish/ (+ report.json):
37 + # overflow, tap targets, console errors, CLS after settle, clipped text, "undefined/NaN/null", footer credits
36 38 ```
37 39
38 40 Playwright comes from `~/Desktop/uqo-eval/node_modules` (symlinked into `node_modules/playwright[-core]`; ESM ignores
@@ -109,9 +111,14 @@ components/
109 111 ```
110 112 `isNotBuilt` covers 503 ("Data not built yet") **and** an unreachable API — the site renders a calm state, never crashes.
111 113 * **Strings:** everything visible goes through `t('key')` in `src/i18n/en.ts`. Dynamic keys → `tOpt`.
112 −* **Numbers/dates:** `lib/format.ts` only, fixed `en-US` locale (Intl hydration bug prevention). Prefer the API's
113 − `formatted` string when present (`m.formatted ?? formatValue(m.value, m)`) so server and client agree.
114 − `tnum` utility on columns of numbers, `pnum` on hero figures.
114 +* **Numbers/dates:** `lib/format.ts` only, fixed `en-US` locale (Intl hydration bug prevention). Display an API
115 + value with `displayValue(m.value, m, m.formatted)`: currencies ALWAYS go through the client formatter (unit
116 + prefix guaranteed — "US$55.7k", "intl $66.7k"), other formats prefer the API's `formatted` (it carries the
117 + indicator precision that `MetricValue` lacks). Never render `m.formatted` alone. `tnum` utility on columns of
118 + numbers, `pnum` on hero figures.
119 +* **Freshness honesty:** a value whose year is ≥ 2 years older than the reference year of its list (ranking rows,
120 + home lists, member rankings, top-3 previews, compact subtopic tables) shows its year next to the value —
121 + `staleYear(year, reference)` in `charts/ranked-bars.tsx`; `Metric`/`IndicatorRow`/snapshot cells always show it.
115 122 * **No cards.** `Section` (rule above, editorial heading, subtitle, right-side actions) + `divide-y divide-rule` lists.
116 123 Grid children that contain long text need `min-w-0` (Section already has it).
117 124 * **Never invent numbers.** Missing → `EmptyState` / "No data" (`t('common.noData')`).
@@ -160,7 +167,7 @@ hairline solid grid. Points type: `SeriesPoint {period, year, value, is_forecast
160 167
161 168 | Component | Kind | Notes |
162 169 |---|---|---|
163 −| `LineChart` / `AreaChart` / `StackedArea` | client | multi-series, dashed `is_forecast`, `log`, crosshair + one tooltip for all series, end labels ≤ 4 series, auto left margin |
170 +| `LineChart` / `AreaChart` / `StackedArea` | client | multi-series, dashed `is_forecast`, `log`, crosshair + one tooltip for all series, end labels ≤ 4 series, auto left margin. **Y domain** (`scales.ts::lineDomain`): line/area use the padded (≈ 6 %) nice extent of the data; zero is included only if min ≤ 0, min < 0.3·max, or a percent/index share spanning > ½ of [0, max]. Stacked keeps the zero baseline. |
164 171 | `Sparkline` | server | tiny, no axes, end dot coloured by `direction` |
165 172 | `RankedBars` (+`rankedRowFromCountry`) | server, HTML | horizontal bars with flags, `highlightId`, values at the tip |
166 173 | `Scatter` | client | bubble `size`, labels for highlighted + extremes only, 24 px hit radius |
@@ -180,6 +187,10 @@ hairline solid grid. Points type: `SeriesPoint {period, year, value, is_forecast
180 187 * `BottomSheet` (native `<dialog>`): `side="drawer"` (right drawer on md+), `"center"`, `"full"`; drag-down to close.
181 188 * `DataTable<T>` → definition list under `sm` (no horizontal overflow). `TopicNav` sticky under the header (52/56 px).
182 189 * `ChangeList` (feed with kind icon + severity chip) and `Timeline` (events by year) both take `ChangeItem[]`.
190 +* `CollapsibleGroup` (+ `SubtopicJumpNav`, `lib/anchors.ts::subtopicAnchor`) — subtopic block for long chart pages:
191 + collapsed → `summary` (compact latest-values table: `compare/SnapshotCompact`, `country/TopicCompact`), expanded →
192 + charts (still lazy on scroll); a `#anchor` hash opens it. Used by compare topic tabs (> 8 charts, first block open)
193 + and `/countries/[slug]/[topic]` (> 12 indicators, first two open).
183 194 * `SiteFooter` renders `<FooterCredits/>` ("Made by Simon-Pierre Boucher · contact@spboucher.ai · Hosted on MacLustr")
184 195 — reuse `FooterCredits` on `/sources` and `/api`.
185 196
modified apps/web/qa/mock_api.py +1 −1
@@ -69,7 +69,7 @@ def series(c: dict, ind: dict) -> list[dict]:
69 69 v = v * (1 + trend + noise) if fmt not in ("percent", "years", "celsius") else v + trend + noise
70 70 v = max(0.001, v) if fmt != "percent" or "balance" not in ind["slug"] else v
71 71 out.append({"period": f"{y}-01-01", "year": y, "frequency": "A", "value": round(v, 3), "is_forecast": False, "is_estimate": y >= 2023, "status": "verified", "source_id": "worldbank", "provenance": prov(ind, c)})
72 − if ind["slug"] in ("gdp", "gdp-per-capita", "gdp-growth", "inflation", "unemployment-rate", "government-debt-pct-gdp"):
72 + if ind["slug"] in ("gdp", "gdp-per-capita", "gdp-growth", "inflation", "unemployment-rate", "general-government-gross-debt-pct-gdp"):
73 73 for y in range(end + 1, end + 4):
74 74 v = v * (1 + trend) if fmt not in ("percent",) else v + trend
75 75 out.append({"period": f"{y}-01-01", "year": y, "frequency": "A", "value": round(v, 3), "is_forecast": True, "is_estimate": False, "status": "imported", "source_id": "imf", "provenance": prov(ind, c, "imf")})
added apps/web/qa/polish-segments.mjs +66 −0
@@ -0,0 +1,66 @@
1 +/**
2 + * Viewport-sized segments for visual review (the full-page PNGs of polish-sweep are too tall to read):
3 + * top + evenly spaced offsets per route × width × theme → qa/screens/polish/seg/<route>-<width>-<theme>-<i>.png
4 + * node qa/polish-segments.mjs [BASE_URL] [routes…] (defaults: key routes, widths 320 + 390 + 1440, light + dark at 390)
5 + */
6 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
7 +import { mkdirSync } from 'node:fs';
8 +
9 +const args = process.argv.slice(2);
10 +const BASE = args.find((a) => a.startsWith('http')) ?? 'http://localhost:8290';
11 +const custom = args.filter((a) => a.startsWith('/'));
12 +const OUT = new URL('./screens/polish/seg/', import.meta.url).pathname;
13 +mkdirSync(OUT, { recursive: true });
14 +const ROUTES = custom.length ? custom : ['/', '/countries', '/countries/canada', '/countries/canada/economy', '/compare/canada/united-states/france', '/compare/canada/united-states/france?tab=economy', '/rankings', '/rankings/gdp-per-capita', '/indicators/life-expectancy', '/regions/oecd', '/explore', '/changes', '/sources', '/methodology', '/api', '/data', '/indicators', '/regions', '/compare'];
15 +const ALL_COMBOS = [
16 + [320, 'light'],
17 + [390, 'light'],
18 + [390, 'dark'],
19 + [1440, 'light'],
20 + [1440, 'dark'],
21 +];
22 +// --desktop → 1440 only; --phone → 320/390 only
23 +const COMBOS = args.includes('--desktop') ? ALL_COMBOS.filter(([w]) => w >= 1024) : args.includes('--phone') ? ALL_COMBOS.filter(([w]) => w < 1024) : ALL_COMBOS;
24 +const slug = (p) => (p === '/' ? 'home' : p.slice(1).replace(/[/?=&]+/g, '_'));
25 +const browser = await chromium.launch();
26 +for (const [width, theme] of COMBOS) {
27 + const mobile = width < 768;
28 + const height = mobile ? 844 : 900;
29 + const ctx = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme });
30 + const page = await ctx.newPage();
31 + for (const path of ROUTES) {
32 + let docH = 0;
33 + for (let attempt = 0; attempt < 3; attempt++) {
34 + try {
35 + await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 60_000 });
36 + await page.evaluate(() => document.fonts.ready);
37 + docH = await page.evaluate(async () => {
38 + const h = document.documentElement.scrollHeight;
39 + for (let y = 0; y < h; y += 700) {
40 + window.scrollTo(0, y);
41 + await new Promise((r) => setTimeout(r, 50));
42 + }
43 + window.scrollTo(0, 0);
44 + return document.documentElement.scrollHeight;
45 + });
46 + break;
47 + } catch (e) {
48 + if (attempt === 2) console.log(`FAILED ${path}: ${String(e).slice(0, 120)}`);
49 + await page.waitForTimeout(1500);
50 + }
51 + }
52 + if (!docH) continue;
53 + await page.waitForLoadState('networkidle').catch(() => {});
54 + await page.waitForTimeout(400);
55 + const n = Math.min(mobile ? 6 : 4, Math.max(1, Math.ceil(docH / height)));
56 + for (let i = 0; i < n; i++) {
57 + const y = n === 1 ? 0 : Math.round((i * (docH - height)) / (n - 1));
58 + await page.evaluate((yy) => window.scrollTo(0, yy), y);
59 + await page.waitForTimeout(250);
60 + await page.screenshot({ path: `${OUT}${slug(path)}-${width}-${theme}-${i}.png`, fullPage: false });
61 + }
62 + console.log(`${width} ${theme} ${path} h=${docH} segments=${n}`);
63 + }
64 + await ctx.close();
65 +}
66 +await browser.close();
added apps/web/qa/polish-sweep.mjs +211 −0
@@ -0,0 +1,211 @@
1 +/**
2 + * Polish sweep (real API): every public route × 320/375/390/430/1280/1440 × light/dark.
3 + * Asserts: no horizontal overflow, tap targets ≥ 44 px on phones, no console errors, no layout shift after
4 + * fonts/charts settle (PerformanceObserver layout-shift, CLS > 0.02 flagged), no mid-word truncation at 320
5 + * (`.truncate` elements that actually clip), no "undefined"/"NaN"/"null" in the visible text, footer credits
6 + * present. Screenshots → qa/screens/polish/<route>-<width>-<theme>.png, report → qa/screens/polish/report.json.
7 + *
8 + * node qa/polish-sweep.mjs [BASE_URL] [--quick] (--quick: 390 + 1440 light only, 10 routes)
9 + */
10 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
11 +import { mkdirSync, writeFileSync } from 'node:fs';
12 +
13 +const args = process.argv.slice(2);
14 +const quick = args.includes('--quick');
15 +const BASE = args.find((a) => a.startsWith('http')) ?? process.env.BASE_URL ?? 'http://localhost:8290';
16 +const OUT = new URL('./screens/polish/', import.meta.url).pathname;
17 +mkdirSync(OUT, { recursive: true });
18 +
19 +const ROUTES_FULL = [
20 + '/', '/countries', '/countries/canada', '/countries/canada/economy', '/countries/nigeria', '/countries/japan/energy',
21 + '/compare', '/compare/canada/united-states/france', '/compare/canada/united-states/france?tab=economy',
22 + '/rankings', '/rankings/gdp-per-capita', '/rankings/life-expectancy?group=oecd',
23 + '/indicators', '/indicators/life-expectancy', '/indicators/inflation',
24 + '/regions', '/regions/oecd', '/explore', '/changes', '/data', '/sources', '/sources/worldbank', '/methodology', '/api', '/?search=can',
25 +];
26 +const ROUTES_QUICK = ['/', '/countries', '/countries/canada', '/countries/canada/economy', '/compare/canada/united-states/france?tab=economy', '/rankings/gdp-per-capita', '/indicators/life-expectancy', '/regions/oecd', '/changes', '/api'];
27 +const ROUTES = quick ? ROUTES_QUICK : ROUTES_FULL;
28 +const WIDTHS = quick ? [390, 1440] : [320, 375, 390, 430, 1280, 1440];
29 +const THEMES = quick ? ['light'] : ['light', 'dark'];
30 +
31 +const slug = (p) => (p === '/' ? 'home' : p.slice(1).replace(/[/?=&]+/g, '_'));
32 +const report = [];
33 +const browser = await chromium.launch();
34 +
35 +for (const theme of THEMES) {
36 + for (const width of WIDTHS) {
37 + const mobile = width < 768;
38 + const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme, reducedMotion: 'no-preference' });
39 + await ctx.addInitScript(() => {
40 + window.__cls = 0;
41 + window.__clsAfterSettle = 0;
42 + window.__settled = false;
43 + try {
44 + new PerformanceObserver((l) => {
45 + for (const e of l.getEntries()) {
46 + if (e.hadRecentInput) continue;
47 + window.__cls += e.value;
48 + if (window.__settled) window.__clsAfterSettle += e.value;
49 + }
50 + }).observe({ type: 'layout-shift', buffered: true });
51 + } catch {}
52 + });
53 + const page = await ctx.newPage();
54 + const errors = [];
55 + page.on('pageerror', (e) => errors.push(`pageerror: ${String(e).slice(0, 200)}`));
56 + page.on('console', (m) => {
57 + if (m.type() === 'error') errors.push(m.text().slice(0, 200));
58 + });
59 + for (const path of ROUTES) {
60 + let status = 0;
61 + try {
62 + const resp = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 60_000 });
63 + status = resp?.status() ?? 0;
64 + } catch (e) {
65 + report.push({ path, width, theme, status: 'ERR', error: String(e).slice(0, 200) });
66 + continue;
67 + }
68 + await page.evaluate(() => document.fonts.ready);
69 + await page.waitForTimeout(700);
70 + // "/?search=<q>" is not a route: open the search dialog from the header and type the query.
71 + const searchQ = /[?&]search=([^&]+)/.exec(path)?.[1];
72 + if (searchQ) {
73 + try {
74 + // The header renders two triggers (icon on phones, field on desktop): click the visible one.
75 + const trigger = page.locator('header button[aria-label="Open search"]').locator('visible=true').first();
76 + await trigger.click({ timeout: 10_000 });
77 + const input = page.locator('dialog input').first();
78 + await input.waitFor({ timeout: 10_000 });
79 + await input.fill(decodeURIComponent(searchQ));
80 + await page.waitForLoadState('networkidle').catch(() => {});
81 + await page.waitForTimeout(800);
82 + } catch (e) {
83 + errors.push(`search dialog: ${String(e).slice(0, 120)}`);
84 + }
85 + }
86 + // Scroll through the page so lazy charts mount, then back to top; measure shift after settle.
87 + if (!searchQ)
88 + await page.evaluate(async () => {
89 + const h = document.documentElement.scrollHeight;
90 + for (let y = 0; y < h; y += 700) {
91 + window.scrollTo(0, y);
92 + await new Promise((r) => setTimeout(r, 60));
93 + }
94 + window.scrollTo(0, 0);
95 + });
96 + await page.waitForLoadState('networkidle').catch(() => {});
97 + await page.waitForTimeout(500);
98 + await page.evaluate(() => {
99 + window.__settled = true;
100 + });
101 + await page.waitForTimeout(600);
102 + let m;
103 + try {
104 + m = await page.evaluate(
105 + ({ mobile, width }) => {
106 + const de = document.documentElement;
107 + const overflow = de.scrollWidth - de.clientWidth;
108 + const vis = (el) => {
109 + const r = el.getBoundingClientRect();
110 + if (r.width === 0 || r.height === 0) return false;
111 + const cs = getComputedStyle(el);
112 + return cs.visibility !== 'hidden' && cs.display !== 'none';
113 + };
114 + const wide = [...document.querySelectorAll('body *')]
115 + .filter((el) => {
116 + const r = el.getBoundingClientRect();
117 + return r.right > de.clientWidth + 1 && r.width > 0;
118 + })
119 + .slice(0, 6)
120 + .map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);
121 + const targets = [...document.querySelectorAll('a,button,[role=button],input,select,summary,[role=radio],[role=tab]')].filter(vis);
122 + const small = mobile
123 + ? targets
124 + .filter((el) => {
125 + const r = el.getBoundingClientRect();
126 + return r.height < 44 && r.width < 44;
127 + })
128 + .map((el) => `${el.tagName.toLowerCase()} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 30)}" ${Math.round(el.getBoundingClientRect().width)}x${Math.round(el.getBoundingClientRect().height)}`)
129 + : [];
130 + const shortH = mobile
131 + ? targets
132 + .filter((el) => el.getBoundingClientRect().height < 44 && !el.closest('svg'))
133 + .map((el) => `${el.tagName.toLowerCase()} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 28)}" h=${Math.round(el.getBoundingClientRect().height)}`)
134 + : [];
135 + // Mid-word truncation: elements with text-overflow: ellipsis whose content is clipped.
136 + const clipped = [...document.querySelectorAll('body *')]
137 + .filter((el) => {
138 + const cs = getComputedStyle(el);
139 + if (cs.textOverflow !== 'ellipsis' || cs.overflow === 'visible') return false;
140 + return el.scrollWidth > el.clientWidth + 1 && el.textContent && el.textContent.trim().length > 0;
141 + })
142 + .map((el) => `${el.tagName.toLowerCase()}: "${el.textContent.trim().slice(0, 40)}" ${el.clientWidth}/${el.scrollWidth}`);
143 + const text = document.body.innerText || '';
144 + const bad = [];
145 + for (const re of [/\bundefined\b/g, /\bNaN\b/g, /(?<![\w"':])null(?![\w"':])/g]) {
146 + let mm;
147 + let n = 0;
148 + while ((mm = re.exec(text)) && n < 3) {
149 + bad.push(`${mm[0]} @ "${text.slice(Math.max(0, mm.index - 40), mm.index + 30).replace(/\s+/g, ' ')}"`);
150 + n++;
151 + }
152 + }
153 + const footer = document.querySelector('footer');
154 + const ft = footer ? footer.innerText : '';
155 + const credits = /Made by\s+Simon-Pierre Boucher/.test(ft) && /contact@spboucher\.ai/.test(ft) && /Hosted on\s+MacLustr/.test(ft);
156 + const mail = !!footer?.querySelector('a[href="mailto:contact@spboucher.ai"]');
157 + const mac = !!footer?.querySelector('a[href="https://www.maclustr.io"]');
158 + const overlapping = [];
159 + // Chart tick labels overlapping each other (same axis group)
160 + for (const g of document.querySelectorAll('svg .axis')) {
161 + const ts = [...g.querySelectorAll('text')].map((tx) => tx.getBoundingClientRect());
162 + for (let i = 0; i < ts.length; i++)
163 + for (let j = i + 1; j < ts.length; j++) {
164 + const a = ts[i];
165 + const b = ts[j];
166 + if (a.width && b.width && a.left < b.right - 1 && b.left < a.right - 1 && a.top < b.bottom - 1 && b.top < a.bottom - 1) overlapping.push(`${Math.round(a.left)},${Math.round(a.top)}`);
167 + }
168 + }
169 + return { overflow, wide, small: small.slice(0, 10), nSmall: small.length, shortH: shortH.slice(0, 8), nShortH: shortH.length, clipped: clipped.slice(0, 10), nClipped: clipped.length, bad, credits, mail, mac, cls: window.__cls, clsAfter: window.__clsAfterSettle, docH: de.scrollHeight, title: document.title, overlapping: overlapping.slice(0, 5), width };
170 + },
171 + { mobile, width },
172 + );
173 + } catch (e) {
174 + report.push({ path, width, theme, status: 'ERR', error: String(e).slice(0, 200), errors: errors.splice(0) });
175 + continue;
176 + }
177 + const file = `${slug(path)}-${width}-${theme}.png`;
178 + await page.screenshot({ path: OUT + file, fullPage: !searchQ }).catch(() => {});
179 + report.push({ path, width, theme, status, ...m, errors: errors.splice(0), file });
180 + writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));
181 + }
182 + await ctx.close();
183 + }
184 +}
185 +await browser.close();
186 +writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));
187 +
188 +let fails = 0;
189 +for (const r of report) {
190 + const flags = [];
191 + if (r.status !== 200) flags.push(`HTTP ${r.status}`);
192 + if (r.overflow > 0) flags.push(`OVERFLOW +${r.overflow}px`);
193 + if (r.nSmall) flags.push(`${r.nSmall} small targets`);
194 + if (r.nShortH) flags.push(`${r.nShortH} targets <44h`);
195 + if (r.nClipped) flags.push(`${r.nClipped} clipped`);
196 + if (r.bad?.length) flags.push(`BAD TEXT ${r.bad.length}`);
197 + if (r.errors?.length) flags.push(`${r.errors.length} console errors`);
198 + if (r.clsAfter > 0.02) flags.push(`CLS-after ${r.clsAfter.toFixed(3)}`);
199 + if (r.cls > 0.1) flags.push(`CLS ${r.cls.toFixed(3)}`);
200 + if (r.credits === false || r.mail === false || r.mac === false) flags.push('NO CREDITS');
201 + if (r.overlapping?.length) flags.push(`${r.overlapping.length} tick overlaps`);
202 + if (flags.length) fails++;
203 + console.log(`${String(r.width).padStart(4)} ${r.theme.padEnd(5)} ${r.path.padEnd(52)} ${flags.join(' · ') || 'ok'} (h=${r.docH})`);
204 + if (r.wide?.length) console.log(' wide:', r.wide.join(' | '));
205 + if (r.small?.length) console.log(' small:', r.small.slice(0, 5).join(' | '));
206 + if (r.shortH?.length) console.log(' <44h:', r.shortH.slice(0, 5).join(' | '));
207 + if (r.clipped?.length) console.log(' clipped:', r.clipped.slice(0, 5).join(' | '));
208 + if (r.bad?.length) console.log(' bad:', r.bad.join(' | '));
209 + if (r.errors?.length) console.log(' errors:', r.errors.slice(0, 3).join(' | '));
210 +}
211 +console.log(`\n${report.length} renders, ${fails} with flags`);
added apps/web/qa/screens/polish/api-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/api-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_apple_icon_png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_countries_canada_opengraph_image +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_icon_png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_icon_svg +1 −0
@@ -0,0 +1 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 32 32" fill="none" stroke="#1c5cab" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect width="32" height="32" rx="7" fill="#fbfaf7" stroke="none"/><circle cx="16" cy="16" r="12"/><path d="M5 19.3h22"/><path d="M10.1 26.6 16 7.6l5.9 19"/><path d="M16 7.6c-3 2.9-4.3 7.1-4.3 11.7" opacity="0.55"/><path d="M16 7.6c3 2.9 4.3 7.1 4.3 11.7" opacity="0.55"/></svg>
added apps/web/qa/screens/polish/brand/_indicators_life_expectancy_opengraph_image +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_manifest_webmanifest +1 −0
@@ -0,0 +1 @@
1 +{"name":"CountryAtlas","short_name":"CountryAtlas","description":"Understand the world, one country at a time.","start_url":"/","display":"standalone","background_color":"#fbfaf7","theme_color":"#1c5cab","icons":[{"src":"/icon.svg","type":"image/svg+xml","sizes":"any"},{"src":"/icon.png","type":"image/png","sizes":"512x512"},{"src":"/apple-icon.png","type":"image/png","sizes":"180x180"}]}
\ No newline at end of file
added apps/web/qa/screens/polish/brand/_opengraph_image +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_rankings_gdp_per_capita_opengraph_image +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/_twitter_image +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/brand/compare_og.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/changes-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/changes-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/changes-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france_tab_economy-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france_tab_economy-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/compare_canada_united-states_france_tab_economy-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada_economy-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada_economy-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_canada_economy-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_japan_energy-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_japan_energy-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_japan_energy-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_nigeria-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_nigeria-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/countries_nigeria-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/data-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/data-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/data-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/explore-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/explore-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/explore-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/home-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/home-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/home-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_inflation-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_inflation-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_inflation-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_life-expectancy-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_life-expectancy-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/indicators_life-expectancy-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/methodology-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/methodology-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_gdp-per-capita-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_gdp-per-capita-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_gdp-per-capita-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_life-expectancy_group_oecd-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_life-expectancy_group_oecd-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/rankings_life-expectancy_group_oecd-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions_oecd-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions_oecd-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/regions_oecd-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/report.json +510 −0
@@ -0,0 +1,510 @@
1 +[
2 + {
3 + "path": "/",
4 + "width": 320,
5 + "theme": "light",
6 + "status": 200,
7 + "overflow": 0,
8 + "wide": [
9 + "li. right=357",
10 + "a.inline-flex.h-11.items-center right=357",
11 + "li. right=501",
12 + "a.inline-flex.h-11.items-center right=501",
13 + "li. right=585",
14 + "a.inline-flex.h-11.items-center right=585"
15 + ],
16 + "small": [
17 + "a \"Skip to content\" 1x1"
18 + ],
19 + "nSmall": 1,
20 + "shortH": [
21 + "a \"Skip to content\" h=1",
22 + "a \"Build your own comparison →\" h=40",
23 + "a \"See all →\" h=21",
24 + "a \"🇺🇸United States\" h=19",
25 + "a \"🇨🇳China\" h=19",
26 + "a \"🇩🇪Germany\" h=19",
27 + "a \"🇯🇵Japan\" h=19",
28 + "a \"🇬🇧United Kingdom\" h=19"
29 + ],
30 + "nShortH": 60,
31 + "clipped": [
32 + "span: \"Search countries, indicators, topics…\" 224/281",
33 + "span: \"CO₂ emissions (fossil fuels and industry\" 192/266",
34 + "span: \"Individuals using the Internet (% of pop\" 192/310",
35 + "span: \"GDP per capita (current US$)\" 192/194",
36 + "span: \"GDP per capita, PPP (current internation\" 192/295",
37 + "span: \"At least upper secondary attainment (age\" 192/332",
38 + "span: \"Education · % of population ages 25–64\" 192/226",
39 + "span: \"Employment rate (ages 15–64)\" 197/200",
40 + "span: \"Labor · % of population ages 15–64\" 197/200",
41 + "span: \"Long-term unemployment (% of unemployed)\" 197/303"
42 + ],
43 + "nClipped": 16,
44 + "bad": [],
45 + "credits": true,
46 + "mail": true,
47 + "mac": true,
48 + "cls": 0,
49 + "clsAfter": 0,
50 + "docH": 10409,
51 + "title": "CountryAtlas — Understand the world, one country at a time.",
52 + "overlapping": [],
53 + "errors": [],
54 + "file": "home-320-light.png"
55 + },
56 + {
57 + "path": "/countries",
58 + "width": 320,
59 + "theme": "light",
60 + "status": 200,
61 + "overflow": 0,
62 + "wide": [],
63 + "small": [
64 + "a \"Skip to content\" 1x1"
65 + ],
66 + "nSmall": 1,
67 + "shortH": [
68 + "a \"Skip to content\" h=1",
69 + "input \"Filter by name…\" h=40",
70 + "select \"NamePopulationGDP per capita\" h=19",
71 + "button \"Filters\" h=40",
72 + "a \"contact@spboucher.ai\" h=15",
73 + "a \"MacLustr\" h=15"
74 + ],
75 + "nShortH": 6,
76 + "clipped": [
77 + "span: \"Northern Mariana Islands\" 164/169",
78 + "span: \"Saint Vincent and the Grenadines\" 164/223",
79 + "span: \"Turks and Caicos Islands\" 164/167"
80 + ],
81 + "nClipped": 3,
82 + "bad": [],
83 + "credits": true,
84 + "mail": true,
85 + "mac": true,
86 + "cls": 0,
87 + "clsAfter": 0,
88 + "docH": 14283,
89 + "title": "Countries — CountryAtlas",
90 + "overlapping": [],
91 + "errors": [],
92 + "file": "countries-320-light.png"
93 + },
94 + {
95 + "path": "/countries/canada",
96 + "width": 320,
97 + "theme": "light",
98 + "status": 200,
99 + "overflow": 0,
100 + "wide": [
101 + "li.snap-start right=342",
102 + "a.inline-flex.h-11.items-center right=342",
103 + "span.tnum.text-2xs.text-ink-3 right=330",
104 + "li.snap-start right=460",
105 + "a.inline-flex.h-11.items-center right=460",
106 + "span.tnum.text-2xs.text-ink-3 right=448"
107 + ],
108 + "small": [
109 + "a \"Skip to content\" 1x1"
110 + ],
111 + "nSmall": 1,
112 + "shortH": [
113 + "a \"Skip to content\" h=1",
114 + "a \"Compare Canada with another \" h=40",
115 + "a \"Download\" h=40",
116 + "button \"Share\" h=40",
117 + "a \"GDP reached a record high of\" h=39",
118 + "a \"🇺🇸 United States\" h=17",
119 + "a \"Sharp dropPopulation growth \" h=36",
120 + "a \"Sharp dropTrade balance fell\" h=36"
121 + ],
122 + "nShortH": 12,
123 + "clipped": [],
124 + "nClipped": 0,
125 + "bad": [],
126 + "credits": true,
127 + "mail": true,
128 + "mac": true,
129 + "cls": 0,
130 + "clsAfter": 0,
131 + "docH": 10266,
132 + "title": "Canada Data & Statistics — CountryAtlas",
133 + "overlapping": [],
134 + "errors": [],
135 + "file": "countries_canada-320-light.png"
136 + },
137 + {
138 + "path": "/countries/canada/economy",
139 + "width": 320,
140 + "theme": "light",
141 + "status": 200,
142 + "overflow": 0,
143 + "wide": [
144 + "li.snap-start right=399",
145 + "a.inline-flex.h-11.items-center right=399",
146 + "li.snap-start right=465",
147 + "a.inline-flex.h-11.items-center right=465",
148 + "li.snap-start right=542",
149 + "a.inline-flex.h-11.items-center right=542"
150 + ],
151 + "small": [
152 + "a \"Skip to content\" 1x1"
153 + ],
154 + "nSmall": 1,
155 + "shortH": [
156 + "a \"Skip to content\" h=1",
157 + "a \"Countries\" h=15",
158 + "a \"🇨🇦 Canada\" h=15",
159 + "button \"Show source and provenance\" h=32",
160 + "button \"View data as table\" h=32",
161 + "button \"Show source and provenance\" h=32",
162 + "button \"View data as table\" h=32",
163 + "button \"Show source and provenance\" h=32"
164 + ],
165 + "nShortH": 22,
166 + "clipped": [
167 + "span: \"Source: World Bank — WDI · NY.GDP.MKTP.C\" 137/366",
168 + "span: \"Source: World Bank — WDI · NY.GDP.MKTP.P\" 137/383",
169 + "span: \"Source: World Bank — WDI · NY.GDP.PCAP.C\" 137/364",
170 + "span: \"Source: World Bank — WDI · NY.GDP.PCAP.P\" 137/381",
171 + "span: \"Source: World Bank — WDI · NY.GDP.MKTP.K\" 137/366",
172 + "span: \"Source: OECD — OECD.SDD.STES,DSD_KEI@DF_\" 137/537",
173 + "span: \"Source: World Bank — WDI · NY.GDP.MKTP.K\" 137/383",
174 + "span: \"Source: World Bank — WDI · NY.GDP.PCAP.K\" 137/381",
175 + "span: \"Household consumption (% of GDP)\" 193/240"
176 + ],
177 + "nClipped": 9,
178 + "bad": [],
179 + "credits": true,
180 + "mail": true,
181 + "mac": true,
182 + "cls": 0,
183 + "clsAfter": 0,
184 + "docH": 7564,
185 + "title": "Canada — Economy — CountryAtlas",
186 + "overlapping": [],
187 + "errors": [],
188 + "file": "countries_canada_economy-320-light.png"
189 + },
190 + {
191 + "path": "/countries/nigeria",
192 + "width": 320,
193 + "theme": "light",
194 + "status": 200,
195 + "overflow": 0,
196 + "wide": [
197 + "li.snap-start right=335",
198 + "a.inline-flex.h-11.items-center right=335",
199 + "span.tnum.text-2xs.text-ink-3 right=323",
200 + "li.snap-start right=453",
201 + "a.inline-flex.h-11.items-center right=453",
202 + "span.tnum.text-2xs.text-ink-3 right=441"
203 + ],
204 + "small": [
205 + "a \"Skip to content\" 1x1"
206 + ],
207 + "nSmall": 1,
208 + "shortH": [
209 + "a \"Skip to content\" h=1",
210 + "a \"Compare Nigeria with another\" h=40",
211 + "a \"Download\" h=40",
212 + "button \"Share\" h=40",
213 + "a \"🇧🇯 Benin\" h=17",
214 + "a \"🇨🇲 Cameroon\" h=17",
215 + "a \"🇹🇩 Chad\" h=17",
216 + "a \"🇳🇪 Niger\" h=17"
217 + ],
218 + "nShortH": 18,
219 + "clipped": [
220 + "span: \"Republic of the Congo\" 126/147"
221 + ],
222 + "nClipped": 1,
223 + "bad": [],
224 + "credits": true,
225 + "mail": true,
226 + "mac": true,
227 + "cls": 0,
228 + "clsAfter": 0,
229 + "docH": 10282,
230 + "title": "Nigeria Data & Statistics — CountryAtlas",
231 + "overlapping": [],
232 + "errors": [],
233 + "file": "countries_nigeria-320-light.png"
234 + },
235 + {
236 + "path": "/countries/japan/energy",
237 + "width": 320,
238 + "theme": "light",
239 + "status": 200,
240 + "overflow": 0,
241 + "wide": [
242 + "li.snap-start right=384",
243 + "a.inline-flex.h-11.items-center right=384",
244 + "li.snap-start right=502",
245 + "a.inline-flex.h-11.items-center right=502",
246 + "li.snap-start right=571",
247 + "a.inline-flex.h-11.items-center right=571"
248 + ],
249 + "small": [
250 + "a \"Skip to content\" 1x1"
251 + ],
252 + "nSmall": 1,
253 + "shortH": [
254 + "a \"Skip to content\" h=1",
255 + "a \"Countries\" h=15",
256 + "a \"🇯🇵 Japan\" h=15",
257 + "button \"Show source and provenance\" h=32",
258 + "button \"View data as table\" h=32",
259 + "button \"Show source and provenance\" h=32",
260 + "button \"View data as table\" h=32",
261 + "button \"Show source and provenance\" h=32"
262 + ],
263 + "nShortH": 23,
264 + "clipped": [
265 + "span: \"Source: Our World in Data — energy · ele\" 137/430",
266 + "span: \"Source: Our World in Data — energy · per\" 137/429",
267 + "span: \"Source: Our World in Data — energy · sol\" 137/401",
268 + "span: \"Source: Our World in Data — energy · win\" 137/400",
269 + "span: \"Source: Our World in Data — energy · hyd\" 137/405",
270 + "span: \"Source: Our World in Data — energy · nuc\" 137/414",
271 + "span: \"Source: Our World in Data — energy · pri\" 137/471",
272 + "span: \"Source: Our World in Data — energy · ene\" 137/414",
273 + "span: \"Source: World Bank — WDI · EG.FEC.RNEW.Z\" 137/363",
274 + "span: \"Net electricity imports (% of demand)\" 202/249"
275 + ],
276 + "nClipped": 13,
277 + "bad": [],
278 + "credits": true,
279 + "mail": true,
280 + "mac": true,
281 + "cls": 0,
282 + "clsAfter": 0,
283 + "docH": 7934,
284 + "title": "Japan — Energy — CountryAtlas",
285 + "overlapping": [],
286 + "errors": [],
287 + "file": "countries_japan_energy-320-light.png"
288 + },
289 + {
290 + "path": "/compare",
291 + "width": 320,
292 + "theme": "light",
293 + "status": 200,
294 + "overflow": 0,
295 + "wide": [],
296 + "small": [
297 + "a \"Skip to content\" 1x1"
298 + ],
299 + "nSmall": 1,
300 + "shortH": [
301 + "a \"Skip to content\" h=1",
302 + "a \"Nordics5 countries🇩🇰🇫🇮🇮\" h=43",
303 + "a \"BRICS5 countries🇧🇷🇷🇺🇮🇳\" h=43",
304 + "a \"USMCA3 countries🇨🇦🇲🇽🇺🇸\" h=43",
305 + "a \"Anglosphere5 countries🇦🇺🇨\" h=43",
306 + "a \"Asian Tigers4 countries🇭🇰\ud83c\" h=43",
307 + "a \"contact@spboucher.ai\" h=15",
308 + "a \"MacLustr\" h=15"
309 + ],
310 + "nShortH": 8,
311 + "clipped": [],
312 + "nClipped": 0,
313 + "bad": [],
314 + "credits": true,
315 + "mail": true,
316 + "mac": true,
317 + "cls": 0,
318 + "clsAfter": 0,
319 + "docH": 2569,
320 + "title": "Compare countries side by side — CountryAtlas",
321 + "overlapping": [],
322 + "errors": [],
323 + "file": "compare-320-light.png"
324 + },
325 + {
326 + "path": "/compare/canada/united-states/france",
327 + "width": 320,
328 + "theme": "light",
329 + "status": 200,
330 + "overflow": 0,
331 + "wide": [
332 + "li.group/chip.shrink-0 right=329",
333 + "span.inline-flex.h-11.items-center right=329",
334 + "button.grid.h-11.w-9 right=328",
335 + "li.group/chip.shrink-0 right=465",
336 + "span.inline-flex.h-11.items-center right=465",
337 + "span.h-2.w-2.shrink-0 right=352"
338 + ],
339 + "small": [
340 + "a \"Skip to content\" 1x1"
341 + ],
342 + "nSmall": 1,
343 + "shortH": [
344 + "a \"Skip to content\" h=1",
345 + "a \"contact@spboucher.ai\" h=15",
346 + "a \"MacLustr\" h=15"
347 + ],
348 + "nShortH": 3,
349 + "clipped": [],
350 + "nClipped": 0,
351 + "bad": [],
352 + "credits": true,
353 + "mail": true,
354 + "mac": true,
355 + "cls": 0,
356 + "clsAfter": 0,
357 + "docH": 4083,
358 + "title": "Canada vs United States vs France — Compare countries — CountryAtlas",
359 + "overlapping": [],
360 + "errors": [],
361 + "file": "compare_canada_united-states_france-320-light.png"
362 + },
363 + {
364 + "path": "/compare/canada/united-states/france?tab=economy",
365 + "width": 320,
366 + "theme": "light",
367 + "status": 200,
368 + "overflow": 0,
369 + "wide": [
370 + "li.group/chip.shrink-0 right=329",
371 + "span.inline-flex.h-11.items-center right=329",
372 + "button.grid.h-11.w-9 right=328",
373 + "li.group/chip.shrink-0 right=465",
374 + "span.inline-flex.h-11.items-center right=465",
375 + "span.h-2.w-2.shrink-0 right=352"
376 + ],
377 + "small": [
378 + "a \"Skip to content\" 1x1"
379 + ],
380 + "nSmall": 1,
381 + "shortH": [
382 + "a \"Skip to content\" h=1",
383 + "a \"GDP (current US$)\" h=17",
384 + "a \"Ranking\" h=32",
385 + "button \"View data as table\" h=32",
386 + "button \"Canada: US$2.32T. Show sourc\" h=32",
387 + "button \"United States: US$30.8T. Sho\" h=32",
388 + "button \"France: US$3.37T. Show sourc\" h=32",
389 + "button \"Source for Canada, United St\" h=28"
390 + ],
391 + "nShortH": 125,
392 + "clipped": [
393 + "span: \"Foreign direct investment, net inflows (\" 272/327",
394 + "span: \"Personal remittances received (% of GDP)\" 272/279",
395 + "span: \"Agriculture, forestry and fishing, value\" 272/378",
396 + "span: \"Industry (incl. construction), value add\" 272/350"
397 + ],
398 + "nClipped": 4,
399 + "bad": [],
400 + "credits": true,
401 + "mail": true,
402 + "mac": true,
403 + "cls": 0,
404 + "clsAfter": 0,
405 + "docH": 13228,
406 + "title": "Canada vs United States vs France — Economy — Compare countries — CountryAtlas",
407 + "overlapping": [],
408 + "errors": [],
409 + "file": "compare_canada_united-states_france_tab_economy-320-light.png"
410 + },
411 + {
412 + "path": "/rankings",
413 + "width": 320,
414 + "theme": "light",
415 + "status": 200,
416 + "overflow": 0,
417 + "wide": [],
418 + "small": [
419 + "a \"Skip to content\" 1x1"
420 + ],
421 + "nSmall": 1,
422 + "shortH": [
423 + "a \"Skip to content\" h=1",
424 + "a \"contact@spboucher.ai\" h=15",
425 + "a \"MacLustr\" h=15"
426 + ],
427 + "nShortH": 3,
428 + "clipped": [],
429 + "nClipped": 0,
430 + "bad": [],
431 + "credits": true,
432 + "mail": true,
433 + "mac": true,
434 + "cls": 0.06249728007569462,
435 + "clsAfter": 0,
436 + "docH": 26715,
437 + "title": "Country rankings — CountryAtlas",
438 + "overlapping": [],
439 + "errors": [
440 + "Failed to load resource: the server responded with a status of 429 (Too Many Requests)"
441 + ],
442 + "file": "rankings-320-light.png"
443 + },
444 + {
445 + "path": "/rankings/gdp-per-capita",
446 + "width": 320,
447 + "theme": "light",
448 + "status": 200,
449 + "overflow": 0,
450 + "wide": [],
451 + "small": [
452 + "a \"Skip to content\" 1x1"
453 + ],
454 + "nSmall": 1,
455 + "shortH": [
456 + "a \"Skip to content\" h=1",
457 + "select \"Year\" h=19",
458 + "button \"View data as table\" h=32",
459 + "a \"contact@spboucher.ai\" h=15",
460 + "a \"MacLustr\" h=15"
461 + ],
462 + "nShortH": 5,
463 + "clipped": [],
464 + "nClipped": 0,
465 + "bad": [],
466 + "credits": true,
467 + "mail": true,
468 + "mac": true,
469 + "cls": 0,
470 + "clsAfter": 0,
471 + "docH": 4739,
472 + "title": "GDP per capita by country (2025) — CountryAtlas",
473 + "overlapping": [],
474 + "errors": [],
475 + "file": "rankings_gdp-per-capita-320-light.png"
476 + },
477 + {
478 + "path": "/rankings/life-expectancy?group=oecd",
479 + "width": 320,
480 + "theme": "light",
481 + "status": 200,
482 + "overflow": 0,
483 + "wide": [],
484 + "small": [
485 + "a \"Skip to content\" 1x1"
486 + ],
487 + "nSmall": 1,
488 + "shortH": [
489 + "a \"Skip to content\" h=1",
490 + "select \"Year\" h=19",
491 + "button \"View data as table\" h=32",
492 + "a \"contact@spboucher.ai\" h=15",
493 + "a \"MacLustr\" h=15"
494 + ],
495 + "nShortH": 5,
496 + "clipped": [],
497 + "nClipped": 0,
498 + "bad": [],
499 + "credits": true,
500 + "mail": true,
501 + "mac": true,
502 + "cls": 0,
503 + "clsAfter": 0,
504 + "docH": 4689,
505 + "title": "Life expectancy in OECD (2024) — CountryAtlas",
506 + "overlapping": [],
507 + "errors": [],
508 + "file": "rankings_life-expectancy_group_oecd-320-light.png"
509 + }
510 +]
\ No newline at end of file
added apps/web/qa/screens/polish/search_q_can-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/search_q_can-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg-desktop.log +38 −0
@@ -0,0 +1,38 @@
1 +1440 light / h=5223 segments=4
2 +1440 light /countries h=13393 segments=4
3 +1440 light /countries/canada h=4473 segments=4
4 +1440 light /countries/canada/economy h=5049 segments=4
5 +1440 light /compare/canada/united-states/france h=1759 segments=2
6 +1440 light /compare/canada/united-states/france?tab=economy h=5592 segments=4
7 +1440 light /rankings h=10106 segments=4
8 +1440 light /rankings/gdp-per-capita h=2667 segments=3
9 +1440 light /indicators/life-expectancy h=4678 segments=4
10 +1440 light /regions/oecd h=4762 segments=4
11 +1440 light /explore h=5594 segments=4
12 +1440 light /changes h=11502 segments=4
13 +1440 light /sources h=2075 segments=3
14 +1440 light /methodology h=7624 segments=4
15 +1440 light /api h=6827 segments=4
16 +1440 light /data h=2789 segments=4
17 +1440 light /indicators h=22031 segments=4
18 +1440 light /regions h=2346 segments=3
19 +1440 light /compare h=1359 segments=2
20 +1440 dark / h=5223 segments=4
21 +1440 dark /countries h=13393 segments=4
22 +1440 dark /countries/canada h=4473 segments=4
23 +1440 dark /countries/canada/economy h=5049 segments=4
24 +1440 dark /compare/canada/united-states/france h=1759 segments=2
25 +1440 dark /compare/canada/united-states/france?tab=economy h=5592 segments=4
26 +1440 dark /rankings h=10106 segments=4
27 +1440 dark /rankings/gdp-per-capita h=2667 segments=3
28 +1440 dark /indicators/life-expectancy h=4678 segments=4
29 +1440 dark /regions/oecd h=4762 segments=4
30 +1440 dark /explore h=5525 segments=4
31 +1440 dark /changes h=11502 segments=4
32 +1440 dark /sources h=2075 segments=3
33 +1440 dark /methodology h=7624 segments=4
34 +1440 dark /api h=6827 segments=4
35 +1440 dark /data h=2789 segments=4
36 +1440 dark /indicators h=22031 segments=4
37 +1440 dark /regions h=2346 segments=3
38 +1440 dark /compare h=1359 segments=2
added apps/web/qa/screens/polish/seg.log +161 −0
@@ -0,0 +1,161 @@
1 +320 light / h=10409 segments=6
2 +320 light /countries h=14283 segments=6
3 +320 light /countries/canada h=10266 segments=6
4 +320 light /countries/canada/economy h=7564 segments=6
5 +320 light /compare/canada/united-states/france h=4083 segments=5
6 +320 light /compare/canada/united-states/france?tab=economy h=13228 segments=6
7 +320 light /rankings h=26767 segments=6
8 +320 light /rankings/gdp-per-capita h=4739 segments=6
9 +320 light /indicators/life-expectancy h=6868 segments=6
10 +320 light /regions/oecd h=9637 segments=6
11 +320 light /explore h=9365 segments=6
12 +320 light /changes h=15227 segments=6
13 +320 light /sources h=3846 segments=5
14 +320 light /methodology h=13226 segments=6
15 +320 light /api h=9658 segments=6
16 +320 light /data h=4366 segments=6
17 +320 light /indicators h=53477 segments=6
18 +320 light /regions h=4569 segments=6
19 +320 light /compare h=2569 segments=4
20 +390 light / h=9854 segments=6
21 +390 light /countries h=14199 segments=6
22 +390 light /countries/canada h=8620 segments=6
23 +390 light /countries/canada/economy h=7460 segments=6
24 +390 light /compare/canada/united-states/france h=4045 segments=5
25 +390 light /compare/canada/united-states/france?tab=economy h=12773 segments=6
26 +390 light /rankings h=24430 segments=6
27 +390 light /rankings/gdp-per-capita h=4603 segments=6
28 +390 light /indicators/life-expectancy h=6494 segments=6
29 +390 light /regions/oecd h=9300 segments=6
30 +390 light /explore h=8606 segments=6
31 +390 light /changes h=13813 segments=6
32 +390 light /sources h=3556 segments=5
33 +390 light /methodology h=11263 segments=6
34 +390 light /api h=9020 segments=6
35 +390 light /data h=4065 segments=5
36 +390 light /indicators h=48272 segments=6
37 +390 light /regions h=4489 segments=6
38 +390 light /compare h=2491 segments=3
39 +390 dark / h=9854 segments=6
40 +390 dark /countries h=14199 segments=6
41 +390 dark /countries/canada h=8620 segments=6
42 +390 dark /countries/canada/economy h=7460 segments=6
43 +390 dark /compare/canada/united-states/france h=4045 segments=5
44 +390 dark /compare/canada/united-states/france?tab=economy h=12773 segments=6
45 +390 dark /rankings h=24430 segments=6
46 +390 dark /rankings/gdp-per-capita h=4603 segments=6
47 +390 dark /indicators/life-expectancy h=6494 segments=6
48 +390 dark /regions/oecd h=9300 segments=6
49 +390 dark /explore h=8606 segments=6
50 +390 dark /changes h=13813 segments=6
51 +390 dark /sources h=3556 segments=5
52 +390 dark /methodology h=11263 segments=6
53 +390 dark /api h=9020 segments=6
54 +390 dark /data h=4065 segments=5
55 +390 dark /indicators h=48272 segments=6
56 +390 dark /regions h=4489 segments=6
57 +390 dark /compare h=2491 segments=3
58 +1440 light / h=5223 segments=4
59 +1440 light /countries h=13393 segments=4
60 +1440 light /countries/canada h=4473 segments=4
61 +1440 light /countries/canada/economy h=5049 segments=4
62 +FAILED /compare/canada/united-states/france: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare/canada/united-states/france
63 +Call log:
64 +[2
65 +FAILED /compare/canada/united-states/france?tab=economy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare/canada/united-states/france?tab=economy
66 +C
67 +FAILED /rankings: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/rankings
68 +Call log:
69 + - navigating to "http://
70 +FAILED /rankings/gdp-per-capita: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/rankings/gdp-per-capita
71 +Call log:
72 + - navigat
73 +FAILED /indicators/life-expectancy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators/life-expectancy
74 +Call log:
75 + - navi
76 +FAILED /regions/oecd: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions/oecd
77 +Call log:
78 + - navigating to "htt
79 +FAILED /explore: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/explore
80 +Call log:
81 + - navigating to "http://l
82 +FAILED /changes: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/changes
83 +Call log:
84 + - navigating to "http://l
85 +FAILED /sources: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/sources
86 +Call log:
87 + - navigating to "http://l
88 +FAILED /methodology: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/methodology
89 +Call log:
90 + - navigating to "http
91 +FAILED /api: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/api
92 +Call log:
93 + - navigating to "http://local
94 +FAILED /data: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/data
95 +Call log:
96 + - navigating to "http://loca
97 +FAILED /indicators: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators
98 +Call log:
99 + - navigating to "http:
100 +FAILED /regions: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions
101 +Call log:
102 + - navigating to "http://l
103 +FAILED /compare: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare
104 +Call log:
105 + - navigating to "http://l
106 +FAILED /: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/
107 +Call log:
108 + - navigating to "http://localhos
109 +FAILED /countries: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/countries
110 +Call log:
111 + - navigating to "http:/
112 +FAILED /countries/canada: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/countries/canada
113 +Call log:
114 + - navigating to
115 +FAILED /countries/canada/economy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/countries/canada/economy
116 +Call log:
117 + - naviga
118 +FAILED /compare/canada/united-states/france: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare/canada/united-states/france
119 +Call log:
120 +[2
121 +FAILED /compare/canada/united-states/france?tab=economy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare/canada/united-states/france?tab=economy
122 +C
123 +FAILED /rankings: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/rankings
124 +Call log:
125 + - navigating to "http://
126 +FAILED /rankings/gdp-per-capita: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/rankings/gdp-per-capita
127 +Call log:
128 + - navigat
129 +FAILED /indicators/life-expectancy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators/life-expectancy
130 +Call log:
131 + - navi
132 +FAILED /regions/oecd: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions/oecd
133 +Call log:
134 + - navigating to "htt
135 +FAILED /explore: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/explore
136 +Call log:
137 + - navigating to "http://l
138 +FAILED /changes: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/changes
139 +Call log:
140 + - navigating to "http://l
141 +FAILED /sources: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/sources
142 +Call log:
143 + - navigating to "http://l
144 +FAILED /methodology: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/methodology
145 +Call log:
146 + - navigating to "http
147 +FAILED /api: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/api
148 +Call log:
149 + - navigating to "http://local
150 +FAILED /data: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/data
151 +Call log:
152 + - navigating to "http://loca
153 +FAILED /indicators: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators
154 +Call log:
155 + - navigating to "http:
156 +FAILED /regions: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions
157 +Call log:
158 + - navigating to "http://l
159 +FAILED /compare: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare
160 +Call log:
161 + - navigating to "http://l
added apps/web/qa/screens/polish/seg/api-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/api-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/changes-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/compare_canada_united-states_france_tab_economy-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/countries_canada_economy-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/data-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/explore-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/home-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/indicators_life-expectancy-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/methodology-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/rankings_gdp-per-capita-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-1440-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-320-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-dark-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/regions_oecd-390-light-5.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-1440-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-320-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-320-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-320-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-320-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-320-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-dark-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-dark-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-dark-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-dark-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-dark-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-light-0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-light-1.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-light-2.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-light-3.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/seg/sources-390-light-4.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/sources-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/sources-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/sources_worldbank-320-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/sources_worldbank-375-light.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/sweep1.log +10 −0
@@ -0,0 +1,10 @@
1 +node:internal/modules/run_main:107
2 + triggerUncaughtException(
3 + ^
4 +
5 +page.waitForTimeout: Target page, context or browser has been closed
6 + at /Users/simon-pierreboucher/Desktop/Projets/apps-web/countryatlas/apps/web/qa/polish-sweep.mjs:69:18 {
7 + log: []
8 +}
9 +
10 +Node.js v25.9.0
added apps/web/qa/screens/polish/verify/can-eco-m.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/can-health-d.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/can-health-m.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/cmp-eco-m.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/cmp-health-d.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/cmp-health-m.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/home-d.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/le-ind-d.png +0 −0

Binary file not shown.

added apps/web/qa/screens/polish/verify/le-ind-m.png +0 −0

Binary file not shown.

modified apps/web/src/app/(home)/page.tsx +9 −5
@@ -18,7 +18,8 @@ import { TopicsGrid } from '@/components/home/topics-grid';
18 18 export const metadata: Metadata = { alternates: { canonical: '/' } };
19 19 export const revalidate = 900;
20 20
21 −const LIST_KEYS = ['largest_economies', 'fastest_population_growth', 'highest_life_expectancy', 'energy_transition_leaders'] as const;
21 +/** Home lists in display order; a key missing from the API payload is simply skipped (e.g. fastest_gdp_growth). */
22 +const LIST_KEYS = ['largest_economies', 'fastest_gdp_growth', 'highest_gdp_per_capita_ppp', 'highest_life_expectancy', 'fastest_population_growth', 'energy_transition_leaders'] as const;
22 23
23 24 export default async function HomePage() {
24 25 let home: Awaited<ReturnType<typeof api.home>> | null = null;
@@ -58,14 +59,17 @@ export default async function HomePage() {
58 59 <div className="grid gap-x-10 gap-y-8 lg:grid-cols-2">
59 60 {LIST_KEYS.map((key) => {
60 61 const list = home.lists[key];
61 − if (!list) return null;
62 + if (!list || !list.rows?.length) return null;
63 + // Freshness honesty: a row ≥ 2 years older than the newest row of the list shows its year.
64 + const latestYear = Math.max(0, ...list.rows.map((r) => r.year ?? 0));
62 65 return (
63 − <div key={key}>
64 − <div className="mb-2 flex items-baseline justify-between gap-3">
66 + <div key={key} className="min-w-0">
67 + <div className="mb-2 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
65 68 <h3 className="text-base font-semibold text-ink">{list.title}</h3>
66 69 <span className="text-xs text-ink-3">{list.description}</span>
67 70 </div>
68 − <RankedBars rows={list.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank))} spec={list.indicator} provenance={list.rows[0]?.provenance ?? null} />
71 + {list.filter_note ? <p className="-mt-1 mb-2 text-xs text-ink-3">{list.filter_note}</p> : null}
72 + <RankedBars rows={list.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank, null, r.year != null && latestYear - r.year >= 2 ? r.year : null))} spec={list.indicator} provenance={list.rows[0]?.provenance ?? null} />
69 73 <Link href={routes.ranking(list.indicator.slug)} className="mt-2 inline-flex min-h-[36px] items-center text-sm text-accent hover:underline">
70 74 {t('common.seeFullRanking')} →
71 75 </Link>
modified apps/web/src/app/compare/[...slugs]/page.tsx +2 −11
@@ -9,6 +9,7 @@ import { topicById } from '@/lib/topics';
9 9 import type { CountrySummary, IndicatorCard } from '@/lib/types';
10 10 import { toCountryLite, type CompareSeries, type CompareSnapshotRow, type CountryLite } from '@/lib/types-compare';
11 11 import { CompareChart } from '@/components/compare/compare-chart';
12 +import { ChartGrid, ChartSections } from '@/components/compare/chart-sections';
12 13 import { CompareControls } from '@/components/compare/compare-controls';
13 14 import { CustomPanel } from '@/components/compare/custom-panel';
14 15 import { SnapshotTable } from '@/components/compare/snapshot-table';
@@ -164,7 +165,7 @@ export default async function CompareViewPage({ params, searchParams }: { params
164 165 {gridRows.length === 0 ? (
165 166 <p className="py-6 text-sm text-ink-3">{t('compare.charts.none', { topic: (topicDef?.short ?? topic).toLowerCase() })}</p>
166 167 ) : (
167 − <ChartGrid rows={gridRows} countries={countries} state={state} eager={eager} maxYear={maxYear} />
168 + <ChartSections rows={gridRows} countries={countries} slugs={slugs} state={state} eager={eager} maxYear={maxYear} anchorPrefix={`sub-${topic}`} />
168 169 )}
169 170 </Section>
170 171 ) : null}
@@ -185,13 +186,3 @@ export default async function CompareViewPage({ params, searchParams }: { params
185 186 </>
186 187 );
187 188 }
188 −
189 −function ChartGrid({ rows, countries, state, eager, maxYear }: { rows: CompareSnapshotRow[]; countries: CountryLite[]; state: CompareState; eager: Map<string, CompareSeries[]>; maxYear: number }) {
190 − return (
191 − <div className="grid gap-x-10 gap-y-8 lg:grid-cols-2">
192 − {rows.map((row) => (
193 − <CompareChart key={row.indicator.slug} indicator={row.indicator} countries={countries} state={state} initial={eager.get(row.indicator.slug)} snapshot={row.values} maxYear={maxYear} />
194 − ))}
195 − </div>
196 − );
197 −}
modified apps/web/src/app/countries/[slug]/[topic]/page.tsx +32 −10
@@ -8,6 +8,9 @@ import { regionShort } from '@/lib/regions';
8 8 import { TOPICS, isTopicId, topicById } from '@/lib/topics';
9 9 import type { CountryTopicResponse, MetricValue, SeriesResponse } from '@/lib/types';
10 10 import { IndicatorRow } from '@/components/country/indicator-row';
11 +import { TopicCompact } from '@/components/country/topic-compact';
12 +import { subtopicAnchor } from '@/lib/anchors';
13 +import { CollapsibleGroup, SubtopicJumpNav } from '@/components/data/collapsible-group';
11 14 import { NotBuiltState } from '@/components/data/empty-state';
12 15 import { Section } from '@/components/data/section';
13 16 import { TopicNav } from '@/components/data/topic-nav';
@@ -16,6 +19,8 @@ export const revalidate = 900;
16 19
17 20 type Params = { slug: string; topic: string };
18 21 const EAGER = 4; // charts rendered with server-fetched full history; the rest mount on scroll
22 +const SECTION_THRESHOLD = 12; // above this many indicators with data → collapsible subtopic blocks
23 +const OPEN_BLOCKS = 2; // blocks expanded by default on a sectioned topic page
19 24
20 25 async function load(slug: string, topic: string): Promise<CountryTopicResponse | 'not-built' | null> {
21 26 if (!isTopicId(topic)) return null;
@@ -59,7 +64,14 @@ export default async function CountryTopicPage({ params }: { params: Promise<Par
59 64 const all = data.subtopics.flatMap((b) => b.indicators);
60 65 const withData = all.filter((m) => m.has_data);
61 66 const noData = all.filter((m) => !m.has_data);
62 − const eagerIds = withData.slice(0, EAGER).map((m) => m.indicator);
67 + const blocks = data.subtopics.map((b) => ({ ...b, rows: b.indicators.filter((m) => m.has_data) })).filter((b) => b.rows.length > 0);
68 + // Long topics (> 12 indicators with data, ≥ 2 subtopics): collapsible subtopic blocks, first two expanded.
69 + const sectioned = withData.length > SECTION_THRESHOLD && blocks.length >= 2;
70 + const openBlocks = sectioned ? blocks.slice(0, OPEN_BLOCKS) : blocks;
71 + const eagerIds = openBlocks
72 + .flatMap((b) => b.rows)
73 + .slice(0, EAGER)
74 + .map((m) => m.indicator);
63 75 const eagerSeries = await Promise.all(eagerIds.map((iid) => safe(api.countrySeries(c.id, iid))));
64 76 const seriesById = new Map<string, SeriesResponse | null>(eagerIds.map((iid, i) => [iid, eagerSeries[i] ?? null]));
65 77 const def = topicById(topic);
@@ -88,16 +100,26 @@ export default async function CountryTopicPage({ params }: { params: Promise<Par
88 100 </header>
89 101 <TopicNav slug={countryRef.slug} />
90 102
91 − {data.subtopics.map((block) => {
92 − const rows = block.indicators.filter((m) => m.has_data);
93 − if (rows.length === 0) return null;
103 + {sectioned ? <SubtopicJumpNav items={blocks.map((b) => ({ id: subtopicAnchor('sub', b.subtopic), label: b.subtopic, count: b.rows.length }))} label={t('topic.jump')} className="mt-3" /> : null}
104 +
105 + {blocks.map((block, i) => {
106 + const id = subtopicAnchor('sub', block.subtopic);
107 + const rowsEl = (
108 + <div>
109 + {block.rows.map((m: MetricValue) => (
110 + <IndicatorRow key={m.indicator} metric={m} country={countryRef} regionName={regionShort(c.region) ?? c.region_name} series={seriesById.get(m.indicator)} eager={seriesById.has(m.indicator)} />
111 + ))}
112 + </div>
113 + );
114 + if (sectioned)
115 + return (
116 + <CollapsibleGroup key={block.subtopic} id={id} title={block.subtopic} count={block.rows.length} defaultOpen={i < OPEN_BLOCKS} summary={<TopicCompact rows={block.rows} country={countryRef} regionName={regionShort(c.region) ?? c.region_name} />}>
117 + {rowsEl}
118 + </CollapsibleGroup>
119 + );
94 120 return (
95 − <Section key={block.subtopic} id={`sub-${block.subtopic.toLowerCase().replace(/\W+/g, '-')}`} title={block.subtopic} level={3} tight className="pb-2 pt-6">
96 − <div>
97 − {rows.map((m: MetricValue) => (
98 − <IndicatorRow key={m.indicator} metric={m} country={countryRef} regionName={regionShort(c.region) ?? c.region_name} series={seriesById.get(m.indicator)} eager={seriesById.has(m.indicator)} />
99 − ))}
100 − </div>
121 + <Section key={block.subtopic} id={id} title={block.subtopic} level={3} tight className="pb-2 pt-6">
122 + {rowsEl}
101 123 </Section>
102 124 );
103 125 })}
modified apps/web/src/app/countries/[slug]/opengraph-image.tsx +2 −1
@@ -1,6 +1,7 @@
1 1 import { ImageResponse } from 'next/og';
2 2 import { t } from '@/i18n';
3 3 import { api, safe } from '@/lib/api';
4 +import { displayValue } from '@/lib/format';
4 5 import { OG_INK2, OG_INK3, OG_RULE, OG_SIZE_H, OG_SIZE_W, OgFrame, OgWordmark } from '../../og-shared';
5 6
6 7 export const alt = 'Country statistics on CountryAtlas';
@@ -35,7 +36,7 @@ export default async function CountryOgImage({ params }: { params: Promise<{ slu
35 36 {facts.map((m) => (
36 37 <div key={m.indicator} style={{ display: 'flex', flexDirection: 'column' }}>
37 38 <div style={{ fontSize: 18, color: OG_INK3, textTransform: 'uppercase', letterSpacing: 1.5 }}>{m.indicator_name ?? m.indicator}</div>
38 − <div style={{ fontSize: 44, fontWeight: 600, marginTop: 6 }}>{m.formatted ?? String(m.value ?? '')}</div>
39 + <div style={{ fontSize: 44, fontWeight: 600, marginTop: 6 }}>{displayValue(m.value, m, m.formatted)}</div>
39 40 <div style={{ fontSize: 18, color: OG_INK3, marginTop: 2 }}>{String(m.year ?? '')}</div>
40 41 </div>
41 42 ))}
modified apps/web/src/app/rankings/[indicator]/opengraph-image.tsx +2 −1
@@ -2,6 +2,7 @@ import { ImageResponse } from 'next/og';
2 2 import { t } from '@/i18n';
3 3 import { safe } from '@/lib/api';
4 4 import { apiCompare } from '@/lib/api-compare';
5 +import { displayValue } from '@/lib/format';
5 6 import { OG_INK2, OG_INK3, OG_RULE, OG_SIZE_H, OG_SIZE_W, OgFrame, OgWordmark } from '../../og-shared';
6 7
7 8 export const alt = 'Country ranking on CountryAtlas';
@@ -31,7 +32,7 @@ export default async function RankingOgImage({ params }: { params: Promise<{ ind
31 32 <div style={{ fontSize: 44, lineHeight: 1, display: 'flex' }}>{r.country.flag ?? ''}</div>
32 33 <div style={{ display: 'flex', flexDirection: 'column' }}>
33 34 <div style={{ fontSize: 20, color: OG_INK2, display: 'flex' }}>{r.country.name ?? r.country.id}</div>
34 − <div style={{ fontSize: 30, fontWeight: 600, display: 'flex' }}>{r.formatted ?? String(r.value ?? '')}</div>
35 + <div style={{ fontSize: 30, fontWeight: 600, display: 'flex' }}>{displayValue(r.value, data!.indicator, r.formatted)}</div>
35 36 </div>
36 37 </div>
37 38 ))}
modified apps/web/src/app/regions/[slug]/page.tsx +1 −1
@@ -41,7 +41,7 @@ export async function generateMetadata({ params }: { params: Promise<Params> }):
41 41 }
42 42
43 43 /** Extra indicators offered in the member ranking picker besides the headline ones. */
44 −const EXTRA_RANKING = ['gdp-per-capita-ppp', 'population-growth', 'fertility-rate', 'renewable-electricity-share', 'internet-users', 'co2-per-capita', 'government-debt-pct-gdp', 'median-age'];
44 +const EXTRA_RANKING = ['gdp-per-capita-ppp', 'population-growth', 'fertility-rate', 'renewable-electricity-share', 'internet-users', 'co2-per-capita', 'general-government-gross-debt-pct-gdp', 'median-age'];
45 45
46 46 export default async function RegionPage({ params }: { params: Promise<Params> }) {
47 47 const { slug } = await params;
modified apps/web/src/app/regions/page.tsx +3 −3
@@ -64,19 +64,19 @@ export default async function RegionsPage() {
64 64 </span>
65 65 <dl className="tnum mt-1 grid grid-cols-3 gap-x-3 text-xs">
66 66 <div className="min-w-0">
67 − <dt className="truncate text-2xs text-ink-3">
67 + <dt className="text-2xs leading-tight text-ink-3">
68 68 {t('regions.population')} <span className="text-ink-3/80">({t('regions.sum')})</span>
69 69 </dt>
70 70 <dd className="truncate text-ink">{isNum(g.population_latest) ? compact(g.population_latest) : t('common.na')}</dd>
71 71 </div>
72 72 <div className="min-w-0">
73 − <dt className="truncate text-2xs text-ink-3">
73 + <dt className="text-2xs leading-tight text-ink-3">
74 74 {t('regions.gdp')} <span className="text-ink-3/80">({t('regions.sum')})</span>
75 75 </dt>
76 76 <dd className="truncate text-ink">{isNum(g.gdp_latest) ? `US$${compact(g.gdp_latest)}` : t('common.na')}</dd>
77 77 </div>
78 78 <div className="min-w-0">
79 − <dt className="truncate text-2xs text-ink-3">
79 + <dt className="text-2xs leading-tight text-ink-3">
80 80 {le ? `${t('regions.lifeExpectancy')} (${t('regions.median')})` : gdppc ? `${t('common.gdpPerCapita')} (${t('regions.weighted_mean')})` : t('regions.lifeExpectancy')}
81 81 </dt>
82 82 <dd className="truncate text-ink">{le ? formatValue(le.value, le.indicator) : gdppc ? formatValue(gdppc.value, gdppc.indicator) : t('common.na')}</dd>
modified apps/web/src/components/charts/line-chart.tsx +10 −5
@@ -7,7 +7,7 @@ import type { FormatSpec as Spec, Provenance } from '@/lib/types';
7 7 import type { ProvenancePayload } from '@/components/data/provenance-context';
8 8 import { ChartFrame, Legend, type TableData } from './chart-frame';
9 9 import { CHART, MARK, seriesVar } from './palette';
10 −import { DEFAULT_MARGIN, extent, periodToX, splitForecast, xYearScale, yScale, yearTicks, type Margin, type SeriesPoint } from './scales';
10 +import { DEFAULT_MARGIN, extent, lineDomain, periodToX, splitForecast, xYearScale, yScale, yearTicks, type Margin, type SeriesPoint } from './scales';
11 11 import { summarizeMulti, summarizeSeries } from './summary';
12 12 import { ChartTooltip, TooltipRow } from './tooltip';
13 13 import { useMeasure } from './use-measure';
@@ -76,9 +76,11 @@ export function LineChart({
76 76 const margin: Margin = { ...DEFAULT_MARGIN, ...marginIn };
77 77 if (endLabels && series.length > 1 && series.length <= 4) margin.right = Math.max(margin.right, 72);
78 78 // Left margin fits the longest y tick label (≈ 6.4 px per character at 11 px) so labels are never clipped.
79 + // Uses the same domain rule as the chart itself (lineDomain) so the probe ticks are the real ticks.
79 80 if (marginIn?.left == null) {
80 81 const probe = extent(series.flatMap((s) => s.points.map((p) => p.value))) ?? [0, 1];
81 − const longest = Math.max(...yScale(probe, [0, 1], { log }).ticks(4).map((tk) => formatTick(tk, spec).length), 3);
82 + const dom = variant === 'stacked' ? ([0, probe[1]] as [number, number]) : log && probe[0] > 0 ? probe : lineDomain(probe, spec);
83 + const longest = Math.max(...yScale(dom, [0, 1], { log, includeZero: variant === 'stacked' }).ticks(4).map((tk) => formatTick(tk, spec).length), 3);
82 84 margin.left = Math.min(84, Math.max(36, Math.round(longest * 6.4) + 12));
83 85 }
84 86 const [hover, setHover] = useState<number | null>(null); // x-value (fractional year)
@@ -110,9 +112,11 @@ export function LineChart({
110 112 const top = Math.max(0, ...stacks.flat().map((d) => d[2]));
111 113 yDom = [0, top];
112 114 } else {
113 − yDom = extent(clean.flatMap((s) => s.points.map((p) => p.value))) ?? [0, 1];
115 + // Line / area: padded nice extent; zero only when the rule says so (see lineDomain). Log keeps the raw extent.
116 + const raw = extent(clean.flatMap((s) => s.points.map((p) => p.value))) ?? [0, 1];
117 + yDom = log && raw[0] > 0 ? raw : lineDomain(raw, spec);
114 118 }
115 − const y = yScale(yDom, [innerH, 0], { log, includeZero: variant !== 'line' || !log });
119 + const y = yScale(yDom, [innerH, 0], { log, includeZero: variant === 'stacked' });
116 120 const xTicks = yearTicks(xDom, Math.max(2, Math.floor(innerW / 90)));
117 121 const yTicks = y.ticks(4);
118 122 const pathFor = d3Line<XY>()
@@ -146,7 +150,8 @@ export function LineChart({
146 150 });
147 151
148 152 return { clean, x, y, innerW, innerH, xTicks, yTicks, pathFor, areaFor, layers, stackPaths, xDom };
149 − }, [series, width, height, margin.left, margin.right, margin.top, margin.bottom, variant, log]);
153 + // eslint-disable-next-line react-hooks/exhaustive-deps -- only spec.format drives the domain rule
154 + }, [series, width, height, margin.left, margin.right, margin.top, margin.bottom, variant, log, spec.format]);
150 155
151 156 const onMove = useCallback(
152 157 (e: PointerEvent<SVGRectElement>) => {
modified apps/web/src/components/charts/ranked-bars.tsx +25 −15
@@ -13,8 +13,10 @@ export interface RankedBarRow {
13 13 href?: string | null;
14 14 value: number | null;
15 15 rank?: number | null;
16 − /** Secondary text shown right of the value (e.g. change). */
16 + /** Secondary text shown right of the value (e.g. change); hidden on phones. */
17 17 hint?: string | null;
18 + /** Observation year — pass it only when it is older than the list's reference year (freshness honesty); always visible. */
19 + year?: number | null;
18 20 }
19 21
20 22 /**
@@ -52,37 +54,40 @@ export function RankedBars({
52 54 {rows.map((r, i) => {
53 55 const pct = isNum(r.value) && max > 0 ? Math.max(0, (Math.abs(r.value) / max) * 100) : 0;
54 56 const hl = highlightId && r.id === highlightId;
57 + // Names wrap on phones (never clipped mid-word); they truncate only in the fixed 13 rem column on sm+.
55 58 const label = (
56 − <span className={cn('flex min-w-0 items-center gap-1.5 truncate text-sm', hl ? 'font-semibold text-ink' : 'text-ink')}>
59 + <span className={cn('flex min-w-0 items-center gap-1.5 text-sm leading-snug', hl ? 'font-semibold text-ink' : 'text-ink')}>
57 60 {r.flag ? (
58 61 <span aria-hidden className="text-base leading-none">
59 62 {r.flag}
60 63 </span>
61 64 ) : null}
62 − <span className="truncate">{r.label}</span>
65 + <span className="min-w-0 sm:truncate">{r.label}</span>
63 66 </span>
64 67 );
68 + // Phones: [rank + name] [value] on line 1, bar on line 2. sm+: [rank + name] [bar] [value hint] on one line.
65 69 return (
66 − <li key={r.id} className="grid grid-cols-[minmax(0,46%)_1fr] items-center gap-x-3 py-1.5 sm:grid-cols-[minmax(0,13rem)_1fr]">
70 + <li key={r.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 py-1.5 sm:grid-cols-[minmax(0,13rem)_1fr_auto] sm:gap-y-0">
67 71 <div className="flex min-w-0 items-center gap-2">
68 72 {showRank ? <span className="tnum w-6 shrink-0 text-right text-xs text-ink-3">{r.rank ?? i + 1}</span> : null}
69 73 {linkRows && r.href ? (
70 − <Link href={r.href} className="link-quiet min-w-0 truncate">
74 + <Link href={r.href} className="link-quiet min-w-0 sm:truncate">
71 75 {label}
72 76 </Link>
73 77 ) : (
74 78 label
75 79 )}
76 80 </div>
77 − <div className="flex min-w-0 items-center gap-2">
78 − <div className="h-4 min-w-0 flex-1" style={{ maxHeight: MARK.barMax }}>
79 − <div
80 − className={cn('h-full rounded-r-sm', hl ? 'bg-accent' : 'bg-series-1')}
81 − style={{ width: `${pct}%`, minWidth: isNum(r.value) ? 2 : 0, borderRadius: `0 ${MARK.barRadius}px ${MARK.barRadius}px 0`, opacity: hl ? 1 : 0.85 }}
82 − />
83 − </div>
84 − <span className={cn('tnum shrink-0 text-right text-sm', hl ? 'font-semibold text-ink' : 'text-ink')} style={{ minWidth: '4.5rem' }}>
81 + <div className="col-span-2 row-start-2 h-3 min-w-0 sm:col-span-1 sm:col-start-2 sm:row-start-1 sm:h-4" style={{ maxHeight: MARK.barMax }}>
82 + <div
83 + className={cn('h-full rounded-r-sm', hl ? 'bg-accent' : 'bg-series-1')}
84 + style={{ width: `${pct}%`, minWidth: isNum(r.value) ? 2 : 0, borderRadius: `0 ${MARK.barRadius}px ${MARK.barRadius}px 0`, opacity: hl ? 1 : 0.85 }}
85 + />
86 + </div>
87 + <div className="col-start-2 row-start-1 flex shrink-0 items-center justify-end gap-2 sm:col-start-3">
88 + <span className={cn('tnum whitespace-nowrap text-right text-sm', hl ? 'font-semibold text-ink' : 'text-ink')} style={{ minWidth: '4.5rem' }}>
85 89 {formatValue(r.value, spec)}
90 + {r.year != null ? <span className="tnum ml-1 rounded-xs bg-surface-2 px-1 text-2xs font-normal text-ink-2">{r.year}</span> : null}
86 91 </span>
87 92 {r.hint ? <span className="tnum hidden shrink-0 text-xs text-ink-3 sm:inline">{r.hint}</span> : null}
88 93 </div>
@@ -101,8 +106,13 @@ export function RankedBars({
101 106 }
102 107
103 108 /** Convenience: a country row for RankedBars from a ranking row. */
104 −export function rankedRowFromCountry(c: { id: string; slug: string | null; name: string | null; flag: string | null }, value: number | null, rank?: number | null, hint?: string | null): RankedBarRow {
105 − return { id: c.id, label: c.name ?? c.id, flag: c.flag, href: c.slug ? routes.country(c.slug) : null, value, rank, hint };
109 +export function rankedRowFromCountry(c: { id: string; slug: string | null; name: string | null; flag: string | null }, value: number | null, rank?: number | null, hint?: string | null, year?: number | null): RankedBarRow {
110 + return { id: c.id, label: c.name ?? c.id, flag: c.flag, href: c.slug ? routes.country(c.slug) : null, value, rank, hint, year };
111 +}
112 +
113 +/** Year to display for a row (freshness honesty): only when ≥ 2 years older than the reference year. */
114 +export function staleYear(year: number | null | undefined, reference: number | null | undefined): number | null {
115 + return year != null && reference != null && reference - year >= 2 ? year : null;
106 116 }
107 117
108 118 export { ordinal };
modified apps/web/src/components/charts/scales.ts +33 −4
@@ -49,6 +49,28 @@ export function extent(values: Array<number | null | undefined>): [number, numbe
49 49 return [lo, hi];
50 50 }
51 51
52 +/**
53 + * Y domain for LINE / AREA charts (dataviz rule: a line need not start at zero — a tight series such as
54 + * life expectancy 60→83 must not render flat). Zero is included only when
55 + * - the data crosses or touches zero (min ≤ 0), or
56 + * - the minimum is within 30 % of the maximum (min < 0.3·max: the zero baseline costs little), or
57 + * - the indicator is a percent share / index whose floor is 0 AND the data spans more than half of [0, max].
58 + * Otherwise the extent is padded by ≈ 6 % on each side (then `.nice()`d by the scale). Bars and stacked
59 + * areas keep the zero baseline (they do not use this).
60 + */
61 +export function lineDomain(dom: [number, number], spec?: { format?: string | null } | null): [number, number] {
62 + const [lo, hi] = dom;
63 + if (lo === hi) {
64 + const pad = Math.abs(lo) * 0.1 || 1;
65 + return [lo - pad, hi + pad];
66 + }
67 + const isShare = spec?.format === 'percent' || spec?.format === 'index';
68 + const includeZero = lo <= 0 || lo < 0.3 * hi || (isShare && hi - lo > 0.5 * hi);
69 + if (includeZero) return [Math.min(0, lo), Math.max(0, hi)];
70 + const pad = (hi - lo) * 0.06;
71 + return [lo - pad, hi + pad];
72 +}
73 +
52 74 /** Y scale: linear (zero-anchored when the data allows) or log for strictly positive values. */
53 75 export function yScale(
54 76 domain: [number, number],
@@ -81,10 +103,17 @@ export function yearTicks(domain: [number, number], n = 5): number[] {
81 103 const [lo, hi] = domain;
82 104 const span = Math.max(1, hi - lo);
83 105 const steps = [1, 2, 5, 10, 20, 25, 50, 100];
84 − const step = steps.find((s) => span / s <= n) ?? 100;
85 − const start = Math.ceil(lo / step) * step;
86 − const out: number[] = [];
87 − for (let y = start; y <= hi; y += step) out.push(y);
106 + let idx = steps.findIndex((s) => span / s <= n);
107 + if (idx < 0) idx = steps.length - 1;
108 + const build = (step: number) => {
109 + const start = Math.ceil(lo / step) * step;
110 + const out: number[] = [];
111 + for (let y = start; y <= hi; y += step) out.push(y);
112 + return out;
113 + };
114 + // Always at least two labelled years (a lone "2000" on a 1960–2025 axis reads as nothing): step down if needed.
115 + let out = build(steps[idx]!);
116 + while (out.length < 2 && idx > 0) out = build(steps[--idx]!);
88 117 if (out.length === 0) out.push(lo, hi);
89 118 return out;
90 119 }
added apps/web/src/components/compare/chart-sections.tsx +53 −0
@@ -0,0 +1,53 @@
1 +import { t } from '@/i18n';
2 +import type { CompareState } from '@/lib/compare-state';
3 +import type { CompareSeries, CompareSnapshotRow, CountryLite } from '@/lib/types-compare';
4 +import { subtopicAnchor } from '@/lib/anchors';
5 +import { CollapsibleGroup, SubtopicJumpNav } from '@/components/data/collapsible-group';
6 +import { CompareChart } from './compare-chart';
7 +import { SnapshotCompact } from './snapshot-table';
8 +
9 +/** Above this many charts a topic tab is split into collapsible subtopic blocks (first one expanded). */
10 +const SECTION_THRESHOLD = 8;
11 +
12 +export function ChartGrid({ rows, countries, state, eager, maxYear }: { rows: CompareSnapshotRow[]; countries: CountryLite[]; state: CompareState; eager: Map<string, CompareSeries[]>; maxYear: number }) {
13 + return (
14 + <div className="grid gap-x-10 gap-y-8 lg:grid-cols-2">
15 + {rows.map((row) => (
16 + <CompareChart key={row.indicator.slug} indicator={row.indicator} countries={countries} state={state} initial={eager.get(row.indicator.slug)} snapshot={row.values} maxYear={maxYear} />
17 + ))}
18 + </div>
19 + );
20 +}
21 +
22 +/** Group rows by `indicator.subtopic`, preserving first-seen order. */
23 +export function groupBySubtopic<T extends { indicator: { subtopic: string | null } }>(rows: T[]): Array<{ subtopic: string; rows: T[] }> {
24 + const m = new Map<string, T[]>();
25 + for (const r of rows) {
26 + const k = r.indicator.subtopic ?? t('common.other');
27 + const cur = m.get(k);
28 + if (cur) cur.push(r);
29 + else m.set(k, [r]);
30 + }
31 + return Array.from(m, ([subtopic, rs]) => ({ subtopic, rows: rs }));
32 +}
33 +
34 +/**
35 + * Charts of a compare topic tab. Short tabs → plain grid. Long tabs (> 8 charts, ≥ 2 subtopics) → a jump nav +
36 + * one CollapsibleGroup per subtopic: the first expanded (charts), the others collapsed showing the latest
37 + * values inline (SnapshotCompact) so nothing is hidden, just deferred. Charts still lazy-mount on scroll.
38 + */
39 +export function ChartSections({ rows, countries, slugs, state, eager, maxYear, anchorPrefix }: { rows: CompareSnapshotRow[]; countries: CountryLite[]; slugs: string[]; state: CompareState; eager: Map<string, CompareSeries[]>; maxYear: number; anchorPrefix: string }) {
40 + const groups = groupBySubtopic(rows);
41 + if (rows.length <= SECTION_THRESHOLD || groups.length < 2) return <ChartGrid rows={rows} countries={countries} state={state} eager={eager} maxYear={maxYear} />;
42 + const items = groups.map((g) => ({ id: subtopicAnchor(anchorPrefix, g.subtopic), label: g.subtopic, count: g.rows.length }));
43 + return (
44 + <div className="min-w-0">
45 + <SubtopicJumpNav items={items} label={t('topic.jump')} className="mb-2" />
46 + {groups.map((g, i) => (
47 + <CollapsibleGroup key={g.subtopic} id={items[i]!.id} title={g.subtopic} count={g.rows.length} defaultOpen={i === 0} summary={<SnapshotCompact rows={g.rows} countries={countries} slugs={slugs} state={state} />}>
48 + <ChartGrid rows={g.rows} countries={countries} state={state} eager={eager} maxYear={maxYear} />
49 + </CollapsibleGroup>
50 + ))}
51 + </div>
52 + );
53 +}
modified apps/web/src/components/compare/compare-chart.tsx +2 −2
@@ -7,7 +7,7 @@ import { t } from '@/i18n';
7 7 import { clientCompare } from '@/lib/client-api-compare';
8 8 import { cn } from '@/lib/cn';
9 9 import { compareQuery, type CompareState } from '@/lib/compare-state';
10 −import { formatDate, formatPeriod, formatValue } from '@/lib/format';
10 +import { displayValue, formatDate, formatPeriod, formatValue } from '@/lib/format';
11 11 import { routes } from '@/lib/site';
12 12 import type { FormatSpec, IndicatorCard, MetricValue } from '@/lib/types';
13 13 import type { CompareSeries, CountryLite } from '@/lib/types-compare';
@@ -186,7 +186,7 @@ export function CompareChart({ indicator, countries, state, initial, snapshot, h
186 186 <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs" aria-label={t('compare.charts.latest')}>
187 187 {latest.map((l) => {
188 188 const has = !!l.last || !!l.snap;
189 − const text = l.last ? (state.mode === 'absolute' && l.snap?.formatted && l.snap.year === l.last.year ? l.snap.formatted : formatValue(l.last.value, spec)) : l.snap ? l.snap.formatted ?? formatValue(l.snap.value, l.snap) : t('common.noData');
189 + const text = l.last ? (state.mode === 'absolute' && l.snap?.formatted && l.snap.year === l.last.year ? displayValue(l.last.value, spec, l.snap.formatted) : formatValue(l.last.value, spec)) : l.snap ? displayValue(l.snap.value, l.snap, l.snap.formatted) : t('common.noData');
190 190 const when = l.last ? formatPeriod(l.last.period, indicator.frequency) : l.snap?.year;
191 191 return (
192 192 <li key={l.c.id}>
modified apps/web/src/components/compare/snapshot-table.tsx +49 −4
@@ -4,7 +4,7 @@ import Link from 'next/link';
4 4 import { t } from '@/i18n';
5 5 import { cn } from '@/lib/cn';
6 6 import { COMPARE_TOPIC_TABS, compareQuery, type CompareState, type CompareTab } from '@/lib/compare-state';
7 −import { formatValue, isNum, ordinal } from '@/lib/format';
7 +import { displayValue, isNum, ordinal } from '@/lib/format';
8 8 import { routes } from '@/lib/site';
9 9 import type { CompareSnapshotRow, CountryLite } from '@/lib/types-compare';
10 10 import type { MetricValue } from '@/lib/types';
@@ -19,6 +19,51 @@ export function chartHrefFor(slugs: string[], state: CompareState, indicator: {
19 19 return `${routes.compare(...slugs)}${compareQuery({ ...state, tab, indicator: indicator.slug, indicators })}`;
20 20 }
21 21
22 +/**
23 + * Compact latest-values table used inside a collapsed subtopic block (no charts): one row per indicator,
24 + * one cell per country (value · year, best highlighted). Indicator name → its chart (hero). Any cell →
25 + * provenance. Phones: value cells wrap under the indicator name.
26 + */
27 +export function SnapshotCompact({ rows, countries, slugs, state }: { rows: CompareSnapshotRow[]; countries: CountryLite[]; slugs: string[]; state: CompareState }) {
28 + const { open } = useProvenance();
29 + const openCell = (m: MetricValue, c: CountryLite, name: string) => open(payloadFor(m, { id: c.id, slug: c.slug, name: c.name, flag: c.flag }, { name }));
30 + if (rows.length === 0) return null;
31 + const latestYear = Math.max(0, ...rows.flatMap((r) => Object.values(r.values).map((v) => v.year ?? 0)));
32 + return (
33 + <ul className="divide-y divide-rule border-b border-rule text-sm" aria-label={t('group.latestCol')}>
34 + {rows.map((r) => {
35 + const name = r.indicator.short_name ?? r.indicator.name ?? r.indicator.slug;
36 + return (
37 + <li key={r.indicator.slug} className="grid gap-x-4 gap-y-1 py-1.5 md:grid-cols-[minmax(0,14rem)_1fr] lg:grid-cols-[minmax(0,18rem)_1fr]">
38 + <Link href={chartHrefFor(slugs, state, r.indicator)} className="group/i inline-flex min-h-[32px] min-w-0 items-center gap-1 text-ink hover:text-accent" aria-label={t('compare.snapshot.openChart', { name })}>
39 + <span className="truncate">{r.indicator.name ?? name}</span>
40 + <ChevronRight size={12} aria-hidden className="shrink-0 text-ink-3 group-hover/i:text-accent" />
41 + </Link>
42 + <ul className="flex flex-wrap gap-x-4 gap-y-0.5">
43 + {countries.map((c, i) => {
44 + const m = r.values[c.id];
45 + const has = !!m && m.has_data && isNum(m.value);
46 + const best = r.best === c.id;
47 + const stale = has && m.year != null && latestYear - m.year >= 2;
48 + return (
49 + <li key={c.id}>
50 + <button type="button" disabled={!has} onClick={() => has && openCell(m, c, r.indicator.name ?? name)} className="inline-flex min-h-[32px] items-center gap-1.5 rounded-sm px-1 hover:bg-surface-2 disabled:hover:bg-transparent" aria-label={has ? `${c.name}: ${displayValue(m.value, m, m.formatted)} (${m.year ?? ''}). ${t('common.openProvenance')}` : `${c.name}: ${t('common.noData')}`}>
51 + <span aria-hidden className="h-2 w-2 shrink-0 rounded-full" style={{ background: seriesVar(i) }} />
52 + <span aria-hidden>{c.flag}</span>
53 + <span className={cn('tnum', has ? (best ? 'font-semibold text-ink' : 'text-ink') : 'text-ink-3')}>{has ? displayValue(m.value, m, m.formatted) : t('common.noData')}</span>
54 + {has && m.year != null ? <span className={cn('tnum text-2xs', stale ? 'rounded-xs bg-surface-2 px-1 text-ink-2' : 'text-ink-3')}>{m.year}</span> : null}
55 + </button>
56 + </li>
57 + );
58 + })}
59 + </ul>
60 + </li>
61 + );
62 + })}
63 + </ul>
64 + );
65 +}
66 +
22 67 /**
23 68 * Snapshot comparison: countries as columns on md+ (value · year · rank chip per cell, best value highlighted);
24 69 * on phones each indicator is a block with one horizontal mini bar per country — no horizontal overflow.
@@ -80,7 +125,7 @@ export function SnapshotTable({ rows, countries, slugs, state }: { rows: Compare
80 125 return (
81 126 <td key={c.id} className={cn('py-1 pr-1 text-right align-top', best && 'bg-accent-soft/60')}>
82 127 <button type="button" onClick={() => openCell(m, c, r.indicator.name ?? name)} className="group/c -my-0 flex min-h-[44px] w-full flex-col items-end rounded-sm px-2 py-1 text-right hover:bg-surface-2" aria-label={t('common.openProvenance')}>
83 − <span className={cn('tnum text-base leading-tight', best ? 'font-semibold text-ink' : 'text-ink')}>{m.formatted ?? formatValue(m.value, m)}</span>
128 + <span className={cn('tnum text-base leading-tight', best ? 'font-semibold text-ink' : 'text-ink')}>{displayValue(m.value, m, m.formatted)}</span>
84 129 <span className="tnum mt-0.5 flex items-center gap-1.5 text-2xs text-ink-3">
85 130 <span>{m.year ?? ''}</span>
86 131 {isNum(m.rank_world) && isNum(m.n_world) ? (
@@ -130,7 +175,7 @@ export function SnapshotTable({ rows, countries, slugs, state }: { rows: Compare
130 175 disabled={!has}
131 176 onClick={() => has && openCell(m, c, r.indicator.name ?? name)}
132 177 className="grid min-h-[44px] w-full grid-cols-[1.5rem_minmax(0,1fr)_auto] items-center gap-x-2 rounded-sm text-left hover:bg-surface-2 disabled:hover:bg-transparent"
133 − aria-label={has ? `${c.name}: ${m.formatted ?? formatValue(m.value, m)} (${m.year ?? ''}). ${t('common.openProvenance')}` : `${c.name}: ${t('common.noData')}`}
178 + aria-label={has ? `${c.name}: ${displayValue(m.value, m, m.formatted)} (${m.year ?? ''}). ${t('common.openProvenance')}` : `${c.name}: ${t('common.noData')}`}
134 179 >
135 180 <span aria-hidden className="text-base leading-none">
136 181 {c.flag}
@@ -141,7 +186,7 @@ export function SnapshotTable({ rows, countries, slugs, state }: { rows: Compare
141 186 </span>
142 187 </span>
143 188 <span className="tnum flex items-baseline gap-1.5 text-right">
144 − <span className={cn('text-sm', best ? 'font-semibold text-ink' : 'text-ink')}>{has ? m.formatted ?? formatValue(m.value, m) : t('common.noData')}</span>
189 + <span className={cn('text-sm', best ? 'font-semibold text-ink' : 'text-ink')}>{has ? displayValue(m.value, m, m.formatted) : t('common.noData')}</span>
145 190 <span className="text-2xs text-ink-3">{has ? m.year : ''}</span>
146 191 </span>
147 192 </button>
modified apps/web/src/components/countries/directory.tsx +2 −1
@@ -7,6 +7,7 @@ import { t } from '@/i18n';
7 7 import { cn } from '@/lib/cn';
8 8 import { compact, formatPct, grouped, isNum } from '@/lib/format';
9 9 import { INCOME_GROUPS, WB_REGIONS } from '@/lib/regions';
10 +import { regionShort } from '@/lib/regions';
10 11 import { routes } from '@/lib/site';
11 12 import type { CountrySummary } from '@/lib/types';
12 13 import { BottomSheet } from '@/components/data/bottom-sheet';
@@ -202,7 +203,7 @@ function Row({ c, rank }: { c: CountrySummary; rank?: number }) {
202 203 {c.name}
203 204 {c.kind === 'territory' ? <span className="ml-1.5 rounded-xs border border-rule px-1 text-2xs font-normal text-ink-3">{t('countries.territory')}</span> : null}
204 205 </span>
205 − <span className="block truncate text-xs text-ink-3 md:hidden">{c.region_name ?? ''}</span>
206 + <span className="block truncate text-xs text-ink-3 md:hidden">{regionShort(c.region) ?? c.region_name ?? ''}</span>
206 207 </span>
207 208 </span>
208 209 <span className="hidden truncate text-xs text-ink-2 md:block">{c.region_name}</span>
modified apps/web/src/components/country/indicator-row.tsx +2 −2
@@ -4,7 +4,7 @@ import Link from 'next/link';
4 4 import { t } from '@/i18n';
5 5 import { clientApi } from '@/lib/client-api';
6 6 import { cn } from '@/lib/cn';
7 −import { formatPeriod, formatValue } from '@/lib/format';
7 +import { displayValue, formatPeriod } from '@/lib/format';
8 8 import { routes } from '@/lib/site';
9 9 import type { MetricValue, SeriesResponse } from '@/lib/types';
10 10 import { LineChart } from '@/components/charts/line-chart';
@@ -83,7 +83,7 @@ export function IndicatorRow({ metric, country, regionName, series: initialSerie
83 83 {m.unit ? <p className="text-xs text-ink-3">{m.unit}</p> : null}
84 84 {m.has_data ? (
85 85 <button type="button" onClick={() => open(payload)} className="-mx-1 mt-2 flex min-h-[44px] flex-col items-start rounded-sm px-1 text-left hover:bg-surface-2" aria-label={t('common.openProvenance')}>
86 − <span className="pnum text-2xl font-semibold leading-none text-ink">{m.formatted ?? formatValue(m.value, spec)}</span>
86 + <span className="pnum text-2xl font-semibold leading-none text-ink">{displayValue(m.value, spec, m.formatted)}</span>
87 87 <span className="mt-1 flex flex-wrap items-baseline gap-x-2 text-xs text-ink-3">
88 88 <span className="tnum">{formatPeriod(m.period, m.frequency)}</span>
89 89 {m.is_estimate ? <span>{t('common.estimate')}</span> : null}
added apps/web/src/components/country/topic-compact.tsx +47 −0
@@ -0,0 +1,47 @@
1 +'use client';
2 +import Link from 'next/link';
3 +import { t } from '@/i18n';
4 +import { cn } from '@/lib/cn';
5 +import { displayValue, formatPeriod } from '@/lib/format';
6 +import { routes } from '@/lib/site';
7 +import type { MetricValue } from '@/lib/types';
8 +import { ChangeChip } from '@/components/data/change-chip';
9 +import { payloadFor, type MetricCountry } from '@/components/data/metric';
10 +import { useProvenance } from '@/components/data/provenance-context';
11 +import { RankBadge } from '@/components/data/rank-badge';
12 +
13 +/**
14 + * Latest values of a subtopic, shown while its charts are collapsed on a long topic page: name · value · period
15 + * (highlighted when ≥ 2 years older than the newest value of the block) · change · world rank. Value → provenance.
16 + */
17 +export function TopicCompact({ rows, country, regionName }: { rows: MetricValue[]; country: MetricCountry; regionName?: string | null }) {
18 + const { open } = useProvenance();
19 + const latestYear = Math.max(0, ...rows.map((m) => m.year ?? 0));
20 + return (
21 + <div className="min-w-0">
22 + <p className="mb-1 text-xs text-ink-3">{t('topic.collapsedHint')}</p>
23 + <ul className="divide-y divide-rule border-b border-rule text-sm">
24 + {rows.map((m) => {
25 + const stale = m.year != null && latestYear - m.year >= 2;
26 + return (
27 + <li key={m.indicator} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 py-1 sm:grid-cols-[minmax(0,1fr)_auto_minmax(5rem,auto)_minmax(6rem,auto)]">
28 + <Link href={routes.indicator(m.indicator)} className="link-quiet inline-flex min-h-[44px] min-w-0 items-center text-ink md:min-h-[36px]">
29 + <span className="truncate">{m.indicator_name ?? m.indicator}</span>
30 + </Link>
31 + <button type="button" onClick={() => open(payloadFor(m, country))} className="tnum -mx-1 inline-flex min-h-[44px] items-baseline justify-end gap-1.5 rounded-sm px-1 text-right hover:bg-surface-2 md:min-h-[36px]" aria-label={`${m.indicator_name ?? m.indicator}: ${displayValue(m.value, m, m.formatted)} (${formatPeriod(m.period, m.frequency)}). ${t('common.openProvenance')}`}>
32 + <span className="font-medium text-ink">{displayValue(m.value, m, m.formatted)}</span>
33 + <span className={cn('text-2xs', stale ? 'rounded-xs bg-surface-2 px-1 text-ink-2' : 'text-ink-3')}>{formatPeriod(m.period, m.frequency)}</span>
34 + </button>
35 + <span className="hidden justify-end sm:flex">
36 + <ChangeChip change={m.change} spec={m} showVs={false} />
37 + </span>
38 + <span className="hidden min-w-0 justify-end truncate text-right sm:flex">
39 + <RankBadge rank={m} regionName={regionName} compact />
40 + </span>
41 + </li>
42 + );
43 + })}
44 + </ul>
45 + </div>
46 + );
47 +}
modified apps/web/src/components/explore/copy-button.tsx +7 −4
@@ -29,14 +29,17 @@ export function CopyButton({ text, className, label }: { text: string; className
29 29 );
30 30 }
31 31
32 −/** Monospace code block with a copy button in the corner. */
32 +/** Monospace code block with a thin toolbar (language · copy) above the scrollable code — the button never covers code. */
33 33 export function CodeBlock({ code, className, lang }: { code: string; className?: string; lang?: string }) {
34 34 return (
35 − <div className={cn('relative min-w-0 rounded-sm border border-rule bg-surface', className)}>
36 − <pre className="overflow-x-auto p-3 pr-20 font-mono text-xs leading-relaxed text-ink" data-lang={lang}>
35 + <div className={cn('min-w-0 rounded-sm border border-rule bg-surface', className)}>
36 + <div className="flex items-center justify-between gap-2 border-b border-rule px-2 py-1">
37 + <span className="font-mono text-2xs uppercase tracking-wide text-ink-3">{lang ?? ''}</span>
38 + <CopyButton text={code} className="min-h-[32px] border-0" />
39 + </div>
40 + <pre className="overflow-x-auto p-3 font-mono text-xs leading-relaxed text-ink" data-lang={lang}>
37 41 <code>{code}</code>
38 42 </pre>
39 − <CopyButton text={code} className="absolute right-2 top-2 bg-surface" />
40 43 </div>
41 44 );
42 45 }
modified apps/web/src/components/indicators/directory.tsx +2 −1
@@ -43,7 +43,8 @@ export function IndicatorDirectory({ items, topicMembers, initialTopic }: { item
43 43 // eslint-disable-next-line react-hooks/exhaustive-deps
44 44 }, [topic, ql, items, topicMembers, bySlug]);
45 45
46 − const shown = sections.reduce((a, s) => a + s.rows.length, 0);
46 + // Unique indicators shown: an indicator can belong to several topics, so summing section rows over-counts ("302 of 261").
47 + const shown = new Set(sections.flatMap((s) => s.rows.map((r) => r.slug))).size;
47 48
48 49 const choose = (id: string | null) => {
49 50 setTopic(id);
modified apps/web/src/components/rankings/ranking-view.tsx +12 −7
@@ -5,12 +5,13 @@ import { usePathname, useRouter } from 'next/navigation';
5 5 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6 6 import { t } from '@/i18n';
7 7 import { cn } from '@/lib/cn';
8 −import { formatValue, grouped, isNum, ordinal } from '@/lib/format';
8 +import { displayValue, grouped, isNum, ordinal } from '@/lib/format';
9 9 import { rankingQuery, type RankingState } from '@/lib/ranking-state';
10 10 import { routes } from '@/lib/site';
11 11 import type { RankingResponse, RankingRow } from '@/lib/types';
12 12 import type { CountryLite, RegionItem } from '@/lib/types-compare';
13 13 import { MARK } from '@/components/charts/palette';
14 +import { staleYear } from '@/components/charts/ranked-bars';
14 15 import { pointsFromSpark } from '@/components/charts/scales';
15 16 import { Sparkline } from '@/components/charts/sparkline';
16 17 import { CountryTypeahead } from '@/components/compare/country-picker';
@@ -52,6 +53,8 @@ export function RankingView({ data, regions, countries, state }: { data: Ranking
52 53 }, [q, state.q, setState]);
53 54
54 55 const rows = data.rows;
56 + // Freshness honesty: rows ≥ 2 years older than the ranking year show their year next to the value.
57 + const refYear = useMemo(() => Math.max(data.year_used ?? 0, ...rows.map((r) => r.year ?? 0)), [rows, data.year_used]);
55 58 const max = useMemo(() => Math.max(0, ...rows.map((r) => (isNum(r.value) ? Math.abs(r.value) : 0))), [rows]);
56 59 const ql = q.trim().toLowerCase();
57 60 const filtered = useMemo(() => (ql ? rows.filter((r) => (r.country.name ?? '').toLowerCase().includes(ql) || r.country.id.toLowerCase() === ql || (r.country.region_name ?? '').toLowerCase().includes(ql)) : rows), [rows, ql]);
@@ -182,7 +185,7 @@ export function RankingView({ data, regions, countries, state }: { data: Ranking
182 185 {/* Pinned highlighted row */}
183 186 {highlightRow ? (
184 187 <div className="mb-2 border-y border-accent/40 bg-accent-soft/40">
185 − <Row r={highlightRow} max={max} spec={ind} highlight onValue={() => open(payloadOf(highlightRow))} />
188 + <Row r={highlightRow} max={max} spec={ind} refYear={refYear} highlight onValue={() => open(payloadOf(highlightRow))} />
186 189 <button type="button" onClick={() => setJump(true)} className="inline-flex min-h-[36px] items-center px-1 text-xs text-accent hover:underline">
187 190 {t('ranking.jumpToHighlight', { name: highlightRow.country.name ?? highlightRow.country.id })} ↓
188 191 </button>
@@ -205,7 +208,7 @@ export function RankingView({ data, regions, countries, state }: { data: Ranking
205 208 {filtered.length === 0 ? <p className="py-10 text-center text-sm text-ink-3">{ql ? t('ranking.noMatch', { q }) : t('ranking.empty')}</p> : null}
206 209 <ol ref={listRef} className="divide-y divide-rule" role="list">
207 210 {visible.map((r) => (
208 − <Row key={r.country.id} r={r} max={max} spec={ind} highlight={highlightRow?.country.id === r.country.id} onValue={() => open(payloadOf(r))} />
211 + <Row key={r.country.id} r={r} max={max} spec={ind} refYear={refYear} highlight={highlightRow?.country.id === r.country.id} onValue={() => open(payloadOf(r))} />
209 212 ))}
210 213 </ol>
211 214 {filtered.length > shown ? (
@@ -244,11 +247,12 @@ export function RankingView({ data, regions, countries, state }: { data: Ranking
244 247 );
245 248 }
246 249
247 −function Row({ r, max, spec, highlight, onValue }: { r: RankingRow; max: number; spec: RankingResponse['indicator']; highlight?: boolean; onValue: () => void }) {
250 +function Row({ r, max, spec, refYear, highlight, onValue }: { r: RankingRow; max: number; spec: RankingResponse['indicator']; refYear: number; highlight?: boolean; onValue: () => void }) {
251 + const stale = staleYear(r.year, refYear);
248 252 const pct = isNum(r.value) && max > 0 ? Math.max(0, (Math.abs(r.value) / max) * 100) : 0;
249 253 const points = pointsFromSpark(r.sparkline);
250 254 const dir = r.change_1y?.abs == null ? null : r.change_1y.abs > 0 ? 'up' : r.change_1y.abs < 0 ? 'down' : 'flat';
251 − const value = r.formatted ?? formatValue(r.value, spec);
255 + const value = displayValue(r.value, spec, r.formatted);
252 256 const worldRank = isNum(r.rank_world) && isNum(r.n_world) ? t('ranking.worldRank', { rank: ordinal(r.rank_world), n: grouped(r.n_world) }) : null;
253 257 return (
254 258 <li data-country={r.country.id} className={cn('py-2 sm:py-1.5', highlight && 'bg-accent-soft/40')}>
@@ -268,8 +272,9 @@ function Row({ r, max, spec, highlight, onValue }: { r: RankingRow; max: number;
268 272 <div className={cn('h-full', highlight ? 'bg-accent' : 'bg-series-1')} style={{ width: `${pct}%`, minWidth: isNum(r.value) ? 2 : 0, borderRadius: `0 ${MARK.barRadius}px ${MARK.barRadius}px 0`, opacity: highlight ? 1 : 0.85 }} />
269 273 </div>
270 274 </div>
271 − <button type="button" onClick={onValue} className="tnum -mx-1 inline-flex min-h-[44px] items-center justify-end rounded-sm px-1 text-right text-sm font-medium text-ink hover:bg-surface-2 sm:min-h-[36px]" aria-label={`${r.country.name}: ${value} (${r.year ?? ''}). ${t('common.openProvenance')}`}>
272 − {value}
275 + <button type="button" onClick={onValue} className="tnum -mx-1 inline-flex min-h-[44px] flex-col items-end justify-center rounded-sm px-1 text-right text-sm font-medium leading-tight text-ink hover:bg-surface-2 sm:min-h-[36px]" aria-label={`${r.country.name}: ${value} (${r.year ?? ''}). ${t('common.openProvenance')}`}>
276 + <span>{value}</span>
277 + {stale ? <span className="tnum rounded-xs bg-surface-2 px-1 text-2xs font-normal text-ink-2">{stale}</span> : null}
273 278 </button>
274 279 <span className="hidden justify-end sm:flex">
275 280 <ChangeChip change={r.change_1y} spec={spec} showVs={false} />
modified apps/web/src/components/rankings/top3-preview.tsx +14 −9
@@ -2,8 +2,9 @@
2 2 import { useEffect, useRef, useState } from 'react';
3 3 import { t } from '@/i18n';
4 4 import { clientCompare } from '@/lib/client-api-compare';
5 −import { formatValue } from '@/lib/format';
5 +import { displayValue } from '@/lib/format';
6 6 import type { IndicatorCard, RankingRow } from '@/lib/types';
7 +import { staleYear } from '@/components/charts/ranked-bars';
7 8
8 9 /** Small client-side fetch queue: at most 3 previews in flight, and no more requests after a 429. */
9 10 const queue: Array<() => void> = [];
@@ -73,14 +74,18 @@ export function Top3Preview({ slug, spec, initial }: { slug: string; spec: Indic
73 74 <span className="text-ink-3">{t('rankings.preview.na')}</span>
74 75 ) : (
75 76 <ol className="flex flex-wrap gap-x-3 gap-y-0.5">
76 − {rows.slice(0, 3).map((r) => (
77 − <li key={r.country.id} className="inline-flex items-center gap-1">
78 − <span className="tnum text-ink-3">{r.rank}</span>
79 − <span aria-hidden>{r.country.flag}</span>
80 − <span className="sr-only">{r.country.name}</span>
81 − <span className="tnum text-ink">{r.formatted ?? formatValue(r.value, spec)}</span>
82 − </li>
83 − ))}
77 + {rows.slice(0, 3).map((r) => {
78 + const stale = staleYear(r.year, Math.max(0, ...rows.map((x) => x.year ?? 0)));
79 + return (
80 + <li key={r.country.id} className="inline-flex items-center gap-1">
81 + <span className="tnum text-ink-3">{r.rank}</span>
82 + <span aria-hidden>{r.country.flag}</span>
83 + <span className="sr-only">{r.country.name}</span>
84 + <span className="tnum text-ink">{displayValue(r.value, spec, r.formatted)}</span>
85 + {stale ? <span className="tnum text-2xs text-ink-3">{stale}</span> : null}
86 + </li>
87 + );
88 + })}
84 89 </ol>
85 90 )}
86 91 </div>
modified apps/web/src/components/regions/member-ranking.tsx +5 −3
@@ -7,7 +7,7 @@ import { cn } from '@/lib/cn';
7 7 import { routes } from '@/lib/site';
8 8 import type { IndicatorCard, RankingResponse } from '@/lib/types';
9 9 import type { RegionResponse } from '@/lib/types-explore';
10 −import { RankedBars, rankedRowFromCountry, type RankedBarRow } from '@/components/charts/ranked-bars';
10 +import { RankedBars, rankedRowFromCountry, staleYear, type RankedBarRow } from '@/components/charts/ranked-bars';
11 11 import { EmptyState } from '@/components/data/empty-state';
12 12
13 13 /**
@@ -42,10 +42,12 @@ export function MemberRanking({ groupSlug, groupName, initial, options }: { grou
42 42 // `undefined` = not fetched yet (loading), `null` = fetch failed.
43 43 const fetched = isDefault ? undefined : cache[indicator];
44 44 const spec: IndicatorCard = fetched ? fetched.indicator : all.find((o) => o.slug === indicator) ?? initial.indicator;
45 + const srcRows = isDefault ? initial.rows : fetched?.rows ?? [];
46 + const refYear = Math.max(0, ...srcRows.map((r) => r.year ?? 0));
45 47 const rows: RankedBarRow[] = isDefault
46 − ? initial.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank, r.rank_world && r.n_world ? t('region.ranking.worldRank', { rank: r.rank_world, n: r.n_world }) : null))
48 + ? initial.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank, r.rank_world && r.n_world ? t('region.ranking.worldRank', { rank: r.rank_world, n: r.n_world }) : null, staleYear(r.year, refYear)))
47 49 : fetched
48 − ? fetched.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank, r.change_1y?.formatted ?? null))
50 + ? fetched.rows.map((r) => rankedRowFromCountry(r.country, r.value, r.rank, r.change_1y?.formatted ?? null, staleYear(r.year, refYear)))
49 51 : [];
50 52 const prov = isDefault ? initial.rows[0]?.provenance ?? null : fetched?.rows[0]?.provenance ?? null;
51 53 const empty = (!isDefault && fetched === null) || (rows.length === 0 && !loading && (isDefault || fetched !== undefined));
modified apps/web/src/components/regions/members-table.tsx +6 −1
@@ -113,13 +113,18 @@ export function MembersTable({ members, indicators }: { members: RegionMember[];
113 113 <span aria-hidden className="text-lg leading-none">{m.flag}</span>
114 114 <span className="truncate">{m.name}</span>
115 115 </Link>
116 + {/* Values never truncate: the year sits on its own line under the value. */}
116 117 <dl className="mt-1 grid grid-cols-3 gap-x-3">
117 118 {cols.slice(0, 3).map((c) => {
118 119 const v = m.values[c.slug];
120 + const has = !!v && isNum(v.value);
119 121 return (
120 122 <div key={c.slug} className="min-w-0">
121 123 <dt className="truncate text-2xs text-ink-3">{c.short_name ?? c.name}</dt>
122 − <dd className="tnum truncate text-sm text-ink">{v && isNum(v.value) ? formatValue(v.value, c) : t('region.members.noValue')}</dd>
124 + <dd className="tnum text-sm leading-tight text-ink">
125 + <span className="block break-words">{has ? formatValue(v.value, c) : t('region.members.noValue')}</span>
126 + {has && v.year ? <span className="block text-2xs text-ink-3">{v.year}</span> : null}
127 + </dd>
123 128 </div>
124 129 );
125 130 })}
modified apps/web/src/i18n/en.ts +11 −0
@@ -76,6 +76,7 @@ export const en = {
76 76
77 77 // --- common
78 78 'common.noData': 'No data',
79 + 'common.other': 'Other',
79 80 'common.noDataLong': 'No data available',
80 81 'common.loading': 'Loading…',
81 82 'common.seeAll': 'See all',
@@ -338,6 +339,16 @@ export const en = {
338 339 'topic.otherTopics': 'Other topics',
339 340 'topic.notFound': 'Unknown topic',
340 341 'topic.overview': 'Overview',
342 + 'topic.jump': 'Jump to a subtopic',
343 + 'topic.collapsedHint': 'Latest values — expand for the charts.',
344 +
345 + // --- collapsible subtopic groups (compare tabs, topic pages)
346 + 'group.count': '{n} indicators',
347 + 'group.expand': 'Show charts',
348 + 'group.collapse': 'Hide charts',
349 + 'group.jump': 'Jump to',
350 + 'group.latestCol': 'Latest',
351 + 'group.yearCol': 'Year',
341 352
342 353 // --- empty state
343 354 'empty.title': 'No data for {indicator} in {country}.',
added apps/web/src/lib/anchors.ts +4 −0
@@ -0,0 +1,4 @@
1 +/** Stable anchor id for a subtopic name (plain module: callable from server and client components). */
2 +export function subtopicAnchor(prefix: string, subtopic: string): string {
3 + return `${prefix}-${subtopic.toLowerCase().replace(/&/g, 'and').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
4 +}
modified apps/web/src/lib/format.ts +17 −1
@@ -78,8 +78,9 @@ export function formatValue(
78 78 const abs = Math.abs(value);
79 79 switch (format) {
80 80 case 'currency': {
81 + // Always carries the unit prefix ("US$55.7k", "intl $66.7k"); compact from 10k, grouped below.
81 82 const prefix = currencyPrefix(spec);
82 − const body = abs >= 1e5 ? compact(value) : grouped(value, abs < 10 ? 2 : 0);
83 + const body = abs >= 1e4 ? compact(value) : grouped(value, abs < 10 ? 2 : abs < 1000 ? 1 : 0);
83 84 const suffix = spec.unit_short && /\/h$/.test(spec.unit_short) ? '/h' : '';
84 85 return `${prefix}${body}${suffix}`;
85 86 }
@@ -120,6 +121,21 @@ export function formatValue(
120 121 }
121 122 }
122 123
124 +/**
125 + * Display string for an API value object. Currencies ALWAYS go through `formatValue` (guaranteed unit
126 + * prefix, identical on server and client); other formats prefer the API's `formatted` string because it
127 + * carries the indicator precision that `MetricValue`/`RankingRow` do not expose. Use this instead of
128 + * `m.formatted ?? formatValue(m.value, m)`.
129 + */
130 +export function displayValue(value: number | null | undefined, spec: FormatSpec, formatted?: string | null): string {
131 + if (!isNum(value)) return NA;
132 + // Unit-bearing formats: the client formatter guarantees the right unit token (the API has shipped "12.3k t"
133 + // for a million-tonnes indicator and bare "55.7k" for currencies).
134 + if (CLIENT_FORMATS.has(spec.format ?? 'number') || !formatted) return formatValue(value, spec);
135 + return formatted;
136 +}
137 +const CLIENT_FORMATS = new Set<string>(['currency', 'tonnes', 'kwh']);
138 +
123 139 /** Units whose change is expressed in points rather than percent. */
124 140 export function isPointsUnit(spec: FormatSpec): boolean {
125 141 return spec.format === 'percent' || spec.format === 'index' || spec.format === 'ratio' || spec.format === 'years';
modified apps/web/src/lib/topics.ts +2 −2
@@ -53,7 +53,7 @@ export const HEADLINE_INDICATORS = [
53 53 'unemployment-rate',
54 54 'life-expectancy',
55 55 'median-age',
56 − 'government-debt-pct-gdp',
56 + 'general-government-gross-debt-pct-gdp',
57 57 'co2-per-capita',
58 58 'renewable-electricity-share',
59 59 'internet-users',
@@ -69,7 +69,7 @@ export const HEADLINE_TOPIC: Record<(typeof HEADLINE_INDICATORS)[number], TopicI
69 69 'unemployment-rate': 'labor',
70 70 'life-expectancy': 'health',
71 71 'median-age': 'population',
72 − 'government-debt-pct-gdp': 'government',
72 + 'general-government-gross-debt-pct-gdp': 'government',
73 73 'co2-per-capita': 'climate',
74 74 'renewable-electricity-share': 'energy',
75 75 'internet-users': 'digital',
modified apps/web/src/lib/types.ts +3 −1
@@ -508,11 +508,13 @@ export interface HomeListRow {
508 508 provenance: Provenance | null;
509 509 }
510 510
511 −export type HomeListKey = 'largest_economies' | 'fastest_population_growth' | 'highest_life_expectancy' | 'energy_transition_leaders' | 'highest_gdp_per_capita_ppp' | 'lowest_unemployment';
511 +export type HomeListKey = 'largest_economies' | 'fastest_population_growth' | 'fastest_gdp_growth' | 'highest_life_expectancy' | 'energy_transition_leaders' | 'highest_gdp_per_capita_ppp' | 'lowest_unemployment';
512 512
513 513 export interface HomeList {
514 514 title: string;
515 515 description: string | null;
516 + /** e.g. "Countries above 1M inhabitants" — optional, rendered under the list title. */
517 + filter_note?: string | null;
516 518 indicator: IndicatorCard;
517 519 sort: 'asc' | 'desc';
518 520 rows: HomeListRow[];
519 521