Commit Graph

17 Commits

Author SHA1 Message Date
Alexander 9fe9d95e36 feat(hikes): unify below-map view transition into one sliding panel
The detail-page enter/exit transition previously slid only the metric
tiles up from the bottom — the wrapper had no background, so its
snapshot was transparent and no containing panel moved. The photo strip
also animated separately, sliding in from the right.

Wrap everything below the hero map (stage nav, photo strip, metrics,
tags, elevation, scroll area, footer) in one `.below-map` element with
`view-transition-name: hike-below-map` and an opaque background, so the
whole sheet — background included — slides up on enter and down on exit
as a single panel. Drop the obsolete hike-strip right-slide rules and
keyframes; rename hike-below-strip → hike-below-map.
2026-05-31 13:29:15 +02:00
Alexander 0f6c50f854 feat(hikes): no-JS elevation SVG + static trail-col map
- Render the elevation profile as an inline SVG at SSR (filled area
  + 5 ticks per axis + soft horizontal helplines). Chart.js takes
  over via a sticky `chartReady` flag once it imports and paints,
  fading the SVG out.
- Pre-rendered medium hero now underlays the desktop trail-col map,
  cover-cropped and `transform: scale(2.25)`d so the bbox fills the
  slot. Fades on first leaflet tile-paint, same handover as the
  hero map further up.
- Wrap everything below the photo strip in `.below-strip` so the
  view-transition into the detail page can slide the metrics,
  tags, charts, scroll-area and footer as a single block.
2026-05-26 22:48:41 +02:00
Alexander 8a67f5fba8 feat(hikes): medium hero variant + Switzerland-framed overview, drop static→live wobble
Three related improvements to the pre-rendered hero map system:

* New medium viewport variant (561–899 CSS px) for the per-hike detail
  hero and the /hikes overview. Tablet/split-pane viewports were
  getting the wide pose (chosen for ~1920 CSS px), which landed too
  zoomed in. Each variant is rendered at a pose matching its
  container, so the static→Leaflet handover aligns at every band.
  Manifest fields are optional — pages fall back to the wide variant
  on tablets until build-hikes regenerates the images.

* Overview frames on Switzerland (fixed center [46.82, 8.23]) with
  explicit per-variant zooms (wide=8, medium=8, narrow=7) rather than
  auto-fitting the union of hike bboxes. The previous behavior zoomed
  in on whichever corner the catalogue clustered in; this reads as
  "hikes across CH". Bumps OVERVIEW_RENDER_VERSION so cached overview
  images get invalidated on the next build.

* Removed the post-tile-load flyToBounds in both HikeMap.svelte and
  HikesOverviewMap.svelte. The map already opens at the static pose
  via setView; the second auto-fit was adding a visible wobble on
  routes whose bbox sits at an integer-zoom boundary (e.g. the
  Einsiedeln–Unteriberg detail), where the build-time fit and
  Leaflet's runtime fit disagree by one zoom step at the user's
  actual container size.
2026-05-26 11:51:48 +02:00
Alexander b49a299371 feat(hikes): view-transition flow across /hikes ↔ /hikes/[slug]
Cards + filter bar fly up from below when arriving at /hikes and drop
back down when leaving (in both directions of /hikes ↔ detail). Clicked
card morphs into the detail hero with a cross-fade so the thumbnail
dissolves into the map instead of snapping. Photo strip slides in from
the right. Root content cross-fades so metrics + content under the hero
phase in rather than appear at the end of the morph.

Track JSON moves from a client-side $effect into +page.ts so the strip
is in the DOM at view-transition snapshot time — also kills the brief
layout shift when it used to pop in post-load.
2026-05-26 10:34:00 +02:00
Alexander 38c3df8187 feat(images): responsive <Image>, gated private images + prose
Build-time image optimization plus auth-gated private content.

- <Image> (src/lib/components/Image.svelte): wraps @sveltejs/enhanced-img
  for public images under src/lib/assets/images/ (AVIF/WebP, multiple
  widths, lazy by default), plus a `private` mode for auth-gated images.
- Private images: scripts/build-private-images.ts encodes sources from
  src/lib/assets/private-images/ into private-assets/ (outside the bundle)
  and a manifest; served only via the auth-checked /private-images/
  endpoint (X-Accel-Redirect in prod, disk read in dev).
- HikeImage gains a `src` prose mode: build-hikes encodes non-waypoint
  images referenced in .svx and exposes them by filename (imagesByName);
  a `private` attr routes them through the gated /hikes/<slug>/private/ path.
