layout update

This commit is contained in:
2026-06-16 15:07:16 +09:00
parent d09946161f
commit 36f0c25721
2 changed files with 310 additions and 176 deletions

View File

@@ -29,8 +29,8 @@
class="slider"
style="transform: translateX({screen === 'worldmap' ? 0 : 100}%);"
></div>
<button onclick={() => onNavigate('worldmap')}>Worldmap</button>
<button onclick={() => onNavigate('timeline')}>Timeline</button>
<button class:active={screen === 'worldmap'} onclick={() => onNavigate('worldmap')}>Worldmap</button>
<button class:active={screen === 'timeline'} onclick={() => onNavigate('timeline')}>Timeline</button>
</div>
</div>
@@ -103,18 +103,18 @@
display: flex;
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: 8px;
padding: 3px;
border-radius: 9999px;
padding: 4px;
}
.slider {
position: absolute;
top: 3px;
left: 3px;
width: calc(50% - 3px);
height: calc(100% - 6px);
background: var(--bg);
border-radius: 6px;
top: 4px;
left: 4px;
width: calc(50% - 4px);
height: calc(100% - 8px);
background: var(--accent);
border-radius: 9999px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
transition: transform 0.25s ease;
pointer-events: none;
@@ -124,15 +124,20 @@
position: relative;
z-index: 1;
flex: 1;
padding: 4px 18px;
padding: 6px 24px;
border: none;
background: none;
cursor: pointer;
font-family: var(--sans);
font-size: 13px;
font-size: 14px;
font-weight: 300;
color: var(--text);
letter-spacing: 0.01em;
transition: color 0.2s ease;
}
.segmented button.active {
color: #fff;
}
.right {
display: flex;

View File

@@ -1,18 +1,46 @@
<script>
import { CONTINENTS, getContinent, continentTotals } from './continents.js';
import { getSelected } from '../layout/selection.svelte.js';
import { getSelected, getTotalCount } from '../layout/selection.svelte.js';
import worldData from 'world-atlas/countries-50m.json';
let hoveredSeg = $state(null);
let collapsed = $state(false);
const continentColors = {
'Europe': '#6366f1',
'Asia': '#f43f5e',
'Africa': '#fb923c',
'N. America': '#06b6d4',
'S. America': '#f59e0b',
'Oceania': '#8b5cf6'
'Europe': '#3b82f6',
'Asia': '#ef4444',
'Africa': '#f97316',
'N. America': '#ec4899',
'S. America': '#eab308',
'Oceania': '#a16207'
};
const countryNameById = $derived.by(() => {
const map = { XK: 'Kosovo' };
for (const g of worldData.objects.countries.geometries) {
map[g.id] = g.properties?.name || g.id;
}
return map;
});
let visitedCountries = $derived(
[...getSelected()].map(id => countryNameById[id]).filter(Boolean).sort()
);
let visitedByContinent = $derived.by(() => {
const map = {};
for (const id of getSelected()) {
const cont = getContinent(id);
if (cont) {
if (!map[cont]) map[cont] = [];
map[cont].push(countryNameById[id] || id);
}
}
for (const cont of Object.keys(map)) {
map[cont].sort();
}
return map;
});
let counts = $derived.by(() => {
const c = {};
for (const cont of CONTINENTS) c[cont] = 0;
@@ -36,9 +64,11 @@
if (angle > 0) {
const startDeg = deg;
const endDeg = deg + angle;
const midDeg = (startDeg + endDeg) / 2;
const rad = (midDeg - 90) * Math.PI / 180;
const sr = (startDeg - 90) * Math.PI / 180;
const er = (endDeg - 90) * Math.PI / 180;
const cx = 50, cy = 50, outerR = 44, innerR = 22;
const cx = 90, cy = 90, outerR = 65, innerR = 30;
const x1 = cx + outerR * Math.cos(sr);
const y1 = cy + outerR * Math.sin(sr);
const x2 = cx + outerR * Math.cos(er);
@@ -49,7 +79,9 @@
const y4 = cy + innerR * Math.sin(sr);
const largeArc = angle > 180 ? 1 : 0;
const path = `M ${x1} ${y1} A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2} L ${x3} ${y3} A ${innerR} ${innerR} 0 ${largeArc} 0 ${x4} ${y4} Z`;
segs.push({ cont, color: continentColors[cont], path, angle });
const lx = cx + 82 * Math.cos(rad);
const ly = cy + 82 * Math.sin(rad);
segs.push({ cont, color: continentColors[cont], path, lx, ly, angle });
deg += angle;
}
}
@@ -57,199 +89,296 @@
});
</script>
<div class="card">
<!-- count -->
<div class="stat-block">
<span class="big-num">{total}</span>
<span class="stat-sub">countries visited</span>
</div>
<div class="panel" class:collapsed>
<button class="collapse-btn" onclick={() => collapsed = !collapsed} data-tip={collapsed ? 'see statistics' : 'close statistics'}>
{collapsed ? '◀' : '▶'}
</button>
<div class="vdivider"></div>
{#if !collapsed}
<div class="panel-content">
<h2 class="headline">your statistics</h2>
<!-- world % -->
<div class="stat-block">
<span class="big-num accent">{pct}%</span>
<span class="stat-sub">of the world</span>
</div>
<div class="total-bar-bg">
<div class="total-bar-fill" style="width: {pct}%"></div>
<span class="bar-pct">{pct}%</span>
</div>
<span class="total-bar-text">{total} / {grandTotal} countries visited</span>
<div class="vdivider"></div>
<div class="divider"></div>
<!-- donut -->
<div class="donut-block">
<svg viewBox="0 0 100 100" class="donut-svg">
{#if segments.length > 0}
{#each segments as seg}
<g class="seg-group"
onmouseenter={() => hoveredSeg = seg}
onmouseleave={() => hoveredSeg = null}>
<path d={seg.path} fill={seg.color} />
</g>
{/each}
<circle cx="50" cy="50" r="22" fill="#fff" />
{:else}
<circle cx="50" cy="50" r="44" fill="#f1f5f9" />
<circle cx="50" cy="50" r="22" fill="#fff" />
{/if}
</svg>
<div class="donut-info">
<span class="section-label">by continent</span>
{#if hoveredSeg}
<div class="tooltip" style="--dot:{hoveredSeg.color}">
<span class="tt-name">{hoveredSeg.cont}</span>
<span class="tt-val">{counts[hoveredSeg.cont]} / {continentTotals[hoveredSeg.cont]}</span>
<span class="bar-label">by continent</span>
{#each CONTINENTS as continent}
{@const contTotal = continentTotals[continent]}
<div class="row tooltip-wrap">
<span class="dot" style="background: {continentColors[continent]}"></span>
<span class="label">{continent}</span>
<span class="value">{counts[continent]}<span class="total">/{contTotal}</span></span>
{#if visitedByContinent[continent]?.length > 0}
<div class="tooltip-list">
{#each visitedByContinent[continent].slice(0, 10) as country}
<span class="tooltip-item">{country}</span>
{/each}
{#if visitedByContinent[continent].length > 10}
<span class="tooltip-item tooltip-more">...</span>
{/if}
</div>
{/if}
</div>
{:else}
<span class="hint">hover a slice</span>
{/if}
</div>
</div>
{/each}
<div class="vdivider"></div>
<div class="donut-wrap">
{#if segments.length > 0}
<svg viewBox="-25 -25 230 230" class="donut-svg">
{#each segments as seg}
<g class="seg-group">
<path d={seg.path} fill={seg.color} />
<text x={seg.lx} y={seg.ly} text-anchor="middle" dominant-baseline="middle" class="donut-label" style="font-size: {seg.angle < 20 ? 12 : 15}px">{seg.cont}</text>
</g>
{/each}
<circle cx="90" cy="90" r="30" fill="var(--bg-raised)" />
</svg>
{:else}
<svg viewBox="-25 -25 230 230" class="donut-svg">
<circle cx="90" cy="90" r="65" fill="var(--border)" />
<circle cx="90" cy="90" r="30" fill="var(--bg-raised)" />
</svg>
{/if}
</div>
<!-- progress bar -->
<div class="bar-block">
<span class="section-label" style="margin-bottom:6px">world coverage</span>
<div class="bar-bg">
<div class="bar-fill" style="width:{pct}%"></div>
<div class="divider"></div>
<div class="disclaimer">Contains all UN countries, Kosovo, Hong Kong and Taiwan</div>
</div>
<span class="disclaimer">All UN countries · Kosovo · HK · Taiwan</span>
</div>
{/if}
</div>
<style>
.card {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.10), 0 1px 4px rgba(0,0,0,0.06);
border: 1px solid rgba(0,0,0,0.06);
.panel {
flex: 0 0 min(360px, 25vw);
background: var(--bg-raised);
border-left: 1px solid var(--border);
display: flex;
flex-direction: row;
align-items: center;
gap: 0;
padding: 0 4px;
height: 110px;
z-index: 10;
font-family: var(--sans);
white-space: nowrap;
transition: flex-basis 0.25s ease;
}
.stat-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 0 36px;
.panel.collapsed {
flex: 0 0 28px;
border-left: none;
}
.big-num {
font-size: 40px;
font-weight: 300;
letter-spacing: -2px;
color: var(--text-h);
.panel-content {
flex: 1;
padding: 24px 28px;
overflow-y: auto;
min-width: 0;
}
.collapse-btn {
flex: 0 0 auto;
align-self: flex-start;
background: var(--accent-bg);
border: none;
border-radius: 0 8px 8px 0;
padding: 14px 5px;
cursor: pointer;
font-size: 16px;
line-height: 1;
color: var(--accent);
transition: background 0.15s ease, padding 0.15s ease;
margin-top: 24px;
position: relative;
}
.big-num.accent { color: var(--accent); }
.stat-sub {
.collapse-btn:hover {
background: var(--lavender-bg);
padding-right: 8px;
}
.collapse-btn::after {
content: attr(data-tip);
position: absolute;
right: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
background: var(--text-h);
color: var(--bg-raised);
font-family: var(--sans);
font-size: 12px;
font-weight: 300;
color: var(--text-sub);
letter-spacing: 0.03em;
padding: 6px 12px;
border-radius: 6px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.vdivider {
width: 1px;
height: 56px;
background: var(--border);
flex-shrink: 0;
.collapse-btn:hover::after {
opacity: 1;
}
/* donut */
.donut-block {
display: flex;
flex-direction: row;
align-items: center;
gap: 14px;
padding: 0 28px;
}
.donut-svg {
width: 72px;
height: 72px;
flex-shrink: 0;
}
.seg-group { cursor: pointer; }
.seg-group:hover path { opacity: 0.8; }
.donut-info {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 130px;
}
.tooltip {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
}
.tooltip::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--dot);
flex-shrink: 0;
}
.tt-name { font-weight: 400; color: var(--text-h); }
.tt-val { font-weight: 300; color: var(--text-sub); }
.section-label {
font-size: 10px;
font-weight: 500;
letter-spacing: 0.14em;
.headline {
font-family: var(--heading);
font-size: var(--text-sm);
font-weight: 400;
text-transform: uppercase;
color: var(--text-sub);
letter-spacing: 0.1em;
color: var(--accent);
margin: 0 0 20px 0;
}
.hint {
font-size: 12px;
.bar-label {
font-family: var(--sans);
font-size: var(--text-xs);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-sub);
opacity: 0.45;
display: block;
margin-bottom: 8px;
}
/* bar */
.bar-block {
display: flex;
flex-direction: column;
padding: 0 28px;
gap: 0;
min-width: 160px;
}
.bar-bg {
width: 100%;
height: 5px;
background: var(--bg-subtle);
border-radius: 4px;
.total-bar-bg {
position: relative;
height: 28px;
background: var(--accent-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.bar-fill {
.total-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), #a78bfa);
border-radius: 4px;
background: linear-gradient(90deg, var(--accent-dark), var(--lavender));
border-radius: 10px;
transition: width 0.3s ease;
min-width: 0;
}
.disclaimer {
font-size: 11px;
.bar-pct {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-h);
}
.total-bar-text {
display: block;
text-align: center;
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-h);
margin-top: 6px;
}
.divider {
height: 1px;
background: var(--border);
margin: 16px 0;
}
.row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.label {
flex: 1;
font-size: var(--text-sm);
font-weight: 300;
color: var(--text);
}
.value {
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-h);
}
.total {
font-weight: 400;
color: var(--text-sub);
opacity: 0.5;
letter-spacing: 0.02em;
font-size: var(--text-xs);
}
.donut-wrap {
display: flex;
justify-content: center;
margin: 8px 0 20px;
}
.donut-svg {
width: 180px;
height: 180px;
filter: drop-shadow(0 2px 8px rgba(99,102,241,0.15));
}
.donut-label {
fill: var(--text-h);
font-family: var(--sans);
font-weight: 300;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.seg-group:hover .donut-label {
opacity: 1;
}
.tooltip-wrap {
position: relative;
}
.tooltip-list {
display: none;
position: absolute;
top: calc(100% + 6px);
left: 0;
background: var(--text-h);
color: var(--bg-raised);
font-family: var(--sans);
font-size: 12px;
line-height: 1.5;
padding: 8px 12px;
border-radius: 8px;
box-shadow: var(--shadow);
z-index: 20;
white-space: nowrap;
min-width: 120px;
}
.tooltip-wrap:hover .tooltip-list {
display: block;
}
.tooltip-item {
display: block;
padding: 2px 0;
}
.tooltip-item + .tooltip-item {
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.disclaimer {
font-size: var(--text-xs);
color: var(--text-sub);
line-height: 1.5;
text-align: center;
}
</style>