All checks were successful
Test and deploy staging / staging (push) Successful in 1m49s
World map with switchable layers (what "liberal" means, what left vs right is about, traditional/secular and survival/self-expression values), country cards, a shared-scale party compass, a values scatter and national narratives of major events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
4.1 KiB
Svelte
119 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import './world-opinions.css';
|
|
import CountryPanel from '$lib/world-opinions/CountryPanel.svelte';
|
|
import CultureMap from '$lib/world-opinions/CultureMap.svelte';
|
|
import PartyCompass from '$lib/world-opinions/PartyCompass.svelte';
|
|
import WorldMap, { type MapMode } from '$lib/world-opinions/WorldMap.svelte';
|
|
import { countries, countryById, narratives, sources } from '$lib/world-opinions/data';
|
|
|
|
const modes: { id: MapMode; label: string }[] = [
|
|
{ id: 'liberal', label: 'what "liberal" means' },
|
|
{ id: 'divide', label: 'what left vs right is about' },
|
|
{ id: 'tradition', label: 'traditional ↔ secular' },
|
|
{ id: 'expression', label: 'survival ↔ self-expression' }
|
|
];
|
|
const sorted = [...countries].sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
let mode = $state<MapMode>('liberal');
|
|
let countryId = $state('us');
|
|
let eventIndex = $state(0);
|
|
let narrative = $derived(narratives[eventIndex]);
|
|
|
|
function select(id: string) {
|
|
countryId = id;
|
|
document.getElementById('country')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
</script>
|
|
|
|
<div class="wo mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
|
<header class="mb-8">
|
|
<h1 class="mb-3 text-4xl font-semibold tracking-tight sm:text-5xl">world opinions</h1>
|
|
<p class="text-lg text-[var(--site-muted)]">
|
|
Same words, different politics. Hover the map, click a country.
|
|
</p>
|
|
</header>
|
|
|
|
<section aria-label="World map">
|
|
<div class="mb-4 flex flex-wrap gap-2" role="tablist" aria-label="Map layer">
|
|
{#each modes as item (item.id)}
|
|
<button
|
|
class="wo-chip"
|
|
role="tab"
|
|
aria-selected={mode === item.id}
|
|
onclick={() => (mode = item.id)}>{item.label}</button
|
|
>
|
|
{/each}
|
|
</div>
|
|
<WorldMap {mode} selected={countryId} onselect={select} />
|
|
</section>
|
|
|
|
<section id="country" class="scroll-mt-20 pt-10" aria-label="Country">
|
|
<label class="mb-3 block max-w-xs text-sm">
|
|
<span class="sr-only">Country</span>
|
|
<select
|
|
bind:value={countryId}
|
|
class="w-full rounded-md border border-[var(--site-border)] bg-[var(--site-surface)] px-3 py-2"
|
|
>
|
|
{#each sorted as country (country.id)}
|
|
<option value={country.id}>{country.name}</option>
|
|
{/each}
|
|
</select>
|
|
</label>
|
|
<CountryPanel id={countryId} />
|
|
</section>
|
|
|
|
<section class="border-t border-[var(--site-border)] mt-12 py-10">
|
|
<h2 class="mb-1 text-2xl font-semibold">party compass</h2>
|
|
<p class="mb-5 text-[var(--site-muted)]">Every party on one shared scale.</p>
|
|
<PartyCompass focus={countryId} />
|
|
</section>
|
|
|
|
<section class="border-t border-[var(--site-border)] py-10">
|
|
<h2 class="mb-1 text-2xl font-semibold">values map</h2>
|
|
<p class="mb-5 text-[var(--site-muted)]">Both value axes at once.</p>
|
|
<CultureMap selected={countryId} onselect={select} />
|
|
</section>
|
|
|
|
<section class="border-t border-[var(--site-border)] py-10">
|
|
<h2 class="mb-4 text-2xl font-semibold">one event, many stories</h2>
|
|
<div class="mb-5 flex flex-wrap gap-2" role="tablist" aria-label="Event">
|
|
{#each narratives as item, index (item.event)}
|
|
<button
|
|
class="wo-chip"
|
|
role="tab"
|
|
aria-selected={eventIndex === index}
|
|
onclick={() => (eventIndex = index)}
|
|
>{item.event.toLowerCase()} <span class="opacity-60">{item.years}</span></button
|
|
>
|
|
{/each}
|
|
</div>
|
|
<div role="tabpanel">
|
|
<ul class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{#each narrative.views as view (view.country)}
|
|
<li>
|
|
<button class="wo-card wo-quote w-full text-left" onclick={() => select(view.country)}>
|
|
<span class="text-sm text-[var(--site-muted)]"
|
|
>{countryById.get(view.country)?.name}</span
|
|
>
|
|
<span class="block text-lg">“{view.story}”</span>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="border-t border-[var(--site-border)] pt-6 text-sm text-[var(--site-muted)]">
|
|
<p class="mb-2">Approximate, hand-placed positions for comparison. Based on:</p>
|
|
<p class="flex flex-wrap gap-x-4 gap-y-1">
|
|
{#each sources as source (source.href)}
|
|
<a
|
|
class="underline underline-offset-2 hover:text-[var(--site-text)]"
|
|
href={source.href}
|
|
rel="external">{source.name}</a
|
|
>
|
|
{/each}
|
|
</p>
|
|
</footer>
|
|
</div>
|