Mobile layout, bottom sheets, swipe pagination - #5
Merged
Merged
Conversation
Addi90
commented
Sep 4, 2026
Owner
- Mobile layout (≤768px): bottom tab bar, ride cards, pagination, floating upload button
- Filters in a bottom sheet with dual-knob RangeSlider for distance/duration
- BottomSheet: swipe-down-to-close, fixed height (no layout reflow)
- Settings split into tabbed sheets (Bikes/Profile/Data/Analysis); stats gain per-bike breakdown + year filter
- Swipe to flip pages in the ride list (touch & mouse, parallax + chevron arrow)
- Backend: ride filters + bounds endpoints, per-bike stats (with tests)
- ≤768px: ride table → tap cards (toolbar: sort select, filter, upload); StatsPanel + PowerBests in collapsible details sections - RideDetail: chart pill switcher (primary chart raised, 240px), drag-select zoom reset pill, map 480→280px, stat cards 2-col, zone cards → horizontal scroll-snap row - BottomSheet component (filters + ride delete confirm; replaces native confirm() which renders broken on iOS) - StreamChart: resetSignal prop (zero setSelect + setData reset) - app.css: 44px touch targets, 16px mobile input font, focus-visible / disabled / active state contract, safe-area padding - Contrast fix: dark text on accent buttons (white was 3.3:1, fails WCAG AA) - i18n: 14 new keys (de + en); docs/plan-mobile-pwa.md
setCursor fires with idx=null both when the pointer leaves the plot AND when uPlot clears the selection by click — the hook cleared selectionRange in both cases, so the reset pill disappeared as soon as the mouse left the chart. Now only clears when uPlot itself has no visible selection; click-to-clear and double-click reset unchanged.
uPlot's selection box is transient — it is hidden on click, mouse-leave AND dblclick, while the axis zoom only resets on dblclick (native autoScaleX). Syncing selectionRange with the box made the reset pill vanish on any click or pointer leave. Now: - setSelect hook only SETS selectionRange (real drag), never clears it - dblclick listener clears it (uPlot already autoScaleX()'d natively) - reset pill clears it explicitly (zero-width setSelect no longer does) - dropped the mouseup-clear and setCursor-clear listeners
highQ: 0.005 (== lowQ) made robustRange return [lo, lo+1] — a 1-unit y-window at the 0.5th percentile — so elevation/power/cadence/HR/temperature data all clipped above the plot area. Speed kept its 0.999 and was fine. Back to 0.995 (the pre-refactor default).
Single 'W/kg'/'W' button replaced by a two-segment pill (both options visible, active filled accent) matching the chart pill visual language.
- drop the hardcoded 55px y-axis size; uPlot auto-sizes from label text - mobile: card padding 14px -> 6px horizontal, tighter title gap
Two tabs only — the app has exactly two screens plus the settings dialog. Rides tab = back escape hatch from ride detail (no scroll-to-top); Settings tab = the header cog, which is equally unreachable at the bottom of a long ride page. Fixed bar with safe-area inset, 56px targets, hidden >=769px; main content padded clear of it on mobile.
Rides | + | Settings. The + (accent circle, big Plus) bumps a monotonic uploadRequest store; RidesList owns the dialog and opens it on change — from RideDetail the + also navigates to the list first. Header cog is hidden on mobile since settings now lives in the footer.
Flex items shrink below their content by default, and the pill's overflow:hidden then cut 'W/kg' off. flex:none on the pill and its buttons; the h3 absorbs the shrink instead.
host: true binds 0.0.0.0 instead of localhost so phones on the same network can load the app (vite prints the Network URL on startup).
Upload is the footer + on mobile, so the toolbar copy is redundant (desktop toolbar keeps its button). The top back link is hidden on mobile ride detail — the Rides footer tab is the back path.
Server-side min/max filters on distance (km) and duration (h) replace text inputs with two-thumb range sliders whose bounds are the DB's shortest/longest ride (GET /api/rides/bounds). In both the desktop aside and the mobile filter sheet.
Track div is inset by half the thumb width (browsers clamp thumbs inside the input box, so the orange bar stuck out past the end knobs). Filter bounds are snapped to the step grid (floor/ceil) so the duration slider moves in even 0.25h steps from a clean start instead of an offset one.
Per-page dropdown (25 / 50 / 100 / all) at the top left, page prev/next + 'page x / y' indicator; filter and page-size changes reset to page 1. 'All' uses limit=0, which the API now treats as no limit (same 0=all convention as the streams endpoint).
…ur loading overlay Per-page dropdown moves to the right (desktop list bar and mobile toolbar). Mobile pager stretches full width with the arrows at each edge and is repeated above and below the card list. Loading now shows a fixed spinner overlay that dims and blurs the page instead of the old inline text.
The all-time bests and statistics cards under the mobile ride list are removed; their slot in the bottom tab bar (previously Settings) now opens them in a bottom sheet. Settings becomes a floating round gear button above the tab bar (mobile only; desktop keeps the header cog).
The stats & bests BottomSheet was mounted after </Router>; its StatsPanel/PowerBestsTable render <Link>, which dereferences the svelte-routing context and throws outside a Router. The crash left the stats stuck on 'loading', the bests section collapsed, and the backdrop click dead. Moving the sheet inside the Router fixes all three.
The sheet had no swipe gesture at all (despite AGENTS.md describing one): a hard swipe-down at the top of the sheet content overscroll-chained to the page behind, and gentle ones did nothing. Standard bottom-sheet gesture (iOS sheets / Material 3): - claim only when content scrollTop is 0 (checked live, so a drag that reaches the top mid-gesture can still claim); 8px slop; ignore horizontal drags and drags starting on input/textarea/select - preventDefault on a non-passive touchmove stops the overscroll chain to the page behind; overscroll-behavior: contain as a native guard - sheet follows the finger (1:1, then rubber-band), backdrop dims - dismiss on release velocity >= 0.3 px/ms or displacement >= max(100px, 30% of sheet height); else spring back; then animate off and close - slide transition animates height, so the off-screen settle composes without a visible teleport Benefits all three sheets (stats, filter, delete-confirm).
Tapping a best or stat inside the sheet navigated to the ride but the sheet stayed open over the loaded detail page. Anchor clicks bubble to the sheet-stack div, so close the sheet on any link tap inside it.
- do: start frontend dev server with --host 0.0.0.0 so it is reachable over the network - app.css: add --elev-1 / --elev-1-pressed elevation tokens - App.svelte: raise the mobile settings FAB so it clears the bottom tab bar
- GET /api/stats?year=N restricts the per-bike and unassigned tables to that year; totals, longest rides and per-year stay all-time. Year is bound as text (SQLite never equates TEXT with INTEGER), 400 on invalid. - StatsPanel: compact year select in the per-bike <details> summary (All-time + each year), aligned right, section expanded by default, table dims while refetching. <summary> keeps its default display so the details marker arrow shows like the per-year section. - Test: per-bike all-time vs 2024/2026, unassigned year filter, totals stay all-time, 400 on out-of-range year.
Settings open as a swipeable bottom sheet on mobile (centered dialog on desktop) and move into the bottom tab bar; upload becomes the floating button. Add viewport-fit=cover so safe-area insets resolve on iOS.
Horizontal drag on the card list (touch or mouse) parallaxes at 0.5x; releasing past 30px slides the list out, loads the neighbouring page, and snaps back. A fading chevron shows pager semantics (next = right chevron, prev = left), biased to the side being swiped away from, and only on swipes that would actually change page. Vertical drags stay native scroll (touch-action: pan-y + direction detection). AGENTS.md: note the iOS Safari date-input width quirk in the filter sheet (blind CSS fixes reverted; needs a real-device screenshot), record the swipe feature, update svelte-check baseline to 12 warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.