SPB Git

spb/countryatlas

Public

Web app: foundation, brand, country pages, compare, rankings, indicators, regions, explore, changes, data, sources, methodology, API docs, admin

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 11, 2026) parent f8d6c18 Browse files

339 changed files +11,249 −329

339 changed files
added apps/web/README.md +219 −0
@@ -0,0 +1,219 @@
1 +# CountryAtlas — web app (`@countryatlas/web`)
2 +
3 +Next.js 16.3.4 · React 19.2.8 · TypeScript strict · Tailwind v4 · own SVG chart kit (d3-geo/scale/shape only for math).
4 +Mobile-first, editorial, provenance-first. The country is the primary unit of navigation.
5 +
6 +This README is the contract for whoever adds pages next (compare / rankings / indicators / regions / sources /
7 +methodology / api / admin). Reuse what is here; do not add a second design system.
8 +
9 +## Run
10 +
11 +```bash
12 +pnpm install # from the repo root (pnpm workspace: apps/*)
13 +cd apps/web
14 +pnpm dev # http://localhost:8290 — API_URL from .env.local (default http://127.0.0.1:8291)
15 +pnpm typecheck && pnpm build && pnpm start # production: next start -p 8290 -H 0.0.0.0
16 +```
17 +
18 +Env (see `.env.example`): `API_URL` (server-side base of the FastAPI service, loopback), `NEXT_PUBLIC_SITE_URL`
19 +(canonical/OG/sitemap). Browser code never talks to the API host directly: `next.config.ts` rewrites
20 +`/api/v1/:path*` → `${API_URL}/api/v1/:path*`.
21 +
22 +Without the real snapshot you can run the **fixture API** (synthetic, deterministic, every provenance says
23 +"Fixture (mock data)"): `cd <repo> && .venv/bin/python apps/web/qa/mock_api.py` (port 8299) and set
24 +`API_URL=http://127.0.0.1:8299` in `apps/web/.env.local`. Never point production at it.
25 +
26 +Real API once the DB exists: `cd <repo> && CA_DATA_DIR=~/countryatlas-data .venv/bin/python -m uvicorn countryatlas.api.main:app --port 8293`
27 +then `API_URL=http://127.0.0.1:8293`.
28 +
29 +## QA
30 +
31 +```bash
32 +node qa/screens.mjs [BASE_URL] # screenshots 320/360/375/390/414/430/1280/1440 → qa/screens/, report.json:
33 + # overflow (scrollWidth>clientWidth), tap targets <44px, tab-bar/footer overlap, CLS, console errors
34 +node qa/segments.mjs [BASE_URL] # viewport-sized segments (top + scroll offsets) → qa/screens/seg/ for visual review
35 +node qa/favicons.mjs # re-rasterise src/app/icon.svg → icon.png (512) + apple-icon.png (180)
36 +```
37 +
38 +Playwright comes from `~/Desktop/uqo-eval/node_modules` (symlinked into `node_modules/playwright[-core]`; ESM ignores
39 +`NODE_PATH`). Last run (2026-09-11, prod build, fixture API): 32 page×width renders, **0 overflow, 0 console errors,
40 +0 layout shift**, only the sr-only skip link below 44 px. Unknown country / topic return a **true HTTP 404**.
41 +
42 +**Soft-404 gotcha (fixed here, keep it that way):** a `loading.tsx` that wraps a segment which calls `notFound()`
43 +streams a 200 shell before the 404 is known. Loading files therefore live only in route groups that never contain
44 +dynamic segments: `app/(home)/loading.tsx`, `app/countries/(list)/loading.tsx`. Do **not** add `loading.tsx` at
45 +`app/`, `app/countries/` or inside `[slug]`. Same rule for your new dynamic routes.
46 +
47 +## Layout of `src/`
48 +
49 +```
50 +app/
51 + layout.tsx fonts, metadata template "%s — CountryAtlas", theme boot script, providers, header/footer/tab bar
52 + globals.css design tokens (@theme inline) + chart chrome classes
53 + (home)/page.tsx / countries/(list)/page.tsx /countries
54 + countries/[slug]/page.tsx + [topic]/page.tsx + opengraph-image.tsx (dynamic per-country OG)
55 + indicators/page.tsx (?topic=) indicators/[slug]/page.tsx + opengraph-image.tsx regions/page.tsx regions/[slug]/page.tsx
56 + explore/ changes/ data/ sources/ + sources/[id]/ methodology/ api/ (developer landing, live examples)
57 + admin/login/page.tsx + admin/actions.ts (server actions) + admin/(panel)/{layout,page,coverage,runs,issues,raw} (cookie gate)
58 + opengraph-image.tsx twitter-image.tsx og-shared.tsx default social image (static)
59 + icon.svg icon.png apple-icon.png manifest.ts robots.ts sitemap.ts not-found.tsx error.tsx
60 +lib/
61 + api.ts server-only typed fetch: api.home() api.countries() api.country(id) api.countryTopic() api.countrySeries()
62 + api.countryChanges/Events/Similar/Insights/Dna() api.indicatorMap() api.ranking() api.search() api.changes()
63 + request<T>(path, query, {revalidate}) for new endpoints · ApiError · safe() · isNotBuilt() · isNotFound()
64 + client-api.ts browser fetches to same-origin /api/v1 (search, series, similar) — keep small
65 + api-explore.ts / client-api-explore.ts / types-explore.ts indicators, trend, series bundle, regions, changes, sources,
66 + methodology, admin payloads (+ browser fetches for the map year slider, trend groups, compare, similar)
67 + admin-auth.ts CA_ADMIN_TOKEN gate: constant-time compare, httpOnly cookie `ca_admin` (sha256 of the token, path /admin)
68 + admin-api.ts X-Admin-Token fetches (no-store) to ADMIN_API_URL ?? API_URL — server components/actions only
69 + types.ts mirror of src/countryatlas/api/schemas.py (see "Types")
70 + format.ts formatValue / formatChange / formatPeriod / formatRank / formatDate / relativeFreshness / formatTick / compact
71 + site.ts SITE_URL, SITE_NAME, TAGLINE, routes.* (every internal href goes through routes)
72 + topics.ts static mirror of registry/topics.yaml (19 topics, HEADLINE_INDICATORS, HEADLINE_TOPIC)
73 + regions.ts WB_REGIONS (7) + INCOME_GROUPS (4) with slugs/short names
74 + map-geo.ts world-atlas 110m → Equal Earth paths (cached per process), classIndex(); iso-numeric.ts numeric→ISO3
75 + og.tsx Satori-safe building blocks for OG images (OgFrame, OgWordmark, OgMark, OgMeridians)
76 + fonts.ts Inter (--font-ui) + Newsreader (--font-display) via next/font/google; fonts.system.ts = offline fallback
77 + severity.ts 0–1 severity → high/medium/low + label
78 +i18n/
79 + en.ts the dictionary (flat dotted keys) · index.ts: t(key, params), tOpt(dynamicKey, fallback), LOCALE='en-US'
80 +components/
81 + brand/Logo.tsx Logo variant="full|mark|wordmark", LogoMark, Wordmark, logoMarkSvg()
82 + layout/ SiteHeader (PRIMARY_NAV), NavLinks, MobileTabBar, SiteFooter (+ FooterCredits), ThemeToggle,
83 + SearchProvider/useSearch/useOpenSearch, SearchTrigger (field|icon|hero), SearchDialog (lazy)
84 + charts/ see "Chart kit"
85 + data/ Section, Metric (+MetricGrid, payloadFor), ChangeChip, RankBadge, FreshnessBadge/StatusBadge,
86 + TopicNav, CountryChip, DataTable, EmptyState/NotBuiltState, BottomSheet, ChangeList,
87 + ProvenanceProvider/useProvenance/ProvenanceSheet
88 + home/ Hero, SnapshotStrip, RegionChips, CompareTeaser (COMPARE_PRESETS), TopicsGrid, IndicatorList
89 + countries/directory.tsx client filter/sort/A–Z directory (URL-synced)
90 + indicators/ IndicatorDirectory (topic nav + filter), IndicatorMap (year slider → /map?year=), IndicatorTrend
91 + (group selector → /trend), IndicatorCompare (≤ 5 countries → /series), map-geometry.ts (server)
92 + regions/ MembersTable (sortable), MemberRanking (indicator picker → /rankings?group=), MemberMap (static SVG)
93 + explore/ EntityPicker (typeahead country|indicator), EntityPickerNav, CopyButton/CodeBlock, PageHeader,
94 + DownloadPicker, SimilarityPlayground
95 + changes/changes-feed.tsx global feed: kind/indicator/region/severity filters (BottomSheet on phones), grouped by year
96 + methodology/toc.tsx sticky TOC with IntersectionObserver highlight
97 + admin/ AdminNav, admin-ui (AdminSection, Kv, AdminTable, Pill), CoverageTable (sortable)
98 + country/ CountryHeader, ShareButton, SimilarPanel, KeyFacts, Timeline, CountryTopicsGrid, IndicatorRow
99 +```
100 +
101 +## Conventions (follow these on new pages)
102 +
103 +* **Server components fetch; client components interact.** One `await` for the primary payload, then
104 + `Promise.all([safe(api.x()), …])` for optional panels — each tolerates failure (`null` → EmptyState). No client
105 + fetch waterfalls. `export const revalidate = 900` on pages.
106 +* **Errors:** `api.*` throws `ApiError`. Pattern used by every page:
107 + ```ts
108 + try { data = await api.thing(id) } catch (e) { if (isNotFound(e)) notFound(); if (isNotBuilt(e)) return <NotBuiltState/>; throw e }
109 + ```
110 + `isNotBuilt` covers 503 ("Data not built yet") **and** an unreachable API — the site renders a calm state, never crashes.
111 +* **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.
115 +* **No cards.** `Section` (rule above, editorial heading, subtitle, right-side actions) + `divide-y divide-rule` lists.
116 + Grid children that contain long text need `min-w-0` (Section already has it).
117 +* **Never invent numbers.** Missing → `EmptyState` / "No data" (`t('common.noData')`).
118 +* **Metadata:** `generateMetadata` uses the API; titles follow `"{Name} Data & Statistics"`; canonical via `routes.*`.
119 +* **Provenance everywhere:** any value → `useProvenance().open(payload)`. Build payloads with `payloadFor(metricValue,
120 + country)` or hand-roll `{ indicator: {slug,name,format,unit,…}, value: {value, period, provenance}, country }`.
121 + Chart source lines take the same `payload` prop.
122 +* **Touch targets ≥ 44 px on mobile** (`tap` utility or `min-h-[44px] md:min-h-[32px]`), `h-11 md:h-9` for chips.
123 +* **Mobile:** bottom tab bar is fixed; `<body>` reserves `pb-[calc(56px+env(safe-area-inset-bottom))] md:pb-0`.
124 + Filters on phones → `BottomSheet side="center"`. Horizontal scrollers use `scrollbar-none -mx-4 px-4`.
125 +* **Reordering between breakpoints via CSS `order` is forbidden** for page sections (DOM order = visual order); it is
126 + acceptable only inside a single small widget row (SimilarPanel bar).
127 +
128 +## Design tokens (`globals.css`)
129 +
130 +Raw values on `:root` / `.dark` (class toggled by `ThemeToggle`, stored in `localStorage['ca-theme']`, system default
131 +applied before paint by `THEME_BOOT`). Tailwind reads them through `@theme inline`:
132 +
133 +| Token | Utility | Light / Dark |
134 +|---|---|---|
135 +| paper / surface / surface-2 | `bg-paper` `bg-surface` `bg-surface-2` | #fbfaf7 / #151513 · #fff / #1c1c1a · #f3f1ec / #24241f |
136 +| ink / ink-2 / ink-3 | `text-ink*` | #1a1917 / #f2f0ea · #5c5a55 / #c9c6bd · #8a877f |
137 +| rule / rule-strong | `border-rule` `divide-rule` | #e6e3dc / #2c2b28 · #c9c6bd / #3a3935 |
138 +| accent / accent-soft / accent-ink | `text-accent` `bg-accent-soft` | #1c5cab / #5598e7 · #e8f0fb / #17304f |
139 +| up / down / warn | `text-up` `text-down` | #006300 / #0ca30c · #b02a2a / #e66767 · #9a6a00 / #e0a100 |
140 +| series-1…8 | `bg-series-1`, `seriesVar(i)` | validated dataviz palette (blue, orange, aqua, yellow, magenta, green, violet, red) |
141 +| seq-1…7 | `seqVar(k)` | blue sequential ramp (choropleths); reversed in dark |
142 +| nodata | `no-data-hatch` | hatched grey |
143 +
144 +Palette validated with the dataviz skill's `validate_palette.js` on both surfaces (adjacent ΔE ≥ 8.4, all-pairs OK for
145 +the first 3 slots). Light-mode aqua/yellow/magenta are < 3:1 on paper → charts always ship direct labels + the table
146 +toggle (relief rule). Positive/negative colours are never used alone: `ChangeChip` pairs them with ↑/↓ glyphs.
147 +
148 +Type: `display` (Newsreader) for headings, Inter for UI, scale `text-2xs … text-5xl`; utilities `eyebrow`, `hairline`,
149 +`tap`, `tnum`, `pnum`, `scrollbar-none`, `safe-bottom`, `container-x`, `link-quiet`. Motion respects
150 +`prefers-reduced-motion`.
151 +
152 +## Chart kit (`components/charts/`)
153 +
154 +All charts: SVG, `role="img"` + `<title>/<desc>` with an auto summary (`summary.ts`), a `ChartFrame` with source line
155 +(`SourceLine`, clickable → provenance), accessible data-table toggle, legend only for ≥ 2 series. Colours via CSS vars
156 +(`palette.ts`), fixed slot order — colour follows the entity, never its rank; never more than 8 series (fold/facet).
157 +Marks per dataviz spec (`MARK`): 2 px lines, ≥ 8 px end dots with surface ring, bars ≤ 24 px with 4 px rounded end,
158 +hairline solid grid. Points type: `SeriesPoint {period, year, value, is_forecast}` — build with
159 +`pointsFromSpark(m.sparkline)` (API `[year,value]` pairs) or `pointsFromSeries(series.values)`.
160 +
161 +| Component | Kind | Notes |
162 +|---|---|---|
163 +| `LineChart` / `AreaChart` / `StackedArea` | client | multi-series, dashed `is_forecast`, `log`, crosshair + one tooltip for all series, end labels ≤ 4 series, auto left margin |
164 +| `Sparkline` | server | tiny, no axes, end dot coloured by `direction` |
165 +| `RankedBars` (+`rankedRowFromCountry`) | server, HTML | horizontal bars with flags, `highlightId`, values at the tip |
166 +| `Scatter` | client | bubble `size`, labels for highlighted + extremes only, 24 px hit radius |
167 +| `SlopeChart` | client | two periods, highlighted row in accent |
168 +| `SmallMultiples` | server | grid wrapper (1 → 2 → 3/4 cols) |
169 +| `PopulationPyramid` | server | 0–14 / 15–64 / 65+ stacked bar (ordinal ramp) |
170 +| `DnaRadial` | server | 9 dims 0–100 fingerprint |
171 +| `Choropleth` → `ChoroplethView` | server → client | Equal Earth 960×470, quantile classes from API `legend.breaks`, hatch for no data, hover / tap label / click → country |
172 +
173 +`useMeasure(defaultWidth)` gives responsive widths with a stable SSR width; reserve heights via `height`/`minHeight`.
174 +
175 +## Data components (`components/data/`)
176 +
177 +* `Metric` — label(link) + sparkline, big value (click → provenance), period, `ChangeChip`, `RankBadge`. `MetricGrid`
178 + = 1 col ≤ 360 px, 2 on phones, 3 md, 4 xl.
179 +* `ProvenanceSheet` is mounted once in the layout; open it from anywhere with `useProvenance().open(payload)`.
180 +* `BottomSheet` (native `<dialog>`): `side="drawer"` (right drawer on md+), `"center"`, `"full"`; drag-down to close.
181 +* `DataTable<T>` → definition list under `sm` (no horizontal overflow). `TopicNav` sticky under the header (52/56 px).
182 +* `ChangeList` (feed with kind icon + severity chip) and `Timeline` (events by year) both take `ChangeItem[]`.
183 +* `SiteFooter` renders `<FooterCredits/>` ("Made by Simon-Pierre Boucher · contact@spboucher.ai · Hosted on MacLustr")
184 + — reuse `FooterCredits` on `/sources` and `/api`.
185 +
186 +## Types — `src/lib/types.ts` (extend this file)
187 +
188 +Aligned 2026-09-11 with `src/countryatlas/api/schemas.py`. Responses are flat with `meta`; collections are `items`
189 +(`/countries`, `/changes`, `/events`, `/insights`), `hits` (`/search`), `peers` (`/similar`), `rows` (rankings/home
190 +lists). Value objects: `MetricValue` (headline/topic), `SeriesValue` (series), `RankingRow`, `HomeListRow`; all carry
191 +`provenance`. Sparklines are `SparkPoint = [year, value]`. Pydantic models already defined but **not yet mirrored**
192 +(add them when you build the pages): `CompareResponse`, `CompareSnapshotResponse`, `RankingsListResponse`,
193 +`RankHistoryResponse`, `IndicatorsResponse`, `IndicatorResponse` (+`IndicatorSource`, `WorldLatest`, `RankedValue`),
194 +`TrendResponse`, `RegionsResponse`, `RegionResponse`, `SourcesResponse`, `SourceResponse`, methodology/admin payloads.
195 +Add the matching `api.*` methods in `lib/api.ts` with `request<T>()`, routes in `lib/site.ts`, strings in `i18n/en.ts`,
196 +and sitemap entries in `app/sitemap.ts`.
197 +
198 +## Brand
199 +
200 +`components/brand/Logo.tsx`: a globe ring whose equator is the crossbar of an "A" (two meridian-like legs to one apex).
201 +Strokes only, `currentColor`, monochrome-capable. `src/app/icon.svg` (rounded paper tile) → `icon.png` 512 /
202 +`apple-icon.png` 180 via `qa/favicons.mjs`; `manifest.ts` theme colour #1c5cab. Social images: `app/opengraph-image.tsx`
203 +(+ `twitter-image.tsx`) and `app/countries/[slug]/opengraph-image.tsx` (flag + name + 3 headline values). Satori
204 +gotchas: children of a multi-child `<div>` need `display:flex`; numeric children must be `String()`-ed.
205 +
206 +## Known issues / not in this round
207 +
208 +* `/admin` needs `CA_ADMIN_TOKEN` in the **web** process env (the mld manifest passes it); without it the login page says
209 + the panel is disabled. `ADMIN_API_URL` (optional) points admin fetches at another API instance. QA for the explore
210 + routes: `node qa/shots-explore.mjs` → `qa/screens/explore/` (+ report.json, touch test of the map year slider).
211 +* Sitemap ≈ 4.9k URLs (countries × topics + indicators + regions + sources + rankings) — switch to `generateSitemaps`
212 + before adding another per-country family.
213 +* Search hit types `country_topic` / `country_indicator` render with the Topic/Indicator chip; `/topics/*` URLs the API
214 + may emit for bare topics are mapped to `/indicators?topic=` client-side (`hrefFor`).
215 +* The API `CountrySummary` has no `iso_numeric`; the choropleth uses the static numeric→ISO3 table (`lib/iso-numeric.ts`)
216 + and will prefer `iso_numeric` automatically if the API adds it.
217 +* Topic page renders the first 4 charts with server-fetched history; the rest fetch on scroll (client). With a slow API
218 + the fold could show "Loading…" placeholders of reserved height (no CLS).
219 +* Fixture screenshots contain synthetic values by design; re-run `qa/screens.mjs` against the real API before launch.
added apps/web/qa/admin-explore.mjs +73 −0
@@ -0,0 +1,73 @@
1 +/**
2 + * Admin panel QA: wrong token → error; right token (deploy/.admin-token) → cookie → overview/coverage/runs/issues/raw
3 + * render; "Clear cache" server action round-trips; screenshots at 390 + 1440 → qa/screens/explore/admin_*.png.
4 + * Needs CA_ADMIN_TOKEN on the web process (and ADMIN_API_URL → an API instance with the same token).
5 + * Run: node qa/admin-explore.mjs [BASE_URL]
6 + */
7 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
8 +import { readFileSync } from 'node:fs';
9 +
10 +const BASE = process.argv[2] ?? 'http://localhost:8290';
11 +const OUT = new URL('./screens/explore/', import.meta.url).pathname;
12 +const TOKEN = readFileSync(new URL('../../../deploy/.admin-token', import.meta.url), 'utf8').trim();
13 +
14 +const browser = await chromium.launch();
15 +for (const width of [390, 1440]) {
16 + const mobile = width < 768;
17 + const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, isMobile: mobile, hasTouch: mobile });
18 + const page = await ctx.newPage();
19 + const errors = [];
20 + page.on('pageerror', (e) => errors.push(String(e)));
21 + page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));
22 +
23 + // unauthenticated → redirect to login
24 + let res = await page.goto(BASE + '/admin/coverage', { waitUntil: 'networkidle' });
25 + console.log(width, 'gate:', page.url().endsWith('/admin/login') ? 'redirected to login OK' : `FAIL ${page.url()}`, res?.status());
26 +
27 + // wrong token
28 + await page.fill('input[name=token]', 'nope');
29 + await page.click('button[type=submit]');
30 + await page.waitForURL(/error=1/, { timeout: 60_000 }).catch(() => {});
31 + await page.waitForLoadState('networkidle');
32 + const err = await page.getByText('Invalid token.').count();
33 + const stillNoCookie = !(await ctx.cookies()).some((k) => k.name === 'ca_admin');
34 + console.log(width, 'wrong token:', err && stillNoCookie ? 'error shown, no cookie OK' : `FAIL url=${page.url()} err=${err} noCookie=${stillNoCookie}`);
35 +
36 + // right token
37 + await page.fill('input[name=token]', TOKEN);
38 + await page.click('button[type=submit]');
39 + await page.waitForURL(/\/admin(\?.*)?$/, { timeout: 60_000 });
40 + await page.waitForLoadState('networkidle');
41 + const cookies = await ctx.cookies();
42 + const c = cookies.find((k) => k.name === 'ca_admin');
43 + console.log(width, 'cookie:', c ? `httpOnly=${c.httpOnly} sameSite=${c.sameSite} path=${c.path} value≠token=${c.value !== TOKEN}` : 'MISSING');
44 + for (const p of ['/admin', '/admin/coverage', '/admin/runs', '/admin/issues', '/admin/raw']) {
45 + res = await page.goto(BASE + p, { waitUntil: 'networkidle', timeout: 120_000 });
46 + const h = await page.locator('h2').first().textContent().catch(() => '');
47 + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
48 + await page.screenshot({ path: `${OUT}admin${p.replace(/\//g, '_')}-${width}.png`, fullPage: true });
49 + console.log(width, p.padEnd(16), res?.status(), `h2="${h?.trim()}"`, overflow > 0 ? `OVERFLOW +${overflow}` : 'ok');
50 + }
51 + // raw files for the latest run id from the runs page
52 + await page.goto(BASE + '/admin/runs', { waitUntil: 'networkidle' });
53 + const runId = await page.locator('a[href*="run_id="]').first().textContent();
54 + await page.goto(BASE + `/admin/raw?run_id=${runId?.trim()}`, { waitUntil: 'networkidle' });
55 + const files = await page.locator('table').count();
56 + console.log(width, 'raw run', runId?.trim(), files ? `${files} tables OK` : 'no table');
57 + // clear cache action
58 + await page.goto(BASE + '/admin', { waitUntil: 'networkidle' });
59 + await page.click('button:has-text("Clear cache")');
60 + await page.waitForURL(/[?&](ok|err)=/, { timeout: 60_000 });
61 + console.log(width, 'clear cache:', decodeURIComponent(page.url().split('?')[1] ?? ''));
62 + // refresh now (expected 409 → err=: no scheduler pid on this dev machine)
63 + await page.goto(BASE + '/admin', { waitUntil: 'networkidle' });
64 + await page.click('button:has-text("Refresh now")');
65 + await page.waitForURL(/[?&](ok|err)=/, { timeout: 60_000 });
66 + console.log(width, 'refresh now:', decodeURIComponent(page.url().split('?')[1] ?? '').slice(0, 120));
67 + // logout
68 + await page.click('button:has-text("Sign out")');
69 + await page.waitForURL(/\/admin\/login/, { timeout: 30_000 });
70 + console.log(width, 'logout OK', errors.length ? `console errors: ${errors.slice(0, 3).join(' | ')}` : 'no console errors');
71 + await ctx.close();
72 +}
73 +await browser.close();
added apps/web/qa/compare-rankings.mjs +87 −0
@@ -0,0 +1,87 @@
1 +/**
2 + * QA for the Compare + Rankings pages: screenshots at 320/375/390/430/1280/1440 → qa/screens/compare-rankings/,
3 + * plus checks: horizontal overflow, tap targets < 44 px (mobile), console errors, sticky controls height,
4 + * URL state round-trip (reload keeps state), 404s. Run: node qa/compare-rankings.mjs [BASE_URL]
5 + */
6 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
7 +import { mkdirSync, writeFileSync } from 'node:fs';
8 +
9 +const BASE = process.argv[2] ?? 'http://localhost:8290';
10 +const OUT = new URL('./screens/compare-rankings/', import.meta.url).pathname;
11 +mkdirSync(OUT, { recursive: true });
12 +
13 +const PAGES = [
14 + ['compare', '/compare'],
15 + ['cmp-snapshot', '/compare/canada/united-states/france'],
16 + ['cmp-economy', '/compare/canada/united-states/france?tab=economy'],
17 + ['cmp-hero', '/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100'],
18 + ['rankings', '/rankings'],
19 + ['rk-gdppc', '/rankings/gdp-per-capita'],
20 + ['rk-life-oecd', '/rankings/life-expectancy?group=oecd'],
21 +];
22 +const WIDTHS = [320, 375, 390, 430, 1280, 1440];
23 +const report = [];
24 +const browser = await chromium.launch();
25 +
26 +for (const width of WIDTHS) {
27 + const mobile = width < 768;
28 + const ctx = await browser.newContext({ viewport: { width, height: mobile ? 800 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: 'light' });
29 + const page = await ctx.newPage();
30 + const errors = [];
31 + page.on('pageerror', (e) => errors.push(String(e)));
32 + page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
33 + for (const [name, path] of PAGES) {
34 + await page.goto(BASE + path, { waitUntil: 'networkidle' });
35 + await page.evaluate(() => document.fonts.ready);
36 + await page.waitForTimeout(700);
37 + const m = await page.evaluate(() => {
38 + const de = document.documentElement;
39 + const overflow = de.scrollWidth - de.clientWidth;
40 + const wide = [...document.querySelectorAll('body *')].filter((el) => { const r = el.getBoundingClientRect(); return r.right > de.clientWidth + 1 && r.width > 0 && getComputedStyle(el).position !== 'fixed'; }).slice(0, 6).map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);
41 + const small = [...document.querySelectorAll('a,button,[role=button],input,select,summary')].filter((el) => { const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); if (cs.visibility === 'hidden') return false; return r.height < 44 && r.width < 44; }).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)}`);
42 + const smallH = [...document.querySelectorAll('a,button,[role=button],input,select,summary')].filter((el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 32; }).map((el) => `${el.tagName.toLowerCase()} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 30)}" h=${Math.round(el.getBoundingClientRect().height)}`);
43 + const sticky = [...document.querySelectorAll('.sticky')].filter((el) => el.getBoundingClientRect().height > 0).map((el) => Math.round(el.getBoundingClientRect().height));
44 + return { overflow, wide, small: small.slice(0, 12), smallH: smallH.slice(0, 12), sticky, docH: de.scrollHeight };
45 + });
46 + await page.screenshot({ path: `${OUT}${name}-${width}.png`, fullPage: true });
47 + report.push({ name, path, width, ...m, errors: errors.splice(0) });
48 + }
49 + await ctx.close();
50 +}
51 +
52 +// URL state round-trip + 404s (desktop)
53 +{
54 + const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
55 + const page = await ctx.newPage();
56 + const checks = [];
57 + await page.goto(`${BASE}/compare/canada/united-states/france?tab=economy`, { waitUntil: 'networkidle' });
58 + await page.getByRole('radio', { name: 'Index = 100' }).click();
59 + await page.waitForTimeout(1200);
60 + const url1 = page.url();
61 + await page.reload({ waitUntil: 'networkidle' });
62 + const modeChecked = await page.getByRole('radio', { name: 'Index = 100' }).getAttribute('aria-checked');
63 + checks.push({ check: 'compare mode round-trip', url: url1, ok: url1.includes('mode=index100') && modeChecked === 'true' });
64 + await page.getByRole('tab', { name: 'Population' }).click();
65 + await page.waitForTimeout(1200);
66 + checks.push({ check: 'tab switch updates URL', url: page.url(), ok: page.url().includes('tab=population') });
67 + await page.goto(`${BASE}/rankings/gdp-per-capita`, { waitUntil: 'networkidle' });
68 + await page.locator('select[aria-label="Year"]:visible').first().selectOption('2015');
69 + await page.waitForTimeout(1500);
70 + const url2 = page.url();
71 + await page.reload({ waitUntil: 'networkidle' });
72 + const yearVal = await page.locator('select[aria-label="Year"]:visible').first().inputValue();
73 + checks.push({ check: 'ranking year round-trip', url: url2, ok: url2.includes('year=2015') && yearVal === '2015' });
74 + for (const p of ['/compare/canada', '/rankings/not-a-thing', '/compare/canada/nowhere-land']) {
75 + const res = await page.goto(BASE + p, { waitUntil: 'domcontentloaded' });
76 + checks.push({ check: `404 ${p}`, status: res?.status(), ok: res?.status() === 404 });
77 + }
78 + report.push({ checks });
79 + await ctx.close();
80 +}
81 +await browser.close();
82 +writeFileSync(`${OUT}report.json`, JSON.stringify(report, null, 2));
83 +for (const r of report) {
84 + if (r.checks) { for (const c of r.checks) console.log(c.ok ? 'OK ' : 'FAIL', c.check, c.url ?? c.status ?? ''); continue; }
85 + const flags = [r.overflow > 0 ? `OVERFLOW ${r.overflow}px ${r.wide.join(' | ')}` : '', r.width < 768 && r.small.length ? `small: ${r.small.join(' | ')}` : '', r.errors.length ? `errors: ${r.errors.join(' | ').slice(0, 300)}` : ''].filter(Boolean);
86 + console.log(`${r.name}@${r.width} h=${r.docH} sticky=[${r.sticky.join(',')}] ${flags.join(' ; ')}`);
87 +}
added apps/web/qa/cr-interact.mjs +21 −0
@@ -0,0 +1,21 @@
1 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
2 +const BASE = 'http://localhost:8290';
3 +const OUT = new URL('./screens/compare-rankings/seg/', import.meta.url).pathname;
4 +const b = await chromium.launch();
5 +const errs = [];
6 +async function ctx(w, dark=false){ const c = await b.newContext({ viewport: { width: w, height: w<768?844:900 }, isMobile: w<768, hasTouch: w<768, colorScheme: dark?'dark':'light' }); const p = await c.newPage(); p.on('pageerror',e=>errs.push(String(e))); p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); return [c,p]; }
7 +// mobile: options sheet on compare
8 +{ const [c,p] = await ctx(390); await p.goto(`${BASE}/compare/canada/united-states/france?tab=economy`, {waitUntil:'networkidle'}); await p.getByRole('button',{name:'Options'}).click(); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-options-390.png`}); await c.close(); }
9 +// mobile: add-country picker
10 +{ const [c,p] = await ctx(390); await p.goto(`${BASE}/compare/canada/united-states/france`, {waitUntil:'networkidle'}); await p.getByRole('button',{name:'Add a country'}).first().click(); await p.waitForTimeout(500); await p.locator('dialog input[type=search]').fill('ger'); await p.waitForTimeout(400); await p.screenshot({path:`${OUT}i-picker-390.png`}); await p.locator('dialog button', {hasText:'Germany'}).click(); await p.waitForURL(/germany/); await p.waitForTimeout(1200); await p.screenshot({path:`${OUT}i-added-390.png`}); console.log('after add', p.url()); await c.close(); }
11 +// desktop: custom tab + indicator picker
12 +{ const [c,p] = await ctx(1280); await p.goto(`${BASE}/compare/canada/united-states/france?tab=custom&indicators=gdp-per-capita,life-expectancy`, {waitUntil:'networkidle'}); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-custom-1280.png`}); await p.getByRole('button',{name:'Add an indicator'}).click(); await p.waitForTimeout(800); await p.locator('dialog input[type=search]').fill('unemploy'); await p.waitForTimeout(400); await p.screenshot({path:`${OUT}i-indpicker-1280.png`}); await p.locator('dialog li button').first().click(); await p.waitForTimeout(1500); console.log('custom url', p.url()); await c.close(); }
13 +// desktop: snapshot cell provenance + indicator link
14 +{ const [c,p] = await ctx(1280); await p.goto(`${BASE}/compare/canada/united-states/france`, {waitUntil:'networkidle'}); await p.getByRole('button',{name:'Show source and provenance'}).first().click(); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-prov-1280.png`}); await p.keyboard.press('Escape'); await p.getByRole('link',{name:'Chart Life expectancy'}).click(); await p.waitForURL(/tab=health/); await p.waitForTimeout(1500); console.log('chart link', p.url()); await p.screenshot({path:`${OUT}i-health-hero-1280.png`}); await c.close(); }
15 +// desktop: ranking highlight + group filter + dark mode
16 +{ const [c,p] = await ctx(1280, true); await p.goto(`${BASE}/rankings/gdp-per-capita?highlight=canada`, {waitUntil:'networkidle'}); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-rk-highlight-dark-1280.png`}); await p.locator('select[aria-label="Group"]:visible').selectOption('g20'); await p.waitForTimeout(1500); console.log('group url', p.url()); await p.screenshot({path:`${OUT}i-rk-g20-dark-1280.png`}); await c.close(); }
17 +// mobile: ranking filters sheet + history panel
18 +{ const [c,p] = await ctx(390); await p.goto(`${BASE}/rankings/life-expectancy?group=oecd&highlight=canada`, {waitUntil:'networkidle'}); await p.getByRole('button',{name:'Filters'}).click(); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-rk-filters-390.png`}); await p.keyboard.press('Escape'); await p.evaluate(()=>document.getElementById('history')?.scrollIntoView()); await p.waitForTimeout(600); await p.screenshot({path:`${OUT}i-rk-history-390.png`}); await c.close(); }
19 +// mobile dark compare economy
20 +{ const [c,p] = await ctx(390, true); await p.goto(`${BASE}/compare/canada/united-states/france?tab=economy`, {waitUntil:'networkidle'}); await p.evaluate(()=>window.scrollTo(0,700)); await p.waitForTimeout(700); await p.screenshot({path:`${OUT}i-eco-dark-390.png`}); await c.close(); }
21 +await b.close(); console.log('errors', errs.length, errs.slice(0,5));
added apps/web/qa/cr-segments.mjs +33 −0
@@ -0,0 +1,33 @@
1 +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';
2 +const BASE = process.argv[2] ?? 'http://localhost:8290';
3 +const OUT = new URL('./screens/compare-rankings/seg/', import.meta.url).pathname;
4 +import { mkdirSync } from 'node:fs'; mkdirSync(OUT, { recursive: true });
5 +const SHOTS = [
6 + ['cmp-snapshot', '/compare/canada/united-states/france', 390, [0, 700]],
7 + ['cmp-economy', '/compare/canada/united-states/france?tab=economy', 390, [0, 900, 1800]],
8 + ['cmp-hero', '/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100', 390, [0, 700]],
9 + ['cmp-economy', '/compare/canada/united-states/france?tab=economy', 1280, [0, 900, 1800]],
10 + ['cmp-hero', '/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100', 1280, [0]],
11 + ['rankings', '/rankings', 390, [0, 800]],
12 + ['rankings', '/rankings', 1280, [0, 900]],
13 + ['rk-gdppc', '/rankings/gdp-per-capita', 390, [0, 800, 2600, 3600]],
14 + ['rk-gdppc', '/rankings/gdp-per-capita', 1280, [900, 1800]],
15 + ['rk-life-oecd', '/rankings/life-expectancy?group=oecd', 390, [0, 1200]],
16 + ['rk-life-oecd', '/rankings/life-expectancy?group=oecd', 1280, [0, 1600]],
17 +];
18 +const b = await chromium.launch();
19 +for (const [name, path, w, offsets] of SHOTS) {
20 + const mobile = w < 768;
21 + const ctx = await b.newContext({ viewport: { width: w, height: mobile ? 844 : 900 }, isMobile: mobile, hasTouch: mobile, deviceScaleFactor: 1 });
22 + const pg = await ctx.newPage();
23 + await pg.goto(BASE + path, { waitUntil: 'networkidle' });
24 + await pg.waitForTimeout(600);
25 + for (const y of offsets) {
26 + await pg.evaluate((yy) => window.scrollTo(0, yy), y);
27 + await pg.waitForTimeout(700);
28 + await pg.screenshot({ path: `${OUT}${name}-${w}-y${y}.png` });
29 + }
30 + await ctx.close();
31 +}
32 +await b.close();
33 +console.log('done');
added apps/web/qa/favicons.mjs +14 −0
@@ -0,0 +1,14 @@
1 +// Rasterise src/app/icon.svg → icon.png (512) + apple-icon.png (180). Run after editing the SVG.
2 +import { chromium } from 'playwright';
3 +import { readFileSync } from 'node:fs';
4 +const svg = readFileSync(new URL('../src/app/icon.svg', import.meta.url), 'utf8');
5 +const browser = await chromium.launch();
6 +for (const [size, file] of [[512, '../src/app/icon.png'], [180, '../src/app/apple-icon.png']]) {
7 + const ctx = await browser.newContext({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });
8 + const page = await ctx.newPage();
9 + await page.setContent(`<html><body style="margin:0;background:transparent">${svg.replace(/width="\d+" height="\d+"/, `width="${size}" height="${size}"`)}</body></html>`);
10 + await page.screenshot({ path: new URL(file, import.meta.url).pathname, omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });
11 + await ctx.close();
12 +}
13 +await browser.close();
14 +console.log('favicons written');
added apps/web/qa/screens/compare-rankings/cmp-economy-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-economy-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-economy-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-economy-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-economy-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-economy-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-hero-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/cmp-snapshot-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/compare-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/compare-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/compare-rankings/compare-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rankings-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rankings-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rankings-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/report.json +1222 −0
@@ -0,0 +1,1222 @@
1 +[
2 + {
3 + "name": "compare",
4 + "path": "/compare",
5 + "width": 320,
6 + "overflow": 0,
7 + "wide": [],
8 + "small": [
9 + "a \"Skip to content\" 1x1"
10 + ],
11 + "smallH": [
12 + "a \"Skip to content\" h=1",
13 + "a \"contact@spboucher.ai\" h=15",
14 + "a \"MacLustr\" h=15"
15 + ],
16 + "sticky": [
17 + 53,
18 + 61
19 + ],
20 + "docH": 2569,
21 + "errors": []
22 + },
23 + {
24 + "name": "cmp-snapshot",
25 + "path": "/compare/canada/united-states/france",
26 + "width": 320,
27 + "overflow": 0,
28 + "wide": [
29 + "li.group/chip.shrink-0 right=329",
30 + "span.inline-flex.h-11.items-center right=329",
31 + "button.grid.h-11.w-9 right=328",
32 + "li.group/chip.shrink-0 right=465",
33 + "span.inline-flex.h-11.items-center right=465",
34 + "span.h-2.w-2.shrink-0 right=352"
35 + ],
36 + "small": [
37 + "a \"Skip to content\" 1x1"
38 + ],
39 + "smallH": [
40 + "a \"Skip to content\" h=1",
41 + "a \"contact@spboucher.ai\" h=15",
42 + "a \"MacLustr\" h=15"
43 + ],
44 + "sticky": [
45 + 53,
46 + 105
47 + ],
48 + "docH": 4083,
49 + "errors": []
50 + },
51 + {
52 + "name": "cmp-economy",
53 + "path": "/compare/canada/united-states/france?tab=economy",
54 + "width": 320,
55 + "overflow": 0,
56 + "wide": [
57 + "li.group/chip.shrink-0 right=329",
58 + "span.inline-flex.h-11.items-center right=329",
59 + "button.grid.h-11.w-9 right=328",
60 + "li.group/chip.shrink-0 right=465",
61 + "span.inline-flex.h-11.items-center right=465",
62 + "span.h-2.w-2.shrink-0 right=352"
63 + ],
64 + "small": [
65 + "a \"Skip to content\" 1x1"
66 + ],
67 + "smallH": [
68 + "a \"Skip to content\" h=1",
69 + "a \"GDP (current US$)\" h=17",
70 + "button \"Source for Canada, United Stat\" h=28",
71 + "button \"Source for Canada, United Stat\" h=28",
72 + "a \"GDP per capita (current US$)\" h=17",
73 + "button \"Source for Canada, United Stat\" h=28",
74 + "button \"Source for Canada, United Stat\" h=28",
75 + "a \"Real GDP growth\" h=17",
76 + "button \"Source for Canada, United Stat\" h=28",
77 + "a \"GDP per capita growth\" h=17",
78 + "button \"Source for Canada, United Stat\" h=28",
79 + "a \"GDP (constant 2015 US$)\" h=17"
80 + ],
81 + "sticky": [
82 + 53,
83 + 105
84 + ],
85 + "docH": 18787,
86 + "errors": []
87 + },
88 + {
89 + "name": "cmp-hero",
90 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
91 + "width": 320,
92 + "overflow": 0,
93 + "wide": [
94 + "li.group/chip.shrink-0 right=329",
95 + "span.inline-flex.h-11.items-center right=329",
96 + "button.grid.h-11.w-9 right=328",
97 + "li.group/chip.shrink-0 right=465",
98 + "span.inline-flex.h-11.items-center right=465",
99 + "span.h-2.w-2.shrink-0 right=352"
100 + ],
101 + "small": [
102 + "a \"Skip to content\" 1x1"
103 + ],
104 + "smallH": [
105 + "a \"Skip to content\" h=1",
106 + "button \"Source for Canada, United Stat\" h=28",
107 + "a \"GDP (current US$)\" h=17",
108 + "button \"Source for Canada, United Stat\" h=28",
109 + "button \"Source for Canada, United Stat\" h=28",
110 + "button \"Source for Canada, United Stat\" h=28",
111 + "a \"Real GDP growth\" h=17",
112 + "button \"Source for Canada, United Stat\" h=28",
113 + "a \"GDP per capita growth\" h=17",
114 + "button \"Source for Canada, United Stat\" h=28",
115 + "a \"GDP (constant 2015 US$)\" h=17",
116 + "button \"Source for Canada, United Stat\" h=28"
117 + ],
118 + "sticky": [
119 + 53,
120 + 105
121 + ],
122 + "docH": 19052,
123 + "errors": []
124 + },
125 + {
126 + "name": "rankings",
127 + "path": "/rankings",
128 + "width": 320,
129 + "overflow": 0,
130 + "wide": [],
131 + "small": [
132 + "a \"Skip to content\" 1x1"
133 + ],
134 + "smallH": [
135 + "a \"Skip to content\" h=1",
136 + "a \"contact@spboucher.ai\" h=15",
137 + "a \"MacLustr\" h=15"
138 + ],
139 + "sticky": [
140 + 53,
141 + 61
142 + ],
143 + "docH": 26234,
144 + "errors": []
145 + },
146 + {
147 + "name": "rk-gdppc",
148 + "path": "/rankings/gdp-per-capita",
149 + "width": 320,
150 + "overflow": 0,
151 + "wide": [],
152 + "small": [
153 + "a \"Skip to content\" 1x1"
154 + ],
155 + "smallH": [
156 + "a \"Skip to content\" h=1",
157 + "select \"Year\" h=19",
158 + "a \"contact@spboucher.ai\" h=15",
159 + "a \"MacLustr\" h=15"
160 + ],
161 + "sticky": [
162 + 53,
163 + 61
164 + ],
165 + "docH": 4759,
166 + "errors": []
167 + },
168 + {
169 + "name": "rk-life-oecd",
170 + "path": "/rankings/life-expectancy?group=oecd",
171 + "width": 320,
172 + "overflow": 0,
173 + "wide": [],
174 + "small": [
175 + "a \"Skip to content\" 1x1"
176 + ],
177 + "smallH": [
178 + "a \"Skip to content\" h=1",
179 + "select \"Year\" h=19",
180 + "a \"contact@spboucher.ai\" h=15",
181 + "a \"MacLustr\" h=15"
182 + ],
183 + "sticky": [
184 + 53,
185 + 61
186 + ],
187 + "docH": 4689,
188 + "errors": []
189 + },
190 + {
191 + "name": "compare",
192 + "path": "/compare",
193 + "width": 375,
194 + "overflow": 0,
195 + "wide": [],
196 + "small": [
197 + "a \"Skip to content\" 1x1"
198 + ],
199 + "smallH": [
200 + "a \"Skip to content\" h=1",
201 + "a \"contact@spboucher.ai\" h=15",
202 + "a \"MacLustr\" h=15"
203 + ],
204 + "sticky": [
205 + 53,
206 + 61
207 + ],
208 + "docH": 2508,
209 + "errors": []
210 + },
211 + {
212 + "name": "cmp-snapshot",
213 + "path": "/compare/canada/united-states/france",
214 + "width": 375,
215 + "overflow": 0,
216 + "wide": [
217 + "li.group/chip.shrink-0 right=465",
218 + "span.inline-flex.h-11.items-center right=465",
219 + "span.max-w-[8rem].truncate.text-ink right=424",
220 + "button.grid.h-11.w-9 right=464",
221 + "svg.[object.SVGAnimatedString] right=453",
222 + "path.[object.SVGAnimatedString] right=449"
223 + ],
224 + "small": [
225 + "a \"Skip to content\" 1x1"
226 + ],
227 + "smallH": [
228 + "a \"Skip to content\" h=1",
229 + "a \"contact@spboucher.ai\" h=15",
230 + "a \"MacLustr\" h=15"
231 + ],
232 + "sticky": [
233 + 53,
234 + 105
235 + ],
236 + "docH": 4049,
237 + "errors": []
238 + },
239 + {
240 + "name": "cmp-economy",
241 + "path": "/compare/canada/united-states/france?tab=economy",
242 + "width": 375,
243 + "overflow": 0,
244 + "wide": [
245 + "li.group/chip.shrink-0 right=465",
246 + "span.inline-flex.h-11.items-center right=465",
247 + "span.max-w-[8rem].truncate.text-ink right=424",
248 + "button.grid.h-11.w-9 right=464",
249 + "svg.[object.SVGAnimatedString] right=453",
250 + "path.[object.SVGAnimatedString] right=449"
251 + ],
252 + "small": [
253 + "a \"Skip to content\" 1x1"
254 + ],
255 + "smallH": [
256 + "a \"Skip to content\" h=1",
257 + "a \"GDP (current US$)\" h=17",
258 + "button \"Source for Canada, United Stat\" h=28",
259 + "a \"GDP, PPP (current internationa\" h=17",
260 + "button \"Source for Canada, United Stat\" h=28",
261 + "a \"GDP per capita (current US$)\" h=17",
262 + "button \"Source for Canada, United Stat\" h=28",
263 + "button \"Source for Canada, United Stat\" h=28",
264 + "a \"Real GDP growth\" h=17",
265 + "button \"Source for Canada, United Stat\" h=28",
266 + "a \"GDP per capita growth\" h=17",
267 + "button \"Source for Canada, United Stat\" h=28"
268 + ],
269 + "sticky": [
270 + 53,
271 + 105
272 + ],
273 + "docH": 18042,
274 + "errors": []
275 + },
276 + {
277 + "name": "cmp-hero",
278 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
279 + "width": 375,
280 + "overflow": 0,
281 + "wide": [
282 + "li.group/chip.shrink-0 right=465",
283 + "span.inline-flex.h-11.items-center right=465",
284 + "span.max-w-[8rem].truncate.text-ink right=424",
285 + "button.grid.h-11.w-9 right=464",
286 + "svg.[object.SVGAnimatedString] right=453",
287 + "path.[object.SVGAnimatedString] right=449"
288 + ],
289 + "small": [
290 + "a \"Skip to content\" 1x1"
291 + ],
292 + "smallH": [
293 + "a \"Skip to content\" h=1",
294 + "button \"Source for Canada, United Stat\" h=28",
295 + "a \"GDP (current US$)\" h=17",
296 + "button \"Source for Canada, United Stat\" h=28",
297 + "a \"GDP, PPP (current internationa\" h=17",
298 + "button \"Source for Canada, United Stat\" h=28",
299 + "button \"Source for Canada, United Stat\" h=28",
300 + "a \"Real GDP growth\" h=17",
301 + "button \"Source for Canada, United Stat\" h=28",
302 + "a \"GDP per capita growth\" h=17",
303 + "button \"Source for Canada, United Stat\" h=28",
304 + "a \"GDP (constant 2015 US$)\" h=17"
305 + ],
306 + "sticky": [
307 + 53,
308 + 105
309 + ],
310 + "docH": 18307,
311 + "errors": []
312 + },
313 + {
314 + "name": "rankings",
315 + "path": "/rankings",
316 + "width": 375,
317 + "overflow": 0,
318 + "wide": [],
319 + "small": [
320 + "a \"Skip to content\" 1x1"
321 + ],
322 + "smallH": [
323 + "a \"Skip to content\" h=1",
324 + "a \"contact@spboucher.ai\" h=15",
325 + "a \"MacLustr\" h=15"
326 + ],
327 + "sticky": [
328 + 53,
329 + 61
330 + ],
331 + "docH": 24751,
332 + "errors": []
333 + },
334 + {
335 + "name": "rk-gdppc",
336 + "path": "/rankings/gdp-per-capita",
337 + "width": 375,
338 + "overflow": 0,
339 + "wide": [],
340 + "small": [
341 + "a \"Skip to content\" 1x1"
342 + ],
343 + "smallH": [
344 + "a \"Skip to content\" h=1",
345 + "select \"Year\" h=19",
346 + "a \"contact@spboucher.ai\" h=15",
347 + "a \"MacLustr\" h=15"
348 + ],
349 + "sticky": [
350 + 53,
351 + 61
352 + ],
353 + "docH": 4695,
354 + "errors": []
355 + },
356 + {
357 + "name": "rk-life-oecd",
358 + "path": "/rankings/life-expectancy?group=oecd",
359 + "width": 375,
360 + "overflow": 0,
361 + "wide": [],
362 + "small": [
363 + "a \"Skip to content\" 1x1"
364 + ],
365 + "smallH": [
366 + "a \"Skip to content\" h=1",
367 + "select \"Year\" h=19",
368 + "a \"contact@spboucher.ai\" h=15",
369 + "a \"MacLustr\" h=15"
370 + ],
371 + "sticky": [
372 + 53,
373 + 61
374 + ],
375 + "docH": 4614,
376 + "errors": []
377 + },
378 + {
379 + "name": "compare",
380 + "path": "/compare",
381 + "width": 390,
382 + "overflow": 0,
383 + "wide": [],
384 + "small": [
385 + "a \"Skip to content\" 1x1"
386 + ],
387 + "smallH": [
388 + "a \"Skip to content\" h=1",
389 + "a \"contact@spboucher.ai\" h=15",
390 + "a \"MacLustr\" h=15"
391 + ],
392 + "sticky": [
393 + 53,
394 + 61
395 + ],
396 + "docH": 2491,
397 + "errors": []
398 + },
399 + {
400 + "name": "cmp-snapshot",
401 + "path": "/compare/canada/united-states/france",
402 + "width": 390,
403 + "overflow": 0,
404 + "wide": [
405 + "li.group/chip.shrink-0 right=465",
406 + "span.inline-flex.h-11.items-center right=465",
407 + "span.max-w-[8rem].truncate.text-ink right=424",
408 + "button.grid.h-11.w-9 right=464",
409 + "svg.[object.SVGAnimatedString] right=453",
410 + "path.[object.SVGAnimatedString] right=449"
411 + ],
412 + "small": [
413 + "a \"Skip to content\" 1x1"
414 + ],
415 + "smallH": [
416 + "a \"Skip to content\" h=1",
417 + "a \"contact@spboucher.ai\" h=15",
418 + "a \"MacLustr\" h=15"
419 + ],
420 + "sticky": [
421 + 53,
422 + 105
423 + ],
424 + "docH": 4031,
425 + "errors": []
426 + },
427 + {
428 + "name": "cmp-economy",
429 + "path": "/compare/canada/united-states/france?tab=economy",
430 + "width": 390,
431 + "overflow": 0,
432 + "wide": [
433 + "li.group/chip.shrink-0 right=465",
434 + "span.inline-flex.h-11.items-center right=465",
435 + "span.max-w-[8rem].truncate.text-ink right=424",
436 + "button.grid.h-11.w-9 right=464",
437 + "svg.[object.SVGAnimatedString] right=453",
438 + "path.[object.SVGAnimatedString] right=449"
439 + ],
440 + "small": [
441 + "a \"Skip to content\" 1x1"
442 + ],
443 + "smallH": [
444 + "a \"Skip to content\" h=1",
445 + "a \"GDP (current US$)\" h=17",
446 + "button \"Source for Canada, United Stat\" h=28",
447 + "a \"GDP, PPP (current internationa\" h=17",
448 + "button \"Source for Canada, United Stat\" h=28",
449 + "a \"GDP per capita (current US$)\" h=17",
450 + "button \"Source for Canada, United Stat\" h=28",
451 + "button \"Source for Canada, United Stat\" h=28",
452 + "a \"Real GDP growth\" h=17",
453 + "button \"Source for Canada, United Stat\" h=28",
454 + "a \"GDP per capita growth\" h=17",
455 + "button \"Source for Canada, United Stat\" h=28"
456 + ],
457 + "sticky": [
458 + 53,
459 + 105
460 + ],
461 + "docH": 17834,
462 + "errors": []
463 + },
464 + {
465 + "name": "cmp-hero",
466 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
467 + "width": 390,
468 + "overflow": 0,
469 + "wide": [
470 + "li.group/chip.shrink-0 right=465",
471 + "span.inline-flex.h-11.items-center right=465",
472 + "span.max-w-[8rem].truncate.text-ink right=424",
473 + "button.grid.h-11.w-9 right=464",
474 + "svg.[object.SVGAnimatedString] right=453",
475 + "path.[object.SVGAnimatedString] right=449"
476 + ],
477 + "small": [
478 + "a \"Skip to content\" 1x1"
479 + ],
480 + "smallH": [
481 + "a \"Skip to content\" h=1",
482 + "button \"Source for Canada, United Stat\" h=28",
483 + "a \"GDP (current US$)\" h=17",
484 + "button \"Source for Canada, United Stat\" h=28",
485 + "a \"GDP, PPP (current internationa\" h=17",
486 + "button \"Source for Canada, United Stat\" h=28",
487 + "button \"Source for Canada, United Stat\" h=28",
488 + "a \"Real GDP growth\" h=17",
489 + "button \"Source for Canada, United Stat\" h=28",
490 + "a \"GDP per capita growth\" h=17",
491 + "button \"Source for Canada, United Stat\" h=28",
492 + "a \"GDP (constant 2015 US$)\" h=17"
493 + ],
494 + "sticky": [
495 + 53,
496 + 105
497 + ],
498 + "docH": 18171,
499 + "errors": []
500 + },
501 + {
502 + "name": "rankings",
503 + "path": "/rankings",
504 + "width": 390,
505 + "overflow": 0,
506 + "wide": [],
507 + "small": [
508 + "a \"Skip to content\" 1x1"
509 + ],
510 + "smallH": [
511 + "a \"Skip to content\" h=1",
512 + "a \"contact@spboucher.ai\" h=15",
513 + "a \"MacLustr\" h=15"
514 + ],
515 + "sticky": [
516 + 53,
517 + 61
518 + ],
519 + "docH": 24327,
520 + "errors": []
521 + },
522 + {
523 + "name": "rk-gdppc",
524 + "path": "/rankings/gdp-per-capita",
525 + "width": 390,
526 + "overflow": 0,
527 + "wide": [],
528 + "small": [
529 + "a \"Skip to content\" 1x1"
530 + ],
531 + "smallH": [
532 + "a \"Skip to content\" h=1",
533 + "select \"Year\" h=19",
534 + "a \"contact@spboucher.ai\" h=15",
535 + "a \"MacLustr\" h=15"
536 + ],
537 + "sticky": [
538 + 53,
539 + 61
540 + ],
541 + "docH": 4623,
542 + "errors": []
543 + },
544 + {
545 + "name": "rk-life-oecd",
546 + "path": "/rankings/life-expectancy?group=oecd",
547 + "width": 390,
548 + "overflow": 0,
549 + "wide": [],
550 + "small": [
551 + "a \"Skip to content\" 1x1"
552 + ],
553 + "smallH": [
554 + "a \"Skip to content\" h=1",
555 + "select \"Year\" h=19",
556 + "a \"contact@spboucher.ai\" h=15",
557 + "a \"MacLustr\" h=15"
558 + ],
559 + "sticky": [
560 + 53,
561 + 61
562 + ],
563 + "docH": 4603,
564 + "errors": []
565 + },
566 + {
567 + "name": "compare",
568 + "path": "/compare",
569 + "width": 430,
570 + "overflow": 0,
571 + "wide": [],
572 + "small": [
573 + "a \"Skip to content\" 1x1"
574 + ],
575 + "smallH": [
576 + "a \"Skip to content\" h=1",
577 + "a \"contact@spboucher.ai\" h=15",
578 + "a \"MacLustr\" h=15"
579 + ],
580 + "sticky": [
581 + 53,
582 + 61
583 + ],
584 + "docH": 2454,
585 + "errors": []
586 + },
587 + {
588 + "name": "cmp-snapshot",
589 + "path": "/compare/canada/united-states/france",
590 + "width": 430,
591 + "overflow": 0,
592 + "wide": [
593 + "li.group/chip.shrink-0 right=465",
594 + "span.inline-flex.h-11.items-center right=465",
595 + "button.grid.h-11.w-9 right=464",
596 + "svg.[object.SVGAnimatedString] right=453",
597 + "path.[object.SVGAnimatedString] right=449",
598 + "path.[object.SVGAnimatedString] right=449"
599 + ],
600 + "small": [
601 + "a \"Skip to content\" 1x1"
602 + ],
603 + "smallH": [
604 + "a \"Skip to content\" h=1",
605 + "a \"contact@spboucher.ai\" h=15",
606 + "a \"MacLustr\" h=15"
607 + ],
608 + "sticky": [
609 + 53,
610 + 105
611 + ],
612 + "docH": 3998,
613 + "errors": []
614 + },
615 + {
616 + "name": "cmp-economy",
617 + "path": "/compare/canada/united-states/france?tab=economy",
618 + "width": 430,
619 + "overflow": 0,
620 + "wide": [
621 + "li.group/chip.shrink-0 right=465",
622 + "span.inline-flex.h-11.items-center right=465",
623 + "button.grid.h-11.w-9 right=464",
624 + "svg.[object.SVGAnimatedString] right=453",
625 + "path.[object.SVGAnimatedString] right=449",
626 + "path.[object.SVGAnimatedString] right=449"
627 + ],
628 + "small": [
629 + "a \"Skip to content\" 1x1"
630 + ],
631 + "smallH": [
632 + "a \"Skip to content\" h=1",
633 + "a \"GDP (current US$)\" h=17",
634 + "button \"Source for Canada, United Stat\" h=28",
635 + "a \"GDP, PPP (current internationa\" h=17",
636 + "button \"Source for Canada, United Stat\" h=28",
637 + "a \"GDP per capita (current US$)\" h=17",
638 + "button \"Source for Canada, United Stat\" h=28",
639 + "a \"GDP per capita, PPP (current i\" h=17",
640 + "button \"Source for Canada, United Stat\" h=28",
641 + "a \"Real GDP growth\" h=17",
642 + "button \"Source for Canada, United Stat\" h=28",
643 + "a \"GDP per capita growth\" h=17"
644 + ],
645 + "sticky": [
646 + 53,
647 + 105
648 + ],
649 + "docH": 16968,
650 + "errors": []
651 + },
652 + {
653 + "name": "cmp-hero",
654 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
655 + "width": 430,
656 + "overflow": 0,
657 + "wide": [
658 + "li.group/chip.shrink-0 right=465",
659 + "span.inline-flex.h-11.items-center right=465",
660 + "button.grid.h-11.w-9 right=464",
661 + "svg.[object.SVGAnimatedString] right=453",
662 + "path.[object.SVGAnimatedString] right=449",
663 + "path.[object.SVGAnimatedString] right=449"
664 + ],
665 + "small": [
666 + "a \"Skip to content\" 1x1"
667 + ],
668 + "smallH": [
669 + "a \"Skip to content\" h=1",
670 + "button \"Source for Canada, United Stat\" h=28",
671 + "a \"GDP (current US$)\" h=17",
672 + "button \"Source for Canada, United Stat\" h=28",
673 + "a \"GDP, PPP (current internationa\" h=17",
674 + "button \"Source for Canada, United Stat\" h=28",
675 + "a \"GDP per capita, PPP (current i\" h=17",
676 + "button \"Source for Canada, United Stat\" h=28",
677 + "a \"Real GDP growth\" h=17",
678 + "button \"Source for Canada, United Stat\" h=28",
679 + "a \"GDP per capita growth\" h=17",
680 + "button \"Source for Canada, United Stat\" h=28"
681 + ],
682 + "sticky": [
683 + 53,
684 + 105
685 + ],
686 + "docH": 17269,
687 + "errors": []
688 + },
689 + {
690 + "name": "rankings",
691 + "path": "/rankings",
692 + "width": 430,
693 + "overflow": 0,
694 + "wide": [],
695 + "small": [
696 + "a \"Skip to content\" 1x1"
697 + ],
698 + "smallH": [
699 + "a \"Skip to content\" h=1",
700 + "a \"contact@spboucher.ai\" h=15",
701 + "a \"MacLustr\" h=15"
702 + ],
703 + "sticky": [
704 + 53,
705 + 61
706 + ],
707 + "docH": 23937,
708 + "errors": []
709 + },
710 + {
711 + "name": "rk-gdppc",
712 + "path": "/rankings/gdp-per-capita",
713 + "width": 430,
714 + "overflow": 0,
715 + "wide": [],
716 + "small": [
717 + "a \"Skip to content\" 1x1"
718 + ],
719 + "smallH": [
720 + "a \"Skip to content\" h=1",
721 + "select \"Year\" h=19",
722 + "a \"contact@spboucher.ai\" h=15",
723 + "a \"MacLustr\" h=15"
724 + ],
725 + "sticky": [
726 + 53,
727 + 61
728 + ],
729 + "docH": 4583,
730 + "errors": []
731 + },
732 + {
733 + "name": "rk-life-oecd",
734 + "path": "/rankings/life-expectancy?group=oecd",
735 + "width": 430,
736 + "overflow": 0,
737 + "wide": [],
738 + "small": [
739 + "a \"Skip to content\" 1x1"
740 + ],
741 + "smallH": [
742 + "a \"Skip to content\" h=1",
743 + "select \"Year\" h=19",
744 + "a \"contact@spboucher.ai\" h=15",
745 + "a \"MacLustr\" h=15"
746 + ],
747 + "sticky": [
748 + 53,
749 + 61
750 + ],
751 + "docH": 4562,
752 + "errors": []
753 + },
754 + {
755 + "name": "compare",
756 + "path": "/compare",
757 + "width": 1280,
758 + "overflow": 0,
759 + "wide": [],
760 + "small": [
761 + "a \"Skip to content\" 1x1",
762 + "a \"API\" 22x32"
763 + ],
764 + "smallH": [
765 + "a \"Skip to content\" h=1",
766 + "a \"contact@spboucher.ai\" h=15",
767 + "a \"MacLustr\" h=15"
768 + ],
769 + "sticky": [
770 + 57,
771 + 40
772 + ],
773 + "docH": 1359,
774 + "errors": []
775 + },
776 + {
777 + "name": "cmp-snapshot",
778 + "path": "/compare/canada/united-states/france",
779 + "width": 1280,
780 + "overflow": 0,
781 + "wide": [],
782 + "small": [
783 + "a \"Skip to content\" 1x1",
784 + "button \"Move Canada up\" 24x36",
785 + "button \"Move Canada down\" 24x36",
786 + "button \"Remove Canada from the compari\" 28x36",
787 + "button \"Move United States up\" 24x36",
788 + "button \"Move United States down\" 24x36",
789 + "button \"Remove United States from the \" 28x36",
790 + "button \"Move France up\" 24x36",
791 + "button \"Move France down\" 24x36",
792 + "button \"Remove France from the compari\" 28x36",
793 + "a \"API\" 22x32"
794 + ],
795 + "smallH": [
796 + "a \"Skip to content\" h=1",
797 + "a \"🇨🇦Canada\" h=18",
798 + "a \"🇺🇸United States\" h=18",
799 + "a \"🇫🇷France\" h=18",
800 + "a \"contact@spboucher.ai\" h=15",
801 + "a \"MacLustr\" h=15"
802 + ],
803 + "sticky": [
804 + 57,
805 + 142
806 + ],
807 + "docH": 1759,
808 + "errors": []
809 + },
810 + {
811 + "name": "cmp-economy",
812 + "path": "/compare/canada/united-states/france?tab=economy",
813 + "width": 1280,
814 + "overflow": 0,
815 + "wide": [],
816 + "small": [
817 + "a \"Skip to content\" 1x1",
818 + "button \"Move Canada up\" 24x36",
819 + "button \"Move Canada down\" 24x36",
820 + "button \"Remove Canada from the compari\" 28x36",
821 + "button \"Move United States up\" 24x36",
822 + "button \"Move United States down\" 24x36",
823 + "button \"Remove United States from the \" 28x36",
824 + "button \"Move France up\" 24x36",
825 + "button \"Move France down\" 24x36",
826 + "button \"Remove France from the compari\" 28x36",
827 + "input \"\" 13x13",
828 + "a \"API\" 22x32"
829 + ],
830 + "smallH": [
831 + "a \"Skip to content\" h=1",
832 + "select \"From\" h=19",
833 + "select \"To\" h=19",
834 + "input \"\" h=13",
835 + "a \"GDP (current US$)\" h=17",
836 + "button \"Source for Canada, United Stat\" h=28",
837 + "a \"GDP, PPP (current internationa\" h=17",
838 + "button \"Source for Canada, United Stat\" h=28",
839 + "a \"GDP per capita (current US$)\" h=17",
840 + "button \"Source for Canada, United Stat\" h=28",
841 + "a \"GDP per capita, PPP (current i\" h=17",
842 + "button \"Source for Canada, United Stat\" h=28"
843 + ],
844 + "sticky": [
845 + 57,
846 + 142
847 + ],
848 + "docH": 7715,
849 + "errors": []
850 + },
851 + {
852 + "name": "cmp-hero",
853 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
854 + "width": 1280,
855 + "overflow": 0,
856 + "wide": [],
857 + "small": [
858 + "a \"Skip to content\" 1x1",
859 + "button \"Move Canada up\" 24x36",
860 + "button \"Move Canada down\" 24x36",
861 + "button \"Remove Canada from the compari\" 28x36",
862 + "button \"Move United States up\" 24x36",
863 + "button \"Move United States down\" 24x36",
864 + "button \"Remove United States from the \" 28x36",
865 + "button \"Move France up\" 24x36",
866 + "button \"Move France down\" 24x36",
867 + "button \"Remove France from the compari\" 28x36",
868 + "input \"\" 13x13",
869 + "button \"10 y\" 42x32"
870 + ],
871 + "smallH": [
872 + "a \"Skip to content\" h=1",
873 + "select \"From\" h=19",
874 + "select \"To\" h=19",
875 + "input \"\" h=13",
876 + "a \"GDP per capita (current US$)\" h=26",
877 + "button \"Source for Canada, United Stat\" h=28",
878 + "a \"GDP (current US$)\" h=17",
879 + "button \"Source for Canada, United Stat\" h=28",
880 + "a \"GDP, PPP (current internationa\" h=17",
881 + "button \"Source for Canada, United Stat\" h=28",
882 + "a \"GDP per capita, PPP (current i\" h=17",
883 + "button \"Source for Canada, United Stat\" h=28"
884 + ],
885 + "sticky": [
886 + 57,
887 + 142
888 + ],
889 + "docH": 8304,
890 + "errors": []
891 + },
892 + {
893 + "name": "rankings",
894 + "path": "/rankings",
895 + "width": 1280,
896 + "overflow": 0,
897 + "wide": [],
898 + "small": [
899 + "a \"Skip to content\" 1x1",
900 + "a \"API\" 22x32"
901 + ],
902 + "smallH": [
903 + "a \"Skip to content\" h=1",
904 + "a \"contact@spboucher.ai\" h=15",
905 + "a \"MacLustr\" h=15"
906 + ],
907 + "sticky": [
908 + 57,
909 + 64
910 + ],
911 + "docH": 10330,
912 + "errors": []
913 + },
914 + {
915 + "name": "rk-gdppc",
916 + "path": "/rankings/gdp-per-capita",
917 + "width": 1280,
918 + "overflow": 0,
919 + "wide": [],
920 + "small": [
921 + "a \"Skip to content\" 1x1",
922 + "button \"Remove Luxembourg\" 28x36",
923 + "button \"Remove Ireland\" 28x36",
924 + "button \"Remove Switzerland\" 28x36",
925 + "a \"API\" 22x32"
926 + ],
927 + "smallH": [
928 + "a \"Skip to content\" h=1",
929 + "a \"Rankings\" h=18",
930 + "a \"Economy\" h=18",
931 + "select \"Year\" h=19",
932 + "select \"Group\" h=19",
933 + "a \"contact@spboucher.ai\" h=15",
934 + "a \"MacLustr\" h=15"
935 + ],
936 + "sticky": [
937 + 57
938 + ],
939 + "docH": 2667,
940 + "errors": []
941 + },
942 + {
943 + "name": "rk-life-oecd",
944 + "path": "/rankings/life-expectancy?group=oecd",
945 + "width": 1280,
946 + "overflow": 0,
947 + "wide": [],
948 + "small": [
949 + "a \"Skip to content\" 1x1",
950 + "a \"Health\" 37x18",
951 + "button \"Remove Switzerland\" 28x36",
952 + "button \"Remove Sweden\" 28x36",
953 + "button \"Remove Japan\" 28x36",
954 + "a \"API\" 22x32"
955 + ],
956 + "smallH": [
957 + "a \"Skip to content\" h=1",
958 + "a \"Rankings\" h=18",
959 + "a \"Health\" h=18",
960 + "select \"Year\" h=19",
961 + "select \"Group\" h=19",
962 + "a \"contact@spboucher.ai\" h=15",
963 + "a \"MacLustr\" h=15"
964 + ],
965 + "sticky": [
966 + 57
967 + ],
968 + "docH": 2667,
969 + "errors": []
970 + },
971 + {
972 + "name": "compare",
973 + "path": "/compare",
974 + "width": 1440,
975 + "overflow": 0,
976 + "wide": [],
977 + "small": [
978 + "a \"Skip to content\" 1x1",
979 + "a \"API\" 22x32"
980 + ],
981 + "smallH": [
982 + "a \"Skip to content\" h=1",
983 + "a \"contact@spboucher.ai\" h=15",
984 + "a \"MacLustr\" h=15"
985 + ],
986 + "sticky": [
987 + 57,
988 + 40
989 + ],
990 + "docH": 1359,
991 + "errors": []
992 + },
993 + {
994 + "name": "cmp-snapshot",
995 + "path": "/compare/canada/united-states/france",
996 + "width": 1440,
997 + "overflow": 0,
998 + "wide": [],
999 + "small": [
1000 + "a \"Skip to content\" 1x1",
1001 + "button \"Move Canada up\" 24x36",
1002 + "button \"Move Canada down\" 24x36",
1003 + "button \"Remove Canada from the compari\" 28x36",
1004 + "button \"Move United States up\" 24x36",
1005 + "button \"Move United States down\" 24x36",
1006 + "button \"Remove United States from the \" 28x36",
1007 + "button \"Move France up\" 24x36",
1008 + "button \"Move France down\" 24x36",
1009 + "button \"Remove France from the compari\" 28x36",
1010 + "a \"API\" 22x32"
1011 + ],
1012 + "smallH": [
1013 + "a \"Skip to content\" h=1",
1014 + "a \"🇨🇦Canada\" h=18",
1015 + "a \"🇺🇸United States\" h=18",
1016 + "a \"🇫🇷France\" h=18",
1017 + "a \"contact@spboucher.ai\" h=15",
1018 + "a \"MacLustr\" h=15"
1019 + ],
1020 + "sticky": [
1021 + 57,
1022 + 142
1023 + ],
1024 + "docH": 1759,
1025 + "errors": []
1026 + },
1027 + {
1028 + "name": "cmp-economy",
1029 + "path": "/compare/canada/united-states/france?tab=economy",
1030 + "width": 1440,
1031 + "overflow": 0,
1032 + "wide": [],
1033 + "small": [
1034 + "a \"Skip to content\" 1x1",
1035 + "button \"Move Canada up\" 24x36",
1036 + "button \"Move Canada down\" 24x36",
1037 + "button \"Remove Canada from the compari\" 28x36",
1038 + "button \"Move United States up\" 24x36",
1039 + "button \"Move United States down\" 24x36",
1040 + "button \"Remove United States from the \" 28x36",
1041 + "button \"Move France up\" 24x36",
1042 + "button \"Move France down\" 24x36",
1043 + "button \"Remove France from the compari\" 28x36",
1044 + "input \"\" 13x13",
1045 + "a \"API\" 22x32"
1046 + ],
1047 + "smallH": [
1048 + "a \"Skip to content\" h=1",
1049 + "select \"From\" h=19",
1050 + "select \"To\" h=19",
1051 + "input \"\" h=13",
1052 + "a \"GDP (current US$)\" h=17",
1053 + "button \"Source for Canada, United Stat\" h=28",
1054 + "a \"GDP, PPP (current internationa\" h=17",
1055 + "button \"Source for Canada, United Stat\" h=28",
1056 + "a \"GDP per capita (current US$)\" h=17",
1057 + "button \"Source for Canada, United Stat\" h=28",
1058 + "a \"GDP per capita, PPP (current i\" h=17",
1059 + "button \"Source for Canada, United Stat\" h=28"
1060 + ],
1061 + "sticky": [
1062 + 57,
1063 + 142
1064 + ],
1065 + "docH": 7705,
1066 + "errors": []
1067 + },
1068 + {
1069 + "name": "cmp-hero",
1070 + "path": "/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100",
1071 + "width": 1440,
1072 + "overflow": 0,
1073 + "wide": [],
1074 + "small": [
1075 + "a \"Skip to content\" 1x1",
1076 + "button \"Move Canada up\" 24x36",
1077 + "button \"Move Canada down\" 24x36",
1078 + "button \"Remove Canada from the compari\" 28x36",
1079 + "button \"Move United States up\" 24x36",
1080 + "button \"Move United States down\" 24x36",
1081 + "button \"Remove United States from the \" 28x36",
1082 + "button \"Move France up\" 24x36",
1083 + "button \"Move France down\" 24x36",
1084 + "button \"Remove France from the compari\" 28x36",
1085 + "input \"\" 13x13",
1086 + "button \"10 y\" 42x32"
1087 + ],
1088 + "smallH": [
1089 + "a \"Skip to content\" h=1",
1090 + "select \"From\" h=19",
1091 + "select \"To\" h=19",
1092 + "input \"\" h=13",
1093 + "a \"GDP per capita (current US$)\" h=26",
1094 + "button \"Source for Canada, United Stat\" h=28",
1095 + "a \"GDP (current US$)\" h=17",
1096 + "button \"Source for Canada, United Stat\" h=28",
1097 + "a \"GDP, PPP (current internationa\" h=17",
1098 + "button \"Source for Canada, United Stat\" h=28",
1099 + "a \"GDP per capita, PPP (current i\" h=17",
1100 + "button \"Source for Canada, United Stat\" h=28"
1101 + ],
1102 + "sticky": [
1103 + 57,
1104 + 142
1105 + ],
1106 + "docH": 8294,
1107 + "errors": []
1108 + },
1109 + {
1110 + "name": "rankings",
1111 + "path": "/rankings",
1112 + "width": 1440,
1113 + "overflow": 0,
1114 + "wide": [],
1115 + "small": [
1116 + "a \"Skip to content\" 1x1",
1117 + "a \"API\" 22x32"
1118 + ],
1119 + "smallH": [
1120 + "a \"Skip to content\" h=1",
1121 + "a \"contact@spboucher.ai\" h=15",
1122 + "a \"MacLustr\" h=15"
1123 + ],
1124 + "sticky": [
1125 + 57,
1126 + 64
1127 + ],
1128 + "docH": 10106,
1129 + "errors": []
1130 + },
1131 + {
1132 + "name": "rk-gdppc",
1133 + "path": "/rankings/gdp-per-capita",
1134 + "width": 1440,
1135 + "overflow": 0,
1136 + "wide": [],
1137 + "small": [
1138 + "a \"Skip to content\" 1x1",
1139 + "button \"Remove Luxembourg\" 28x36",
1140 + "button \"Remove Ireland\" 28x36",
1141 + "button \"Remove Switzerland\" 28x36",
1142 + "a \"API\" 22x32"
1143 + ],
1144 + "smallH": [
1145 + "a \"Skip to content\" h=1",
1146 + "a \"Rankings\" h=18",
1147 + "a \"Economy\" h=18",
1148 + "select \"Year\" h=19",
1149 + "select \"Group\" h=19",
1150 + "a \"contact@spboucher.ai\" h=15",
1151 + "a \"MacLustr\" h=15"
1152 + ],
1153 + "sticky": [
1154 + 57
1155 + ],
1156 + "docH": 2667,
1157 + "errors": []
1158 + },
1159 + {
1160 + "name": "rk-life-oecd",
1161 + "path": "/rankings/life-expectancy?group=oecd",
1162 + "width": 1440,
1163 + "overflow": 0,
1164 + "wide": [],
1165 + "small": [
1166 + "a \"Skip to content\" 1x1",
1167 + "a \"Health\" 37x18",
1168 + "button \"Remove Switzerland\" 28x36",
1169 + "button \"Remove Sweden\" 28x36",
1170 + "button \"Remove Japan\" 28x36",
1171 + "a \"API\" 22x32"
1172 + ],
1173 + "smallH": [
1174 + "a \"Skip to content\" h=1",
1175 + "a \"Rankings\" h=18",
1176 + "a \"Health\" h=18",
1177 + "select \"Year\" h=19",
1178 + "select \"Group\" h=19",
1179 + "a \"contact@spboucher.ai\" h=15",
1180 + "a \"MacLustr\" h=15"
1181 + ],
1182 + "sticky": [
1183 + 57
1184 + ],
1185 + "docH": 2667,
1186 + "errors": []
1187 + },
1188 + {
1189 + "checks": [
1190 + {
1191 + "check": "compare mode round-trip",
1192 + "url": "http://localhost:8290/compare/canada/united-states/france?tab=economy&mode=index100",
1193 + "ok": true
1194 + },
1195 + {
1196 + "check": "tab switch updates URL",
1197 + "url": "http://localhost:8290/compare/canada/united-states/france?tab=population&mode=index100",
1198 + "ok": true
1199 + },
1200 + {
1201 + "check": "ranking year round-trip",
1202 + "url": "http://localhost:8290/rankings/gdp-per-capita?year=2015",
1203 + "ok": true
1204 + },
1205 + {
1206 + "check": "404 /compare/canada",
1207 + "status": 404,
1208 + "ok": true
1209 + },
1210 + {
1211 + "check": "404 /rankings/not-a-thing",
1212 + "status": 404,
1213 + "ok": true
1214 + },
1215 + {
1216 + "check": "404 /compare/canada/nowhere-land",
1217 + "status": 404,
1218 + "ok": true
1219 + }
1220 + ]
1221 + }
1222 +]
\ No newline at end of file
added apps/web/qa/screens/compare-rankings/rk-gdppc-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-gdppc-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-gdppc-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-gdppc-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-gdppc-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-gdppc-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/rk-life-oecd-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-1280-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-1280-y1800.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-1280-y900.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-390-y1800.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-economy-390-y900.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-hero-1280-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-hero-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-hero-390-y700.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-snapshot-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/cmp-snapshot-390-y700.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-added-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-custom-1280.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-health-hero-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-indpicker-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-options-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-picker-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-prov-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-rk-filters-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-rk-g20-dark-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-rk-highlight-dark-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/i-rk-history-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rankings-1280-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rankings-1280-y900.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rankings-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rankings-390-y800.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-1280-y1800.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-1280-y900.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-390-y2600.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-390-y3600.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-gdppc-390-y800.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-life-oecd-1280-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-life-oecd-1280-y1600.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-life-oecd-390-y0.png +0 −0

Binary file not shown.

added apps/web/qa/screens/compare-rankings/seg/rk-life-oecd-390-y1200.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-1280.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-1440.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-320.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-360.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-375.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-390.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-414.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries-430.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-1280.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-1440.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-320.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-360.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-375.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-390.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-414.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada-430.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-1280.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-1440.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-320.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-360.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-375.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-390.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-414.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/countries_canada_economy-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/admin_admin-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_admin_coverage-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_admin_issues-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_admin_raw-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_admin_runs-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_login-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/admin_login-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/admin_login-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/api-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/api-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/api-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/changes-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/changes-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/changes-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/data-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/data-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/data-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/explore-1280.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/explore-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/indicators-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-1440.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-320.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-375.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-390.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_gdp-per-capita-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_life-expectancy-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_life-expectancy-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/indicators_life-expectancy-390-touch.png +0 −0

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/indicators_life-expectancy-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_topic_health-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/indicators_topic_health-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/indicators_topic_health-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/methodology-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/methodology-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/methodology-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/regions-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/regions-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/regions-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/regions_oecd-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/regions_oecd-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/regions_oecd-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/report.json +1141 −0
Large diff (1,142 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/qa/screens/explore/sources-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/sources-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/sources-430.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/sources_worldbank-1280.png +0 −0

Binary file not shown.

added apps/web/qa/screens/explore/sources_worldbank-1440.png +0 −0

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

added apps/web/qa/screens/explore/sources_worldbank-430.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-320.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-360.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-375.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-390.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-414.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/home-430.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/report.json +227 −299
Large diff (878 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/qa/screens/seg/canada-1440-dna.png +0 −0

Binary file not shown.

added apps/web/qa/screens/seg/canada-320-similar.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-1440-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-1440-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-1440-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-1440-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-1440-4.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-390-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-390-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-390-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries-390-4.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-1440-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-1440-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-1440-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-1440-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-1440-4.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-390-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-390-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-390-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-390-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada-390-4.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-1440-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-1440-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-1440-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-1440-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-1440-4.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-390-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-390-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-390-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-390-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/countries_canada_economy-390-4.png +0 −0

Binary file not shown.

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

Binary file not shown.

modified apps/web/qa/screens/seg/home-390-0.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/home-390-1.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/home-390-2.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/home-390-3.png +0 −0

Binary file not shown.

modified apps/web/qa/screens/seg/home-390-4.png +0 −0

Binary file not shown.

added apps/web/qa/shots-explore.mjs +120 −0
Large diff (120 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/qa/shots-real.mjs +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/coverage/page.tsx +20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/issues/page.tsx +73 −0
Large diff (73 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/layout.tsx +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/page.tsx +135 −0
Large diff (135 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/raw/page.tsx +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/(panel)/runs/page.tsx +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/actions.ts +61 −0
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/admin/login/page.tsx +37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/api/page.tsx +253 −0
Large diff (253 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/app/apple-icon.png +0 −0

Binary file not shown.

added apps/web/src/app/changes/page.tsx +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/compare/[...slugs]/page.tsx +197 −0
Large diff (197 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/compare/og/route.tsx +50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/compare/page.tsx +45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/app/countries/[slug]/[topic]/page.tsx +4 −3
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/app/countries/[slug]/page.tsx +2 −1
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/explore/page.tsx +159 −0
Large diff (159 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/app/icon.png +0 −0

Binary file not shown.

modified apps/web/src/app/icon.svg +1 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/indicators/[slug]/opengraph-image.tsx +62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/indicators/[slug]/page.tsx +350 −0
Large diff (350 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/indicators/page.tsx +61 −0
Large diff (61 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/methodology/page.tsx +283 −0
Large diff (283 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/rankings/[indicator]/opengraph-image.tsx +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/rankings/[indicator]/page.tsx +135 −0
Large diff (135 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/rankings/page.tsx +48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/regions/[slug]/page.tsx +117 −0
Large diff (117 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/regions/page.tsx +94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/app/sitemap.ts +25 −1
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/sources/[id]/page.tsx +190 −0
Large diff (190 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/app/sources/page.tsx +91 −0
Large diff (91 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/admin/admin-nav.tsx +34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/admin/admin-ui.tsx +100 −0
Large diff (100 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/admin/coverage-table.tsx +128 −0
Large diff (128 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/brand/Logo.tsx +1 −1
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/changes/changes-feed.tsx +254 −0
Large diff (254 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/charts/dna-radial.tsx +4 −4
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/charts/line-chart.tsx +7 −1
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/charts/ranked-bars.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/compare-builder.tsx +210 −0
Large diff (210 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/compare-chart.tsx +222 −0
Large diff (222 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/compare-controls.tsx +256 −0
Large diff (256 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/country-picker.tsx +169 −0
Large diff (169 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/custom-panel.tsx +110 −0
Large diff (110 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/compare/snapshot-table.tsx +158 −0
Large diff (158 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/countries/directory.tsx +4 −4
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/country/indicator-row.tsx +4 −4
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/country/similar-panel.tsx +5 −5
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/copy-button.tsx +42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/download-picker.tsx +37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/entity-picker-nav.tsx +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/entity-picker.tsx +164 −0
Large diff (164 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/page-header.tsx +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/explore/similarity-playground.tsx +90 −0
Large diff (90 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/home/region-chips.tsx +2 −2
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/indicators/directory.tsx +161 −0
Large diff (161 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/indicators/indicator-compare.tsx +117 −0
Large diff (117 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/indicators/indicator-map.tsx +134 −0
Large diff (134 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/indicators/indicator-trend.tsx +121 −0
Large diff (121 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/indicators/map-geometry.ts +17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/components/layout/site-footer.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/methodology/toc.tsx +36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/rankings/rank-history-chart.tsx +151 −0
Large diff (151 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/rankings/rank-history-panel.tsx +82 −0
Large diff (82 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/rankings/ranking-view.tsx +295 −0
Large diff (295 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/rankings/rankings-directory.tsx +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/rankings/top3-preview.tsx +88 −0
Large diff (88 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/regions/member-map.tsx +40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/regions/member-ranking.tsx +83 −0
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/components/regions/members-table.tsx +132 −0
Large diff (132 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/i18n/en.compare.ts +198 −0
Large diff (198 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/i18n/en.explore.ts +550 −0
Large diff (550 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/i18n/en.ts +5 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/admin-api.ts +55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/admin-auth.ts +49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/api-compare.ts +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/api-explore.ts +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/client-api-compare.ts +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/client-api-explore.ts +41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/compare-presets.ts +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/compare-state.ts +107 −0
Large diff (107 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/ranking-state.ts +50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
modified apps/web/src/lib/site.ts +14 −1
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/types-compare.ts +124 −0
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
added apps/web/src/lib/types-explore.ts +394 −0
Large diff (395 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme