docent/web/src/routes/+page.svelte

134 lines
2.9 KiB
Svelte
Raw Normal View History

2026-05-05 09:33:51 -05:00
<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>