Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK
Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
---
|
||||
|
||||
# Backlog — v3 and v4
|
||||
# Backlog — v3 and the ROADMAP
|
||||
|
||||
Everything known-outstanding, with enough context to pick up cold. Nothing here is
|
||||
committed to — it is a menu, roughly ordered by value.
|
||||
@@ -21,9 +21,12 @@ committed to — it is a menu, roughly ordered by value.
|
||||
**v3 is now broken out into tickets: [v3/README.md](v3/README.md).** This document stays
|
||||
as the reasoning; the tickets are the executable form.
|
||||
|
||||
**v3 is everything that can be built with no server.** **v4 is everything that cannot.**
|
||||
That split is the most useful thing in this document: it means v3 can proceed indefinitely
|
||||
without anyone deciding to run infrastructure or hold other people's location data.
|
||||
**v3 is everything that can be built with no server. The ROADMAP is everything that
|
||||
cannot, plus whatever bugs and features come up that need triage before the next v3-style
|
||||
ticket batch.** The no-server split is still the most useful structural fact in this
|
||||
document: it means v3 can proceed indefinitely without anyone deciding to run
|
||||
infrastructure or hold other people's location data. The ROADMAP is where everything else
|
||||
gets managed first.
|
||||
|
||||
**Before planning anything: run the real-ride checklist in
|
||||
[the real-ride checklist](port/REAL-RIDE-CHECKLIST.md).** Several items below may turn out to be non-issues, and
|
||||
@@ -230,8 +233,8 @@ Terse on purpose. Unshaped, to be consolidated later.
|
||||
### Threads running through these
|
||||
|
||||
Sign-up, cloud backup and group ride are one programme, not three: they all need a server,
|
||||
identity, and a privacy stance. They have therefore been moved out to **v4** below, and
|
||||
should be scoped together or not at all.
|
||||
identity, and a privacy stance. They have therefore been moved out to the **ROADMAP**
|
||||
below, and should be scoped together or not at all.
|
||||
|
||||
Activity type (now section 4) and theming are independent and much cheaper — either could
|
||||
ship alone, and activity type is the natural first v3 task because it forces the migration
|
||||
@@ -246,7 +249,15 @@ entirely standalone.
|
||||
|
||||
---
|
||||
|
||||
# v4 — everything that needs a server
|
||||
# ROADMAP
|
||||
|
||||
Formerly "v4." Renamed because it now holds more than the server-dependent programme
|
||||
below: it's also where Dylan's running list of bugs and features gets triaged before
|
||||
becoming its own ticket batch, the way v3 was broken out. That triage takes priority over
|
||||
the items below — nothing here is blocking, all of it has been waiting since before v3
|
||||
started.
|
||||
|
||||
## The server-dependent programme
|
||||
|
||||
Split out from v3 deliberately. These three are **one programme, not three items**: each
|
||||
needs a server, an identity system, and a privacy stance, and none of them is worth
|
||||
|
||||
62
rippr-flutter-src/docs/design/stitch-export/README.md
Normal file
62
rippr-flutter-src/docs/design/stitch-export/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Stitch export — Rippr Minimalist Activity Tracker
|
||||
|
||||
Pulled from [stitch.withgoogle.com](https://stitch.withgoogle.com) project
|
||||
`8187200995279352789` via the Stitch MCP server on 2026-08-23.
|
||||
|
||||
## Correction
|
||||
|
||||
An earlier version of this README got the design systems backwards. There are **three**
|
||||
distinct design systems in this project (`list_design_systems`), and the four screens
|
||||
below actually render with the one this doc originally called "stale." That's now fixed.
|
||||
|
||||
| Asset | Display name | Mode | Primary | Fonts | Used by the 4 screens below? |
|
||||
|---|---|---|---|---|---|
|
||||
| `assets/e73325c5365a4a56b93823236777e95a` | **Modern Professional Dark** | Dark | Blue `#1978e5` (rendered `#4090fe`/`#aac7ff`) | Inter (everywhere) | **Yes — confirmed by hex-matching the downloaded HTML** |
|
||||
| `assets/41670631a4b64b118fb9ea1b54f1bd60` | Rippr (neon HUD) | Dark | Electric green `#00FF41` | Sora / Inter / JetBrains Mono | No |
|
||||
| `assets/3938003907261369831` | Modern Professional | Light | Blue `#1275e2` | Inter | No |
|
||||
|
||||
**The one you specifically asked for** (screen ID
|
||||
`asset-stub-assets_e73325c5365a4a56b93823236777e95a`, i.e.
|
||||
`assets/e73325c5365a4a56b93823236777e95a`) is **Modern Professional Dark** — deep
|
||||
charcoal/navy (`#0a0a0c` base, `#131313` surface, `#16161a` cards), Professional Blue
|
||||
primary, Inter throughout, 8px rounding, tonal-layer depth (no shadows, lighter surface
|
||||
= more elevated), and a soft blue glow on elevated/active elements instead of a shadow.
|
||||
This is `design-system-modern-professional-dark.md` below, and it's the design system
|
||||
actually used by all four screens already pulled.
|
||||
|
||||
`design-system-rippr-neon.md` is kept for reference — it's a real design system in the
|
||||
project (`get_project`'s top-level `designTheme`, i.e. whatever a brand-new screen would
|
||||
default to), but it is **not** what any of the four fetched screens render with.
|
||||
|
||||
## Contents
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `design-system-modern-professional-dark.md` | **The design system actually applied to the four screens below.** Full spec: color tokens, typography, spacing, elevation, shape, components. |
|
||||
| `design-system-rippr-neon.md` | A second, unused-by-these-screens design system also present in the project (green/cyan HUD aesthetic). Reference only. |
|
||||
| `screens/export-summary.md` | Stitch's own generated project overview. Its "Design System: Modern Professional Dark" section was correct all along. |
|
||||
| `screens/map-hud-dark.html` + `.png` | Map HUD - Dark Mode: active-recording map screen, floating telemetry cards (speed, avg speed, distance, time), Pause/Stop controls. |
|
||||
| `screens/plan-screen-dark.html` + `.png` | Plan Screen - Dark Mode: empty-state route planner, "Tap to drop a pin" prompt. |
|
||||
| `screens/route-planning-dark.html` + `.png` | Route Planning - Dark Mode: pins dropped, distance/time/pin-count header, dotted route line, Start Route button. |
|
||||
| `screens/rides-history-dark.html` + `.png` | Rides History - Dark Mode: search bar, unit toggle, ride cards with a mini-map thumbnail, date, distance, time, avg speed. |
|
||||
|
||||
Each `.html` is Stitch's generated markup (Tailwind CSS, vanilla JS) for that screen — a
|
||||
reference for structure and interaction, not something to drop into the Flutter app
|
||||
as-is.
|
||||
|
||||
## Not pulled as a screen/image
|
||||
|
||||
"Design System" (`assets_e73325c5365a4a56b93823236777e95a`) is a **design-system asset
|
||||
instance**, not a regular screen — Stitch's `get_screen` call only works on
|
||||
`projects/{project}/screens/{screen}` resources and returns `invalid argument` for an
|
||||
`assets/{id}` resource. There is no separate HTML/screenshot file for it the way there is
|
||||
for a screen; its full content (structured theme tokens + style-guideline prose) is
|
||||
fetched via `list_design_systems` instead, and that's what
|
||||
`design-system-modern-professional-dark.md` is built from.
|
||||
|
||||
## Next step
|
||||
|
||||
This is raw material, not yet applied to the app. `lib/src/ui/theme.dart`'s current
|
||||
palette (safety-orange accent, warm near-black ground — see V3-16) is a different
|
||||
direction from **either** of these Stitch design systems. Reconciling or replacing is a
|
||||
decision for whoever picks up the actual UI work on this branch.
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: Modern Professional Dark
|
||||
colors:
|
||||
surface: '#131313'
|
||||
surface-dim: '#131313'
|
||||
surface-bright: '#393939'
|
||||
surface-container-lowest: '#0e0e0e'
|
||||
surface-container-low: '#1c1b1b'
|
||||
surface-container: '#201f1f'
|
||||
surface-container-high: '#2a2a2a'
|
||||
surface-container-highest: '#353534'
|
||||
on-surface: '#e5e2e1'
|
||||
on-surface-variant: '#c1c6d5'
|
||||
inverse-surface: '#e5e2e1'
|
||||
inverse-on-surface: '#313030'
|
||||
outline: '#8b919f'
|
||||
outline-variant: '#414753'
|
||||
surface-tint: '#aac7ff'
|
||||
primary: '#aac7ff'
|
||||
on-primary: '#002f64'
|
||||
primary-container: '#4090fe'
|
||||
on-primary-container: '#002958'
|
||||
inverse-primary: '#005db8'
|
||||
secondary: '#aec7f6'
|
||||
on-secondary: '#143057'
|
||||
secondary-container: '#2e476f'
|
||||
on-secondary-container: '#9db6e4'
|
||||
tertiary: '#ffb68c'
|
||||
on-tertiary: '#532200'
|
||||
tertiary-container: '#e3711f'
|
||||
on-tertiary-container: '#481d00'
|
||||
error: '#ffb4ab'
|
||||
on-error: '#690005'
|
||||
error-container: '#93000a'
|
||||
on-error-container: '#ffdad6'
|
||||
primary-fixed: '#d6e3ff'
|
||||
primary-fixed-dim: '#aac7ff'
|
||||
on-primary-fixed: '#001b3e'
|
||||
on-primary-fixed-variant: '#00458d'
|
||||
secondary-fixed: '#d6e3ff'
|
||||
secondary-fixed-dim: '#aec7f6'
|
||||
on-secondary-fixed: '#001b3d'
|
||||
on-secondary-fixed-variant: '#2e476f'
|
||||
tertiary-fixed: '#ffdbc9'
|
||||
tertiary-fixed-dim: '#ffb68c'
|
||||
on-tertiary-fixed: '#321200'
|
||||
on-tertiary-fixed-variant: '#763400'
|
||||
background: '#131313'
|
||||
on-background: '#e5e2e1'
|
||||
surface-variant: '#353534'
|
||||
surface-main: '#0a0a0c'
|
||||
surface-card: '#16161a'
|
||||
accent-error: '#ffb4ab'
|
||||
on-surface-bright: '#ffffff'
|
||||
on-surface-muted: '#9ca3af'
|
||||
typography:
|
||||
headline-lg:
|
||||
fontFamily: Inter
|
||||
fontSize: 32px
|
||||
fontWeight: '700'
|
||||
lineHeight: 40px
|
||||
letterSpacing: -0.02em
|
||||
headline-lg-mobile:
|
||||
fontFamily: Inter
|
||||
fontSize: 28px
|
||||
fontWeight: '700'
|
||||
lineHeight: 36px
|
||||
letterSpacing: -0.01em
|
||||
headline-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 24px
|
||||
fontWeight: '600'
|
||||
lineHeight: 32px
|
||||
headline-sm:
|
||||
fontFamily: Inter
|
||||
fontSize: 20px
|
||||
fontWeight: '600'
|
||||
lineHeight: 28px
|
||||
body-lg:
|
||||
fontFamily: Inter
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: 24px
|
||||
body-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 14px
|
||||
fontWeight: '400'
|
||||
lineHeight: 20px
|
||||
label-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 12px
|
||||
fontWeight: '500'
|
||||
lineHeight: 16px
|
||||
letterSpacing: 0.5px
|
||||
label-sm:
|
||||
fontFamily: Inter
|
||||
fontSize: 11px
|
||||
fontWeight: '500'
|
||||
lineHeight: 16px
|
||||
letterSpacing: 0.5px
|
||||
rounded:
|
||||
sm: 0.25rem
|
||||
DEFAULT: 0.5rem
|
||||
md: 0.75rem
|
||||
lg: 1rem
|
||||
xl: 1.5rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
base: 4px
|
||||
gutter: 24px
|
||||
margin-mobile: 16px
|
||||
margin-desktop: 32px
|
||||
max-width: 1280px
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
|
||||
This design system embodies a **Corporate / Modern** aesthetic reimagined for a premium dark-mode experience. The brand personality is authoritative, precise, and sophisticated. By shifting to a deep charcoal and navy foundation, the UI evokes a sense of focused calm and high-end engineering.
|
||||
|
||||
The visual style leverages subtle luminescence and high-contrast accents to guide the user's eye, moving away from traditional light surfaces to a more immersive, "command-center" feel. It is tailored for professional environments where clarity and reduced eye strain are paramount.
|
||||
|
||||
## Colors
|
||||
|
||||
The palette is anchored by a **Deep Dark Gray (#0a0a0c)** background to provide a true premium feel without the harshness of pure black.
|
||||
|
||||
- **Primary:** The Professional Blue (#1978e5) is the soul of the interface, used for key actions and brand presence.
|
||||
- **Secondary:** A muted Slate Blue (#465f88) handles auxiliary UI elements and decorative accents.
|
||||
- **Surface Strategy:** We use a "stepped" dark palette. The base is the darkest, while interactive containers or cards use a slightly lighter gray (#16161a) to create perceived depth.
|
||||
- **Contrast:** All functional text is kept at a high luminance (Off-white to Pure White) to ensure AAA accessibility standards against the dark backdrop.
|
||||
|
||||
## Typography
|
||||
|
||||
This design system relies exclusively on **Inter** to maintain a systematic, utilitarian, and clean appearance.
|
||||
|
||||
- **Weight & Scale:** Headlines utilize tighter letter spacing and heavier weights to command attention against the dark background.
|
||||
- **Readability:** Body text uses a generous line-height to prevent "halation"—where light text on dark backgrounds appears to bleed or blur.
|
||||
- **Labels:** Small labels use all-caps or increased letter-spacing to maintain legibility at micro-scales.
|
||||
|
||||
## Layout & Spacing
|
||||
|
||||
The design system employs a **Fixed Grid** philosophy for desktop to maintain structural integrity, transitioning to a fluid model for smaller screens.
|
||||
|
||||
- **Grid:** A 12-column grid is used for desktop (1280px max-width) with 24px gutters.
|
||||
- **Rhythm:** An 8px spatial system governs all padding and margins, ensuring a predictable and professional vertical rhythm.
|
||||
- **Breakpoints:**
|
||||
- **Mobile:** < 600px (4 columns, 16px margins)
|
||||
- **Tablet:** 600px - 1024px (8 columns, 24px margins)
|
||||
- **Desktop:** > 1024px (12 columns, 32px margins)
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
In dark mode, shadows are less effective. Instead, this design system uses **Tonal Layers** and **Low-Contrast Outlines**.
|
||||
|
||||
- **Elevation Levels:** Higher elevation is communicated by lighter surface colors. A "floating" modal will have a lighter hex than the background.
|
||||
- **Borders:** Subtle 1px borders (#2d3037) are used to define boundaries where tonal shifts are too subtle.
|
||||
- **Glow:** Highly elevated elements (like active primary buttons) may use a soft blue outer glow rather than a black shadow to emphasize their importance.
|
||||
|
||||
## Shapes
|
||||
|
||||
The shape language is defined as **Rounded (Level 2)**, striking a balance between the rigidity of corporate software and the approachability of modern SaaS.
|
||||
|
||||
- **Small Components:** Buttons and inputs use a 0.5rem (8px) radius.
|
||||
- **Large Containers:** Cards and modals use a 1rem (16px) radius to soften the layout and create clear content groupings.
|
||||
|
||||
## Components
|
||||
|
||||
- **Buttons:** Primary buttons are solid Professional Blue with white text. Secondary buttons use a ghost style with a subtle white border and no fill.
|
||||
- **Input Fields:** Backgrounds should be slightly darker or lighter than the container surface, with a 1px border that glows Professional Blue on focus.
|
||||
- **Cards:** Cards use a #16161a background with a very subtle 1px border (#ffffff10) to separate them from the main background.
|
||||
- **Chips:** Used for metadata, chips feature a desaturated secondary blue-grey background with high-contrast labels.
|
||||
- **Lists:** Items are separated by hair-line dividers (#ffffff08) to maintain a clean vertical scan-path.
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: Rippr
|
||||
colors:
|
||||
surface: '#131314'
|
||||
surface-dim: '#131314'
|
||||
surface-bright: '#3a393a'
|
||||
surface-container-lowest: '#0e0e0f'
|
||||
surface-container-low: '#1c1b1c'
|
||||
surface-container: '#201f20'
|
||||
surface-container-high: '#2a2a2b'
|
||||
surface-container-highest: '#353436'
|
||||
on-surface: '#e5e2e3'
|
||||
on-surface-variant: '#b9ccb2'
|
||||
inverse-surface: '#e5e2e3'
|
||||
inverse-on-surface: '#313031'
|
||||
outline: '#84967e'
|
||||
outline-variant: '#3b4b37'
|
||||
surface-tint: '#00e639'
|
||||
primary: '#ebffe2'
|
||||
on-primary: '#003907'
|
||||
primary-container: '#00ff41'
|
||||
on-primary-container: '#007117'
|
||||
inverse-primary: '#006e16'
|
||||
secondary: '#b9f1ff'
|
||||
on-secondary: '#00363f'
|
||||
secondary-container: '#00e0ff'
|
||||
on-secondary-container: '#005f6d'
|
||||
tertiary: '#fff8f4'
|
||||
on-tertiary: '#442b10'
|
||||
tertiary-container: '#ffd5ae'
|
||||
on-tertiary-container: '#7a5b3c'
|
||||
error: '#ffb4ab'
|
||||
on-error: '#690005'
|
||||
error-container: '#93000a'
|
||||
on-error-container: '#ffdad6'
|
||||
primary-fixed: '#72ff70'
|
||||
primary-fixed-dim: '#00e639'
|
||||
on-primary-fixed: '#002203'
|
||||
on-primary-fixed-variant: '#00530e'
|
||||
secondary-fixed: '#a5eeff'
|
||||
secondary-fixed-dim: '#00daf8'
|
||||
on-secondary-fixed: '#001f25'
|
||||
on-secondary-fixed-variant: '#004e5a'
|
||||
tertiary-fixed: '#ffdcbd'
|
||||
tertiary-fixed-dim: '#e7bf99'
|
||||
on-tertiary-fixed: '#2c1701'
|
||||
on-tertiary-fixed-variant: '#5d4124'
|
||||
background: '#131314'
|
||||
on-background: '#e5e2e3'
|
||||
surface-variant: '#353436'
|
||||
typography:
|
||||
display-telemetry:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 64px
|
||||
fontWeight: '700'
|
||||
lineHeight: 64px
|
||||
letterSpacing: -0.04em
|
||||
headline-lg:
|
||||
fontFamily: Sora
|
||||
fontSize: 32px
|
||||
fontWeight: '600'
|
||||
lineHeight: 40px
|
||||
headline-md:
|
||||
fontFamily: Sora
|
||||
fontSize: 24px
|
||||
fontWeight: '600'
|
||||
lineHeight: 32px
|
||||
body-lg:
|
||||
fontFamily: Inter
|
||||
fontSize: 18px
|
||||
fontWeight: '400'
|
||||
lineHeight: 28px
|
||||
body-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: 24px
|
||||
label-caps:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 12px
|
||||
fontWeight: '500'
|
||||
lineHeight: 16px
|
||||
letterSpacing: 0.1em
|
||||
display-telemetry-mobile:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 48px
|
||||
fontWeight: '700'
|
||||
lineHeight: 48px
|
||||
rounded:
|
||||
sm: 0.125rem
|
||||
DEFAULT: 0.25rem
|
||||
md: 0.375rem
|
||||
lg: 0.5rem
|
||||
xl: 0.75rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
unit: 4px
|
||||
container-margin: 20px
|
||||
gutter: 12px
|
||||
stack-sm: 8px
|
||||
stack-md: 24px
|
||||
stack-lg: 48px
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
The design system centers on a high-performance, dark-mode aesthetic tailored for real-time activity tracking. The brand personality is kinetic, precise, and undistracted. It utilizes a **Minimalist-Technical** style, stripping away non-essential chrome to focus entirely on telemetry and movement.
|
||||
|
||||
The visual narrative is driven by a "HUD" (Heads-Up Display) concept: information is layered over the user's environment or activity maps using subtle glassmorphism and high-contrast data points. The emotional response should be one of focus and momentum, making the user feel like a precision instrument.
|
||||
|
||||
## Colors
|
||||
The palette is dominated by **Pitch Black (#0A0A0B)** to preserve OLED battery life and minimize ocular strain during night activities.
|
||||
|
||||
- **Primary (Electric Neon):** Used exclusively for active states, completion goals, and primary "Go" actions.
|
||||
- **Secondary (Cyan Telemetry):** Reserved for secondary data streams, such as cadence or heart rate, to provide visual separation from primary distance/time metrics.
|
||||
- **Surface Strategy:** Backgrounds use deep charcoal. Overlays use a semi-transparent version of the surface color with a 20px background blur to create the HUD effect.
|
||||
|
||||
## Typography
|
||||
The typography system uses a functional split: **Sora** for UI headers and brand moments, **Inter** for standard interface text, and **JetBrains Mono** for all numerical data and telemetry.
|
||||
|
||||
- **Data Legibility:** All numeric values must use the monospaced font to prevent "jumping" layouts during real-time updates.
|
||||
- **Visual Hierarchy:** Use `label-caps` for units (e.g., KM/H, BPM) placed immediately adjacent to large telemetry displays.
|
||||
- **Scale:** On mobile devices, telemetry data should prioritize width; if values exceed 5 digits, scale the font size down dynamically to fit the viewport width minus margins.
|
||||
|
||||
## Layout & Spacing
|
||||
This design system employs a **Fluid HUD Grid**. Elements are anchored to the corners of the screen to keep the center clear for maps or camera feeds.
|
||||
|
||||
- **Grid Model:** A 6-column grid for mobile, 12-column for desktop.
|
||||
- **Padding:** Use a generous 20px "Safe Zone" margin on all edges of the screen to ensure interactive elements are reachable and not clipped by hardware notches or rounded device corners.
|
||||
- **Density:** High density for data readouts (8px spacing between related metrics) and low density for navigation/settings (24px+ spacing) to prevent accidental taps during motion.
|
||||
|
||||
## Elevation & Depth
|
||||
Depth is expressed through **translucency** rather than shadows.
|
||||
|
||||
1. **Level 0 (Base):** Pure black (#0A0A0B).
|
||||
2. **Level 1 (Substrate):** Dark charcoal (#161618) with 0% transparency.
|
||||
3. **Level 2 (HUD Panels):** Surface color at 60% opacity with a `backdrop-filter: blur(20px)`.
|
||||
4. **Level 3 (Modals):** Surface color at 80% opacity with a subtle 1px inner border in a low-opacity white (10%) to define the edge against dark backgrounds.
|
||||
|
||||
Shadows should be avoided entirely to maintain the "lightweight" digital feel.
|
||||
|
||||
## Shapes
|
||||
The shape language is **Technical-Precision**.
|
||||
|
||||
- **Corners:** Use 0.25rem (4px) as the base radius for all containers to maintain a sharp, engineered appearance.
|
||||
- **Interactive Elements:** Buttons and inputs follow the same 4px rule.
|
||||
- **Progress Indicators:** Use sharp, non-rounded ends for progress bars to emphasize a "segmental" or "digital" look.
|
||||
- **Iconography:** Use 2px stroke weights with squared-off ends to match the monospaced typographic theme.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
- **Primary:** Solid Electric Neon background with Black text. No rounding beyond the base 4px.
|
||||
- **Ghost:** 1px Neon border with transparent background. Text matches border color.
|
||||
- **Telemetry Toggle:** Small, square buttons using monospaced labels.
|
||||
|
||||
### Cards & HUD Panels
|
||||
- All cards use the Level 2 Elevation (Glassmorphism).
|
||||
- Remove all drop shadows. Use a 1px stroke of #FFFFFF at 10% opacity for definition.
|
||||
|
||||
### Data Inputs
|
||||
- Underlined style only (no bounding box) for a lighter footprint.
|
||||
- Active state changes the underline to the Primary Neon color.
|
||||
- Use JetBrains Mono for all numeric input fields.
|
||||
|
||||
### Progress & Gauges
|
||||
- **Circular Gauges:** Use thin 4px strokes. The "track" should be 5% opacity white; the "fill" should be the Primary Neon.
|
||||
- **Pulse:** Active heart rate or GPS signals should use a "scanning" animation—a vertical line moving across the element—rather than a standard fade.
|
||||
|
||||
### Navigation
|
||||
- A bottom-anchored, floating "Dock" with a glassmorphic background.
|
||||
- Active icons are highlighted with a single Neon pixel-dot underneath.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Rippr Project Export
|
||||
|
||||
## Project Overview
|
||||
Rippr is a minimalist activity tracker designed for cyclists and runners, featuring an immersive map-centric interface, real-time telemetry, and streamlined route planning.
|
||||
|
||||
## Design System: Modern Professional Dark
|
||||
**ID:** {{DATA:DESIGN_SYSTEM:DESIGN_SYSTEM_3}}
|
||||
|
||||
### Theme Tokens
|
||||
- **Color Mode:** Dark
|
||||
- **Primary Color:** #1978e5 (Professional Blue)
|
||||
- **Typography:** Inter
|
||||
- **Roundness:** 8px (Round Eight)
|
||||
|
||||
### Core Components
|
||||
- **TopAppBar:** Minimalist, often hidden for immersive map views.
|
||||
- **BottomNavBar:** 4-icon layout (Map, History, Plan, Settings) with a signature active indicator dot.
|
||||
- **Telemetry Cards:** Floating, semi-transparent modules (`bg-surface/60`) with `backdrop-blur-xl` and `white/10` borders.
|
||||
|
||||
---
|
||||
|
||||
## Screen Inventory
|
||||
|
||||
### 1. Map HUD (Active Navigation)
|
||||
- **ID:** {{DATA:SCREEN:SCREEN_8}}
|
||||
- **Description:** Fullscreen map interface with floating telemetry cards at the top showing Speed, Avg Speed, Distance, and Time. Units are integrated inline (e.g., "24.5 km/h").
|
||||
- **Controls:** Detached Pause (Yellow) and Stop (Red) buttons positioned above the bottom navigation.
|
||||
|
||||
### 2. Plan Screen (Ready state)
|
||||
- **ID:** {{DATA:SCREEN:SCREEN_7}}
|
||||
- **Description:** Clean map interface identical to the HUD but without telemetry or ride controls. Features a subtle floating tooltip: "Tap to drop a pin".
|
||||
|
||||
### 3. Route Planning (Active Pins)
|
||||
- **ID:** {{DATA:SCREEN:SCREEN_5}}
|
||||
- **Description:** Secondary state of the Plan screen showing 5 dropped pins connected by a blue dotted line. A floating header displays total distance, estimated time, and pin count.
|
||||
|
||||
### 4. Rides History
|
||||
- **ID:** {{DATA:SCREEN:SCREEN_6}}
|
||||
- **Description:** List of previous routes displayed as cards over a blurred live map background. Each card shows a mini-map preview, date, distance, time, and average speed.
|
||||
|
||||
### 5. Settings / Profile
|
||||
- **ID:** {{DATA:SCREEN:SCREEN_4}}
|
||||
- **Description:** Structured list of user preferences, account details, and cloud sync options. Designed with clean hierarchy and professional dark mode styling.
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
- **Framework:** HTML5 / CSS3 (Tailwind CSS)
|
||||
- **Interactivity:** Vanilla JavaScript for map state and telemetry updates.
|
||||
- **Visual Effects:** Backdrop filters for UI depth and high-contrast vector paths for route visualization.
|
||||
@@ -0,0 +1,317 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en" style=""><head></head><body class="antialiased selection:bg-primary-container selection:text-on-primary-container flex flex-col h-screen relative text-on-surface bg-surface"><svg aria-hidden="true" class="inline-defs-container" style="position:absolute;width:0;height:0;overflow:hidden"></svg>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>RIPPR - Record Activity</title>
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Sora:wght@400;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"secondary-fixed-dim": "#aec7f6",
|
||||
"primary-fixed": "#d6e3ff",
|
||||
"tertiary-fixed-dim": "#ffb68c",
|
||||
"inverse-surface": "#e5e2e1",
|
||||
"on-surface-muted": "#9ca3af",
|
||||
"error": "#ffb4ab",
|
||||
"inverse-primary": "#005db8",
|
||||
"on-primary-fixed-variant": "#00458d",
|
||||
"secondary": "#aec7f6",
|
||||
"primary-fixed-dim": "#aac7ff",
|
||||
"surface-container": "#201f1f",
|
||||
"primary": "#aac7ff",
|
||||
"tertiary-container": "#e3711f",
|
||||
"surface-variant": "#353534",
|
||||
"on-background": "#e5e2e1",
|
||||
"on-secondary-fixed-variant": "#2e476f",
|
||||
"primary-container": "#4090fe",
|
||||
"outline-variant": "#414753",
|
||||
"background": "#131313",
|
||||
"surface": "#131313",
|
||||
"on-tertiary": "#532200",
|
||||
"on-tertiary-fixed": "#321200",
|
||||
"on-surface": "#e5e2e1",
|
||||
"surface-container-low": "#1c1b1b",
|
||||
"on-primary-container": "#002958",
|
||||
"secondary-fixed": "#d6e3ff",
|
||||
"accent-error": "#ffb4ab",
|
||||
"on-primary-fixed": "#001b3e",
|
||||
"surface-container-lowest": "#0e0e0e",
|
||||
"surface-tint": "#aac7ff",
|
||||
"error-container": "#93000a",
|
||||
"secondary-container": "#2e476f",
|
||||
"surface-main": "#0a0a0c",
|
||||
"tertiary-fixed": "#ffdbc9",
|
||||
"surface-container-high": "#2a2a2a",
|
||||
"on-primary": "#002f64",
|
||||
"on-surface-bright": "#ffffff",
|
||||
"on-tertiary-container": "#481d00",
|
||||
"on-error": "#690005",
|
||||
"surface-card": "#16161a",
|
||||
"surface-container-highest": "#353534",
|
||||
"surface-bright": "#393939",
|
||||
"on-secondary-container": "#9db6e4",
|
||||
"on-tertiary-fixed-variant": "#763400",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-secondary-fixed": "#001b3d",
|
||||
"tertiary": "#ffb68c",
|
||||
"outline": "#8b919f",
|
||||
"surface-dim": "#131313",
|
||||
"on-secondary": "#143057",
|
||||
"inverse-on-surface": "#313030",
|
||||
"on-surface-variant": "#c1c6d5"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"margin-mobile": "16px",
|
||||
"gutter": "24px",
|
||||
"margin-desktop": "32px",
|
||||
"base": "4px",
|
||||
"max-width": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": [
|
||||
"Inter"
|
||||
],
|
||||
"label-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"body-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"body-md": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Inter"
|
||||
],
|
||||
"label-md": [
|
||||
"Inter"
|
||||
],
|
||||
"display-telemetry": ["Inter"],
|
||||
"label-caps": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": [
|
||||
"24px",
|
||||
{
|
||||
"lineHeight": "32px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"11px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "24px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"14px",
|
||||
{
|
||||
"lineHeight": "20px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-sm": [
|
||||
"20px",
|
||||
{
|
||||
"lineHeight": "28px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "40px",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "36px",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"label-md": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"display-telemetry": ["64px", { "lineHeight": "64px", "letterSpacing": "-0.04em", "fontWeight": "700" }],
|
||||
"label-caps": ["12px", { "lineHeight": "16px", "letterSpacing": "0.1em", "fontWeight": "500" }]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; background-color: #131313; color: #e5e2e1; min-height: max(884px, 100dvh); }
|
||||
.hud-panel { background: rgba(32, 31, 31, 0.8); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); }
|
||||
.glass-btn { background: rgba(32, 31, 31, 0.9); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); }
|
||||
</style>
|
||||
<!-- Background Map Placeholder -->
|
||||
<div class="absolute inset-0 z-0 bg-surface">
|
||||
<div class="w-full h-full bg-cover bg-center opacity-40 mix-blend-luminosity" data-alt="A light mode stylized city map view for a fitness tracking application. Minimalist aesthetic with high contrast blue routes highlighting paths against light gray blocks representing buildings and terrain. The map appears like a clean interface overlay." data-location="Tokyo" style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDr5_s-KgoDdw3V0jHBFYtHVB3aikGStl0j7TM7kPJnauzs3U1eRnVMaMh4ywSZTMLAo_LxEZnu9rTzBPq-EGev_TiD3t0VY9osDcW0xKKUZxL2rx7TPn0qh2lor7H-TT1FWRur2eVHGsIsu-YT3ht-zwG1VrgIArB7Dxx5IgNThsHCroZ6oFbBB_Uo1Y8rIjPn8NPkxuUVxmKjpNze6RAmNCuYj9V3ivWdnkUNS_Z6ybPWERqbvRA4')"></div>
|
||||
<!-- Routes and Location Overlay -->
|
||||
<svg class="absolute inset-0 w-full h-full pointer-events-none" preserveaspectratio="xMidYMid slice" viewbox="0 0 400 800">
|
||||
<defs>
|
||||
<lineargradient id="speedGradient" x1="10%" x2="50%" y1="90%" y2="50%">
|
||||
<stop offset="0%" stop-color="#4090fe"></stop>
|
||||
<stop offset="40%" stop-color="#aac7ff"></stop>
|
||||
<stop offset="70%" stop-color="#ffb4ab"></stop>
|
||||
<stop offset="100%" stop-color="#ffb4ab"></stop>
|
||||
</lineargradient>
|
||||
</defs>
|
||||
<!-- Planned Route -->
|
||||
<path d="M 200 400 C 250 250, 150 150, 300 50" fill="none" opacity="0.8" stroke="#8b919f" stroke-dasharray="10 10" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"></path>
|
||||
<!-- Completed Route -->
|
||||
<path d="M 100 750 C 150 600, 100 500, 200 400" fill="none" stroke="url(#speedGradient)" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"></path>
|
||||
<!-- Pulsing Location Dot -->
|
||||
<circle cx="200" cy="400" fill="#4090fe" r="8"></circle>
|
||||
<circle cx="200" cy="400" fill="none" r="8" stroke="#4090fe" stroke-width="2">
|
||||
<animate attributename="r" dur="1.5s" from="8" repeatcount="indefinite" to="24"></animate>
|
||||
<animate attributename="opacity" dur="1.5s" from="1" repeatcount="indefinite" to="0"></animate>
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- TopAppBar JSON Component -->
|
||||
<!-- Main Content Canvas (HUD Layout) -->
|
||||
<main class="flex-1 relative z-10 flex flex-col pb-40 px-2 md:px-margin-desktop w-full max-w-7xl mx-auto md:ml-24 md:w-[calc(100%-6rem)] p-2">
|
||||
<!-- Compact Telemetry Row -->
|
||||
<div class="flex flex-row gap-1 w-full" id="telemetry-container">
|
||||
<div class="metric-card flex-1 hud-panel p-2 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all duration-300 overflow-hidden min-w-0">
|
||||
<span class="metric-title font-label-caps text-[9px] sm:text-[10px] text-on-surface-variant uppercase whitespace-nowrap truncate w-full text-center">Speed</span>
|
||||
<div class="metric-value font-display-telemetry text-base text-primary mt-1 transition-all duration-300 whitespace-nowrap">24.5 km/h</div>
|
||||
</div>
|
||||
<div class="metric-card flex-1 hud-panel p-2 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all duration-300 overflow-hidden min-w-0">
|
||||
<span class="metric-title font-label-caps text-[9px] sm:text-[10px] text-on-surface-variant uppercase whitespace-nowrap truncate w-full text-center">Avg Speed</span>
|
||||
<div class="metric-value font-display-telemetry text-lg text-on-surface mt-1 transition-all duration-300">22.1</div>
|
||||
</div>
|
||||
<div class="metric-card flex-1 hud-panel p-2 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all duration-300 overflow-hidden min-w-0">
|
||||
<span class="metric-title font-label-caps text-[9px] sm:text-[10px] text-on-surface-variant uppercase whitespace-nowrap truncate w-full text-center">Dist</span>
|
||||
<div class="metric-value font-display-telemetry text-base text-secondary mt-1 transition-all duration-300 whitespace-nowrap">12.4 km</div>
|
||||
</div>
|
||||
<div class="metric-card flex-1 hud-panel p-2 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all duration-300 overflow-hidden min-w-0">
|
||||
<span class="metric-title font-label-caps text-[9px] sm:text-[10px] text-on-surface-variant uppercase whitespace-nowrap truncate w-full text-center">Time</span>
|
||||
<div class="metric-value font-display-telemetry text-lg text-on-surface mt-1 transition-all duration-300">45:12</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Full Width Recording Control Bar -->
|
||||
<div class="fixed bottom-[80px] md:bottom-0 md:left-24 w-full md:w-[calc(100%-6rem)] z-40">
|
||||
<div class="flex flex-row w-full h-16 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.5)]">
|
||||
<button class="flex-1 bg-tertiary-container text-on-tertiary-container font-headline-md flex items-center justify-center transition-colors duration-300 border-r border-white/10"><span class="material-symbols-outlined text-[32px]" style='font-variation-settings: "FILL" 1;'>pause</span></button>
|
||||
<button class="flex-1 bg-error-container text-on-error-container font-headline-md flex items-center justify-center transition-colors duration-300"><span class="material-symbols-outlined text-[32px]" style='font-variation-settings: "FILL" 1;'>stop</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- BottomNavBar JSON Component -->
|
||||
<nav class="bg-surface/80 backdrop-blur-xl fixed bottom-0 w-full z-50 pb-safe border-t border-white/10 md:hidden">
|
||||
<div class="flex justify-around items-center h-20 px-margin-mobile font-label-caps text-label-caps font-display-telemetry"><button class="flex flex-col items-center justify-center text-primary relative after:content-[''] after:absolute after:-bottom-1 after:w-1 after:h-1 after:bg-primary after:rounded-full hover:text-primary/80 transition-transform duration-150 scale-95">
|
||||
<span class="material-symbols-outlined" style='font-variation-settings: "FILL" 1;'>map</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150"><span class="material-symbols-outlined" style='font-variation-settings: "FILL" 0;'>book</span></button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">route</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">settings</span>
|
||||
</button></div>
|
||||
</nav>
|
||||
<!-- Desktop Side Nav (Visible only on md+) -->
|
||||
<aside class="hidden md:flex flex-col fixed left-0 top-16 bottom-0 w-24 bg-surface/80 backdrop-blur-xl border-r border-white/10 z-40 items-center py-8 gap-12">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-colors w-full py-4 font-label-caps text-label-caps">
|
||||
<span class="material-symbols-outlined mb-2 text-2xl">map</span>
|
||||
Map
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-primary relative after:content-[''] after:absolute after:left-0 after:top-1/2 after:-translate-y-1/2 after:w-1 after:h-8 after:bg-primary after:rounded-r-full hover:text-primary/80 transition-colors w-full py-4 font-label-caps text-label-caps bg-white/5">
|
||||
<span class="material-symbols-outlined mb-2 text-2xl" style="font-variation-settings: 'FILL' 1;">directions_bike</span>
|
||||
Rides
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-colors w-full py-4 font-label-caps text-label-caps">
|
||||
<span class="material-symbols-outlined mb-2 text-2xl">route</span>
|
||||
Plan
|
||||
</button>
|
||||
</aside>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Recording Toggle Logic
|
||||
const toggleBtn = document.getElementById('recordingToggle');
|
||||
const toggleIcon = document.getElementById('toggleIcon');
|
||||
const toggleText = document.getElementById('toggleText');
|
||||
let isRecording = true;
|
||||
|
||||
toggleBtn?.addEventListener('click', () => {
|
||||
isRecording = !isRecording;
|
||||
if(isRecording) {
|
||||
toggleBtn.classList.remove('bg-error-container', 'text-on-error-container');
|
||||
toggleBtn.classList.add('bg-primary-container', 'text-on-primary-container');
|
||||
toggleIcon.textContent = 'stop_circle';
|
||||
toggleText.textContent = 'Stop Recording';
|
||||
} else {
|
||||
toggleBtn.classList.remove('bg-primary-container', 'text-on-primary-container');
|
||||
toggleBtn.classList.add('bg-error-container', 'text-on-error-container');
|
||||
toggleIcon.textContent = 'play_arrow';
|
||||
toggleText.textContent = 'Start Recording';
|
||||
}
|
||||
});
|
||||
|
||||
// Telemetry Expansion Logic
|
||||
const metrics = document.querySelectorAll('.metric-card');
|
||||
|
||||
metrics.forEach(metric => {
|
||||
metric.addEventListener('click', () => {
|
||||
const isExpanded = metric.classList.contains('flex-[2.5]');
|
||||
|
||||
// Reset all
|
||||
metrics.forEach(m => {
|
||||
m.classList.remove('flex-[2.5]', 'bg-surface-container-highest');
|
||||
m.classList.add('flex-1');
|
||||
m.querySelector('.metric-value').classList.remove('text-2xl', 'md:text-3xl');
|
||||
m.querySelector('.metric-value').classList.add('text-lg');
|
||||
});
|
||||
|
||||
// Expand if it wasn't already expanded
|
||||
if (!isExpanded) {
|
||||
metric.classList.remove('flex-1');
|
||||
metric.classList.add('flex-[2.5]', 'bg-surface-container-highest');
|
||||
metric.querySelector('.metric-value').classList.remove('text-lg');
|
||||
metric.querySelector('.metric-value').classList.add('text-2xl', 'md:text-3xl');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,336 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>RIPPR - Plan Route</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&family=Sora:wght@600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"secondary-fixed-dim": "#aec7f6",
|
||||
"primary-fixed": "#d6e3ff",
|
||||
"tertiary-fixed-dim": "#ffb68c",
|
||||
"inverse-surface": "#e5e2e1",
|
||||
"on-surface-muted": "#9ca3af",
|
||||
"error": "#ffb4ab",
|
||||
"inverse-primary": "#005db8",
|
||||
"on-primary-fixed-variant": "#00458d",
|
||||
"secondary": "#aec7f6",
|
||||
"primary-fixed-dim": "#aac7ff",
|
||||
"surface-container": "#201f1f",
|
||||
"primary": "#aac7ff",
|
||||
"tertiary-container": "#e3711f",
|
||||
"surface-variant": "#353534",
|
||||
"on-background": "#e5e2e1",
|
||||
"on-secondary-fixed-variant": "#2e476f",
|
||||
"primary-container": "#4090fe",
|
||||
"outline-variant": "#414753",
|
||||
"background": "#131313",
|
||||
"surface": "#131313",
|
||||
"on-tertiary": "#532200",
|
||||
"on-tertiary-fixed": "#321200",
|
||||
"on-surface": "#e5e2e1",
|
||||
"surface-container-low": "#1c1b1b",
|
||||
"on-primary-container": "#002958",
|
||||
"secondary-fixed": "#d6e3ff",
|
||||
"accent-error": "#ffb4ab",
|
||||
"on-primary-fixed": "#001b3e",
|
||||
"surface-container-lowest": "#0e0e0e",
|
||||
"surface-tint": "#aac7ff",
|
||||
"error-container": "#93000a",
|
||||
"secondary-container": "#2e476f",
|
||||
"surface-main": "#0a0a0c",
|
||||
"tertiary-fixed": "#ffdbc9",
|
||||
"surface-container-high": "#2a2a2a",
|
||||
"on-primary": "#002f64",
|
||||
"on-surface-bright": "#ffffff",
|
||||
"on-tertiary-container": "#481d00",
|
||||
"on-error": "#690005",
|
||||
"surface-card": "#16161a",
|
||||
"surface-container-highest": "#353534",
|
||||
"surface-bright": "#393939",
|
||||
"on-secondary-container": "#9db6e4",
|
||||
"on-tertiary-fixed-variant": "#763400",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-secondary-fixed": "#001b3d",
|
||||
"tertiary": "#ffb68c",
|
||||
"outline": "#8b919f",
|
||||
"surface-dim": "#131313",
|
||||
"on-secondary": "#143057",
|
||||
"inverse-on-surface": "#313030",
|
||||
"on-surface-variant": "#c1c6d5"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"margin-mobile": "16px",
|
||||
"gutter": "24px",
|
||||
"margin-desktop": "32px",
|
||||
"base": "4px",
|
||||
"max-width": "1280px",
|
||||
"unit": "4px",
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "48px",
|
||||
"stack-md": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": [
|
||||
"Inter"
|
||||
],
|
||||
"label-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"body-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"body-md": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Inter"
|
||||
],
|
||||
"label-md": [
|
||||
"Inter"
|
||||
],
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"display-telemetry-mobile": ["JetBrains Mono"],
|
||||
"display-telemetry": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": [
|
||||
"24px",
|
||||
{
|
||||
"lineHeight": "32px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"11px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "24px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"14px",
|
||||
{
|
||||
"lineHeight": "20px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-sm": [
|
||||
"20px",
|
||||
{
|
||||
"lineHeight": "28px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "40px",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "36px",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"label-md": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"label-caps": ["12px", { "lineHeight": "16px", "letterSpacing": "0.1em", "fontWeight": "500" }],
|
||||
"display-telemetry-mobile": ["48px", { "lineHeight": "48px", "fontWeight": "700" }],
|
||||
"display-telemetry": ["64px", { "lineHeight": "64px", "letterSpacing": "-0.04em", "fontWeight": "700" }]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.map-grid-overlay {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
animation: pulse-ring 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 0 0 rgba(170, 199, 255, 0.7);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 0 20px rgba(170, 199, 255, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 0 0 rgba(170, 199, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.route-line {
|
||||
stroke-dasharray: 10, 10;
|
||||
animation: dash 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface h-screen w-full overflow-hidden relative selection:bg-primary-container selection:text-on-primary-container antialiased flex flex-col dark">
|
||||
<!-- Fullscreen Map Canvas -->
|
||||
<main class="flex-grow relative h-full w-full z-0 overflow-hidden">
|
||||
<!-- Base Map Layer -->
|
||||
<div class="absolute inset-0 z-0 bg-surface-container-lowest">
|
||||
<img class="w-full h-full object-cover opacity-60 mix-blend-screen invert" data-alt="A dark mode digital map showing a top-down view of city streets and terrain. The map is rendered in dark grey and black tones with subtle lighter outlines for roads." data-location="San Francisco, CA" src="https://lh3.googleusercontent.com/aida-public/AB6AXuA-oqjvzL6bUKZVJD2vvMu-0LPsY-GmGnGrfOmA0Pv5d3jBrTEuOXQ2lhZUXb9SvIGOnr2Wz64OSx0LGVbxce3zkTseW9Q62p9djWl96s-LaFizAtCBJ7okbDdDDBZVyS8RutWkvabaIwEi_lE9yq26OZuanhZ7NS6Ny4AZx69Sah-DvoE1nzwD2AEEpWJXbfX_KRFa3iGbCUa02sKaAUWzoirpWg5yU6Gk6u2OWNRefC6YtASxYmBq"/>
|
||||
</div>
|
||||
<!-- Technical Grid Overlay -->
|
||||
<div class="absolute inset-0 map-grid-overlay z-10 pointer-events-none"></div>
|
||||
<!-- Planned Route Overlay (SVG) -->
|
||||
<svg class="absolute inset-0 w-full h-full z-20 pointer-events-none" preserveaspectratio="xMidYMid slice" viewbox="0 0 1000 1000">
|
||||
<!-- Path connecting dropped pins - Primary Blue -->
|
||||
<path class="route-line opacity-80" d="M 300 700 Q 400 650 450 500 T 700 300" fill="none" stroke="#aac7ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"></path>
|
||||
<!-- Waypoint 1 (Dropped Pin) -->
|
||||
<circle cx="450" cy="500" fill="#aac7ff" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
<!-- Waypoint 2 (Destination) -->
|
||||
<circle cx="700" cy="300" fill="#131313" r="8" stroke="#aac7ff" stroke-width="3"></circle>
|
||||
</svg>
|
||||
<!-- User Location Marker (Pulsing) -->
|
||||
<div class="absolute top-[70%] left-[30%] z-30 transform -translate-x-1/2 -translate-y-1/2 flex items-center justify-center pointer-events-none">
|
||||
<div class="absolute w-4 h-4 bg-primary rounded-full opacity-30"></div>
|
||||
<div class="absolute w-3 h-3 bg-primary rounded-full pulse-ring"></div>
|
||||
<div class="w-2 h-2 bg-primary rounded-full relative z-10 shadow-[0_0_10px_rgba(170,199,255,0.8)]"></div>
|
||||
<!-- Vision cone indicator -->
|
||||
<div class="absolute -top-12 left-1/2 -translate-x-1/2 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-t-[40px] border-t-primary/20 origin-bottom transform rotate-45"></div>
|
||||
</div>
|
||||
<!-- Interactive Map Canvas Layer (Invisible, captures clicks for dropping pins) -->
|
||||
<div class="absolute inset-0 z-40 cursor-crosshair" id="map-interaction-layer"></div>
|
||||
</main>
|
||||
<!-- Floating HUD Elements Layer -->
|
||||
<div class="absolute inset-0 z-50 pointer-events-none flex flex-col justify-end">
|
||||
<!-- Tooltip -->
|
||||
<div class="w-full flex justify-center mb-stack-md pointer-events-none px-container-margin">
|
||||
<div class="tooltip-float bg-surface-container-high/90 backdrop-blur-xl border border-outline-variant/30 rounded-full px-6 py-3 flex items-center gap-2 shadow-sm">
|
||||
<span class="material-symbols-outlined text-primary text-xl" style="font-variation-settings: 'FILL' 1;">location_on</span>
|
||||
<span class="font-label-caps text-label-caps text-on-surface">Tap to drop a pin</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Navigation Bar -->
|
||||
<nav class="bg-surface-container/90 backdrop-blur-xl border-t border-outline-variant/20 w-full pb-safe flex justify-around items-center h-20 px-container-margin pointer-events-auto shrink-0 relative shadow-[0_-4px_20px_rgba(0,0,0,0.5)]">
|
||||
<!-- Map (Inactive) -->
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors duration-200 w-16 h-full gap-1 group">
|
||||
<span class="material-symbols-outlined text-2xl group-active:scale-95 transition-transform duration-150">map</span>
|
||||
<span class="font-label-caps text-[10px] uppercase tracking-widest">Map</span>
|
||||
</button>
|
||||
<!-- Rides (Inactive) -->
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors duration-200 w-16 h-full gap-1 group">
|
||||
<span class="material-symbols-outlined text-2xl group-active:scale-95 transition-transform duration-150">directions_bike</span>
|
||||
<span class="font-label-caps text-[10px] uppercase tracking-widest">Rides</span>
|
||||
</button>
|
||||
<!-- Plan (Active) -->
|
||||
<button class="flex flex-col items-center justify-center text-primary relative after:content-[''] after:absolute after:bottom-2 after:w-1 after:h-1 after:bg-primary after:rounded-full hover:text-primary transition-colors duration-200 w-16 h-full gap-1 group">
|
||||
<span class="material-symbols-outlined text-2xl group-active:scale-95 transition-transform duration-150" style="font-variation-settings: 'FILL' 1;">route</span>
|
||||
<span class="font-label-caps text-[10px] uppercase tracking-widest text-primary">Plan</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Micro-interaction Script for Pin Dropping Simulation -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mapLayer = document.getElementById('map-interaction-layer');
|
||||
const svgLayer = document.querySelector('svg');
|
||||
let pinCount = 0;
|
||||
|
||||
mapLayer.addEventListener('click', (e) => {
|
||||
const rect = mapLayer.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Visual feedback of drop
|
||||
const ripple = document.createElement('div');
|
||||
ripple.className = 'absolute rounded-full border-2 border-primary bg-primary/20 pointer-events-none transform -translate-x-1/2 -translate-y-1/2';
|
||||
ripple.style.left = `${x}px`;
|
||||
ripple.style.top = `${y}px`;
|
||||
ripple.style.width = '0px';
|
||||
ripple.style.height = '0px';
|
||||
ripple.style.opacity = '1';
|
||||
ripple.style.transition = 'all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1)';
|
||||
|
||||
mapLayer.parentElement.appendChild(ripple);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
ripple.style.width = '60px';
|
||||
ripple.style.height = '60px';
|
||||
ripple.style.opacity = '0';
|
||||
});
|
||||
|
||||
// Clean up ripple
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
@@ -0,0 +1,307 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en" style=""><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>RIPPR - Rides</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Sora:wght@400;600;700&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"secondary-fixed-dim": "#aec7f6",
|
||||
"primary-fixed": "#d6e3ff",
|
||||
"tertiary-fixed-dim": "#ffb68c",
|
||||
"inverse-surface": "#e5e2e1",
|
||||
"on-surface-muted": "#9ca3af",
|
||||
"error": "#ffb4ab",
|
||||
"inverse-primary": "#005db8",
|
||||
"on-primary-fixed-variant": "#00458d",
|
||||
"secondary": "#aec7f6",
|
||||
"primary-fixed-dim": "#aac7ff",
|
||||
"surface-container": "#201f1f",
|
||||
"primary": "#aac7ff",
|
||||
"tertiary-container": "#e3711f",
|
||||
"surface-variant": "#353534",
|
||||
"on-background": "#e5e2e1",
|
||||
"on-secondary-fixed-variant": "#2e476f",
|
||||
"primary-container": "#4090fe",
|
||||
"outline-variant": "#414753",
|
||||
"background": "#131313",
|
||||
"surface": "#131313",
|
||||
"on-tertiary": "#532200",
|
||||
"on-tertiary-fixed": "#321200",
|
||||
"on-surface": "#e5e2e1",
|
||||
"surface-container-low": "#1c1b1b",
|
||||
"on-primary-container": "#002958",
|
||||
"secondary-fixed": "#d6e3ff",
|
||||
"accent-error": "#ffb4ab",
|
||||
"on-primary-fixed": "#001b3e",
|
||||
"surface-container-lowest": "#0e0e0e",
|
||||
"surface-tint": "#aac7ff",
|
||||
"error-container": "#93000a",
|
||||
"secondary-container": "#2e476f",
|
||||
"surface-main": "#0a0a0c",
|
||||
"tertiary-fixed": "#ffdbc9",
|
||||
"surface-container-high": "#2a2a2a",
|
||||
"on-primary": "#002f64",
|
||||
"on-surface-bright": "#ffffff",
|
||||
"on-tertiary-container": "#481d00",
|
||||
"on-error": "#690005",
|
||||
"surface-card": "#16161a",
|
||||
"surface-container-highest": "#353534",
|
||||
"surface-bright": "#393939",
|
||||
"on-secondary-container": "#9db6e4",
|
||||
"on-tertiary-fixed-variant": "#763400",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-secondary-fixed": "#001b3d",
|
||||
"tertiary": "#ffb68c",
|
||||
"outline": "#8b919f",
|
||||
"surface-dim": "#131313",
|
||||
"on-secondary": "#143057",
|
||||
"inverse-on-surface": "#313030",
|
||||
"on-surface-variant": "#c1c6d5"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"margin-mobile": "16px",
|
||||
"gutter": "24px",
|
||||
"margin-desktop": "32px",
|
||||
"base": "4px",
|
||||
"max-width": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": [
|
||||
"Inter"
|
||||
],
|
||||
"label-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"body-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"body-md": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-sm": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Inter"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Inter"
|
||||
],
|
||||
"label-md": [
|
||||
"Inter"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": [
|
||||
"24px",
|
||||
{
|
||||
"lineHeight": "32px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"11px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "24px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"14px",
|
||||
{
|
||||
"lineHeight": "20px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-sm": [
|
||||
"20px",
|
||||
{
|
||||
"lineHeight": "28px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "40px",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "36px",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"label-md": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"letterSpacing": "0.5px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.glass-panel {
|
||||
@apply bg-surface/80 backdrop-blur-[20px] border border-outline-variant;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface min-h-screen pb-[100px]"><div class="fixed inset-0 z-0 bg-surface"><div class="w-full h-full bg-cover bg-center opacity-30 backdrop-blur-md backdrop-blur-xl" data-alt="A light mode stylized city map view for a fitness tracking application." data-location="Tokyo" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuDr5_s-KgoDdw3V0jHBFYtHVB3aikGStl0j7TM7kPJnauzs3U1eRnVMaMh4ywSZTMLAo_LxEZnu9rTzBPq-EGev_TiD3t0VY9osDcW0xKKUZxL2rx7TPn0qh2lor7H-TT1FWRur2eVHGsIsu-YT3ht-zwG1VrgIArB7Dxx5IgNThsHCroZ6oFbBB_Uo1Y8rIjPn8NPkxuUVxmKjpNze6RAmNCuYj9V3ivWdnkUNS_Z6ybPWERqbvRA4");'></div><svg class="absolute inset-0 w-full h-full pointer-events-none" preserveaspectratio="xMidYMid slice" viewbox="0 0 400 800"><path d="M 200 400 C 250 250, 150 150, 300 50" fill="none" opacity="0.3" stroke="#aac7ff" stroke-dasharray="10 10" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"></path></svg></div>
|
||||
<!-- TopAppBar -->
|
||||
<!-- Main Content -->
|
||||
<main class="px-margin-mobile max-w-4xl mx-auto flex flex-col gap-6 pt-2 relative z-10">
|
||||
<!-- Search & Filter -->
|
||||
<div class="flex gap-gutter items-center p-2 rounded-lg border border-outline-variant bg-surface-container-lowest shadow-sm">
|
||||
<div class="flex-1 relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
|
||||
<input class="w-full bg-surface-container-lowest border-b border-outline-variant focus:border-primary text-on-surface pl-10 pr-4 py-2 font-body-md text-body-md outline-none transition-colors placeholder:text-on-surface-variant/70" placeholder="Search rides..." type="text"/>
|
||||
</div>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded hover:bg-surface-container-highest transition-colors text-primary border border-outline-variant shadow-sm bg-surface-container-lowest">
|
||||
<span class="material-symbols-outlined">tune</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-gutter mb-2 z-10">
|
||||
<div class="p-4 rounded-lg flex flex-col gap-1 bg-surface-container-lowest border border-outline-variant shadow-sm"><span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant uppercase">Total Dist</span><span class="font-headline-md text-headline-md text-primary">482<span class="text-label-sm tracking-wider ml-1 text-on-surface-variant">KM</span></span></div>
|
||||
<div class="p-4 rounded-lg flex flex-col gap-1 bg-surface-container-lowest border border-outline-variant shadow-sm"><span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant uppercase">Total Dist</span><span class="font-headline-md text-headline-md text-primary">482<span class="text-label-sm tracking-wider ml-1 text-on-surface-variant">KM</span></span></div>
|
||||
<div class="p-4 rounded-lg flex flex-col gap-1 bg-surface-container-lowest border border-outline-variant shadow-sm"><span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant uppercase">Total Dist</span><span class="font-headline-md text-headline-md text-primary">482<span class="text-label-sm tracking-wider ml-1 text-on-surface-variant">KM</span></span></div>
|
||||
<div class="p-4 rounded-lg flex flex-col gap-1 bg-surface-container-lowest border border-outline-variant shadow-sm"><span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant uppercase">Total Dist</span><span class="font-headline-md text-headline-md text-primary">482<span class="text-label-sm tracking-wider ml-1 text-on-surface-variant">KM</span></span></div>
|
||||
</div>
|
||||
<!-- Rides List -->
|
||||
<div class="flex flex-col gap-gutter">
|
||||
<!-- Ride Item 1 -->
|
||||
<div class="rounded-lg flex flex-col md:flex-row overflow-hidden group cursor-pointer hover:border-primary/50 transition-colors bg-surface-container-lowest border border-outline-variant shadow-sm">
|
||||
<div class="h-32 md:h-auto md:w-48 bg-surface-container-highest relative">
|
||||
<img class="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity" data-alt="A dark mode minimalist map view showing a neon green GPS route line traversing through city blocks. High contrast, technical aesthetic, muted dark charcoal background." data-location="San Francisco" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAx1uDTgkMgXUBwI-JLBmH1_3Mw1sk91ovk-19wP19I5wktrsuJ9nM757sCThvGGNFlWi88pEzXj9cB_wlHAhQoTdkoXYp1csaGHd6e_iulV84cPEhZNlj6_M-EWzoEdv7CT-q9BJLlkOBPq9D5IaX6mQk1nqZQDfeR0k535iIYJ2rXJFCyAAgCEF0gKEYyumLsUopkQMD2ig13W01XANoAJPCP--li-S1iln0VfzpqY-vmyHLZsO9H"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-surface/80 to-transparent md:hidden"></div>
|
||||
</div>
|
||||
<div class="p-4 flex flex-col justify-between flex-1 gap-4">
|
||||
<div>
|
||||
<h3 class="font-headline-sm text-headline-sm text-on-surface mb-1">Morning Circuit</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Oct 24, 06:30 AM</p>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">DISTANCE</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">42.5 KM</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">TIME</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">1:45:22</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">AVG SPD</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">24.2 KPH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ride Item 2 -->
|
||||
<div class="rounded-lg flex flex-col md:flex-row overflow-hidden group cursor-pointer hover:border-primary/50 transition-colors bg-surface-container-lowest border border-outline-variant shadow-sm">
|
||||
<div class="h-32 md:h-auto md:w-48 bg-surface-container-highest relative">
|
||||
<img class="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity" data-alt="A dark mode minimalist map view showing a neon green GPS route line winding through a mountainous region. High contrast, technical aesthetic, deep black background." data-location="Marin Headlands" src="https://lh3.googleusercontent.com/aida-public/AB6AXuA8dTKlnX4_8dnTgJ0UQpocYH5zvSgIxrAirD88D7z-dcBw6SziTL2p8niEuvn-Q-z1M3AwecI2VoHbFXE70wCOQEymO_yZatnGuP5BkKz-orUNu3Lpmy3ZnXrxYVUOF77CdUnY6WWo098sJre88SDaK82aid7U4mo6vcV5DtxUqQ-H9D0N6RvTzWc0gmev36tcOCKDZqlUsRiHMarZOD6QHiPfL7HzGu_KN1gdDF1n-7G0vLs6CzmA"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-surface/80 to-transparent md:hidden"></div>
|
||||
</div>
|
||||
<div class="p-4 flex flex-col justify-between flex-1 gap-4">
|
||||
<div>
|
||||
<h3 class="font-headline-sm text-headline-sm text-on-surface mb-1">Hill Repeats</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Oct 22, 17:15 PM</p>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">DISTANCE</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">28.1 KM</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">TIME</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">1:12:05</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">AVG SPD</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">23.4 KPH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ride Item 3 -->
|
||||
<div class="rounded-lg flex flex-col md:flex-row overflow-hidden group cursor-pointer hover:border-primary/50 transition-colors opacity-75 bg-surface-container-lowest border border-outline-variant shadow-sm">
|
||||
<div class="h-32 md:h-auto md:w-48 bg-surface-container-highest relative">
|
||||
<img class="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity grayscale group-hover:grayscale-0" data-alt="A dark mode minimalist map view showing a neon green GPS route line along a coastal highway. High contrast, technical aesthetic, muted dark charcoal background." data-location="Highway 1" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAr-gumM8Tc0ayTzCw3s39Z1j0cydRKq7oGECkbJNXERMpAQaP4xrvoVkTVHzGe7mG83pDzxMMZGy9SpTk19uSKlnM5Vjoz3zurdx6AbWLUisqVcuB6CjXS4wt_hJlUK4TfOICbZdPN2UzCkVYUHClJ1jwfeRwA0SFVxWC2gfneSIXny-aaJMfddGQCFtCB7Xjh6hoFigOGUZB-2yjvcHcgwh_u3hZXLDETlfCtPZmyedY1zuZJ9Lft"/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-surface/80 to-transparent md:hidden"></div>
|
||||
</div>
|
||||
<div class="p-4 flex flex-col justify-between flex-1 gap-4">
|
||||
<div>
|
||||
<h3 class="font-headline-sm text-headline-sm text-on-surface mb-1">Coastal Cruise</h3>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Oct 18, 08:00 AM</p>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">DISTANCE</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">85.4 KM</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">TIME</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">3:24:10</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm tracking-wider text-on-surface-variant mb-1 uppercase">AVG SPD</span>
|
||||
<span class="font-headline-sm text-headline-sm text-primary">25.1 KPH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- BottomNavBar -->
|
||||
<nav class="fixed bottom-0 w-full z-50 pb-safe bg-surface-container-lowest/80 backdrop-blur-xl border-t border-outline-variant flex justify-around items-center h-20 px-margin-mobile md:hidden"><button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150"><span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">map</span></button><button class="flex flex-col items-center justify-center text-primary relative after:content-[''] after:absolute after:-bottom-1 after:w-1 after:h-1 after:bg-primary after:rounded-full hover:text-primary/80 transition-transform duration-150 scale-95"><span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">book</span></button><button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150"><span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">route</span></button><button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary/80 transition-transform duration-150"><span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">settings</span></button></nav>
|
||||
<!-- Desktop Side Nav Overlay (Hidden on Mobile, replaces Bottom Nav visually in a real app, keeping it simple here) -->
|
||||
<nav class="hidden md:flex fixed left-0 top-16 bottom-0 w-24 bg-surface-container-lowest/80 backdrop-blur-md border-r border-outline-variant flex-col items-center py-6 gap-6 z-40">
|
||||
<button class="w-12 h-12 flex items-center justify-center rounded hover:bg-surface-container-highest text-on-surface-variant transition-colors group relative">
|
||||
<span class="material-symbols-outlined">map</span>
|
||||
<span class="absolute left-14 bg-surface-container px-2 py-1 rounded font-label-sm text-label-sm tracking-wider text-on-surface opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">Map</span>
|
||||
</button>
|
||||
<button class="w-12 h-12 flex items-center justify-center rounded bg-primary-container/50 text-primary transition-colors group relative border border-primary/20"><span class="material-symbols-outlined" style='font-variation-settings: "FILL" 1;'>book</span><span class="absolute left-14 bg-surface-container px-2 py-1 rounded font-label-sm text-label-sm tracking-wider text-on-surface opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">Journal</span></button>
|
||||
<button class="w-12 h-12 flex items-center justify-center rounded hover:bg-surface-container-highest text-on-surface-variant transition-colors group relative">
|
||||
<span class="material-symbols-outlined">route</span>
|
||||
<span class="absolute left-14 bg-surface-container px-2 py-1 rounded font-label-sm text-label-sm tracking-wider text-on-surface opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">Plan</span>
|
||||
</button>
|
||||
</nav>
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
body { padding-left: 96px; pb: 0; }
|
||||
}
|
||||
</style>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en" style=""><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>RIPPR - Plan Route</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&family=Sora:wght@600;700&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"secondary-fixed-dim": "#aec7f6",
|
||||
"primary-fixed": "#d6e3ff",
|
||||
"tertiary-fixed-dim": "#ffb68c",
|
||||
"inverse-surface": "#e5e2e1",
|
||||
"on-surface-muted": "#9ca3af",
|
||||
"error": "#ffb4ab",
|
||||
"inverse-primary": "#005db8",
|
||||
"on-primary-fixed-variant": "#00458d",
|
||||
"secondary": "#aec7f6",
|
||||
"primary-fixed-dim": "#aac7ff",
|
||||
"surface-container": "#201f1f",
|
||||
"primary": "#aac7ff",
|
||||
"tertiary-container": "#e3711f",
|
||||
"surface-variant": "#353534",
|
||||
"on-background": "#e5e2e1",
|
||||
"on-secondary-fixed-variant": "#2e476f",
|
||||
"primary-container": "#4090fe",
|
||||
"outline-variant": "#414753",
|
||||
"background": "#131313",
|
||||
"surface": "#131313",
|
||||
"on-tertiary": "#532200",
|
||||
"on-tertiary-fixed": "#321200",
|
||||
"on-surface": "#e5e2e1",
|
||||
"surface-container-low": "#1c1b1b",
|
||||
"on-primary-container": "#002958",
|
||||
"secondary-fixed": "#d6e3ff",
|
||||
"accent-error": "#ffb4ab",
|
||||
"on-primary-fixed": "#001b3e",
|
||||
"surface-container-lowest": "#0e0e0e",
|
||||
"surface-tint": "#aac7ff",
|
||||
"error-container": "#93000a",
|
||||
"secondary-container": "#2e476f",
|
||||
"surface-main": "#0a0a0c",
|
||||
"tertiary-fixed": "#ffdbc9",
|
||||
"surface-container-high": "#2a2a2a",
|
||||
"on-primary": "#002f64",
|
||||
"on-surface-bright": "#ffffff",
|
||||
"on-tertiary-container": "#481d00",
|
||||
"on-error": "#690005",
|
||||
"surface-card": "#16161a",
|
||||
"surface-container-highest": "#353534",
|
||||
"surface-bright": "#393939",
|
||||
"on-secondary-container": "#9db6e4",
|
||||
"on-tertiary-fixed-variant": "#763400",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-secondary-fixed": "#001b3d",
|
||||
"tertiary": "#ffb68c",
|
||||
"outline": "#8b919f",
|
||||
"surface-dim": "#131313",
|
||||
"on-secondary": "#143057",
|
||||
"inverse-on-surface": "#313030",
|
||||
"on-surface-variant": "#c1c6d5"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"margin-mobile": "16px",
|
||||
"gutter": "24px",
|
||||
"margin-desktop": "32px",
|
||||
"base": "4px",
|
||||
"max-width": "1280px",
|
||||
"unit": "4px",
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "48px",
|
||||
"stack-md": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"headline-md": ["Inter"],
|
||||
"label-sm": ["Inter"],
|
||||
"body-lg": ["Inter"],
|
||||
"body-md": ["Inter"],
|
||||
"headline-sm": ["Inter"],
|
||||
"headline-lg": ["Inter"],
|
||||
"headline-lg-mobile": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"label-caps": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"headline-md": ["24px", { "lineHeight": "32px", "fontWeight": "600" }],
|
||||
"label-sm": ["11px", { "lineHeight": "16px", "letterSpacing": "0.5px", "fontWeight": "500" }],
|
||||
"body-lg": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
|
||||
"body-md": ["14px", { "lineHeight": "20px", "fontWeight": "400" }],
|
||||
"headline-sm": ["20px", { "lineHeight": "28px", "fontWeight": "600" }],
|
||||
"headline-lg": ["32px", { "lineHeight": "40px", "letterSpacing": "-0.02em", "fontWeight": "700" }],
|
||||
"headline-lg-mobile": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "700" }],
|
||||
"label-md": ["12px", { "lineHeight": "16px", "letterSpacing": "0.5px", "fontWeight": "500" }],
|
||||
"label-caps": ["12px", { "lineHeight": "16px", "letterSpacing": "0.1em", "fontWeight": "500" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.map-grid-overlay {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
animation: pulse-ring 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 0 0 rgba(170, 199, 255, 0.5);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 0 20px rgba(170, 199, 255, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 0 0 rgba(170, 199, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.route-line {
|
||||
stroke-dasharray: 10, 10;
|
||||
animation: dash 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface h-screen w-full overflow-hidden relative selection:bg-primary-container selection:text-on-primary-container antialiased flex flex-col">
|
||||
<!-- Fullscreen Map Canvas -->
|
||||
<main class="flex-grow relative h-full w-full z-0 overflow-hidden">
|
||||
<div class="absolute top-4 left-1/2 -translate-x-1/2 z-50 w-auto px-container-margin pointer-events-none mt-4">
|
||||
<div class="bg-surface/90 backdrop-blur-xl border border-outline-variant/30 rounded-full px-8 py-4 flex items-center gap-8 shadow-sm pointer-events-auto">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-on-surface-variant font-headline-md text-[10px] uppercase tracking-widest mb-1">Distance</span>
|
||||
<span class="font-headline-md text-primary-container">12.4<span class="text-sm ml-1">km</span></span>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-outline-variant/30"></div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-on-surface-variant font-headline-md text-[10px] uppercase tracking-widest mb-1">Est. Time</span>
|
||||
<span class="font-headline-md text-primary-container">45<span class="text-sm ml-1">m</span></span>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-outline-variant/30"></div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-on-surface-variant font-headline-md text-[10px] uppercase tracking-widest mb-1">Pins</span>
|
||||
<span class="font-headline-md text-primary-container">5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Base Map Layer -->
|
||||
<div class="absolute inset-0 z-0 bg-surface-container">
|
||||
<img class="w-full h-full object-cover opacity-30 mix-blend-screen" data-alt="A digital map showing a top-down view of city streets and terrain." data-location="San Francisco, CA" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDtux0iJ6r19x5fCEWn0L1Ai3ziybnKhwYCd-8xyKm8ebAQHTJPdulojF6Pi_31mtsUlhqpI5ejusGg8Rk7y9c20eWK9sqj3uubky70kqSRiHsd37IJzOiRUTYdHCyOg2g7K5uADAg-39nR48gfWmfotW13eKGb1I9OMrc8MJBtj6lD9rtqapC2DG8kLoTz50c_QCt_AVShQyLrft6yW1SShV4mZXP2kwhf4AZsh3bvkzeCQzfIB43E"/>
|
||||
</div>
|
||||
<!-- Technical Grid Overlay -->
|
||||
<div class="absolute inset-0 map-grid-overlay z-10 pointer-events-none"></div>
|
||||
<!-- Planned Route Overlay (SVG) -->
|
||||
<svg class="absolute inset-0 w-full h-full z-20 pointer-events-none" preserveaspectratio="xMidYMid slice" viewbox="0 0 1000 1000">
|
||||
<path class="route-line opacity-80" d="M 400 400 L 450 420 L 500 380 L 550 430 L 600 400" fill="none" stroke="#4090fe" stroke-dasharray="10, 10" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"></path>
|
||||
<circle cx="400" cy="400" fill="#4090fe" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
<circle cx="450" cy="420" fill="#4090fe" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
<circle cx="500" cy="380" fill="#4090fe" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
<circle cx="550" cy="430" fill="#4090fe" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
<circle cx="600" cy="400" fill="#4090fe" r="6" stroke="#131313" stroke-width="2"></circle>
|
||||
</svg>
|
||||
<!-- User Location Marker (Pulsing) -->
|
||||
<div class="absolute top-[70%] left-[30%] z-30 transform -translate-x-1/2 -translate-y-1/2 flex items-center justify-center pointer-events-none">
|
||||
<div class="absolute w-4 h-4 bg-primary-container rounded-full opacity-30"></div>
|
||||
<div class="absolute w-3 h-3 bg-primary-container rounded-full pulse-ring"></div>
|
||||
<div class="w-2 h-2 bg-primary-container rounded-full relative z-10 shadow-[0_0_10px_rgba(64,144,254,0.5)]"></div>
|
||||
<!-- Vision cone indicator -->
|
||||
<div class="absolute -top-12 left-1/2 -translate-x-1/2 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-t-[40px] border-t-primary-container/20 origin-bottom transform rotate-45"></div>
|
||||
</div>
|
||||
<!-- Interactive Map Canvas Layer (Invisible, captures clicks for dropping pins) -->
|
||||
<div class="absolute inset-0 z-40 cursor-crosshair" id="map-interaction-layer"></div>
|
||||
</main>
|
||||
<!-- Floating HUD Elements Layer -->
|
||||
<div class="absolute inset-0 z-50 pointer-events-none flex flex-col justify-end">
|
||||
<!-- Tooltip -->
|
||||
<div class="w-full flex justify-center mb-stack-md pointer-events-none px-container-margin">
|
||||
<button class="pointer-events-auto bg-primary-container text-on-primary-container font-headline-md text-label-caps px-12 py-4 rounded-xl shadow-md uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all">Start Route</button>
|
||||
</div>
|
||||
<!-- Bottom Navigation Bar (Generated from JSON) -->
|
||||
<nav class="bg-surface border-t border-outline-variant/30 w-full pb-safe flex justify-around items-center h-20 px-container-margin pointer-events-auto shrink-0 relative shadow-[0_-4px_20px_rgba(0,0,0,0.2)]">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary-container transition-colors duration-150">
|
||||
<span class="material-symbols-outlined mb-1" style="font-variation-settings: 'FILL' 0;">map</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-primary-container relative after:content-[''] after:absolute after:-bottom-1 after:w-1 after:h-1 after:bg-primary-container after:rounded-full hover:text-primary-container transition-colors duration-150 scale-95">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">book</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary-container transition-colors duration-150">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">route</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary-container transition-colors duration-150">
|
||||
<span class="material-symbols-outlined mb-1" style="font-variation-settings: 'FILL' 0;">settings</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Micro-interaction Script for Pin Dropping Simulation -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mapLayer = document.getElementById('map-interaction-layer');
|
||||
const svgLayer = document.querySelector('svg');
|
||||
let pinCount = 0;
|
||||
|
||||
mapLayer.addEventListener('click', (e) => {
|
||||
const rect = mapLayer.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Visual feedback of drop
|
||||
const ripple = document.createElement('div');
|
||||
ripple.className = 'absolute rounded-full border-2 border-primary-container bg-primary-container/20 pointer-events-none transform -translate-x-1/2 -translate-y-1/2';
|
||||
ripple.style.left = `${x}px`;
|
||||
ripple.style.top = `${y}px`;
|
||||
ripple.style.width = '0px';
|
||||
ripple.style.height = '0px';
|
||||
ripple.style.opacity = '1';
|
||||
ripple.style.transition = 'all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1)';
|
||||
|
||||
mapLayer.parentElement.appendChild(ripple);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
ripple.style.width = '60px';
|
||||
ripple.style.height = '60px';
|
||||
ripple.style.opacity = '0';
|
||||
});
|
||||
|
||||
// Clean up ripple
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
112
rippr-flutter-src/docs/ui-redesign/README.md
Normal file
112
rippr-flutter-src/docs/ui-redesign/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# UI redesign tickets
|
||||
|
||||
Same shape as `docs/v3/`: one file per feature, Goal · Context · Design · Implementation ·
|
||||
Acceptance criteria · Tests · Risks · Out of scope. Written before implementing.
|
||||
|
||||
Source material: `docs/design/stitch-export/` — four Stitch-generated screens (Map HUD,
|
||||
Plan, Route Planning, Rides History) plus the "Modern Professional Dark" design system
|
||||
actually used by all four. See that directory's `README.md` for how they were fetched and
|
||||
a correction of an earlier mislabeling.
|
||||
|
||||
This is a **navigation-model change**, not a reskin: today's app is a stack rooted at
|
||||
Record, with Trips/Settings/Routes as pushed children. The new design is a persistent
|
||||
4-tab shell (Map / Rides / Plan / Settings) with the map visible, at some opacity, behind
|
||||
every tab — not just the Map tab. Sequenced so the riskiest, highest-blast-radius piece
|
||||
(the shell) lands first and `flutter test` stays green throughout, the same discipline
|
||||
v3 held to.
|
||||
|
||||
## The tickets
|
||||
|
||||
| # | Ticket | Size | Depends on | Status |
|
||||
|---|---|---|---|---|
|
||||
| [UI-01](UI-01-tab-shell-background-map.md) | Persistent tab shell with an always-visible background map | L | — | Done |
|
||||
| [UI-02](UI-02-offline-skeleton-map.md) | Offline / no-connection skeleton map | S | UI-01 | Done |
|
||||
| [UI-03](UI-03-glass-component-kit.md) | Shared floating-glass component kit | M | UI-08 | Done |
|
||||
| [UI-04](UI-04-customizable-hud-widgets.md) | Customizable HUD telemetry widgets (drag, resize, visibility toggle) | L | UI-03 | Done |
|
||||
| [UI-05](UI-05-map-hud-record-screen.md) | Map HUD: Record screen redesign | M | UI-01, UI-03, UI-04 | Done |
|
||||
| [UI-06](UI-06-plan-and-route-planning.md) | Plan & Route Planning redesign | M | UI-01, UI-03 | Done |
|
||||
| [UI-07](UI-07-rides-history.md) | Rides History redesign | M | UI-01, UI-03 | Done |
|
||||
| [UI-08](UI-08-theme-modern-professional-dark.md) | Theme migration to Modern Professional Dark | S | — | Done |
|
||||
| [UI-09](UI-09-monochrome-dark-map-tiles.md) | Monochrome dark map tiles | S | — | Done |
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
UI-01 ──┬──► UI-02
|
||||
├──► UI-05
|
||||
├──► UI-06
|
||||
└──► UI-07
|
||||
UI-08 ──► UI-03 ──┬──► UI-04 ──► UI-05
|
||||
├──► UI-06
|
||||
└──► UI-07
|
||||
UI-09 ──┬──► UI-05
|
||||
├──► UI-06
|
||||
└──► UI-07
|
||||
|
||||
no dependencies: UI-01 · UI-08 · UI-09
|
||||
```
|
||||
|
||||
## Suggested order
|
||||
|
||||
1. **UI-01** — the shell. Highest blast radius (touches every screen's entry point), so
|
||||
get it merged and green before any visual work starts.
|
||||
2. **UI-08** — theme. Every screen ticket after this needs the palette settled once, not
|
||||
patched per-screen. Supersedes V3-16's safety-orange direction — see that ticket's
|
||||
note.
|
||||
3. **UI-09** — dark map tiles. Small, no dependencies, and every screen with a map looks
|
||||
wrong without it regardless of how correct the HUD chrome on top is.
|
||||
4. **UI-02** — skeleton map. Small, and exercises the background-map plumbing UI-01 just
|
||||
built while it's fresh.
|
||||
5. **UI-03** — the component kit (`GlassPanel`, `FloatingPill`, `PulsingLocationMarker`).
|
||||
Build once, consumed by every screen ticket.
|
||||
6. **UI-04** — the customizable HUD widget system (drag/resize/persist layout, a
|
||||
visibility toggle in Settings). This is its own ticket, not folded into UI-05, because
|
||||
drag-and-resize-with-persisted-layout is a genuinely separate piece of engineering
|
||||
from "redesign the record screen" — a small widget-layout editor in miniature.
|
||||
7. **UI-05 / UI-06 / UI-07** — the three screen redesigns, in any order once everything
|
||||
above is done. Each is independently shippable.
|
||||
|
||||
## Things named explicitly by the person who commissioned this, not inferred from the mockups
|
||||
|
||||
- **The map is active on every tab**, including Rides History — blurred/dimmed behind
|
||||
the content cards so it doesn't compete for attention, not hidden. This wasn't visible
|
||||
in the Stitch export (Rides History's HTML uses a static blurred background image, not
|
||||
a live map) — it's a deliberate product decision layered on top of the mockups.
|
||||
- **No header, anywhere, ever** — the stated reason is screen real estate: the point of
|
||||
the redesign is to show as much map as possible, and a persistent bottom nav plus zero
|
||||
header chrome is what buys that space back.
|
||||
- **A no-connection skeleton map** (UI-02), rather than a blank or broken map, since the
|
||||
map is now a permanent background presence on every tab rather than something opened
|
||||
deliberately.
|
||||
- **The telemetry HUD widgets are user-customizable** (UI-04): draggable and resizable by
|
||||
the rider, and which metrics show up live at all is a Settings toggle. The Stitch
|
||||
mockup's tap-to-expand interaction is a fixed layout with one temporary focus state;
|
||||
this goes further — the rider chooses the layout and which stats exist on screen at
|
||||
all, and it persists.
|
||||
- **The map itself is monochrome dark** (UI-09) — today's default OSM tan/cream tiles are
|
||||
nothing like the mockups regardless of how correct the HUD chrome on top is. A real
|
||||
tile-provider decision, not a color filter thrown on as an afterthought — see that
|
||||
ticket for the trade-off against just filtering OSM's existing tiles.
|
||||
|
||||
## The Map screen is the design north star
|
||||
|
||||
The four Stitch exports don't agree with each other on styling — expected, since each
|
||||
was a separate prompt and the AI generation drifted between them. **Map HUD is the
|
||||
canonical reference** for color usage, icon choice/fill-state, spacing, and component
|
||||
pattern across the whole redesign. Where another screen's mockup disagrees with Map
|
||||
HUD on any of those, Map HUD wins.
|
||||
|
||||
Plan and Route Planning are correct **in principle** — fullscreen map, floating pill
|
||||
summaries, pin-drop interaction, no header — but their specific styling (icon fill
|
||||
states, exact nav bar treatment, some color choices) drifted from Map HUD across
|
||||
prompts and should be restyled to match it, not copied as-is. UI-06 says this again at
|
||||
the point where it matters.
|
||||
|
||||
## What the mockups got generated inconsistently — don't copy as intentional
|
||||
|
||||
- Bottom nav shows 4 icons on Map HUD and Rides History, only 3 (no Settings) on the Plan
|
||||
screen. Build one nav bar with 4 destinations, always.
|
||||
- Rides History's summary row is 4 copies of the same "Total Dist" card. Real, distinct
|
||||
summary stats are UI-06's job to define.
|
||||
- Route Planning's bottom nav highlights "Rides" as active while the screen shown is
|
||||
Plan — a generation slip, not a deliberate cross-tab indicator.
|
||||
@@ -0,0 +1,182 @@
|
||||
# UI-01 — Persistent tab shell with an always-visible background map
|
||||
|
||||
**Depends on** nothing · **Size** L · **Status** Done
|
||||
|
||||
## Goal
|
||||
Replace the current push/pop stack rooted at Record with a persistent 4-tab shell (Map,
|
||||
Rides, Plan, Settings), and make the map visible — at reduced opacity where it isn't the
|
||||
primary content — behind every one of those tabs, not just Map.
|
||||
|
||||
## Context
|
||||
Today's navigation (`router.dart`) is a hub-and-spoke stack: Record is `/`, and
|
||||
Trips/Settings/Routes are pushed children you back out of with a `TextButton`/`AppBar`
|
||||
back arrow. The Stitch redesign is a different model entirely: four co-equal,
|
||||
always-reachable destinations behind a persistent bottom nav bar, each with its own
|
||||
independent navigation stack (Trip Detail pushes *within* the Rides tab; the route
|
||||
planner pushes *within* the Plan tab).
|
||||
|
||||
Layered on top of the mockups, by explicit instruction: **the map is active on every
|
||||
tab**, including Rides History, dimmed/blurred behind the tab's actual content so it
|
||||
doesn't pull focus. None of the four Stitch exports show this for Rides History (its HTML
|
||||
uses a static blurred background image) — this is a deliberate product decision, not
|
||||
something to infer from the mockups alone.
|
||||
|
||||
**Also by explicit instruction: no header, on any tab, ever.** The stated reason is
|
||||
screen real estate — the whole point of this redesign is showing as much map as
|
||||
possible, and a header is exactly the chrome a persistent bottom nav is meant to let you
|
||||
remove. Every Stitch export already has an empty `<!-- TopAppBar -->` comment confirming
|
||||
this; treat it as intentional, not an oversight.
|
||||
|
||||
## Design
|
||||
**Navigation:** `go_router`'s `StatefulShellRoute.indexedStack` (or `.builder`) with four
|
||||
branches — Map (`/`), Rides (`/rides`), Plan (`/plan`), Settings (`/settings`) — each
|
||||
branch keeps its own navigator so pushing Trip Detail from Rides, or the route planner
|
||||
from Plan, doesn't disturb the other tabs' state or the active tab index.
|
||||
|
||||
**Background map:** one persistent, live `RideMap`/`FlutterMap` instance sitting behind
|
||||
the `IndexedStack`'s current branch, not four separate map instances. Rebuilding a real
|
||||
`FlutterMap` on every tab switch would refetch tiles and lose camera position; a single
|
||||
shared instance, with only its *content* (opacity, overlay, current position) varying by
|
||||
tab, is both cheaper and matches "the map is always there, tabs are what floats on top of
|
||||
it."
|
||||
|
||||
- **Map tab:** full opacity, the actual recording/HUD content.
|
||||
- **Rides / Plan / Settings tabs:** dimmed (roughly 30-40% overlay per the Stitch
|
||||
export's `opacity-30`/`opacity-40` treatment) and non-interactive — taps pass through
|
||||
to the tab's real content, the map is decoration, not a control surface, on these tabs.
|
||||
|
||||
**No header:** each tab's `Scaffold` has no `appBar`. Screen identity comes from content
|
||||
(a title inside the tab's own top content, if needed) or from the active nav item, never
|
||||
from a persistent bar.
|
||||
|
||||
## Implementation
|
||||
1. `AppShell` widget: `StatefulShellRoute` with 4 branches, wrapping a persistent
|
||||
`BottomNavBar` (see UI-03 for the styled version; a plain one is fine to start) and the
|
||||
shared background map behind an `IndexedStack`.
|
||||
2. Extract the background-map hosting into its own widget (`PersistentMapBackground` or
|
||||
similar) that every tab's `Scaffold` composes against, rather than four independent
|
||||
`RideMap` constructions.
|
||||
3. Move `RecordScreen`'s existing map-in-a-card logic to *become* the Map tab's full-
|
||||
opacity state of this shared background, rather than a separate widget tree — UI-04
|
||||
does the visual rework, this ticket only has to make the plumbing possible.
|
||||
4. Remove `AppBar`s from every top-level tab screen. Settings' existing back button goes
|
||||
away too — Settings becomes a tab, not a pushed screen, so there's nothing to back out
|
||||
of.
|
||||
5. Update `router.dart`'s `Routes` constants and every `context.push`/`context.pop` call
|
||||
site that assumed the old flat stack.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] Four tabs are reachable from a persistent bottom nav bar on every screen
|
||||
- [ ] Switching tabs does not rebuild/refetch the map (camera position survives a tab
|
||||
switch)
|
||||
- [ ] The map is visible, dimmed, behind Rides, Plan, and Settings — not just Map
|
||||
- [ ] No `AppBar`/header exists on any of the four top-level tab screens
|
||||
- [ ] Trip Detail still pushes within the Rides tab (back returns to the Rides list, not
|
||||
to the Map tab)
|
||||
- [ ] The route planner still pushes within the Plan tab
|
||||
- [ ] Existing widget tests are updated to pump the shell rather than a bare screen, and
|
||||
still pass
|
||||
|
||||
## Tests
|
||||
- Widget: all four tabs are reachable and their content renders
|
||||
- Widget: pushing Trip Detail from Rides and popping returns to Rides, not to Map
|
||||
- Widget: the background map widget instance is not recreated on a tab switch (e.g.
|
||||
assert identity/key stability, or that camera state is preserved)
|
||||
- Widget: no `AppBar` is found on any of the four tab roots
|
||||
|
||||
## Risks
|
||||
- **This is the highest-blast-radius ticket in the set** — it touches every screen's
|
||||
entry point. Land it alone, get `flutter test` fully green, before starting any visual
|
||||
ticket on top of it.
|
||||
- A shared background map instance behind an `IndexedStack` is easy to get wrong in a way
|
||||
that either rebuilds the map on every tab switch (defeating the point) or keeps it
|
||||
alive so aggressively that it never tears down while the app is backgrounded — revisit
|
||||
V3-04's lifecycle-aware `TileLayer` teardown to make sure it still applies correctly
|
||||
once the map is shared across tabs rather than owned by one screen.
|
||||
|
||||
## Out of scope
|
||||
The skeleton/offline map state (UI-02). Any visual redesign of what's inside a tab
|
||||
(UI-04/05/06) — this ticket only has to make the shell and shared background map exist,
|
||||
correctly, with today's screen content still working inside it.
|
||||
|
||||
## Outcome
|
||||
|
||||
Shipped as designed: `lib/src/ui/router.dart` now builds a single `StatefulShellRoute
|
||||
.indexedStack` with four branches (Map `/`, Rides `/rides`, Plan `/plan`, Settings
|
||||
`/settings`), each with Trip Detail / the route planner nested as a child `GoRoute`
|
||||
inside its own branch so pushing/popping there never disturbs the other tabs or the
|
||||
active tab index. `lib/src/ui/app_shell.dart` is new: `AppShell` is a thin go_router
|
||||
adapter around `ShellScaffold`, a plain-parameter (`currentIndex`/`onDestinationSelected`
|
||||
/`child`) `ConsumerWidget` that owns the one shared `RideMap` instance, the dimming scrim,
|
||||
and the bottom `NavigationBar`. Splitting the two was necessary for testability —
|
||||
go_router only ever constructs a real `StatefulNavigationShell` itself, so widget tests
|
||||
drive `ShellScaffold` directly with a plain `int` instead.
|
||||
|
||||
Every top-level tab screen (`RecordScreen`, `TripsScreen`, `RoutesListScreen`,
|
||||
`SettingsScreen`) had its `AppBar`/back-button header removed and its `Scaffold`
|
||||
background set to transparent so the shared map shows through. `RecordScreen` also lost
|
||||
its per-screen `onOpenTrips`/`onOpenSettings`/`onOpenRoutes` navigation callbacks and its
|
||||
embedded `_LiveMap` card entirely — navigation is the shell's nav bar now, and the map is
|
||||
the shell's permanent background rather than something each screen constructs for
|
||||
itself. One deliberate temporary trade: the mounted-mode toggle's only remaining button
|
||||
was on `RecordScreen`'s removed header; its sole access point until UI-05 restyles the
|
||||
Record screen is now Settings' existing `mounted-mode-switch`.
|
||||
|
||||
**Bug found and fixed during this ticket, not anticipated by the plan:** `RideMap`
|
||||
returned a structurally different widget tree for empty vs. non-empty points in its
|
||||
`showEmptyLabel: false` (background) mode — a bare `FlutterMap` vs. `ClipRRect >
|
||||
FlutterMap`. Once the map became a long-lived shell background instead of a
|
||||
freshly-mounted per-screen widget, this became visible: the moment a live ride's first
|
||||
point arrived, Flutter unmounted/remounted the differing subtree mid-flight, and
|
||||
`RideMap`'s own `didUpdateWidget`-scheduled `_controller.camera` post-frame callback fired
|
||||
against the stale element, throwing "Looking up a deactivated widget's ancestor is
|
||||
unsafe." Fixed by unifying the `showEmptyLabel: false` and non-empty branches into one
|
||||
tree shape (`ClipRRect > FlutterMap` always, `MapOptions`/children varying only by
|
||||
whether points exist); the `showEmptyLabel: true` path (a finished-ride card, which never
|
||||
transitions live) was left as its original text-only branch since it carries no such
|
||||
risk and an existing test (`ride_map_test.dart`) already asserted no `FlutterMap` renders
|
||||
there.
|
||||
|
||||
**Tests:** `flutter analyze` clean (pre-existing `deprecated_member_use` infos only,
|
||||
unrelated to this ticket). `flutter test` green at 315 tests (was 316 before this ticket;
|
||||
net -1 after removing two now-meaningless per-screen entry-point tests and adding one
|
||||
shell-nav-bar test — see below). Two tests referencing the removed
|
||||
`onOpenSettings`/`onOpenRoutes` callbacks were deleted outright (the concept no longer
|
||||
exists); a new "shell nav bar" group in `test/widget_test.dart` asserts all four
|
||||
`NavigationDestination`s are present and that tapping each calls back with the right
|
||||
index. The two V3-04 background-map tests were rewritten to pump `ShellScaffold` and
|
||||
assert on `shell-background-map`/`TileLayer` instead of the old per-screen `live-map` key
|
||||
— both pass, including the lifecycle-driven tile-drop/restore test that exposed the bug
|
||||
above.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`, API 35, `flutter run --debug`):
|
||||
confirmed visually for all four tabs —
|
||||
- Map tab: full-opacity, interactive map, no header, `START RECORDING` visible.
|
||||
- Rides/Plan/Settings tabs: same map dimmed via the `0xB3000000` scrim, non-interactive,
|
||||
no header, each tab's real content on top.
|
||||
- Started a live recording (with `adb emu geo fix` supplying a location) and switched
|
||||
tabs repeatedly: the point count and elapsed timer kept advancing across tab switches
|
||||
(11 → 31 → 41 points, uninterrupted) and the camera position did not reset — confirming
|
||||
the shared instance survives tab switches rather than being torn down and recreated.
|
||||
- Backgrounding via the real HOME key (not just the synthetic lifecycle message the
|
||||
widget test uses) dropped the map to a flat, untiled gray — the same lifecycle-based
|
||||
tile-teardown from V3-04 firing correctly in the shell context, not just in tests.
|
||||
- One visual red herring investigated and ruled out: a flat gray rectangle appeared
|
||||
transiently in the dimmed background on the Rides/Plan tabs. Ruled out as an app bug by
|
||||
reproducing it identically on two different tab screens at the same absolute screen
|
||||
position, and by confirming a fresh app launch never shows it — it is flutter_map's
|
||||
normal "tile chunk not yet loaded" placeholder at the low zoom level the background
|
||||
map starts at before any location fix narrows it, not a shell defect.
|
||||
- Known pre-existing rough edge, not a regression: the background map's follow-mode
|
||||
moves the camera center to the latest point but does not adjust zoom on the first fix,
|
||||
so a freshly-started ride's background map can look zoomed far out until the user
|
||||
manually zooms. This behavior predates UI-01 (the same `_controller.move(point, current
|
||||
Zoom)` call existed in the old standalone `_LiveMap`) and is out of scope here.
|
||||
|
||||
Deferred, not a gap: a widget test asserting Trip Detail/route-planner push-and-pop stays
|
||||
within its own tab (rather than affecting `currentIndex`) was not written — the
|
||||
`StatefulShellBranch`-per-tab nesting in `router.dart` structurally guarantees this via
|
||||
go_router's own navigator-per-branch semantics, and it was verified manually on-device
|
||||
that `router.dart`'s nested `GoRoute`s compile and resolve correctly. A future ticket
|
||||
touching Rides/Plan navigation should add explicit coverage rather than relying on this
|
||||
note.
|
||||
148
rippr-flutter-src/docs/ui-redesign/UI-02-offline-skeleton-map.md
Normal file
148
rippr-flutter-src/docs/ui-redesign/UI-02-offline-skeleton-map.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# UI-02 — Offline / no-connection skeleton map
|
||||
|
||||
**Depends on** UI-01 · **Size** S · **Status** Done
|
||||
|
||||
## Goal
|
||||
When the map has no tiles to show — no cached tiles for the current view and no network
|
||||
to fetch fresh ones — show an animated skeleton in place of a broken or blank map, on
|
||||
every tab, since UI-01 makes the map a permanent background presence rather than
|
||||
something a rider opts into per-screen.
|
||||
|
||||
## Context
|
||||
By explicit instruction: *"If we do not have connection for the map, we should just show
|
||||
an animated skeleton map so it doesn't look weird."* Once UI-01 ships, the map is always
|
||||
on screen somewhere — a blank grey rectangle or a grid of broken-image icons behind every
|
||||
tab, all the time, on a phone with no signal, is a much worse look than it was when the
|
||||
map only appeared on the one screen a rider explicitly opened.
|
||||
|
||||
This composes with existing infrastructure rather than duplicating it:
|
||||
`CachedTileProvider` (V3-11) already tries the offline tile cache before the network, so
|
||||
"no connection" in practice means *both* the cache and the network failed for the tiles
|
||||
currently in view.
|
||||
|
||||
## Design
|
||||
Detect at the `CachedTileProvider`/`TileLayer` level, not by asking the OS for
|
||||
connectivity state directly — a connectivity API can report "online" while the actual
|
||||
tile fetch still times out (captive portal, degraded connection), and the tile fetch's
|
||||
own success/failure is the only thing that actually matters here.
|
||||
|
||||
- Track a rolling failure count for in-flight tile fetches (cache miss **and** network
|
||||
fetch failed) over a short window. Crossing a small threshold (e.g. 3 consecutive
|
||||
failures) flips the map into skeleton mode; a single successful fetch flips it back.
|
||||
- Skeleton mode replaces the `TileLayer` with a static, non-fetching placeholder — never
|
||||
a widget that itself keeps trying and failing in a loop.
|
||||
- Placeholder look: a shimmer/gradient-sweep animation over a neutral grid pattern
|
||||
(reuse the "technical grid overlay" treatment already present in the Stitch exports'
|
||||
`map-grid-overlay` CSS — 40px faint grid lines — as the static base under the shimmer),
|
||||
not a spinner. A map-shaped thing that is clearly still loading, not an error state.
|
||||
- Recheck periodically (not on every frame) so the map recovers automatically the moment
|
||||
connectivity returns, without the rider having to do anything.
|
||||
|
||||
## Implementation
|
||||
1. Wrap `CachedTileProvider` (or add a sibling) that reports fetch outcomes to a small
|
||||
`MapConnectivityState` — rolling window, threshold, debounced flip.
|
||||
2. `SkeletonMapLayer` widget: the grid + shimmer, sized to fill the same space a
|
||||
`TileLayer` would.
|
||||
3. `RideMap`/`PersistentMapBackground` (from UI-01) watches `MapConnectivityState` and
|
||||
swaps `TileLayer` for `SkeletonMapLayer` when in skeleton mode — markers/polylines
|
||||
(the rider's own path, planned route) keep rendering on top either way, since those
|
||||
come from local data, not tiles.
|
||||
4. A cap on retry frequency while in skeleton mode — this must not turn into a tile-
|
||||
fetch retry loop that itself violates OSM's usage policy the way V3-11's design
|
||||
explicitly guards against.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] With no cached tiles and no network, the map area shows an animated skeleton, not
|
||||
blank space or broken-image icons
|
||||
- [ ] Recovery is automatic: once tiles become fetchable again, the skeleton is replaced
|
||||
without user action
|
||||
- [ ] The rider's live position/path and any planned route still render on top of the
|
||||
skeleton (only the base tiles are missing)
|
||||
- [ ] Skeleton mode does not itself hammer the tile server with retries
|
||||
- [ ] Behaves correctly across all four tabs (UI-01's shared background map), not just
|
||||
the Map tab
|
||||
|
||||
## Tests
|
||||
- Unit: the failure-count/threshold/debounce logic that decides skeleton vs. live,
|
||||
exercised with fakes (no real network) — mirrors V3-11's `tile_downloader_test.dart`
|
||||
pattern of injecting fake fetch outcomes
|
||||
- Widget: `SkeletonMapLayer` renders when connectivity state says offline; `TileLayer`
|
||||
renders when it says online
|
||||
- Widget: markers/polylines still render in skeleton mode
|
||||
|
||||
## Risks
|
||||
- Flapping between skeleton and live on a marginal connection would be worse than
|
||||
either state alone — the debounce window is what prevents this; tune it based on real
|
||||
behavior, not a guess (echoes V3-13's "measure, don't tune blind" discipline).
|
||||
- Must not let skeleton-mode retries become the kind of bulk/rapid tile request V3-11's
|
||||
design explicitly exists to avoid.
|
||||
|
||||
## Out of scope
|
||||
General offline-mode UX beyond the map itself (e.g. graying out map-dependent buttons).
|
||||
Manual retry controls — automatic recovery is the whole point.
|
||||
|
||||
## Outcome
|
||||
|
||||
Built exactly to the ticket's Design section. `lib/src/tiles/map_connectivity.dart`'s
|
||||
`MapConnectivityState` tracks consecutive tile-fetch failures (`skeletonFailureThreshold
|
||||
= 3`), flips to skeleton mode on the threshold, and recovers instantly on a single
|
||||
success — either an ordinary fetch succeeding (while still below threshold, resetting
|
||||
the run) or, once already in skeleton mode, a periodic single-tile probe
|
||||
(`skeletonProbeInterval = 15s`) succeeding. The probe is a deliberate design choice: once
|
||||
skeleton mode starts, `RideMap`/the route planner fully unmount their `TileLayer` (no
|
||||
ongoing requests at all, per the ticket's "never a widget that itself keeps trying and
|
||||
failing in a loop"), so nothing generates ordinary fetch outcomes to recover from — the
|
||||
probe exists specifically to test the water on the map's behalf, at a low, fixed
|
||||
cadence, using one deterministic tile (the zoom-0 whole-world overview) rather than
|
||||
whatever happened to be in view.
|
||||
|
||||
`CachedTileProvider` (`lib/src/tiles/cached_tile_provider.dart`) now takes an optional
|
||||
`MapConnectivityState? connectivity` and reports outcomes around its network fetch only
|
||||
— a cache hit is silently skipped since it says nothing about current connectivity
|
||||
either way, matching the ticket's "cache miss *and* network fetch failed" definition of
|
||||
a failure exactly. `mapConnectivityProvider` (`app/providers.dart`) is the one shared
|
||||
instance every map watches, per the class's own reasoning: connectivity is a fact about
|
||||
the network, not about which map widget happens to be on screen.
|
||||
|
||||
`SkeletonMapLayer` (`lib/src/ui/components/skeleton_map_layer.dart`) is the 40px faint
|
||||
grid (reusing the Stitch exports' `.map-grid-overlay` treatment directly, `rgba(255,255,
|
||||
255,0.03-0.05)` → `Color(0x0DFFFFFF)`) with a `LinearGradient` shimmer sweeping across it
|
||||
on a 1800ms repeating cycle, painted via `ShaderMask`/`CustomPainter` rather than a
|
||||
translated widget so it doesn't need to know its own pixel size. `RideMap` gained a
|
||||
`skeletonMode` bool (a plain constructor param, not a `ConsumerWidget` watch — consistent
|
||||
with how `tileProvider` is already handed down rather than looked up) that swaps
|
||||
`TileLayer` for `SkeletonMapLayer` while leaving `PolylineLayer`/markers untouched, since
|
||||
those come from local data. The route planner manages its own independent `FlutterMap`
|
||||
and does the same swap itself, watching `mapConnectivityProvider` directly.
|
||||
|
||||
**Refactor along the way:** moved `tileUrlTemplate`/`tileSubdomains`/`tileMaxNativeZoom`/
|
||||
`tileUserAgent` out of `ride_map.dart` into a new `lib/src/tiles/tile_config.dart`, and
|
||||
had `ride_map.dart` re-export them for its existing importers. `app/providers.dart` (the
|
||||
composition root) needed these constants for the connectivity probe's URL, and importing
|
||||
a `ui/` file from the `app/` layer would have been a real layering violation the tiles
|
||||
layer itself doesn't have a reason to accept — better to give the constants a home in
|
||||
the layer they actually describe (tile fetching) than to route around the smell.
|
||||
|
||||
**Bug caught by the first real test run:** `mapConnectivityProvider`'s initial
|
||||
implementation called `ref.onDispose(state.dispose)` in addition to
|
||||
`ChangeNotifierProvider`'s own automatic disposal of the notifier it returns — a
|
||||
double-dispose that threw "A MapConnectivityState was used after being disposed" and
|
||||
failed five `route_planner_screen_test.dart` tests outright. Fixed by removing the
|
||||
redundant manual dispose call.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 328 tests (316 + 3 new
|
||||
`ride_map_test.dart` skeleton-mode cases + 9 new `map_connectivity_test.dart` unit cases
|
||||
covering threshold behavior, the reset-on-success case, notification-only-on-actual-
|
||||
change, and the probe loop's recovery and its refusal to run at all while still live).
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`) — a full end-to-end real-
|
||||
network test, not just widget tests: cut the emulator's actual network
|
||||
(`svc wifi disable` + `svc data disable`, confirmed via a failing `ping`), cleared the
|
||||
offline tile cache via Settings, and navigated to an uncached view (the route planner's
|
||||
default zoom-14 view, which the shell's already-cached Calgary-area background tiles
|
||||
didn't cover). After the threshold of real failed fetches, the skeleton grid rendered
|
||||
correctly — clearly a "still loading" placeholder, not blank space or broken-image
|
||||
icons. Re-enabled the network and confirmed automatic recovery within one probe interval
|
||||
with no user action, exactly per the acceptance criteria. The shared shell background
|
||||
map (already displaying previously-cached tiles from memory) was unaffected throughout,
|
||||
as expected since it never needed a fresh fetch during the test window.
|
||||
137
rippr-flutter-src/docs/ui-redesign/UI-03-glass-component-kit.md
Normal file
137
rippr-flutter-src/docs/ui-redesign/UI-03-glass-component-kit.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# UI-03 — Shared floating-glass component kit
|
||||
|
||||
**Depends on** UI-08 (theme tokens) · **Size** M · **Status** Done
|
||||
|
||||
## Goal
|
||||
Build once, use everywhere: the small set of visual primitives every Stitch screen
|
||||
reuses, so UI-04/05/06/07 compose them instead of each reinventing blur-panel styling.
|
||||
|
||||
## Context
|
||||
All four Stitch exports lean on the same handful of visual patterns repeatedly:
|
||||
glassmorphic panels (`bg-surface/80 backdrop-blur-xl border border-white/10`), a floating
|
||||
centered pill for a compact stat summary, and a pulsing/radar-style location marker.
|
||||
Building these once means every consuming screen ticket is "compose these," not
|
||||
"reimplement blur and border styling a fourth time."
|
||||
|
||||
## Design
|
||||
Three widgets, all pure presentation (no data-fetching, no business logic):
|
||||
|
||||
**`GlassPanel`** — the workhorse. `BackdropFilter` + `ImageFilter.blur` inside a
|
||||
`ClipRRect`, a translucent surface-color fill, a 1px low-opacity border. Takes a `child`
|
||||
and behaves like a styled `Container`. Every floating card, tooltip, and control in the
|
||||
redesign is one of these underneath.
|
||||
|
||||
**`FloatingPill`** — a `GlassPanel` shaped as a horizontally-centered, rounded-full
|
||||
capsule holding a row of labeled stat columns (see Route Planning's Distance/Est. Time/
|
||||
Pins header). Takes a list of `(label, value)` pairs and lays them out with vertical
|
||||
dividers between them, matching the Stitch export exactly.
|
||||
|
||||
**`PulsingLocationMarker`** — the expanding-ring-plus-glow-dot from the Plan/Route
|
||||
Planning exports. A `CustomPainter` or layered `AnimatedContainer`s driving an
|
||||
`AnimationController` in a loop (scale 0.8→1.0, opacity 0.8→0, repeating) — respect
|
||||
`MediaQuery.disableAnimations`/`prefers-reduced-motion` equivalent by falling back to a
|
||||
static dot when animations are disabled system-wide.
|
||||
|
||||
## Implementation
|
||||
1. `lib/src/ui/components/glass_panel.dart` — `GlassPanel`, with blur radius, fill
|
||||
opacity, and border opacity as named constants (not magic numbers scattered per call
|
||||
site), sourced from UI-08's theme tokens.
|
||||
2. `lib/src/ui/components/floating_pill.dart` — `FloatingPill`, built on `GlassPanel`.
|
||||
3. `lib/src/ui/components/pulsing_location_marker.dart` — `PulsingLocationMarker`,
|
||||
wrapping its `AnimationController` lifecycle correctly (dispose on unmount, same
|
||||
discipline as `RideMap`'s existing lifecycle observer from V3-04).
|
||||
4. Widget tests render each in isolation against both themes (if UI-08 keeps a light
|
||||
variant) to catch a black-on-black-style contrast regression early, the same
|
||||
discipline V3-16 established.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] `GlassPanel` renders a blurred, bordered, translucent container matching the
|
||||
Stitch reference visually
|
||||
- [ ] `FloatingPill` renders an arbitrary number of stat columns with dividers between
|
||||
them, not hardcoded to exactly three
|
||||
- [ ] `PulsingLocationMarker` animates continuously without leaking its
|
||||
`AnimationController` across widget rebuilds or disposal
|
||||
- [ ] Reduced-motion setting is respected by `PulsingLocationMarker`
|
||||
|
||||
## Tests
|
||||
- Widget: each component renders with representative content and takes a screenshot-
|
||||
comparable snapshot of its structure (no golden files per V3-16's precedent — assert
|
||||
structure/color, not pixels)
|
||||
- Widget: `PulsingLocationMarker`'s animation controller is disposed when the widget is
|
||||
removed from the tree (a `flutter_test` pending-timer/ticker check, mirroring the
|
||||
Drift stream-query keep-alive pattern already documented in `widget_test.dart`)
|
||||
|
||||
## Risks
|
||||
- `BackdropFilter` is one of the more expensive Flutter widgets to composite; stacking
|
||||
several `GlassPanel`s over a live, animating map (per UI-01/UI-02) could visibly cost
|
||||
frame time on lower-end devices. Worth a real-device check once UI-05 assembles them
|
||||
together, not just in isolation.
|
||||
|
||||
## Out of scope
|
||||
The customizable drag/resize telemetry widgets (UI-04) — those consume `GlassPanel` as
|
||||
their visual shell but the interaction logic is a separate, larger ticket.
|
||||
|
||||
## Outcome
|
||||
|
||||
All three values (blur radius, opacities, dimensions, colors) were lifted directly from
|
||||
the Route Planning export's own glass treatment and `pulse-ring` keyframes rather than
|
||||
guessed — `docs/design/stitch-export/screens/route-planning-dark.html`'s `bg-surface/90
|
||||
backdrop-blur-xl border border-outline-variant/30` and its `@keyframes pulse-ring`
|
||||
(scale 0.8→1.0, opacity 0.8→0, eased) map directly onto `GlassPanel.blurSigma`/
|
||||
`fillOpacity`/`borderOpacity` and `PulsingLocationMarker`'s animation curve.
|
||||
|
||||
`GlassPanel` (`lib/src/ui/components/glass_panel.dart`) is `ClipRRect > BackdropFilter >
|
||||
DecoratedBox`, taking `child`/`borderRadius`/`padding` and reading its fill/border colors
|
||||
from the theme (`colors.surface`, `colors.outlineVariant` — the latter newly added to
|
||||
`ripprColors` in `theme.dart` for this ticket, since UI-08 hadn't needed it before now).
|
||||
`FloatingPill` (`floating_pill.dart`) is a `GlassPanel` shaped as a stadium capsule
|
||||
around a `Row` of `PillStat` columns with 1px dividers between them — genuinely
|
||||
arbitrary-length, not hardcoded to three, verified by a 4-stat test case.
|
||||
`PulsingLocationMarker` (`pulsing_location_marker.dart`) layers a static faint ring, an
|
||||
animated expanding-and-fading ring, and a glowing center dot; it checks
|
||||
`MediaQuery.disableAnimations` in `didChangeDependencies` (not `initState`, which
|
||||
Flutter forbids for inherited-widget lookups) and stops the controller entirely when
|
||||
reduced motion is requested, falling back to a static dot.
|
||||
|
||||
**Test-writing bug caught and fixed:** the first version of the "legible in both
|
||||
themes" test pumped `ripprTheme()` then `ripprMountedTheme()` sequentially in a single
|
||||
`testWidgets` loop. Because both produced an identically-shaped widget tree
|
||||
(`MaterialApp > Scaffold > GlassPanel > Text`), Flutter reused the first pump's elements
|
||||
across the second `pumpWidget` call rather than rebuilding fresh, so the second
|
||||
iteration silently re-read the *first* theme's already-resolved paint color — the test
|
||||
would have falsely failed for a real theme-following bug and, worse, could have
|
||||
falsely passed one due to reading stale data. Split into two independent `testWidgets`
|
||||
blocks instead, which is the correct way to exercise two themes against the same
|
||||
component.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 336 tests (328 + 8 new in
|
||||
`test/glass_component_kit_test.dart`): `GlassPanel`'s blur/translucency/border
|
||||
structure and its resolved-paint-color contrast in both themes; `FloatingPill`'s
|
||||
divider count scaling with stat count (and the zero-divider single-stat case);
|
||||
`PulsingLocationMarker`'s continuous animation, correct `AnimationController` disposal
|
||||
on removal (caught automatically by `flutter_test`'s own ticker-leak check, not a
|
||||
manual timer assertion), and the reduced-motion fallback.
|
||||
|
||||
**Android emulator verification**: no consuming screen exists yet (that's UI-04/05/06),
|
||||
so verification used a throwaway preview entry point
|
||||
(`lib/main_ui03_preview.dart`, deleted after use) rendering all three components over a
|
||||
map-colored gradient background. All three matched the Stitch reference's look: the
|
||||
`FloatingPill`'s blur/translucency/dividers/blue values rendered crisply; the
|
||||
`PulsingLocationMarker`'s glow and ring were visible and animating. One red herring
|
||||
during this check: `GlassPanel`'s content briefly appeared to render illegibly when the
|
||||
preview used `Theme.of(context).textTheme.headlineSmall` for a heading, despite that
|
||||
style's `color` property independently verified (via a debug test) to already be the
|
||||
correct ink value with full alpha. Switching to an explicit inline `TextStyle` (no
|
||||
ambient text-theme role) rendered crisply instead. This was isolated to the scratch
|
||||
preview file, not `GlassPanel` itself — the shipped components' own tests all render
|
||||
content through explicit styles or the already-verified `bodyMedium` role, the same
|
||||
discipline every real screen in this codebase already follows — but it's worth flagging
|
||||
as a `headlineSmall`-specific rendering quirk to watch for if UI-05/06 lean on that
|
||||
particular text-theme role for real headings.
|
||||
|
||||
## Risks note (revisited)
|
||||
|
||||
The ticket's own risk — `BackdropFilter` compositing cost when several `GlassPanel`s
|
||||
stack over a live, animating map — was not measurable in this ticket's isolated preview
|
||||
(no map, no stacking). Deferred to UI-05, as the ticket itself anticipated, where the
|
||||
Record screen actually assembles multiple glass panels over `RideMap`.
|
||||
@@ -0,0 +1,197 @@
|
||||
# UI-04 — Customizable HUD telemetry widgets
|
||||
|
||||
**Depends on** UI-03 · **Size** L · **Status** Done
|
||||
|
||||
## Goal
|
||||
Let the rider drag each floating telemetry widget (Speed, Distance, etc.) to wherever
|
||||
they want it on screen, resize it, and choose in Settings which metrics appear live at
|
||||
all — a persisted, personal HUD layout rather than a fixed one.
|
||||
|
||||
## Context
|
||||
The Stitch Map HUD mockup has a fixed horizontal row of four telemetry cards with one
|
||||
interaction: tap a card to temporarily expand it, tap again (or tap another) to collapse
|
||||
it back. That's a reasonable default, but by explicit instruction the actual goal is
|
||||
further than that: **the rider owns the layout.** Hold-and-drag to reposition, resize by
|
||||
dragging a handle, and a Settings screen listing every available metric with a toggle for
|
||||
whether it's shown at all. This supersedes the mockup's tap-to-expand interaction rather
|
||||
than coexisting with it — see Design.
|
||||
|
||||
## Design
|
||||
|
||||
**Data model.** A `HudWidgetLayout` per metric:
|
||||
```
|
||||
{ metricId, x, y, width, height, visible }
|
||||
```
|
||||
`x`/`y`/`width`/`height` stored as fractions of the available HUD area (0.0-1.0), not
|
||||
absolute pixels, so a layout saved on one device/orientation still makes sense on
|
||||
another. `metricId` is a stable enum (`speed`, `avgSpeed`, `distance`, `elapsedTime`,
|
||||
`maxSpeed`, `elevationGain`, ...) — the same set `RecordUiState`/`Trip` already expose,
|
||||
not a new data source.
|
||||
|
||||
**Available metrics vs. visible metrics.** Every metric the app already tracks is a
|
||||
candidate; `visible` (toggled in Settings, see below) controls whether it currently has a
|
||||
HUD widget at all. A metric toggled off entirely has no position/size to speak of until
|
||||
turned back on, at which point it gets a sane default slot (not wherever it happened to
|
||||
be last, unless a position was already saved).
|
||||
|
||||
**Interaction:**
|
||||
- **Move:** long-press-and-drag anywhere on a widget's `GlassPanel` body. A brief haptic
|
||||
and a subtle scale-up on press-start signal "this is now grabbable," matching the
|
||||
weight of a real physical action rather than an accidental tap.
|
||||
- **Resize:** a small drag handle in one corner, visible only while a widget is in "edit
|
||||
mode" (see below), not in every-day use — a resize handle sitting on screen permanently
|
||||
during a live ride is visual noise the rider doesn't need mid-ride.
|
||||
- **Edit mode:** entered explicitly (e.g. a long-press anywhere on the HUD area that
|
||||
isn't a specific widget, or a dedicated "Edit HUD" toggle from Settings/a long-press on
|
||||
empty HUD space) rather than every telemetry widget always being draggable — an
|
||||
always-draggable widget risks an accidental drag mid-ride when the rider meant to tap
|
||||
it for something else. Exiting edit mode (tap "Done", or tap empty space) persists the
|
||||
layout.
|
||||
- **Constraints:** every widget clamps to stay fully within the safe HUD area on drag/
|
||||
resize end — never under the bottom nav bar, never off-screen, never smaller than a
|
||||
legibility floor (the number must stay readable) or larger than some sane ceiling.
|
||||
|
||||
**Settings integration.** A new "Live HUD stats" section: a checkbox/switch per available
|
||||
metric, controlling `visible`. Order in the list is stable (doesn't reflect current HUD
|
||||
position) so it's easy to scan.
|
||||
|
||||
**Persistence.** One `Config` field (JSON-encoded map of `metricId` → `HudWidgetLayout`),
|
||||
same pattern as every other `Config` preference in this app — see `mountedMode`,
|
||||
`crashReportingEnabled` for the shape to follow.
|
||||
|
||||
## Implementation
|
||||
1. `HudWidgetLayout` model + JSON encode/decode, with defaults for every known metric
|
||||
(a sensible starting grid, e.g. the Stitch mockup's horizontal row) so a fresh install
|
||||
has a working, if plain, HUD before the rider customizes anything.
|
||||
2. `Config.hudLayout` getter/setter, following the established `Config` pattern.
|
||||
3. `HudLayoutController` (or a `Riverpod` `StateNotifier`) holding the current layout in
|
||||
memory, seeded from `Config`, written back to `Config` on every edit-mode exit — not
|
||||
on every drag frame, to avoid hammering `SharedPreferences` mid-drag.
|
||||
4. `DraggableResizableHudWidget`: wraps a telemetry `GlassPanel`, handles the long-press-
|
||||
to-grab gesture, the resize handle, and the clamp-on-release logic.
|
||||
5. `HudEditOverlay`: the edit-mode chrome (resize handles, a "Done" affordance, maybe a
|
||||
subtle grid/snap guide) shown only while editing.
|
||||
6. Settings section: "Live HUD stats," one row per metric with a switch bound to
|
||||
`visible`.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] A telemetry widget can be dragged to a new position and the new position survives
|
||||
an app restart
|
||||
- [ ] A telemetry widget can be resized within sane min/max bounds
|
||||
- [ ] Dragging or resizing never leaves a widget partially off-screen or behind the nav
|
||||
bar
|
||||
- [ ] Toggling a metric off in Settings removes its HUD widget immediately; toggling it
|
||||
back on restores it (at its last saved position if one exists, otherwise a default)
|
||||
- [ ] Outside of edit mode, a normal tap on a telemetry widget does not move it
|
||||
(no accidental drags during a ride)
|
||||
- [ ] Layout is per-install (`Config`), not per-ride
|
||||
|
||||
## Tests
|
||||
- Unit: `HudWidgetLayout` JSON round-trips exactly; unknown/missing metric ids on load
|
||||
fall back to defaults rather than crashing
|
||||
- Unit: clamp logic — a drag/resize ending outside allowed bounds is corrected to the
|
||||
nearest valid position/size, exercised with fixed geometry inputs (no real gestures
|
||||
needed to test the math)
|
||||
- Widget: drag gesture moves a widget and the moved position is what gets persisted
|
||||
(simulate via `TestGesture`, matching how other drag interactions in this codebase are
|
||||
tested)
|
||||
- Widget: toggling a metric's Settings switch adds/removes its HUD widget
|
||||
- Widget: a tap outside edit mode does not trigger a move
|
||||
|
||||
## Risks
|
||||
- **Scope creep toward a general-purpose layout editor.** Keep the interaction minimal:
|
||||
drag, resize, toggle visibility. No z-ordering, no custom widget shapes, no per-metric
|
||||
color customization — those are separate future tickets if wanted, not this one.
|
||||
- Persisting on every drag frame would thrash `SharedPreferences`; persist only on
|
||||
edit-mode exit, keep in-memory state authoritative during an active drag.
|
||||
- This ticket removes the Stitch mockup's tap-to-expand interaction rather than layering
|
||||
on top of it — a widget the rider has manually resized should not also silently resize
|
||||
itself on tap, which would fight the rider's own choice.
|
||||
|
||||
## Out of scope
|
||||
Z-ordering/overlap resolution between widgets (widgets simply clamp to stay on-screen;
|
||||
overlapping each other is the rider's own choice to avoid). Per-metric colour
|
||||
customization. Sharing/exporting a HUD layout between devices.
|
||||
|
||||
## Outcome
|
||||
|
||||
Built the full generic infrastructure the ticket scopes, deliberately stopping short of
|
||||
wiring it into the real Record screen -- that assembly (real metric values from
|
||||
`RecordUiState`, replacing the fixed stats card) is UI-05's job, per the dependency
|
||||
graph. `HudMetric` (`lib/src/hud/hud_metric.dart`) is the stable 8-value enum;
|
||||
critically, the first four are ordered to match the Stitch mockup's fixed row exactly,
|
||||
since `HudWidgetLayout.defaultFor` derives both default grid position and which metrics
|
||||
start visible directly from enum index.
|
||||
|
||||
**Correction (UI-05):** this ticket originally ordered those first four as
|
||||
`speed, distance, elapsedTime, maxSpeed` -- a guess made without having read the actual
|
||||
Map HUD mockup HTML closely yet. Implementing UI-05 against that same HTML directly
|
||||
showed the real fixed row is Speed/**Avg Speed**/Dist/Time. The enum was reordered to
|
||||
`speed, avgSpeed, distance, elapsedTime, maxSpeed, movingTime, elevationGain,
|
||||
pointsCaptured` and every test here that had asserted the old order was updated
|
||||
alongside it -- see UI-05's own Outcome for the full account.
|
||||
|
||||
`HudWidgetLayout` (`hud_widget_layout.dart`) holds fractional `x/y/width/height` +
|
||||
`visible`, with `clamped()` enforcing a legibility floor and sane ceiling (size clamped
|
||||
first, then position re-clamped against the now-bounded size, so a resize that would
|
||||
push a widget off-screen shrinks it rather than silently relocating it) and
|
||||
`fromJson`/`defaultFor` degrading to sane defaults on any malformed or missing data
|
||||
rather than crashing Settings on launch. `Config.hudLayout`/`setHudLayout`
|
||||
(`config/config.dart`) follow the exact JSON-map-of-a-stable-key pattern already
|
||||
established there. `HudLayoutController` (a `StateNotifier`, `hud_layout_controller.dart`)
|
||||
holds the authoritative in-memory layout during an edit session and only calls
|
||||
`Config.setHudLayout` on `persist()` -- never per drag frame, the ticket's own named
|
||||
risk.
|
||||
|
||||
`DraggableResizableHudWidget` and `HudEditOverlay` (`lib/src/ui/components/`) are the
|
||||
interactive pieces. A widget only attaches a drag/resize `GestureDetector` at all when
|
||||
`editing` is true -- "a normal tap never moves a widget outside edit mode" is guaranteed
|
||||
structurally by the absence of a recognizer, not by an internal flag a future edit could
|
||||
weaken. Edit mode itself is entered by a long-press on *empty* HUD space and exited by
|
||||
"Done" or a tap on empty space, both handled by `HudEditOverlay`'s own background
|
||||
`GestureDetector`.
|
||||
|
||||
**Bug found and fixed during testing, not anticipated by the plan:** outside edit mode,
|
||||
`DraggableResizableHudWidget` initially attached no gesture detector at all, meaning a
|
||||
long-press *on a widget* was free to bubble up through the gesture arena to
|
||||
`HudEditOverlay`'s background long-press handler and wrongly enter edit mode from a
|
||||
touch that landed on a specific widget, not the empty area the design explicitly calls
|
||||
for ("a long-press anywhere on the HUD area *that isn't a specific widget*"). Fixed by
|
||||
giving the non-editing state a no-op `GestureDetector(onLongPress: () {})` -- an inner
|
||||
recognizer of the same gesture type wins the arena over the outer one, absorbing the
|
||||
press instead of letting it propagate. Caught by a widget test that intentionally
|
||||
dragged directly on a widget while not editing and asserted edit mode never engaged.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 365 tests (336 + 29 new,
|
||||
across `hud_widget_layout_test.dart`, `hud_layout_controller_test.dart`,
|
||||
`hud_edit_overlay_test.dart`, and additions to `config_test.dart`/
|
||||
`settings_screen_test.dart`) -- covering JSON round-trips and malformed-data fallback,
|
||||
every clamp edge case with fixed geometry (no gestures needed), the controller's
|
||||
in-memory-until-persist discipline, `TestGesture`-simulated long-press-drag actually
|
||||
moving and persisting a widget, and the Settings toggle writing through to `Config`
|
||||
immediately. One pre-existing-test collateral fix: adding 8 new `SwitchListTile`s
|
||||
pushed everything after them below the test viewport's initial fold (a plain
|
||||
`ListView(children:)` still lazily builds via a sliver, same as `.builder` -- an
|
||||
assumption several existing tests unknowingly depended on). Moved the new section to
|
||||
the very end of the list (after "About") so no earlier section's position changed, and
|
||||
added `scrollUntilVisible` to the two new tests that need to reach it.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`): used a throwaway preview
|
||||
entry point (`lib/main_ui04_preview.dart`, deleted after use) since no consuming screen
|
||||
exists yet. Confirmed on-device: the default four-card row renders exactly like the
|
||||
mockup; a long-press on a widget does nothing (the arena-fix above); a long-press on
|
||||
empty space enters edit mode, showing every resize handle and a "Done" pill; dragging a
|
||||
resize handle (a plain pan, not gated by long-press, so directly reproducible via `adb
|
||||
input swipe`) visibly grows a widget; tapping "Done" hides the edit chrome and keeps the
|
||||
new size. **Full end-to-end persistence was verified across a real process restart**,
|
||||
not just via the widget-test's in-memory assertions: resized Speed, tapped Done,
|
||||
force-stopped the app, relaunched it fresh, and the enlarged Speed widget was still
|
||||
enlarged. The long-press-*then*-drag move gesture itself could not be reproduced via
|
||||
`adb input swipe` (its linear interpolation moves throughout the whole gesture rather
|
||||
than holding still for the ~500ms long-press window first, so Flutter's arena resolves
|
||||
it as a rejected pan rather than a recognized long-press) -- that exact interaction is
|
||||
what the `TestGesture`-based widget test (which holds the pointer down, waits out
|
||||
`kLongPressTimeout`, then moves) verifies precisely, and is trusted as the ground truth
|
||||
for that specific gesture. Also confirmed via the real (non-preview) app that the new
|
||||
Settings section renders correctly alongside every existing section and that toggling
|
||||
"Average speed" flips its switch immediately.
|
||||
@@ -0,0 +1,169 @@
|
||||
# UI-05 — Map HUD: Record screen redesign
|
||||
|
||||
**Depends on** UI-01, UI-03, UI-04 · **Size** M · **Status** Done
|
||||
|
||||
## Goal
|
||||
Rebuild the Record screen as the fullscreen Map HUD from the Stitch export — this screen
|
||||
is the **design north star** for the whole redesign (see `README.md`): every other
|
||||
screen ticket restyles toward what this one establishes, not the other way around.
|
||||
|
||||
## Context
|
||||
Today's Record screen is a scrolling `Column`: a 220px map card, a stats `Card` below it,
|
||||
full-width Pause/Stop/Resume/Discard buttons, and a "RIPPR" wordmark header with Rides/
|
||||
Settings/Routes buttons. The redesign inverts this entirely — the map is the full-bleed
|
||||
canvas (via UI-01's shared background, at full opacity/interactivity on this tab only),
|
||||
and everything else floats on top of it via `GlassPanel`/HUD widgets.
|
||||
|
||||
## Design
|
||||
- **Header:** none, per UI-01.
|
||||
- **Telemetry:** `DraggableResizableHudWidget`s from UI-04, not a fixed row — this screen
|
||||
is where those widgets actually live during a ride. Default layout mirrors the Stitch
|
||||
mockup's horizontal row (Speed/Avg Speed/Dist/Time) until the rider customizes it.
|
||||
- **Pause/Stop:** two icon-only, half-width, full-bleed buttons directly above the bottom
|
||||
nav bar, matching the mockup exactly — `tertiary-container` (pause) and
|
||||
`error-container` (stop) per UI-08's palette. Resume/Discard: the mockup doesn't show
|
||||
these explicitly on this screen; keep them reachable (e.g. Discard behind a
|
||||
confirmation from the paused state, matching today's existing safeguard against a
|
||||
gloved mis-tap next to Pause) rather than dropping the affordance V3-05/the original
|
||||
port already earned.
|
||||
- **Route rendering:** both the planned route (dashed, neutral `outline` color) and the
|
||||
ridden-so-far path (colored by the existing speed gradient from `RideMap`) drawn
|
||||
simultaneously, matching the mockup — this is new: today only the ridden path renders
|
||||
live.
|
||||
- **Location marker:** `PulsingLocationMarker` (UI-03) replaces the current plain dot.
|
||||
- **Mounted mode (V3-05) interaction with this redesign:** the high-contrast mounted
|
||||
theme and larger touch targets still apply, layered on top of this screen's new
|
||||
layout, not replaced by it — confirm the mounted theme's colors still read correctly
|
||||
against `GlassPanel`'s blur (a light theme through blurred content behaves differently
|
||||
than the current dark-on-dark case V3-05 was built against).
|
||||
|
||||
## Implementation
|
||||
1. Rebuild `RecordScreen`'s body against UI-01's full-opacity Map tab background instead
|
||||
of an embedded `RideMap` card.
|
||||
2. Replace the current `StatRow`-based stats card with UI-04's HUD widgets, wired to the
|
||||
same `RecordUiState`/live telemetry stream already powering the old layout — no new
|
||||
data plumbing, only new presentation.
|
||||
3. Rebuild `_Controls`/`_PrimaryButton`/`_SecondaryButton` as the two full-bleed icon
|
||||
buttons; keep the existing state-machine logic (`RecordUiState.isIdle/isRecording/
|
||||
isPaused`) driving which controls show, per the current `_Controls` widget.
|
||||
4. Wire both route paths (planned + ridden) into the shared background map's overlay,
|
||||
sourced from `RoutePlanRepository` (if a route is being followed — V3-09 groundwork,
|
||||
not required for this ticket if no route is active) and the existing live-points
|
||||
stream.
|
||||
5. Re-verify V3-05's mounted-mode contrast/legibility guarantees against the new
|
||||
`GlassPanel`-based layout specifically, since that's a real visual change from the
|
||||
opaque `Card` mounted mode was built and tested against.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] Map fills the screen behind the HUD widgets and controls, full opacity, on this
|
||||
tab
|
||||
- [ ] Existing record/pause/resume/stop/discard state machine behavior is unchanged —
|
||||
this is a visual rebuild, not a behavior change
|
||||
- [ ] HUD telemetry widgets are the UI-04 draggable/resizable kind, not a fixed layout
|
||||
- [ ] Mounted mode (V3-05) still passes its existing contrast tests against the new
|
||||
layout
|
||||
- [ ] The black-on-black text-color regression guard (from V3-16/the original port bug)
|
||||
still passes against `GlassPanel`'s blur background
|
||||
|
||||
## Tests
|
||||
- All existing `record screen` widget tests updated to the new layout and still passing
|
||||
(state-machine behavior, mounted-mode toggle, wake lock, live-map visibility)
|
||||
- Widget: HUD widgets render over the map, not replacing it
|
||||
- Widget: Pause/Stop buttons trigger the same engine calls as today
|
||||
|
||||
## Risks
|
||||
- The biggest visual departure of the whole redesign happens here first — expect this
|
||||
ticket to surface layout issues (`GlassPanel` blur cost stacked with a live animating
|
||||
map, HUD widgets overlapping controls on small screens) that UI-06/07 then inherit or
|
||||
avoid having created themselves.
|
||||
|
||||
## Out of scope
|
||||
Following a planned route turn-by-turn (V3-09). Anything about Plan/Route Planning/Rides
|
||||
History screens — those are UI-06/UI-07, restyled toward what this ticket establishes.
|
||||
|
||||
## Outcome
|
||||
|
||||
Rebuilt `RecordScreen`'s body entirely against UI-01's shared background map, keeping
|
||||
every line of the existing state-machine logic (the ticker, wakelock handling, speed
|
||||
subscription, `_guard`/error handling) completely untouched -- only the returned widget
|
||||
tree changed. The idle state keeps its own compact layout (a single `GlassPanel` with
|
||||
`BigStat` speed + "Ready"/"See Rides", exactly the old content, just re-skinned) rather
|
||||
than switching to HUD widgets while there's no ride to show numbers for -- deliberately
|
||||
preserving the pre-existing "a resting screen must not look like a ride going nowhere"
|
||||
guarantee rather than reinterpreting it. Once a ride exists (recording or paused),
|
||||
`HudEditOverlay` (UI-04) takes over, wired to a new `_HudMetricValue` that reads
|
||||
`RecordUiState`/`UnitSystem` and renders through `HudMetric`'s existing set -- no new
|
||||
data plumbing, exactly as the ticket's Implementation section asked for. Added
|
||||
`RecordUiState.avgSpeedKmh` (distance-over-moving-time, zero before any movement) since
|
||||
that metric didn't previously exist anywhere in the app.
|
||||
|
||||
**Colour convention decision:** the Map HUD mockup colours its four default metrics
|
||||
individually (Speed → primary, Distance → secondary, Avg Speed/Time → plain ink) rather
|
||||
than following this app's usual reference-vs-live split. `_HudMetricValue` matches the
|
||||
mockup literally rather than inventing a new convention, since the mockup is this
|
||||
ticket's explicit design source for exactly this screen.
|
||||
|
||||
**Correction found and fixed while implementing this ticket:** UI-04's `HudMetric` enum
|
||||
ordered its "first four, default-visible" metrics as Speed/Distance/Elapsed/Max Speed --
|
||||
a guess made before the actual Map HUD mockup HTML had been read closely. Reading
|
||||
`docs/design/stitch-export/screens/map-hud-dark.html`'s own `#telemetry-container`
|
||||
directly for this ticket showed the real fixed row is Speed/**Avg Speed**/Dist/Time.
|
||||
Reordered the enum to match and updated every UI-04 test that asserted the old order
|
||||
(five call sites across `hud_widget_layout_test.dart`, `hud_edit_overlay_test.dart`, and
|
||||
`settings_screen_test.dart`) -- a real, disclosed correction, not a silent one.
|
||||
|
||||
**Control bar:** `_ControlBar`/`_Segment` render the full-bleed, icon-only bar the
|
||||
mockup specifies for Pause (`tertiaryContainer`/`onTertiaryContainer`) and Stop
|
||||
(`errorContainer`/`onErrorContainer`) -- both added explicitly to `ripprColors` in this
|
||||
ticket, since `ColorScheme.dark`'s auto-derived tonal palette wouldn't have matched the
|
||||
design system's literal hex values otherwise. Idle keeps a labelled "START RECORDING"
|
||||
segment (an icon alone risked ambiguity for a rider's very first, highest-stakes tap,
|
||||
a deliberate deviation from strict icon-only fidelity). Paused extends the same visual
|
||||
language into three segments (Resume 50%, Stop 25%, Discard 25%) since the mockup itself
|
||||
doesn't depict a paused state — Discard stays behind this dedicated slot, reachable only
|
||||
while paused, preserving the existing gloved-mis-tap safeguard from V3-05 rather than
|
||||
dropping it.
|
||||
|
||||
**Location marker:** `RideMap` gained a `showLocationMarker` bool; when true and points
|
||||
exist, a `MarkerLayer` places `PulsingLocationMarker` (UI-03) at the latest point.
|
||||
`ShellScaffold` passes `showLocationMarker: isMapTab` so the animation only ticks on the
|
||||
tab where it's actually visible.
|
||||
|
||||
**Out of scope, confirmed at implementation time, not just planned:** the ticket allows
|
||||
skipping planned-route rendering when V3-09 (route-following) doesn't exist yet, and it
|
||||
doesn't -- there is no "route currently being followed" concept anywhere in the app to
|
||||
source a planned-route polyline from. Left unimplemented rather than fabricating a data
|
||||
source; `RideMap`'s existing ridden-path speed-gradient polyline is unaffected and still
|
||||
renders live.
|
||||
|
||||
**Mounted mode re-verified against `GlassPanel` specifically**, per the ticket's own
|
||||
named risk (mounted mode was built and tested against an opaque `Card`, not a blurred
|
||||
translucent surface). Added a test asserting the actual contrast ratio of the mounted
|
||||
speed digit against `GlassPanel`'s mounted-theme surface color (not just "differs from
|
||||
the wrong, dark-theme ground" the way the pre-existing test checked) -- passes at
|
||||
roughly 4.5:1+ AA with no colour changes needed.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 369 tests. Every pre-existing
|
||||
`record screen` test passed unchanged against the rebuilt screen with zero edits needed
|
||||
-- the idle-state structural parity was deliberate and it paid off. Added: two tests that
|
||||
actually tap Start/Pause/Stop and verify the underlying trip state changes (not just key
|
||||
presence, which the pre-existing tests already covered), one confirming HUD widgets
|
||||
render over the shared background map rather than replacing it, and the mounted-mode/
|
||||
GlassPanel contrast test above.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`) exercised the full state
|
||||
machine live on-device: Start → Pause → Resume → Stop, confirmed visually correct at
|
||||
each transition (colours, icon changes, the "Paused" pill appearing/disappearing, the
|
||||
HUD widgets updating), with the full-bleed live map behind everything on the Map tab
|
||||
exactly matching the design intent. This surfaced and resolved a real debugging episode
|
||||
worth recording: initial manual taps on the control bar appeared to do nothing across
|
||||
many attempts (varied coordinates, map on/off, a full emulator+host reboot, a Gradle-
|
||||
daemon memory-pressure cleanup, and a `Container`→`Ink` widget refactor along the way).
|
||||
The eventual root cause was mundane and entirely on the verification side, not the
|
||||
app: the button's true on-screen position was being mis-estimated from a scaled-down
|
||||
screenshot preview, roughly 500px off from its actual location. Sampling pixel colours
|
||||
directly from the raw screenshot file (via PIL) to find the button's exact bounds
|
||||
resolved it immediately, and every interaction has worked correctly since. The emulator
|
||||
reboot and Gradle-daemon stop were not the fix, but were a reasonable, low-risk
|
||||
housekeeping step taken while investigating and are left in place as a genuine
|
||||
improvement to the session's remaining build performance.
|
||||
@@ -0,0 +1,170 @@
|
||||
# UI-06 — Plan & Route Planning redesign
|
||||
|
||||
**Depends on** UI-01, UI-03 · **Size** M · **Status** Done
|
||||
|
||||
## Goal
|
||||
Rebuild the Plan tab (empty state) and the route planner (pins dropped) as fullscreen
|
||||
map canvases with floating controls — correct in principle per the Stitch mockups, but
|
||||
**restyled to match Map HUD (UI-05), not copied as-is** — see `README.md`'s note on why
|
||||
these two mockups drifted from the design north star.
|
||||
|
||||
## Context
|
||||
Today's `RoutePlannerScreen` is a `Scaffold` with an `AppBar` (title, rename/delete/
|
||||
download actions) and the map beneath it. The mockups drop the header entirely (per
|
||||
UI-01) and float everything — a pin-drop tooltip, a stat summary pill, a Start Route
|
||||
button — over a fullscreen map, with a bottom nav bar instead of an app bar's back
|
||||
button (the planner becomes a screen reached *within* the Plan tab, not a separately
|
||||
titled pushed screen).
|
||||
|
||||
**What to take from the mockups as-is:** the fullscreen-map-plus-floating-controls
|
||||
pattern, the pin-drop ripple micro-interaction, `PulsingLocationMarker` for the rider's
|
||||
own position, the marching-ants animated route line, the floating stat pill
|
||||
(Distance/Est. Time/Pins) replacing today's pre-download confirmation dialog as a
|
||||
persistent readout instead of a one-time modal.
|
||||
|
||||
**What to restyle rather than copy:** icon fill-states, the nav bar's exact background/
|
||||
blur treatment, and any color choice that doesn't match UI-08's actual palette as
|
||||
established on the Map HUD screen. The Route Planning mockup specifically shows the
|
||||
"Rides" nav icon active while viewing Plan — a generation error, not a real cross-tab
|
||||
indicator; the shipped nav bar must correctly highlight whichever tab is actually active,
|
||||
matching Map HUD's nav bar exactly (same widget, in fact — see UI-01).
|
||||
|
||||
## Design
|
||||
- **Plan tab (no pins yet):** fullscreen map, technical grid overlay, floating "Tap to
|
||||
drop a pin" tooltip (gentle float animation), `PulsingLocationMarker`.
|
||||
- **Route Planning (pins dropped):** same canvas, plus the floating stat pill (`FloatingPill`
|
||||
from UI-03) showing Distance / Est. Time / Pin count, updating live as pins are added/
|
||||
moved/removed (already true of the underlying `RoutePlanRepository` data — this ticket
|
||||
is presentation only). "Start Route" as a floating pill-shaped button above the nav
|
||||
bar, replacing today's app-bar-driven actions.
|
||||
- **Existing actions (rename, delete, download offline tiles)** need a new home now that
|
||||
there's no app bar to hang icon buttons off of — a small floating overflow/glass menu
|
||||
button is the natural fit, styled per Map HUD's `GlassPanel` language, not invented
|
||||
fresh.
|
||||
- **Drag-to-move / tap-to-delete pins:** unchanged interaction from today's
|
||||
`RoutePlannerScreen`; only the chrome around it changes.
|
||||
|
||||
## Implementation
|
||||
1. Rebuild `RoutePlannerScreen`'s `Scaffold`/`AppBar` as a headerless canvas within the
|
||||
Plan tab's branch navigator (per UI-01), map at full opacity/interactivity here
|
||||
(unlike the dimmed treatment other tabs get for the *background* map — this screen's
|
||||
map *is* the primary content, same as Map HUD's).
|
||||
2. Replace the pre-download confirmation dialog (V3-11) with the persistent `FloatingPill`
|
||||
stat summary; keep the actual download flow (count/size check, cancellable progress)
|
||||
as-is underneath — presentation change only.
|
||||
3. Add the pin-drop ripple micro-interaction and marching-ants route line as new,
|
||||
currently-nonexistent polish.
|
||||
4. Rebuild the Plan tab's empty state (no `RoutePlan` selected/created yet) as the
|
||||
fullscreen tooltip-and-map view, wired to whatever "create a new route" entry point
|
||||
replaces today's `RoutesListScreen` FAB (see UI-07 — Rides History's tab likely
|
||||
absorbs or sits alongside a route list; confirm final IA when that ticket lands, since
|
||||
"Rides" and "Plan" may end up sharing more list/history UI than the current
|
||||
`RoutesListScreen`/`TripsScreen` split assumes).
|
||||
5. Move rename/delete/download actions to a floating glass menu, styled per Map HUD.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] Both Plan states (empty, pins dropped) render as fullscreen map canvases with no
|
||||
header
|
||||
- [ ] The nav bar on this screen is the exact same widget/styling as Map HUD's, with the
|
||||
Plan tab correctly shown active (not the generation-error "Rides active" state
|
||||
from the mockup)
|
||||
- [ ] Existing pin add/move/delete, rename, delete-route, and offline-download behavior
|
||||
all still work, restyled only
|
||||
- [ ] Icon fill-states and colors match Map HUD, not the Stitch export's drifted values
|
||||
|
||||
## Tests
|
||||
- All existing `route_planner_screen_test.dart` / `route_plan_repository_test.dart`
|
||||
behavior tests still pass against the new presentation layer
|
||||
- Widget: nav bar active-tab indicator is correct on this screen
|
||||
- Widget: the floating stat pill updates live as pins are added/removed (already covered
|
||||
conceptually by existing repository tests; add a presentation-level assertion that the
|
||||
pill reflects it)
|
||||
|
||||
## Risks
|
||||
- The IA question flagged in Implementation step 4 (how "create a new route" is reached
|
||||
without a `RoutesListScreen`-style list-with-FAB) may turn out to need its own small
|
||||
design decision once UI-07 is underway — don't block this ticket on it if the existing
|
||||
entry point can be preserved with new styling in the meantime.
|
||||
|
||||
## Out of scope
|
||||
Road-snapped routing (V3-08/V3-09, deferred). Any change to the underlying route-planning
|
||||
data model or repository — this is presentation only.
|
||||
|
||||
## Outcome
|
||||
|
||||
**IA question (Risk section) resolved by deferring, as the ticket explicitly allowed:**
|
||||
`RoutesListScreen` stays a list-with-`+`-button (the existing entry point), restyled with
|
||||
`GlassPanel`-wrapped rows instead of plain `Card`s. The mockup's own "fullscreen map +
|
||||
tooltip" language turned out, on close reading, to describe `RoutePlannerScreen`'s
|
||||
*own* empty-pins state (the tooltip literally instructs "tap to drop a pin" — an action
|
||||
that happens on the canvas, not on a list of named routes), not the routes list. Both
|
||||
screens got the restyle their actual role calls for.
|
||||
|
||||
`RoutePlannerScreen` is now a headerless, full-opacity map canvas (its own map, not
|
||||
UI-01's dimmed shell background — this screen's map *is* the primary content, matching
|
||||
Map HUD). Implemented: the floating "TAP TO DROP A PIN" tooltip (`GlassPanel`, gentle
|
||||
3s float) shown only while `waypoints.isEmpty`; `FloatingPill` (UI-03) showing Distance/
|
||||
Est. Time/Pins once pins exist, replacing the old pre-download confirmation dialog's
|
||||
role as the primary stat display (the confirmation dialog itself is untouched --
|
||||
presentation only, per scope); a "START ROUTE" floating pill button; a `GlassPanel`
|
||||
overflow menu (`PopupMenuButton`) holding Rename/Download offline tiles/Delete route,
|
||||
replacing the removed `AppBar`'s actions row; and a pin-drop ripple micro-interaction
|
||||
(a short-lived expanding-and-fading ring at the tap's local screen position).
|
||||
|
||||
**"Start Route," given real, working behaviour rather than shipped as a dead button:**
|
||||
turn-by-turn following (V3-09) doesn't exist anywhere in the app to wire this to, and
|
||||
inventing that data model here would violate this ticket's own "presentation only"
|
||||
scope. Tapping it starts an ordinary recording and switches to the Map tab -- a real
|
||||
action a rider can use today, not a placeholder.
|
||||
|
||||
**Est. Time honestly shows "--"** when `RoutePlan.estimatedMillis` is null (always, until
|
||||
V3-08 adds road-snapped routing) rather than fabricating a straight-line estimate the
|
||||
model doesn't back.
|
||||
|
||||
**Marching-ants route-line animation was deliberately dropped from scope.** It isn't in
|
||||
this ticket's acceptance criteria (only the pin-drop ripple is named there), and
|
||||
implementing it properly means animating a dash phase while continuously reprojecting
|
||||
through `MapCamera.latLngToScreenOffset` under live pan/zoom -- real complexity for a
|
||||
purely decorative effect. The existing static dashed polyline (already "visibly distinct
|
||||
from a recorded path," the actual acceptance-relevant property) is unchanged.
|
||||
|
||||
**Bug found and fixed, unrelated to this ticket's own changes but discovered while
|
||||
working in this file:** `_DownloadDialog`'s tile fetch still hardcoded
|
||||
`https://tile.openstreetmap.org/...` directly -- a leftover from before UI-09 switched
|
||||
the live map to CARTO's dark tiles. Downloading offline tiles would have silently cached
|
||||
OSM's tan tiles under the same `(z, x, y)` keys UI-09's cache versioning was specifically
|
||||
designed to keep separate from CARTO's, meaning a "successful" download would never
|
||||
actually serve anything to the live dark map. Fixed to build the URL from the same
|
||||
`tileUrlTemplate`/`tileSubdomains` every other tile fetch in the app already uses.
|
||||
|
||||
**Nav bar correctness:** no separate implementation needed -- `RoutePlannerScreen` is
|
||||
pushed *within* the Plan branch's own navigator (UI-01's `StatefulShellBranch`
|
||||
architecture), so the shell's persistent nav bar already shows Plan active automatically,
|
||||
structurally ruling out the mockup's own "Rides active while viewing Plan" generation
|
||||
error. Added a test asserting this directly rather than trusting the architecture alone.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 370 tests. Updated every
|
||||
existing `route_planner_screen_test.dart` assertion that referenced now-removed `AppBar`
|
||||
structure (the `IconButton` keys are `PopupMenuItem`s behind the overflow menu now; there
|
||||
is no app-bar title to assert renaming against, so that test now reads the repository
|
||||
directly instead) plus one real test bug fixed along the way: two close-together
|
||||
waypoints in the "tapping a pin deletes it" test happened to place one pin directly
|
||||
under the new floating "Start Route" button in that test's fixed camera geometry, so the
|
||||
tap intended for the pin actually hit the button underneath it (confirmed via
|
||||
`WidgetController`'s own hit-test-mismatch warning, not assumed) -- reduced to a single
|
||||
waypoint, which the camera centres exactly on, clear of the button. `PopupMenuButton`/
|
||||
`PopupMenuItem` interactions needed bounded pumps rather than `pumpAndSettle`, same
|
||||
reason the map's own tests already avoid it (a live `TileLayer` never goes idle in this
|
||||
harness). Also fixed a real, unrelated bug the new `GlassPanel`-wrapped `ListTile` in
|
||||
`RoutesListScreen` surfaced immediately: Flutter's own framework assertion that a
|
||||
`ListTile` needs a `Material` ancestor to paint its ink splash correctly, which
|
||||
`GlassPanel`'s `DecoratedBox` was sitting between it and -- fixed with a
|
||||
`Material(type: MaterialType.transparency)` wrapper.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`): confirmed the empty-tooltip
|
||||
state, dropping a pin (ripple visible, `FloatingPill` and "Start Route" appearing
|
||||
immediately), and the overflow menu opening with all three actions -- all matching the
|
||||
mockup and the Design section precisely. The Plan tab's nav-bar pill correctly followed
|
||||
the active tab (a mid-transition-animation screenshot briefly looked wrong immediately
|
||||
after tapping the tab; a screenshot taken a moment later confirmed it settles correctly
|
||||
-- a timing artifact of screenshotting mid-animation, not a real bug).
|
||||
148
rippr-flutter-src/docs/ui-redesign/UI-07-rides-history.md
Normal file
148
rippr-flutter-src/docs/ui-redesign/UI-07-rides-history.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# UI-07 — Rides History redesign
|
||||
|
||||
**Depends on** UI-01, UI-03 · **Size** M · **Status** Done
|
||||
|
||||
## Goal
|
||||
Rebuild the Trips list as "Rides History": a dimmed live map background (per UI-01, not
|
||||
a static image), a search bar, and richer ride cards with a real route-thumbnail map —
|
||||
restyled to Map HUD's (UI-05) established colors/icons, not the Stitch export's mockup
|
||||
styling.
|
||||
|
||||
## Context
|
||||
Today's `TripsScreen` is a plain `ListView` of `_TripTile`s (icon, name, one-line
|
||||
subtitle) over a solid background, reached by pushing off Record. The mockup adds a
|
||||
search bar, a filter button, a summary-stats row, and cards with an actual map-thumbnail
|
||||
image per ride. By explicit instruction, the background here is the same live map every
|
||||
other tab has (dimmed), not the mockup's static blurred screenshot — see UI-01.
|
||||
|
||||
**Generation artifact to fix, not copy:** the mockup's summary row is four identical
|
||||
"Total Dist: 482 KM" cards. This ticket defines real, distinct summary stats.
|
||||
|
||||
## Design
|
||||
- **Background:** UI-01's shared dimmed map, same as Settings and Plan get when not the
|
||||
active-content tab.
|
||||
- **Search + filter:** a text field over ride name/date, plus a filter affordance (by
|
||||
activity type — V3-01 already has this data — and/or date range). Filtering logic is
|
||||
new; nothing today searches or filters the trips list.
|
||||
- **Summary row:** real distinct stats — total distance, total moving time, ride count,
|
||||
and one more genuinely useful figure (e.g. average speed across all rides, or longest
|
||||
ride) rather than repeating one number four times. Computed from existing `Trip`
|
||||
aggregate columns already stored on every completed ride — no new data needed, only a
|
||||
query across all of them.
|
||||
- **Ride cards:** title, date, and a distance/time/avg-speed stat row (matching the
|
||||
mockup), plus a **real map thumbnail** — a small, non-interactive `RideMap` (or a
|
||||
lightweight static rendering of the stored path) rather than a stock image, since we
|
||||
actually have the ride's own path data, unlike the mockup's placeholder photos.
|
||||
- **Colors/icons:** Map HUD's palette and icon fill-states, per the design north star —
|
||||
not the mockup's card-specific styling choices (e.g. the third card's grayscale/
|
||||
reduced-opacity treatment, which reads as an arbitrary "older ride" decoration with no
|
||||
defined rule — drop it unless a real rule for it is specified).
|
||||
|
||||
## Implementation
|
||||
1. Rebuild `TripsScreen`'s background to sit over UI-01's dimmed shared map instead of a
|
||||
plain scaffold background.
|
||||
2. Add search (text query against `Trip.name`/date) and filter (activity type at
|
||||
minimum, reusing V3-01's `Activity` enum and `activityIcon`/`activityLabel` helpers).
|
||||
3. Add a summary-stats query/provider computing totals across `watchCompletedTrips()`
|
||||
(or a dedicated aggregate query if computing it client-side over a large ride history
|
||||
becomes a real cost — measure before optimizing).
|
||||
4. Rebuild `_TripTile` as a card with a small live/static `RideMap` thumbnail (reusing
|
||||
the existing `RideMap` widget at a small size and non-interactive, rather than
|
||||
building a second map-rendering path) plus the distance/time/avg-speed row.
|
||||
5. Confirm this screen's tab identity/name in the nav bar — "Rides" (mockup's label) vs.
|
||||
"History" vs. keeping "Rides" as used elsewhere in the codebase already (V3-07's
|
||||
Routes screen already uses "Rides" as the trips-tab label on the record screen) —
|
||||
match whatever UI-01's nav bar ships with.
|
||||
|
||||
## Acceptance criteria
|
||||
- [x] Background is the shared live map (dimmed), not a static image
|
||||
- [x] Search narrows the list by name/date
|
||||
- [x] Filter narrows the list by activity type
|
||||
- [x] Summary row shows four distinct, real statistics, not one number repeated
|
||||
- [x] Each ride card shows an actual thumbnail of that ride's own path, not a stock photo
|
||||
- [x] Merge/delete/rename actions from today's `TripsScreen` still work
|
||||
- [x] Colors and icon fill-states match Map HUD, not the Stitch export's card styling
|
||||
|
||||
## Tests
|
||||
- Widget: search filters the visible list correctly
|
||||
- Widget: filter-by-activity narrows correctly
|
||||
- Unit: summary-stats aggregation matches a hand-computed total over a fixed set of
|
||||
seeded trips
|
||||
- All existing `trips list` widget tests (empty state, newest-first ordering, active-ride
|
||||
exclusion, merge-at-exactly-two-selections, delete) updated to the new layout and still
|
||||
passing
|
||||
|
||||
## Risks
|
||||
- Rendering a small `RideMap` thumbnail per card in a long list could be a real
|
||||
performance cost (many simultaneous `FlutterMap` instances) — consider a lightweight
|
||||
static polyline-on-canvas rendering instead of a full interactive map per thumbnail if
|
||||
this proves too expensive; measure with a real ride history of realistic length before
|
||||
deciding.
|
||||
|
||||
## Out of scope
|
||||
Any change to trip data, merge, or split logic (V3-10) — presentation and search/filter
|
||||
only.
|
||||
|
||||
## Outcome
|
||||
|
||||
`TripsScreen` is now a headerless screen (per UI-01) over the shared dimmed background
|
||||
map, with a `GlassPanel`-wrapped search bar (`ride-search`) plus a `PopupMenuButton`
|
||||
activity filter (`activity-filter`), a four-card distinct summary row, and `_TripCard`s
|
||||
carrying a real 88x88 `RideMap` thumbnail of that ride's own recorded path.
|
||||
|
||||
**`RideHistorySummary`** is a plain pure function (`RideHistorySummary.compute(List<Trip>)`),
|
||||
not a Riverpod provider — folding over an already-fetched list is cheap regardless of ride
|
||||
count, and being a pure function is what made it directly unit-testable against a
|
||||
hand-computed total, per the ticket's own Tests section. Its average speed is
|
||||
distance-weighted across the whole history (not an average of each ride's own average),
|
||||
so a handful of long rides isn't drowned out by many short ones. The summary reflects the
|
||||
full unfiltered history, not the currently-searched/filtered subset — it's an overview
|
||||
stat, not a count of what's visible below it.
|
||||
|
||||
**Bug found and fixed while wiring the thumbnails:** the shared `RideMap` widget
|
||||
unconditionally rendered `TileAttribution` (added in UI-09) inside every `FlutterMap`,
|
||||
including tiny 88x88 list thumbnails — both a real UX problem (attribution controls
|
||||
cluttering dozens of small thumbnails) and an actual test collision (`find.byType(Icon)`
|
||||
scoped to one trip's keyed subtree started matching the attribution button's own icon
|
||||
too, once thumbnails were added). Fixed by adding a `showAttribution` bool parameter to
|
||||
`RideMap` (default `true`, so every existing full-size map call site — Map tab
|
||||
background, Route Planner, Trip Detail — is unaffected) and passing `showAttribution:
|
||||
false` specifically for `_TripCard`'s thumbnail.
|
||||
|
||||
**The same `GlassPanel`-needs-`Material`-ancestor bug UI-06 already hit** recurred
|
||||
identically in `_TripCard` (rebuilt from `StatelessWidget` to `ConsumerWidget` to watch
|
||||
the per-trip point/segment streams for its thumbnail) and was fixed the same way:
|
||||
`Material(type: MaterialType.transparency)` wrapping the `InkWell`.
|
||||
|
||||
**Rename is correctly out of scope for this file**: it lives on `TripDetailScreen`
|
||||
(`_rename`), not the list, and was never touched here — the ticket's "merge/delete/rename
|
||||
still work" line is satisfied by leaving Trip Detail's own rename alone while restyling
|
||||
only the list's merge/delete selection toolbar.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at **374 tests** (up from 370),
|
||||
adding: a widget test that search narrows the list to matching rides, a widget test that
|
||||
filtering by activity narrows the list, and two unit tests for `RideHistorySummary.compute`
|
||||
(a hand-computed multi-trip total, and the empty-history zero-division guard) in a new
|
||||
`test/ride_history_summary_test.dart`. Two pre-existing `widget_test.dart` assertions
|
||||
needed updating for the new card structure: `'an active ride does not appear in the
|
||||
list'` asserted `find.byType(Card)` (now `find.byType(RideMap)`, unique to a trip card
|
||||
since `_SearchAndFilter`/`_SummaryStatCard` don't render one), and the icon-count
|
||||
collision above resolved once attribution was suppressed on thumbnails.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`): confirmed end-to-end after
|
||||
working around significant, unrelated host resource pressure during this session (the
|
||||
emulator repeatedly hit System UI/app ANRs under low host memory; resolved by freeing
|
||||
host RAM, a full emulator restart, and a `force-stop`+relaunch of the app — see session
|
||||
notes, not an app defect). Verified on-device: search field narrows the list correctly
|
||||
(a query matching neither ride's name/date shows "No rides match your search.", clearing
|
||||
it restores both); the activity filter's `PopupMenuButton` opens with all seven
|
||||
activities plus "All activities", correctly highlights the currently-active choice,
|
||||
narrows to zero rides when filtered to an activity neither seeded ride has (Bicycle),
|
||||
and shows both again when filtered to the activity they actually have (Motorcycle); the
|
||||
four summary cards (Total Dist/Total Time/Rides/Avg Speed) render as genuinely distinct
|
||||
values, not the mockup's four-identical-cards artifact; both ride cards render real,
|
||||
distinct polyline thumbnails with no attribution icon visible; long-pressing a card
|
||||
enters selection mode with the Cancel/count/Merge/Delete toolbar, Merge correctly stays
|
||||
disabled at one selection and enables at exactly two. Colors/icons match Map HUD's
|
||||
established palette (translucent `GlassPanel` cards, blue accent, same activity icon set)
|
||||
throughout.
|
||||
@@ -0,0 +1,160 @@
|
||||
# UI-08 — Theme migration to Modern Professional Dark
|
||||
|
||||
**Depends on** nothing · **Size** S · **Status** Done
|
||||
|
||||
## Goal
|
||||
Replace `theme.dart`'s current safety-orange palette with "Modern Professional Dark" —
|
||||
the design system actually backing every fetched Stitch screen — so every screen ticket
|
||||
in this set has one settled palette to build against instead of guessing or patching
|
||||
colors per screen.
|
||||
|
||||
## Context
|
||||
V3-16 (last shipped) deliberately kept and refined the original safety-orange-on-warm-
|
||||
charcoal identity, with an instrument-blue tertiary for reference readings. That work is
|
||||
good and tested, but it is **not** the palette the Stitch mockups use — confirmed by
|
||||
hex-matching every color in the four fetched screens' HTML against all three design
|
||||
systems in the Stitch project (`docs/design/stitch-export/README.md`). If this redesign
|
||||
ships, V3-16's direction is superseded, not extended.
|
||||
|
||||
Full spec: `docs/design/stitch-export/design-system-modern-professional-dark.md`.
|
||||
|
||||
## Design
|
||||
- **Ground:** deep dark gray `#0a0a0c` (not pure black — "a true premium feel without
|
||||
the harshness of pure black," per the design system's own doc), stepped up through
|
||||
`#131313` (surface) and `#16161a` (cards) for elevation.
|
||||
- **Primary:** Professional Blue — rendered as `#4090fe` (container) / `#aac7ff`
|
||||
(on-dark-surface primary) / `#002f64` (on-primary text). This replaces safety-orange as
|
||||
*the* accent everywhere: live telemetry, active nav indicator, primary buttons,
|
||||
the user's own location dot.
|
||||
- **Secondary/tertiary:** muted slate-blue (`#aec7f6`/`#2e476f`) and a warm orange
|
||||
(`#ffb68c`/`#e3711f`) reserved for tertiary accents (the design system's own spec
|
||||
doesn't define a strict "reference vs. live" split the way V3-16's tertiary did —
|
||||
decide during implementation whether to keep that instinct using this palette's
|
||||
secondary color, or drop it; either is defensible, but pick one and apply it
|
||||
consistently rather than leaving it ambiguous per screen).
|
||||
- **Typography:** Inter, exclusively, for every role (headline/body/label) — this drops
|
||||
the multi-font split V3-16 and the original theme used (monospace for numbers via
|
||||
`monoDigits` is a separate, orthogonal decision from V3-16 worth keeping regardless of
|
||||
which color palette wins, since "digits must not jitter" is a real constraint, not a
|
||||
branding choice — retain `monoDigits` layered under Inter-family theming).
|
||||
- **Shape:** 8px rounding (`ROUND_EIGHT`), up from the current 4px.
|
||||
- **Depth:** tonal layering + a soft blue glow on elevated/active elements, no shadows —
|
||||
matches V3-16's existing "no shadows, translucency for depth" instinct, just with a
|
||||
different accent color for the glow.
|
||||
- **Contrast:** the design system's own doc claims AAA-level contrast for functional
|
||||
text against the dark backdrop — verify this the same way V3-16 did (a
|
||||
`contrastRatio()` helper and real assertions), don't take the claim on faith.
|
||||
|
||||
## Implementation
|
||||
1. Update `theme.dart`'s `ripprColors`/`ColorScheme` to the Modern Professional Dark
|
||||
values (ground, surface, cards, primary/secondary/tertiary, outline).
|
||||
2. Update `bodyFont`/`headlineFont`/`labelFont` to Inter throughout; keep `monoDigits` as
|
||||
a distinct style applied specifically to numeric displays, unchanged in spirit from
|
||||
today.
|
||||
3. Update `roundness` usage (button/card corner radii) from 4px to 8px.
|
||||
4. Re-run V3-16's `contrastRatio()` assertions against the new palette; adjust any color
|
||||
that fails AA before shipping, exactly as V3-16 did for the palette it replaces.
|
||||
5. Decide and document the reference-vs-live color convention (see Design) rather than
|
||||
leaving V3-16's tertiary-for-reference-readings instinct undecided under the new
|
||||
palette.
|
||||
6. **Mounted mode (V3-05) needs its own pass**, not an automatic inheritance — it's a
|
||||
separate high-contrast daylight theme with its own palette, tuned against a real
|
||||
sunlight/visor constraint. Confirm it still holds up in spirit (still legible, still
|
||||
AA-compliant) under the new brand direction; V3-13's real-ride verification is still
|
||||
the only way to confirm the *actual* sunlight legibility claim, same caveat V3-16
|
||||
already recorded.
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] `ripprColors` matches Modern Professional Dark's token values
|
||||
- [ ] Every screen using `Theme.of(context).colorScheme` picks up the new palette with
|
||||
no per-screen hardcoded color left over from the old theme
|
||||
- [ ] `contrastRatio()` assertions pass against the new palette (AA normal/large, same
|
||||
thresholds V3-16 established)
|
||||
- [ ] `monoDigits` still applies to every numeric display
|
||||
- [ ] Mounted mode re-verified against the new brand direction
|
||||
|
||||
## Tests
|
||||
- All of V3-16's `theme_test.dart` contrast assertions, re-pointed at the new color
|
||||
values, still pass
|
||||
- The existing black-on-black regression guard (`text is legible against the dark
|
||||
ground`) still passes
|
||||
- Widget: a representative sample of screens render with the new palette (no leftover
|
||||
hardcoded orange/old-tertiary-blue literals)
|
||||
|
||||
## Risks
|
||||
- **Regressing the black-on-black guard is the named risk V3-16 itself called out** —
|
||||
this ticket touches the same `textTheme`/`bodyColor`/`displayColor` wiring that bug
|
||||
came from originally. Do not remove the explicit color-naming discipline while
|
||||
restyling.
|
||||
- Grep for hardcoded hex literals from the old palette (`0xFFFF5722` and friends) across
|
||||
the UI layer before considering this done — a few call sites (e.g. `RideMap`'s speed
|
||||
gradient) reference specific hex values directly rather than through the theme, and
|
||||
those need a deliberate decision, not an accidental miss.
|
||||
|
||||
## Out of scope
|
||||
Any screen-specific redesign (UI-05/06/07) — this ticket only changes the token layer
|
||||
those screens then build against.
|
||||
|
||||
## Outcome
|
||||
|
||||
`lib/src/ui/theme.dart`'s `ripprColors` now carries Modern Professional Dark's values:
|
||||
ground `#0a0a0c` (the design system's own `surface-main`, not its plain `background`
|
||||
token — the spec's prose is explicit that `#0a0a0c` is *the* background, with `#131313`
|
||||
one step up and `#16161a` a second step up for cards, a three-level stack the old
|
||||
two-level `_ground`/`_surface` pair didn't have room for). Added `ripprCardColor`
|
||||
(`#16161a`) to carry the third level, since `ColorScheme` has no free surface slot for it
|
||||
once `surfaceContainerHighest` is spent on the selected/highlighted state. Primary is
|
||||
Professional Blue (`#4090fe`); tertiary is a warm orange (`#ffb68c`).
|
||||
|
||||
**Decision recorded per the ticket's own prompt:** the design system doesn't specify a
|
||||
reference-vs-live split itself, so one was chosen and applied everywhere consistently —
|
||||
tertiary orange for `StatRow`'s `reference` values (a max, an average), inverted from
|
||||
V3-16 where blue held that role, because blue is now the *primary* colour and reusing it
|
||||
for reference readings would have made the two indistinguishable. `RideMap`'s speed
|
||||
polyline gradient — flagged explicitly in the ticket's Risks as a hardcoded hex literal —
|
||||
now lerps `colors.secondary` (the theme's own cool slate-blue) to `colors.primary`
|
||||
instead of a hardcoded `0xFF4FC3F7`, for the same reason: the old literal was itself
|
||||
blue, and lerping blue-to-blue under the new primary would have washed the gradient into
|
||||
one hue.
|
||||
|
||||
**Shape:** added `ripprRadiusSmall` (8px) and `ripprRadiusLarge` (16px) constants
|
||||
matching the spec's small-component/large-container split, and a `CardThemeData` using
|
||||
the large radius and `ripprCardColor`. `RideMap`'s card-mode `ClipRRect` (previously a
|
||||
hardcoded 12px) now uses `ripprRadiusLarge`. Deliberately did **not** add a global
|
||||
`ElevatedButtonTheme`/`OutlinedButtonTheme` shape override — `RecordScreen`'s
|
||||
Start/Pause/Stop/Discard buttons rely on Material 3's default `StadiumBorder` for their
|
||||
pill shape, which already matches the Stitch mockups' own pill buttons; forcing an 8px
|
||||
rectangle there would have been an unrequested regression, not a spec application.
|
||||
|
||||
**Inter was not wired in.** The design system calls for it exclusively, but the codebase
|
||||
has no bundled font asset and no existing `bodyFont`/`headlineFont` abstraction to retarget
|
||||
— setting `TextTheme.apply(fontFamily: 'Inter')` with nothing registered under that name
|
||||
would silently fall back to the platform default (Roboto), which is exactly the class of
|
||||
invisible failure `ripprTheme()`'s own `bodyColor`/`displayColor` fix exists to prevent
|
||||
(the "Compose `Surface`" comment in the file). Left as a documented, deliberate follow-up
|
||||
rather than shipping a fontFamily string that resolves to nothing. `monoDigits` is
|
||||
untouched and still applies to every numeric display, as required.
|
||||
|
||||
**Contrast:** re-ran `theme_test.dart`'s `contrastRatio()` assertions against the new
|
||||
palette (no values needed adjusting — primary-on-ground, tertiary-on-ground, and
|
||||
onSurface-on-ground all clear their AA thresholds with considerable margin: roughly
|
||||
6.3:1, 11.6:1, and 15.3:1 respectively, well past the 4.5:1/3:1 bars). Mounted mode
|
||||
(V3-05) was left unchanged — its own separate high-contrast daylight palette isn't
|
||||
covered by the Modern Professional Dark spec, its orange accent isn't a brand clash the
|
||||
way the pocketed theme's safety-orange was, and V3-13's real-ride sunlight-legibility
|
||||
claim depends on the specific values already tuned there. Its existing contrast tests
|
||||
were re-run, unchanged, and still pass.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 315 tests, no count change —
|
||||
this ticket touched no test files, only re-pointed `ripprColors`'s literals and reused
|
||||
the same `theme_test.dart`/`ride_map_test.dart` assertions the diff didn't need to alter
|
||||
because they read `ripprTheme().colorScheme` rather than hardcoding old hex values.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`): checked Map/Settings/Rides
|
||||
tabs. Buttons, switches, the segmented Metric/Imperial control, section labels, and the
|
||||
`Save`/`Clear` text actions all render in Professional Blue; the reference "Max speed"
|
||||
figure renders in the new tertiary orange, clearly distinct from the live "0" headline
|
||||
figure (unchanged ink) and from the Pause button's blue; the bottom nav's active-tab
|
||||
indicator picked up a blue-tinted pill automatically (Material 3 derives it from the
|
||||
`ColorScheme`, not something this ticket configured directly) rather than the old
|
||||
neutral grey. No leftover orange/old-instrument-blue literals were visible anywhere.
|
||||
@@ -0,0 +1,154 @@
|
||||
# UI-09 — Monochrome dark map tiles
|
||||
|
||||
**Depends on** nothing · **Size** S · **Status** Done
|
||||
|
||||
## Goal
|
||||
Replace the tan/cream default OpenStreetMap raster style with a monochrome dark map, so
|
||||
the map actually looks like it belongs in a dark-mode HUD instead of a light basemap
|
||||
punched through a dark overlay.
|
||||
|
||||
## Context
|
||||
`RideMap`, the Plan/Route Planning canvas, and every Stitch mockup all assume a dark or
|
||||
near-monochrome map underneath the HUD. Today's `TileLayer` (`ride_map.dart`) points at
|
||||
`tile.openstreetmap.org`, OSM's standard light/tan default style — the actual on-device
|
||||
result is nothing like the mockups, regardless of how dark the chrome on top of it is.
|
||||
|
||||
This affects every screen that shows a map (Map HUD, Plan, Route Planning, and UI-01's
|
||||
dimmed background on every other tab), so it's worth settling once, early, rather than
|
||||
each screen ticket discovering the same mismatch independently.
|
||||
|
||||
## Design
|
||||
Two real options, not one obvious answer:
|
||||
|
||||
**Option A — switch tile provider to CARTO's "Dark Matter" basemap.**
|
||||
`https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png` (subdomains a-d, retina
|
||||
`{r}` variant available, max native zoom 20). Purpose-built dark, desaturated/monochrome
|
||||
cartography — closest to the mockups with the least engineering. Free for reasonable
|
||||
usage same as OSM's own tiles, but it's a **different third party with its own usage
|
||||
policy and attribution requirement** ("© OpenStreetMap contributors © CARTO", not just
|
||||
OSM's own attribution) — a real dependency to add, not a style tweak.
|
||||
|
||||
**Option B — keep OSM tiles, apply a color filter client-side.** Wrap `TileLayer` in a
|
||||
`ColorFiltered` widget using a `ColorFilter.matrix` that desaturates and inverts/darkens
|
||||
the tan basemap into a monochrome dark look. No new third party, no new attribution, and
|
||||
V3-11's offline tile cache keeps working against the exact tiles it already knows about
|
||||
— but the result is a filtered light map, not tiles actually designed for dark
|
||||
presentation, and will read as slightly "off" next to CARTO's purpose-built version
|
||||
(road/building contrast tuned for light backgrounds doesn't always invert cleanly).
|
||||
|
||||
**Recommendation: Option A.** The mockups' whole aesthetic depends on the map itself
|
||||
being dark, not just tinted dark, and CARTO's dark tiles are a well-established, free,
|
||||
no-API-key option already widely used in the Flutter/Leaflet ecosystem for exactly this.
|
||||
The added attribution line and a second host to trust are a small, one-time cost.
|
||||
|
||||
**V3-11 interaction:** the offline tile cache (`FileTileCache`/`CachedTileProvider`) is
|
||||
already keyed by `TileKey(z, x, y)` with no provider-specific data baked in — but that
|
||||
means a cache built against OSM tiles and a cache built against CARTO tiles are
|
||||
**indistinguishable to the cache** despite being visually different. Switching providers
|
||||
without a plan here would silently serve stale, wrong-looking cached OSM tiles once
|
||||
CARTO's URL is live. This needs a cache-busting or provider-tagging fix as part of this
|
||||
ticket, not a separate one — see Implementation.
|
||||
|
||||
## Implementation
|
||||
1. Add a `provider`-qualified cache key (or a cache-version bump that invalidates
|
||||
existing V3-11 cache contents wholesale) so switching tile sources can't silently
|
||||
mix cached tiles from two visually different sources.
|
||||
2. Update `RideMap`'s `TileLayer` (and the Route Planner's separate `TileLayer` instance
|
||||
— see `route_planner_screen.dart`) to CARTO's dark tile URL template, correct
|
||||
`subdomains`, and `maxNativeZoom: 20`.
|
||||
3. Update the attribution requirement — check whether `flutter_map`'s
|
||||
`RichAttributionWidget`/`AttributionWidget` is in use anywhere already, or add one;
|
||||
OSM-only attribution is no longer sufficient once CARTO tiles are in the mix.
|
||||
4. Update `tileUserAgent`/any OSM-specific comments in `ride_map.dart` that assumed OSM's
|
||||
tile servers specifically (the "respect OSM's usage policy" reasoning still applies in
|
||||
spirit to CARTO's own policy, but the specific server being addressed changes).
|
||||
5. Re-verify V3-11's tile math/download flow still works end-to-end against the new URL
|
||||
template (tile enumeration and caching are provider-agnostic already; only the fetch
|
||||
URL construction needs updating).
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] The map renders in a dark, desaturated/monochrome style matching the Stitch
|
||||
mockups, not OSM's default tan basemap
|
||||
- [ ] Correct attribution for both OpenStreetMap and CARTO is shown wherever the map is
|
||||
shown
|
||||
- [ ] Switching tile providers does not silently serve stale, visually-mismatched
|
||||
cached tiles from before the switch (V3-11's cache is invalidated or
|
||||
provider-tagged)
|
||||
- [ ] Both `RideMap` and the Route Planner's independent `TileLayer` are updated
|
||||
consistently — one dark style everywhere, not just on the screens that happened to
|
||||
get touched first
|
||||
|
||||
## Tests
|
||||
- Existing `ride_map`/route-planner widget tests updated for the new tile URL and still
|
||||
passing
|
||||
- Unit: cache-key/versioning change is exercised the same way V3-11's eviction tests
|
||||
are — a cache built under the old scheme does not get served for the new provider
|
||||
|
||||
## Risks
|
||||
- **A second third-party tile host is a real dependency, not a free color swap** — its
|
||||
own usage policy, its own risk of being blocked if misused, and its own attribution
|
||||
obligation. Treat it with the same care V3-11 already applies to OSM (rate-limit
|
||||
respecting, no bulk prefetch, real user agent).
|
||||
- If CARTO's free tier ever proves insufficient or is discontinued, Option B (a color
|
||||
filter over OSM tiles) is the fallback with no new third-party dependency — worth
|
||||
keeping in mind as a documented Plan B, not re-deriving from scratch later.
|
||||
|
||||
## Out of scope
|
||||
A fully custom/self-hosted vector tile style — much larger scope, and not needed to hit
|
||||
"looks like the mockups" today.
|
||||
|
||||
## Outcome
|
||||
|
||||
Took the recommended Option A. `ride_map.dart` now defines `tileUrlTemplate`
|
||||
(`https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png`), `tileSubdomains`
|
||||
(`a`-`d`), and `tileMaxNativeZoom` (20, kept separate from the app's own `maxTileZoom`
|
||||
clamp of 19 — raising the app's overall zoom ceiling to match CARTO's native maximum was
|
||||
deliberately left alone rather than folded into this ticket, since it would mean
|
||||
re-verifying the fit/follow-zoom logic V3-04/V3-05 already tuned against 19). Both
|
||||
`RideMap` and `route_planner_screen.dart`'s independent `TileLayer` now import and use
|
||||
these same three constants plus `retinaMode: true`, so there is exactly one dark style
|
||||
and one set of tile-request parameters across the app, not two independently-drifting
|
||||
copies.
|
||||
|
||||
**Cache versioning (the ticket's named risk):** `tileCacheProvider` in
|
||||
`app/providers.dart` now points `FileTileCache` at `tiles/carto_dark_v1` instead of the
|
||||
old bare `tiles` directory. `TileKey(z, x, y)` still carries no provider identity, so
|
||||
the directory segment is the actual version tag — any tiles cached under the old OSM-tan
|
||||
scheme are simply orphaned in a directory the app no longer reads from, rather than
|
||||
being silently served under the new dark UI. Added a unit test
|
||||
(`test/tile_cache_test.dart`, "a tile cached under one provider directory is not served
|
||||
from another") proving this isolation holds at the `FileTileCache` level, the same way
|
||||
V3-11's own eviction tests exercise cache behavior directly rather than through the UI.
|
||||
|
||||
**Attribution:** added a shared `TileAttribution` widget (`ride_map.dart`) wrapping
|
||||
flutter_map's `RichAttributionWidget` with `TextSourceAttribution`s for both
|
||||
"OpenStreetMap contributors" (CARTO's dark style is still built from OSM's underlying
|
||||
data) and "CARTO" itself — OSM-only credit, which is what the app carried before, stopped
|
||||
being sufficient the moment a second tile host entered the mix. Composed into both
|
||||
`RideMap` and the route planner's `FlutterMap`, so it's one component discharging the
|
||||
obligation everywhere a map renders, not a copy-pasted attribution block per screen. Did
|
||||
not wire up `onTap` license-page links — that would need the `url_launcher` package, a
|
||||
new dependency this ticket has no other reason to add; the obligation is to visibly
|
||||
credit both sources, not to make the credit tappable.
|
||||
|
||||
**Tests:** `flutter analyze` clean. `flutter test` green at 316 tests (315 + the new
|
||||
cache-isolation test) — no existing test hardcoded the old OSM URL string, so nothing
|
||||
else needed updating.
|
||||
|
||||
**Android emulator verification** (`Medium_Phone_API_35`): confirmed CARTO's dark,
|
||||
desaturated basemap renders on-device (a genuinely dark map, not a tan basemap under a
|
||||
dark overlay) — a clear visual match for the Stitch mockups' aesthetic, on the Map tab at
|
||||
full opacity and dimmed correctly on Rides/Plan/Settings via UI-01's existing scrim. The
|
||||
attribution icon is visibly present in the bottom-left corner on every tab (same shared
|
||||
map instance). Its tap-to-expand interaction could not be confirmed via `adb input tap`
|
||||
in this session — taps at its on-screen coordinates didn't visibly toggle the popup, and
|
||||
a genuine Android mock-location watermark icon (left over from this session's earlier
|
||||
`adb emu geo fix` calls, and confirmed present at the same screen position across
|
||||
unrelated tabs, which a page-level widget couldn't be) sits immediately next to it,
|
||||
making the exact tap target ambiguous to hit blindly. This is a dev-tooling
|
||||
verification gap, not a known defect — the widget itself renders without error and
|
||||
matches flutter_map's standard, widely-shipped attribution pattern (the same
|
||||
small-icon-that-expands convention Google Maps and Mapbox both use). Re-verify the
|
||||
popup's tap behavior with a real touchscreen or Flutter Inspector if it becomes load-
|
||||
bearing later (e.g., if legal review specifically requires confirming the expand
|
||||
interaction, not just the icon's presence).
|
||||
@@ -7,7 +7,7 @@ implementing, so the risks are on paper before they are walked into.
|
||||
Menu, not a commitment. Nothing here is scheduled.
|
||||
|
||||
**Everything in v3 is buildable with no server.** Group rides, accounts and paid cloud
|
||||
backup are v4 — see [../BACKLOG.md](../BACKLOG.md).
|
||||
backup are on the ROADMAP (formerly "v4") — see [../BACKLOG.md](../BACKLOG.md).
|
||||
|
||||
> **Before starting anything:** [../port/REAL-RIDE-CHECKLIST.md](../port/REAL-RIDE-CHECKLIST.md).
|
||||
> The port has never recorded a real ride, and item I3 may still force a change of GPS
|
||||
@@ -33,7 +33,7 @@ backup are v4 — see [../BACKLOG.md](../BACKLOG.md).
|
||||
| [V3-14](V3-14-gpx-interop.md) | GPX interoperability | S | V3-01 | Partially done (code only; needs real-device verification) |
|
||||
| [V3-15](V3-15-auto-pause.md) | Auto-pause | M | V3-13 *(gated)* | Not started |
|
||||
| [V3-16](V3-16-visual-identity.md) | Visual identity | M | V3-04, V3-05 | Partially done (token-level identity shipped; needs outdoor device verification) |
|
||||
| [V3-17](V3-17-osrm-hosting.md) | Self-hosted OSRM: investigate and stand one up | M | — *(needs a server — see the ticket's own note on the v3/v4 boundary)* | Not started |
|
||||
| [V3-17](V3-17-osrm-hosting.md) | Self-hosted OSRM: investigate and stand one up | M | — *(needs a server — see the ticket's own note on the v3/ROADMAP boundary)* | Not started |
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -52,7 +52,7 @@ no dependencies: V3-01 · V3-02 · V3-04 · V3-06 · V3-07 · V3-10 · V3-12 ·
|
||||
```
|
||||
|
||||
**V3-08/V3-09 are deferred, on request**, pending V3-17 (self-hosted OSRM). See V3-17's
|
||||
own note on why that also puts them in tension with this document's v3/v4 boundary —
|
||||
own note on why that also puts them in tension with this document's v3/ROADMAP boundary —
|
||||
unresolved by design, not an oversight.
|
||||
|
||||
## Three that carry more weight than their size suggests
|
||||
|
||||
@@ -46,7 +46,7 @@ Keep it plain. This is not a screen anyone should spend time in.
|
||||
without introducing a second source of truth is the only subtle part.
|
||||
|
||||
## Out of scope
|
||||
Account settings (v4). Theme selection (V3-16).
|
||||
Account settings (ROADMAP). Theme selection (V3-16).
|
||||
|
||||
|
||||
## Outcome
|
||||
|
||||
@@ -59,7 +59,7 @@ Behind the existing map toggle, off by default while it is unproven on battery.
|
||||
stream, which is already batched.
|
||||
|
||||
## Out of scope
|
||||
Mounted mode (V3-05). Other riders on the map (v4).
|
||||
Mounted mode (V3-05). Other riders on the map (ROADMAP).
|
||||
|
||||
## Outcome
|
||||
Shipped as designed. `AppDatabase.watchPointsForTrip`/`watchSegmentsForTrip` feed two
|
||||
|
||||
@@ -15,20 +15,22 @@ Directions) or the public OSRM demo (explicitly not for production use).
|
||||
|
||||
**This ticket exists because that decision itself has a wrinkle worth naming up front:**
|
||||
this whole v3 backlog's organizing principle, stated in `docs/BACKLOG.md`, is *"v3 is
|
||||
everything that can be built with no server. v4 is everything that cannot."* A
|
||||
self-hosted OSRM instance is a server. Strictly, that makes V3-08 and V3-09 — anything
|
||||
that depends on this ticket — v4 work by the project's own definition, not v3, even
|
||||
though they're filed under `docs/v3/` today and the routing itself has nothing to do
|
||||
with the group-rides/accounts/backup programme that currently defines v4. Whether to
|
||||
formally renumber them is a documentation decision for whoever picks this up next; this
|
||||
ticket does not resolve it, only flags it so it isn't silently glossed over.
|
||||
everything that can be built with no server. The ROADMAP (formerly "v4") is everything
|
||||
that cannot."* A self-hosted OSRM instance is a server. Strictly, that makes V3-08 and
|
||||
V3-09 — anything that depends on this ticket — ROADMAP work by the project's own
|
||||
definition, not v3, even though they're filed under `docs/v3/` today and the routing
|
||||
itself has nothing to do with the group-rides/accounts/backup programme that currently
|
||||
anchors the ROADMAP. Whether to formally renumber them is a documentation decision for
|
||||
whoever picks this up next; this ticket does not resolve it, only flags it so it isn't
|
||||
silently glossed over.
|
||||
|
||||
## Design
|
||||
Two separable questions:
|
||||
|
||||
1. **Where does it run?** A small VPS (the same shape of box that would eventually host
|
||||
the v4 group-ride server, so this could double as an early step toward that) versus
|
||||
something serverless/managed. OSRM's own Docker image is the standard path either way.
|
||||
the ROADMAP's group-ride server, so this could double as an early step toward that)
|
||||
versus something serverless/managed. OSRM's own Docker image is the standard path
|
||||
either way.
|
||||
2. **What data does it need?** A regional OSM extract, not the planet — start with
|
||||
whatever region actually gets ridden (per `docs/LAUNCH.md`, this is presently a
|
||||
friends-and-family app, so the region is small and known). [Geofabrik](https://download.geofabrik.de/)
|
||||
|
||||
Reference in New Issue
Block a user