Deleting waypoints down to <2 left the last computed route rendered —
nothing cleared the stale geometry (recompute only runs for >=2). Add a
waypoints observer that drops the computed geometry once fewer than two
waypoints remain, via a new clearComputedRoute (keeps the routing
profile, unlike clearRouteData). Covers all delete paths (sidebar, map,
undo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fetchNearbyPois defaulted sessionId to the placeholder "nearby", which
/api/pois's requireSession rejects (401 Unauthorized) — so the nearby-POI
lookup for a selected waypoint always failed on real deployments. Thread
the live planner sessionId through useNearbyPois → fetchNearbyPois →
queryPois (same session the main POI markers already use), and make
sessionId a required arg so the placeholder can't silently return.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ColoredRoute rendered one interactive-registered <Polyline> per
coordinate pair — ~14.6k on a 485 km route — flooding Leaflet's SVG
layer and event-target map. Run-length group consecutive same-color
segments into a single polyline each (new pure buildColorRuns helper,
unit-tested). Discrete modes (surface/highway/…) collapse to a handful;
the elevation gradient is quantized into 24 buckets so its runs merge
too. Rendering is otherwise identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the waypoint marker into a React.memo'd component. The marker's
position/icon were new references every render, so any unrelated
PlannerMap re-render during a drag (e.g. the route-hover chart-sync
state update) re-applied the stale saved position mid-drag — orphaning
the drag so it never committed and the pin jumped back on release.
Memoizing means unrelated re-renders no longer touch the marker, so the
drag commits and the moved position persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waypoint markers had no click listener, so Leaflet routed the click to
the map — MapClickHandler then added a new waypoint on top of the one
you clicked (and made markers feel unresponsive). Add a click handler
that consumes the event, so Leaflet treats the marker as the target and
suppresses the map click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docked-but-collapsible placement (the chosen direction):
- A collapse toggle (chevron) in the chart header; state persists to
localStorage.
- Collapsed = a thin summary bar: a mini sage sparkline + distance and
ascent/descent + an expand toggle. Reclaims map space on demand.
- Summary figures come from the same authoritative routeStats the
sidebar uses (distance, elevationGain, elevationLoss), so the numbers
match the sidebar exactly — not recomputed from the raw elevation
points (which over-counted ascent ~2x from noise).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drag-to-zoom on the chart now tracks via window mousemove/mouseup, so
it keeps going when the pointer leaves the chart and still completes if
released outside it (previously mouseleave aborted the drag and an
outside release was lost). Listeners are detached on mouseup/unmount.
- Move the "reset zoom" button from top-right (which overlapped the
color-mode dropdown) to the bottom-right of the chart, and restyle it
on tokens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The topbar's "Route wird berechnet…" text appeared/disappeared with
routing, shifting the topbar layout. Move it onto the map as a floating
status pill (spinner + label, top-center, pointer-events-none) that
appears temporarily without affecting the topbar. Restyle the ambient
top-edge progress bar to accent tokens. Drop the now-unused `computing`
prop from Topbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The restyled Export split-button's dropdown toggle got aria-label
"Export GPX", colliding with the main button's text and breaking the
E2E selector that clicked the old "▾" glyph. Give it a dedicated
exportOptions label (en + de) and target it in the E2E test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the two slotted topbar actions onto the design system so the bar
reads as one cohesive surface:
- SaveToJournalButton: primary Button primitive; "saved" uses the
accent, error uses a new --color-danger token, and the return link
gets secondary token styling.
- ExportButton: token split-button (secondary look, chevron icon,
single divider) and a token dropdown menu (raised surface, soft
shadow, muted descriptions).
- New --color-danger token (#a03c3c, the no-go hue at full strength)
for error text.
Behavior and i18n unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The elevation chart's 10-mode color selector now uses the Select
primitive (token-styled, consistent with ProfileSelector). Chart-header
chrome (top border, title link, legend text) moved onto tokens too.
Kept as a dropdown rather than a segmented control — 10 modes don't fit
a segmented toggle. Canvas/legend data-viz colors are unchanged (task
1.5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Select primitive in @trails-cool/ui: token-styled native <select>
(appearance-none + overlaid chevron for a consistent closed control),
sm/md sizes, sibling of Input. Unit-tested + added to /dev/ui.
- ProfileSelector uses Select (size sm) with token label; the profile
label now hides on small screens. Completes the topbar's migration
onto the design system.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app body is h-screen overflow-hidden (for the full-screen map
editor), which clipped the taller gallery so it couldn't scroll. Give
the gallery route its own full-height scroll container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract a presentational Topbar driven by plain props, so SessionView
is the container (reads Yjs/awareness) and the topbar can render in the
/dev/ui gallery without a live session.
- New useParticipants(yjs) hook: awareness -> sorted Participant[] +
renameLocal. Replaces ParticipantList's inline logic.
- New presentational ParticipantAvatars (Avatar + Host Badge + inline
rename) and Topbar (token-styled shell; undo/redo via IconButton;
connection status with a live dot). ProfileSelector/Export/Save are
passed in as slots (their own restyle is separate).
- SessionView composes <Topbar> from real data; delete ParticipantList.
- /dev/ui gains a "Topbar - configurations" section (solo, multiplayer
+ computing, guest/connecting). Surfaced a narrow-width overflow, now
fixed by making the participant strip the shrink/clip element.
Connection status text ("Connected"/"Verbunden") and behavior are
unchanged, so existing planner E2E selectors still match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the shared primitives the planner topbar redesign needs, on the
design-system tokens:
- IconButton — icon-only button (undo/redo), ghost/secondary variants,
requires an accessible label.
- SegmentedControl — generic single-select toggle (color mode
Plain/Elevation/Surface), radiogroup semantics.
- Avatar — colored initial circle for participants, per-user color with
an accent fallback.
Co-located jsdom unit tests for each (roles, aria-checked, onChange,
disabled, color fallback). Extends the dev-only /dev/ui gallery with
sections for all three.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second step of the visual-redesign (tokens -> primitives -> surfaces):
a small set of shared, token-driven primitives in @trails-cool/ui so
both apps compose from one styled vocabulary instead of hand-rolling
bg-white/text-gray-* per screen.
- @trails-cool/ui now ships React components (Button variants+sizes,
Badge tones, Card raised/subtle, Input) plus a tiny cn() helper.
React is a peer dep; package builds with no bundling step.
- Co-located jsdom unit tests (@testing-library/react) assert roles,
token classes, variant switching, and behavior — run in the existing
Unit Tests gate, cross-platform, no snapshot baseline needed.
- Tailwind @source directive in both apps' styles.css so the package's
token utility classes get generated.
- Dev-only /dev/ui gallery route in the planner renders every primitive
in all states — a zero-dependency stand-in for Storybook. Excluded
from production builds (NODE_ENV guard in routes.ts).
No app surfaces are refactored yet; adopting the primitives in the
topbar/sidebar/etc. is the surface task groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New @trails-cool/ui workspace package needs its package.json in the
deps stage so pnpm install --frozen-lockfile can resolve the
workspace:* dependency. Fixes the Dockerfile Package Check and the
journal image build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establish the visual-redesign foundation (task group 1): a single
shared source of design tokens both apps consume, so Planner and
Journal read from one palette/typography layer instead of drifting
per-app.
- New @trails-cool/ui package exporting theme.css: Tailwind v4 @theme
tokens (warm off-white surfaces, one sage accent, earthy overnight
tones, elevation-gradient colors, shadows) extracted from the
visual-redesign mockup.
- Self-hosted Outfit (body) + Geist Mono (stats) via @fontsource-variable
— privacy-first, no Google Fonts CDN.
- Both apps import the theme and set the base canvas to the warm
off-white token + Outfit as the default font.
Foundation only: tokens are now available as Tailwind utilities
(bg-bg-raised, text-accent, font-mono, …) and raw CSS vars. Wiring
them into specific surfaces (topbar, sidebar, markers, elevation
chart) is later task groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refines #594: a fixed width made short notes an oversized box and wrapped
long notes early. Use `width: max-content` capped at `max-width: 280` so
the tooltip hugs short notes on one line and grows to 280px before wrapping
long ones (instead of collapsing to the longest word, the pre-#593 bug).
Keeps the centered text.
Verified in local dev with both a short ("Zug zurück?") and a long note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #593. With a long note the tooltip still squeezed to ~1 word
wide: an inline-block with only max-width, inside Leaflet's auto-width
tooltip, shrinks to its longest word rather than filling max-width.
Give it an explicit `width: 200` (a consistent, comfortably wide box that
wraps long notes cleanly) and `text-align: center`. Verified live in the
browser against a long repeated note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>