openspec: propose journal route/activity detail catch-up changes
Three OpenSpec proposals to bring the journal's route/activity detail and feed closer to best-in-class outdoor apps, in the order we'll implement them: - activity-sport-type: nullable sport enum on activities (hike/walk/run/ ride/gravel/mtb/ski/other), set on create or normalized on import, shown as a badge + feed verb, federated as a Note PropertyValue. - activity-stats: one reusable StatRow + canonical metric set, sport-aware avg pace/speed, moving-vs-elapsed time. Derive-only, no schema change. - journal-elevation-profile: elevation profile chart + chart<->map "active distance" hover/click sync on the read-only detail pages. All three validate with `openspec validate --strict`. Specs only; no code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e3e01a34ae
commit
26b59b9c95
15 changed files with 602 additions and 0 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-06-12
|
||||
60
openspec/changes/journal-elevation-profile/design.md
Normal file
60
openspec/changes/journal-elevation-profile/design.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
## Context
|
||||
|
||||
`@trails-cool/gpx` already parses trackpoints with elevation; the detail loaders
|
||||
already build GeoJSON for the map. The Planner's `elevation-map-interaction`
|
||||
spec defines the hover/click/drag behavior in an editing context (Yjs-backed).
|
||||
The Journal detail pages are read-only React Router routes that render a Leaflet
|
||||
map (`ClientMap` / `RouteMapThumbnail.client`) but no chart.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
- A self-contained, read-only elevation profile on both detail pages.
|
||||
- A shared "active distance" so the map and chart highlight the same point.
|
||||
|
||||
**Non-Goals**
|
||||
- No editing (the Planner owns reshape/drag-select-to-zoom); clicking the chart
|
||||
centers the map but changes no data.
|
||||
- No multi-series overlay (HR/pace/cadence) — we don't store per-point fitness
|
||||
data, and it's out of scope/ethos.
|
||||
- No schema or API changes; the series is derived from existing `gpx`.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Series derivation
|
||||
A helper returns an array of `{ distance, elevation, lat, lng }` samples from
|
||||
the GPX (reusing the gpx package's parse), downsampled to a sensible cap (e.g.
|
||||
~500 points) for chart performance. Computed in the loader (server) so the chart
|
||||
component stays presentational.
|
||||
|
||||
### D2: Chart rendering
|
||||
An SVG area chart (no charting dependency): x = cumulative distance, y =
|
||||
elevation, gradient fill keyed to gradient steepness using the existing
|
||||
`map-core` elevation/gradient color scale. A summary strip beneath shows ascent,
|
||||
descent, highest, and lowest — formatted via the shared stat helpers from
|
||||
`activity-stats` when present, else local formatting.
|
||||
|
||||
### D3: Shared "active distance" state
|
||||
The detail page lifts an `activeDistance | null` state above both the map and
|
||||
the chart:
|
||||
- Hovering the chart sets `activeDistance`; the map shows a marker at the
|
||||
interpolated lat/lng for that distance.
|
||||
- Hovering the route polyline on the map sets `activeDistance` (nearest sample);
|
||||
the chart draws a crosshair/marker at that x.
|
||||
- Clicking the chart pans/centers the map on that point.
|
||||
This mirrors the Planner's interaction conceptually but is read-only and
|
||||
implemented independently for the Journal surface.
|
||||
|
||||
### D4: Graceful absence
|
||||
If the derived series has no elevation variation or no elevation data (e.g. a
|
||||
flat import or a planned route without elevation), the chart is not rendered and
|
||||
the existing gain/loss numbers stand alone.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Map↔chart coupling**: the shared state must avoid render thrash on hover;
|
||||
throttle pointer updates and memoize the interpolation.
|
||||
- **Reuse vs. duplication with the Planner**: we deliberately implement a
|
||||
separate read-only component rather than lifting the Planner's editing
|
||||
component, to keep the Journal free of Yjs/editing concerns. The shared piece
|
||||
is the gpx series helper and the map-core color scale, not the component.
|
||||
50
openspec/changes/journal-elevation-profile/proposal.md
Normal file
50
openspec/changes/journal-elevation-profile/proposal.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
## Why
|
||||
|
||||
The Journal's route and activity detail pages show elevation only as two numbers
|
||||
(gain/loss) even though the full elevation series is already parsed from the
|
||||
GPX. Both Komoot and Strava treat the elevation profile as the second hero
|
||||
object after the map, and the interaction that makes a route feel
|
||||
"inspectable" — hovering the profile to move a marker on the map, and
|
||||
vice-versa — is the single most satisfying detail-page feature. We already have
|
||||
this interaction specced and built on the **Planner**
|
||||
(`elevation-map-interaction`); this change ports a read-only version to the
|
||||
Journal detail pages.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Render an elevation profile chart on the route detail and activity detail
|
||||
pages: a gradient-filled area chart (x = distance, y = elevation) with a
|
||||
summary strip (ascent · descent · highest · lowest).
|
||||
- Add a shared "active distance" interaction: hovering the chart highlights the
|
||||
corresponding point on the Leaflet map (a marker), and hovering the route line
|
||||
highlights the corresponding position on the chart. Both stay in sync.
|
||||
- Clicking a point on the chart centers the map on that location (read-only —
|
||||
no editing, unlike the Planner).
|
||||
- Degrade gracefully: when the GPX has no usable elevation series, the chart is
|
||||
omitted (the gain/loss numbers remain).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `journal-elevation-profile`: the elevation profile chart and the
|
||||
chart↔map "active distance" interaction on the Journal's read-only route and
|
||||
activity detail pages.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- None. `elevation-map-interaction` describes the Planner's editing-context
|
||||
interaction; this is a separate read-only capability on the Journal detail
|
||||
pages. They share the "active distance" concept but not the same surface or
|
||||
requirements. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Journal app**: a new `ElevationProfile` chart component, a shared
|
||||
"active distance" state lifted over the detail page's map + chart, and wiring
|
||||
in `routes.$id.tsx` and `activities.$id.tsx`. Loaders expose the elevation
|
||||
series (derived from existing `gpx`).
|
||||
- **Packages**: reuse `@trails-cool/gpx` for the elevation/distance series and
|
||||
`@trails-cool/map-core` for the elevation color scale; no new dependency.
|
||||
- **i18n**: `journal.elevation.*` keys (en + de) for the summary labels.
|
||||
- No schema or wire-contract change.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Elevation profile chart on detail pages
|
||||
The route and activity detail pages SHALL render an elevation profile chart (distance on the x-axis, elevation on the y-axis) with a gradient fill and a summary strip showing ascent, descent, highest point, and lowest point, whenever the activity or route has a usable elevation series.
|
||||
|
||||
#### Scenario: Chart shown for a route with elevation
|
||||
- **WHEN** a user views a route whose GPX contains an elevation series
|
||||
- **THEN** an elevation profile chart is shown with a summary strip (ascent, descent, highest, lowest)
|
||||
|
||||
#### Scenario: Chart omitted without elevation
|
||||
- **WHEN** an activity's GPX has no usable elevation data
|
||||
- **THEN** no chart is rendered and the existing gain/loss figures remain
|
||||
|
||||
### Requirement: Chart-to-map highlight
|
||||
The detail page SHALL highlight the corresponding location on the map when the user points at a position on the elevation chart.
|
||||
|
||||
#### Scenario: Hover chart highlights map
|
||||
- **WHEN** the user moves the pointer along the elevation chart
|
||||
- **THEN** a marker appears on the map at the location matching that distance along the route
|
||||
|
||||
### Requirement: Map-to-chart highlight
|
||||
The detail page SHALL highlight the corresponding position on the elevation chart when the user points at the route line on the map.
|
||||
|
||||
#### Scenario: Hover map highlights chart
|
||||
- **WHEN** the user moves the pointer along the route line on the map
|
||||
- **THEN** the elevation chart marks the position matching the nearest point on the route
|
||||
|
||||
### Requirement: Click chart to center map
|
||||
The detail page SHALL center the map on the corresponding location when the user clicks a position on the elevation chart, without modifying any data.
|
||||
|
||||
#### Scenario: Click chart pans map
|
||||
- **WHEN** the user clicks a point on the elevation chart
|
||||
- **THEN** the map centers on the matching location
|
||||
- **AND** no route or activity data is changed
|
||||
|
||||
### Requirement: Localized summary labels
|
||||
The elevation summary strip SHALL render its labels and units through localized strings.
|
||||
|
||||
#### Scenario: German summary labels
|
||||
- **WHEN** the UI locale is German
|
||||
- **THEN** the ascent, descent, highest, and lowest labels render in German
|
||||
28
openspec/changes/journal-elevation-profile/tasks.md
Normal file
28
openspec/changes/journal-elevation-profile/tasks.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
## 1. Series helper
|
||||
|
||||
- [ ] 1.1 Add a helper (in `@trails-cool/gpx` or the journal lib) that turns GPX into `{ distance, elevation, lat, lng }[]`, downsampled to ~500 points; unit-test it (incl. the no-elevation case → empty/invalid series).
|
||||
- [ ] 1.2 Expose the series + summary (ascent/descent/high/low) from the route and activity detail loaders.
|
||||
|
||||
## 2. Chart component
|
||||
|
||||
- [ ] 2.1 Build a presentational `ElevationProfile` SVG area chart (gradient fill via the `map-core` elevation/gradient scale) with the ascent/descent/high/low summary strip.
|
||||
- [ ] 2.2 Render nothing when the series has no usable elevation.
|
||||
|
||||
## 3. Active-distance interaction
|
||||
|
||||
- [ ] 3.1 Lift an `activeDistance | null` state above the detail page's map + chart.
|
||||
- [ ] 3.2 Chart hover → set `activeDistance`; map shows a marker at the interpolated lat/lng.
|
||||
- [ ] 3.3 Map route-line hover → set `activeDistance` (nearest sample); chart shows a crosshair/marker.
|
||||
- [ ] 3.4 Chart click → center the map on that location (read-only). Throttle pointer updates; memoize interpolation.
|
||||
|
||||
## 4. Wire-in & i18n
|
||||
|
||||
- [ ] 4.1 Mount `ElevationProfile` + shared state in `routes.$id.tsx` and `activities.$id.tsx`.
|
||||
- [ ] 4.2 Add `journal.elevation.*` keys (ascent, descent, highest, lowest) to en + de.
|
||||
|
||||
## 5. Tests & checks
|
||||
|
||||
- [ ] 5.1 Unit: series helper (sampling, summary numbers, no-elevation → omitted).
|
||||
- [ ] 5.2 Component: chart renders for a series; renders nothing for an empty series.
|
||||
- [ ] 5.3 E2E: open a route detail with elevation, assert the chart + summary appear; hovering the chart shows a map marker.
|
||||
- [ ] 5.4 `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e` green.
|
||||
Loading…
Add table
Add a link
Reference in a new issue