- <Private> (src/lib/components/Private.svelte): renders prose only to
  logged-in viewers (cosmetic gating — text still ships in the bundle).
- deploy.sh rsyncs private-assets/; prod needs an nginx internal
  /protected-images/ location.
2026-05-24 20:53:22 +02:00
Alexander c155fc33b4 fix(hikes): keep stat value + descriptor together in the metrics row
The metrics flex row stretched each tile to the tall route-icon's height,
so the grid's auto rows spread apart and pushed the value and its label to
opposite halves. Center the items instead of stretching.
2026-05-24 10:17:45 +02:00
Alexander 2347a02fcb feat(hikes): worldwide maps via a region-switching tile proxy
Add tile-proxy/: a small Rust (axum) service behind nginx that serves one
canonical XYZ scheme (/{karte,luftbild,dufour}/{z}/{x}/{y}) and, per tile,
picks the provider by geometry — swisstopo when the tile overlaps a
swisstopo-covered region (Switzerland or Liechtenstein, each simplified +
2 km buffer; tile-bbox ∩ polygon at every zoom), otherwise OpenTopoMap
(schematic) / Esri World Imagery (satellite), with an auto-fallback for
border 404s. Includes the region generator (gen-regions.mjs), a Makefile,
nginx caching-proxy + systemd examples, and a README. Listen address is
env-driven (TILE_PROXY_ADDR).

App side:
- New mapTiles.ts is the single source for the proxy URLs + combined
  attribution; HikeMap / HikesOverviewMap / EditMap fetch through
  maps.bocken.org instead of swisstopo directly, on-map attribution
  controls removed, preconnect + footer credits updated (swisstopo /
  OpenStreetMap+OpenTopoMap / Esri).
- Region-aware schematic max zoom (isSwissRegion helper): detail map caps
  at z17 abroad and hides the CH/LI-only Dufour layer; overview caps at
  z18 when a shown hike is abroad.
- Route-builder: add the satellite layer via the same bottom-right layer
  popover as the other maps.
2026-05-22 16:26:22 +02:00
Alexander 6483c55fce feat(hikes): multi-day stages (separate GPX tracks, stage nav, builder)
Represent a multi-day hike as separate named GPX <trk> elements, one per
stage, while still treating the whole thing as one route on the overview.

GPX & build:
- gpx.ts: parseGpxStages (one stage per <trk>) + multi-track buildGpx.
- build-hikes.ts: per-stage stats with totals summed across stages so the
  overnight gaps (distance, time) and the altitude jump between stages are
  excluded; previewBreaks recorded where stages sit >1 km apart.
- types: HikeStage, manifest `stages?` and `previewBreaks?` (both optional —
  single-stage hikes are unchanged).

Detail page:
- HikeStageNav: a light itinerary-stepper switcher (numbered nodes, active
  glows in the accent) writing a shared stageStore.
- Selecting a stage scopes the metrics, elevation profile (x-window),
  map (highlight + zoom, dim the rest) and photo strip/markers; "Alle
  Etappen" shows the whole route.

Overview: live map and the prerendered static composite both break the
preview line across >1 km inter-stage transfers (previewBreaks).

Route builder:
- Mark any placed waypoint as a stage start (named) from the waypoint list
  or the detail panel; export assembles each stage independently into its
  own <trk>; import re-marks stage boundaries from a multi-track GPX.
2026-05-22 14:14:57 +02:00
Alexander bb1d494c48 feat(hikes): forgiving map selection, photo lightbox, detail polish
Map interaction:
- Overview map: widen the canvas renderer hit-test (tolerance) so a route
  can be hovered/clicked from a comfortable margin instead of demanding a
  pixel-perfect click on the thin line.
- Detail map: drive the elevation cursor from a whole-map mousemove that
  snaps to the nearest track point within ~70 px (track cached in
  layer-point space, refreshed on zoom/move), instead of requiring the
  pointer to ride exactly on the trail. The hover pin now renders for
  map-sourced hovers too, and is recoloured to nord red as a distinct
  "you are here" marker. Trail polyline made non-interactive.

Detail page:
- Move the photo strip above the stats row and trim it (3:2 cards).
- Add a fullscreen lightbox: an expand button on each card opens the
  full-res image with prev/next, arrow keys, Esc, backdrop-close and a
  body-scroll lock; opening/stepping syncs the map + strip. The card's
  existing click (map-position sync) is preserved.
