134 lines
2.9 KiB
Svelte
134 lines
2.9 KiB
Svelte
|
|
<script lang="ts">
|
||
|
|
import { exhibit, stops } from '$lib/stops';
|
||
|
|
import { precacheState } from '$lib/precache.svelte';
|
||
|
|
|
||
|
|
const pre = $derived(precacheState());
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<header class="hero">
|
||
|
|
<h1>{exhibit.title}</h1>
|
||
|
|
{#if exhibit.subtitle}
|
||
|
|
<p class="subtitle">{exhibit.subtitle}</p>
|
||
|
|
{/if}
|
||
|
|
</header>
|
||
|
|
|
||
|
|
{#if pre.state === 'running'}
|
||
|
|
<div class="precache" role="status" aria-live="polite">
|
||
|
|
<span class="dot" aria-hidden="true"></span>
|
||
|
|
<span class="text">
|
||
|
|
Loading exhibit for offline listening… {pre.done}/{pre.total}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
|
||
|
|
<main class="grid" aria-label="Exhibit stops">
|
||
|
|
{#each stops as stop (stop.id)}
|
||
|
|
<a class="tile" href="/stop/{stop.id}" data-sveltekit-preload-data="hover">
|
||
|
|
<div class="tile-image">
|
||
|
|
<img src={stop.image} alt="" loading="lazy" />
|
||
|
|
</div>
|
||
|
|
<div class="tile-meta">
|
||
|
|
<span class="tile-num">{String(stop.id).padStart(2, '0')}</span>
|
||
|
|
<span class="tile-title">{stop.title}</span>
|
||
|
|
</div>
|
||
|
|
</a>
|
||
|
|
{/each}
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.hero {
|
||
|
|
padding: 2rem 1.25rem 1rem;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
.precache {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.6rem;
|
||
|
|
margin: 0 auto 0.5rem;
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
max-width: 32rem;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
color: var(--ink-mute);
|
||
|
|
background: var(--bg-elev);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
box-shadow: var(--shadow);
|
||
|
|
}
|
||
|
|
.precache .dot {
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: var(--accent);
|
||
|
|
animation: pulse 1.4s ease-in-out infinite;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
@keyframes pulse {
|
||
|
|
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||
|
|
50% { opacity: 1; transform: scale(1.1); }
|
||
|
|
}
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
.precache .dot { animation: none; opacity: 0.7; }
|
||
|
|
}
|
||
|
|
.hero h1 {
|
||
|
|
font-size: clamp(2rem, 5vw, 3rem);
|
||
|
|
}
|
||
|
|
.subtitle {
|
||
|
|
margin: 0.5rem 0 0;
|
||
|
|
color: var(--ink-mute);
|
||
|
|
}
|
||
|
|
.grid {
|
||
|
|
display: grid;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 1rem;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||
|
|
}
|
||
|
|
@media (min-width: 768px) {
|
||
|
|
.grid {
|
||
|
|
gap: 1.25rem;
|
||
|
|
padding: 1.25rem 2rem 3rem;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.tile {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.75rem;
|
||
|
|
color: inherit;
|
||
|
|
border-radius: var(--radius-lg);
|
||
|
|
background: var(--bg-elev);
|
||
|
|
box-shadow: var(--shadow);
|
||
|
|
overflow: hidden;
|
||
|
|
transition: transform 0.15s ease;
|
||
|
|
}
|
||
|
|
.tile:active {
|
||
|
|
transform: scale(0.98);
|
||
|
|
}
|
||
|
|
.tile-image {
|
||
|
|
aspect-ratio: 4 / 3;
|
||
|
|
background: var(--rule);
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
.tile-image img {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
.tile-meta {
|
||
|
|
display: flex;
|
||
|
|
align-items: baseline;
|
||
|
|
gap: 0.6rem;
|
||
|
|
padding: 0.25rem 1rem 1rem;
|
||
|
|
}
|
||
|
|
.tile-num {
|
||
|
|
font-family: var(--font-serif);
|
||
|
|
font-size: 0.95rem;
|
||
|
|
color: var(--ink-mute);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
.tile-title {
|
||
|
|
font-family: var(--font-serif);
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
line-height: 1.2;
|
||
|
|
}
|
||
|
|
</style>
|