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>
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 | +[2m - 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 | +[2m - navigat | |
| 73 | +FAILED /indicators/life-expectancy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators/life-expectancy | |
| 74 | +Call log: | |
| 75 | +[2m - navi | |
| 76 | +FAILED /regions/oecd: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions/oecd | |
| 77 | +Call log: | |
| 78 | +[2m - navigating to "htt | |
| 79 | +FAILED /explore: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/explore | |
| 80 | +Call log: | |
| 81 | +[2m - navigating to "http://l | |
| 82 | +FAILED /changes: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/changes | |
| 83 | +Call log: | |
| 84 | +[2m - navigating to "http://l | |
| 85 | +FAILED /sources: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/sources | |
| 86 | +Call log: | |
| 87 | +[2m - navigating to "http://l | |
| 88 | +FAILED /methodology: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/methodology | |
| 89 | +Call log: | |
| 90 | +[2m - navigating to "http | |
| 91 | +FAILED /api: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/api | |
| 92 | +Call log: | |
| 93 | +[2m - navigating to "http://local | |
| 94 | +FAILED /data: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/data | |
| 95 | +Call log: | |
| 96 | +[2m - navigating to "http://loca | |
| 97 | +FAILED /indicators: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators | |
| 98 | +Call log: | |
| 99 | +[2m - navigating to "http: | |
| 100 | +FAILED /regions: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions | |
| 101 | +Call log: | |
| 102 | +[2m - navigating to "http://l | |
| 103 | +FAILED /compare: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare | |
| 104 | +Call log: | |
| 105 | +[2m - navigating to "http://l | |
| 106 | +FAILED /: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/ | |
| 107 | +Call log: | |
| 108 | +[2m - navigating to "http://localhos | |
| 109 | +FAILED /countries: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/countries | |
| 110 | +Call log: | |
| 111 | +[2m - navigating to "http:/ | |
| 112 | +FAILED /countries/canada: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/countries/canada | |
| 113 | +Call log: | |
| 114 | +[2m - 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 | +[2m - 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 | +[2m - 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 | +[2m - navigat | |
| 129 | +FAILED /indicators/life-expectancy: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators/life-expectancy | |
| 130 | +Call log: | |
| 131 | +[2m - navi | |
| 132 | +FAILED /regions/oecd: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions/oecd | |
| 133 | +Call log: | |
| 134 | +[2m - navigating to "htt | |
| 135 | +FAILED /explore: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/explore | |
| 136 | +Call log: | |
| 137 | +[2m - navigating to "http://l | |
| 138 | +FAILED /changes: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/changes | |
| 139 | +Call log: | |
| 140 | +[2m - navigating to "http://l | |
| 141 | +FAILED /sources: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/sources | |
| 142 | +Call log: | |
| 143 | +[2m - navigating to "http://l | |
| 144 | +FAILED /methodology: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/methodology | |
| 145 | +Call log: | |
| 146 | +[2m - navigating to "http | |
| 147 | +FAILED /api: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/api | |
| 148 | +Call log: | |
| 149 | +[2m - navigating to "http://local | |
| 150 | +FAILED /data: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/data | |
| 151 | +Call log: | |
| 152 | +[2m - navigating to "http://loca | |
| 153 | +FAILED /indicators: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/indicators | |
| 154 | +Call log: | |
| 155 | +[2m - navigating to "http: | |
| 156 | +FAILED /regions: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/regions | |
| 157 | +Call log: | |
| 158 | +[2m - navigating to "http://l | |
| 159 | +FAILED /compare: Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8290/compare | |
| 160 | +Call log: | |
| 161 | +[2m - 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 | |