- Cap inline prose images at 680 px (centered) so they don't blow up to
  full width in the single-column layout on wider screens; the desktop
  two-column layout is unaffected.
2026-05-22 12:36:06 +02:00
Alexander a1aa722512 feat(hikes): use SAC-tier colours for the detail-page trail
Live and static heroes on the detail page were rendered in Nord red,
while the same trail on the /hikes overview map already used the
high-contrast SAC palette (orange/red/blue). The mismatch made the
detail trail look muted against the Pixelkarte; the overview's choice
also doubles as a navigational hint ("the orange trail you saw on
the map is this one").

Introduce $lib/data/sacColors as the single source of truth so
HikeMap, HikesOverviewMap, and the build-side static renderer all
pull the same palette. Bump HERO_RENDER_VERSION to v6 so stale
Nord-red static heroes get re-rendered on the next build.
2026-05-19 10:27:31 +02:00
Alexander 2a8721fde0 feat(hikes): clickable tag chips + tag filter on the overview
Detail-page tag chips become anchor links to `/hikes?tag=<name>`.
HikesFilterBar grows a tags fieldset (sorted by frequency, with the
hash prefix the chips use) so the user can keep narrowing from there.

Multi-tag filtering is OR — a hike matching any selected tag stays
visible. AND would shrink the listing fast given how few tags most
hikes carry; OR matches what "show me more like this" feels like.

The overview page reads `tag` query params on mount and pre-fills the
filter — supports repeated params (`?tag=winter&tag=easy`).
2026-05-19 10:13:26 +02:00
Alexander 3331536ddd feat(hikes): show tags + relegate GPX download to a quiet meta footer
Tag chips slot between the metric tiles and the elevation profile —
muted pills with a hash prefix so they read as framing context, not
another row of glance-info competing with the numbers.

The standalone "GPX herunterladen" pill (centred, primary-styled,
above the photo strip) moves into a small footer at the bottom of
the article, grouped with waypoint count, publish date, and the
required swisstopo attribution. GPX is a power-user export, not a
CTA — placing it with other ancillary metadata matches its weight.
2026-05-19 10:04:45 +02:00
Alexander cfdd58fb18 feat(hikes): inline cantonal Wappen next to region label
26 public-domain coats of arms fetched once from Wikimedia Commons
via scripts/download-cantons.ts and committed under static/cantons/.
$lib/data/cantons.ts maps Swisstopo's free-form name (German default,
French/Italian alternates for Romandie / Ticino) to the ISO code +
emblem URL.

Card shows an 18×22 emblem, detail page a 24×30 one — both with a
drop-shadow so they read against the dark hero gradient. Unknown
canton names fall back to plain text without the emblem.

The downloaded SVGs are written verbatim — earlier draft prepended a
provenance HTML comment but that breaks the leading `<?xml … ?>` and
browsers refuse to render the image. Provenance lives in the script's
CANTONS table instead.
2026-05-19 08:44:30 +02:00
Alexander 2c3886296c fix(hikes): square-ish SAC red/blue painted markers (44px → 28px) 2026-05-19 08:30:02 +02:00
Alexander c082da700d feat(hikes): phone-sized static hero variant for ≤560 px viewports
The wide static hero picks its zoom for a desktop-sized container
(fitWidth 1920), so on phones the bbox lands too zoomed-in: most of
the route falls outside the visible 400 CSS px band.

Build now emits a second pose per hero — rendered with fitWidth 400 /
fitHeight 480 onto a 1200² canvas — so the auto-fit zoom matches what
Leaflet picks at the same container size. Per-hike hero gains four
variants total (theme × viewport); overview hero gains two.

The page picks which `<img>` to show via a `max-width: 560px` media
query (no JS needed for the swap), and `matchMedia` decides which
pose to hand to Leaflet's first `setView` so the static→live cross-
fade aligns regardless of viewport.

Drive-by: replace the long-stale `hike.heroMapUrl` reference in the
detail page's track-loading fallback with `hike.heroMapUrlLight`.
2026-05-19 08:27:08 +02:00
Alexander fd2d8a58d9 feat(hikes): pre-rendered static hero map with smooth Leaflet handover
Each hike now ships two SSR-friendly hero images (light + dark theme),
composited at build time from Swisstopo tiles plus an SVG overlay of the
trail polyline, start/end markers, and per-photo camera badges. The
detail page renders the right variant immediately at first paint, then
hands over to live Leaflet without visible jumps.

Renderer (scripts/staticHikeMap.ts):
- Parallel tile fetcher with on-disk cache (scripts/.cache/swisstopo-
  tiles/) for re-build idempotency.
- `computeStaticMapPose` picks the zoom + centre Leaflet's fitBounds
  would land on at a reference 1920x640 viewport, so the static frames
  the full route on every typical desktop hero.
- Canvas rendered at 3840x2400 — large enough to fully cover ultrawide
  / 4K displays at native pixel size, so `object-fit: none` keeps the
  trail pixel-aligned with Leaflet's tile pane.
- SVG overlay: trail in Nord red, start dot Nord green, end dot Nord
  red, Lucide `camera` icon inside each photo badge. Photo badge
  fill / border / icon-stroke colours are passed per theme so light and
  dark variants match the live `.hike-photo-marker .badge` styling
  exactly (Nord10/Nord8 fill, Nord6/Nord1 border, white/Nord0 icon
  stroke). Map tiles themselves are identical across themes — no naive
  invert (it mangles the Pixelkarte palette).
- Public photo markers only — private positions are filtered out so
  they don't leak in the SSR image.

Build wiring (scripts/build-hikes.ts):
- `processHero` renders both variants in parallel, hashes inputs per
  theme, skips on cache hit. Output filenames carry the content hash so
  changes invalidate cleanly via the existing orphan sweep.
- `HikeManifestEntry` gains `heroMapUrlLight`, `heroMapUrlDark`,
  `heroMapZoom`, `heroMapCenter`.

Detail page (src/routes/hikes/[slug]/+page.svelte):
- Reserves the hero box height up front (kills CLS).
- Renders both `<img>` tags; CSS picks the right one via `data-theme`
  with `prefers-color-scheme` as the fallback.
- `object-fit: none; object-position: center` so the image displays at
  native pixel size, perfectly aligned with Leaflet's tile rendering.
- `isolation: isolate` on the hero gives Leaflet's z-index:200+ panes
  a stacking context so they can't bleed over the sticky nav.

HikeMap (src/lib/components/hikes/HikeMap.svelte):
- New `initialCenter` / `initialZoom` props — when set, the map opens
  with `setView` at the static hero's pose instead of `fitBounds`.
- New `onReady` callback — fires after the post-fly-to-bounds tile
  batch finishes loading (or a 350 ms safety timeout), letting the
  detail page fade the static out onto fully-painted tiles instead of
  onto a brief grey gap.
- Sequence: render static -> Leaflet `setView` to match -> first tile
  load -> `flyToBounds(track)` to the natural fit -> wait for new
  tiles -> fade static out.
2026-05-18 23:38:24 +02:00
Alexander f3d16d5187 feat(hikes): route-builder, overview map + cards, SAC-coloured pipeline
Build pipeline (scripts/build-hikes.ts) parses per-hike GPX, encodes
images via sharp, reverse-geocodes the centroid against Swisstopo and
emits a typed manifest under src/lib/data/hikes.generated.ts (gitignored).
Track JSON + image binaries live outside /static; served in dev by a
small hike-images plugin in vite.config.ts, in prod by nginx (private/
images proxied through Node + X-Accel-Redirect for auth-gating).

/hikes overview: full-bleed Swisstopo hero map (HikesOverviewMap) sits
under the sticky nav, drawing one polyline per route coloured by SAC
tier (T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue-
white). Click navigates, hover thickens + tooltips. Layer toggle,
recenter, GPS controls mirror the detail map (minus images toggle).
Cards drop the trail SVG, gain a per-route icon + SAC marker
pictogram on the cover, altitude range, season label, and "Neu" badge
for recently-published hikes. Filter bar + totals strip recompute over
the currently-visible set.

/hikes/[slug]: hero map with elevation profile, photo strip with map
sync, scroll-position pin, GPX download, SAC marker stats + min/max
altitude + season.

Route-builder (/hikes/route-builder): client-side draft persisted to
localStorage, EXIF-driven image placement, snap-to-route via BRouter
(OSRM + linear fallback) and Swisstopo profile.json elevation
enrichment that handles degenerate same-coord segments via the height
endpoint.

Filter init switched from a script-time snapshot of data.hikes (which
sporadically returned a one-hike subset during dev hydration and
locked the page to that single hike) to a post-mount \$effect.

Content under src/content/hikes/ intentionally not included (WIP).
2026-05-18 21:13:00 +02:00