Mobile follow on number 2 #75

Closed
opened 2026-04-08 14:56:06 +00:00 by maxtkc · 0 comments
Owner

Summary

Follow-up polish pass for mobile support (#74). Fixes six concrete issues: (1) page overscroll/bounce when dragging the navbar, (2) a ~5-10px gap between the dock and the bottom sheet caused by the dock's actual rendered height differing from the hardcoded --dock-height CSS variable, (3) the basemap/projection/shape-toggle buttons partially hidden behind the dock due to incorrect z/y positioning, (4) notifications appearing at the bottom-left (wrong place) instead of below the search area, (5) the search box being in the navbar on mobile (should be a map overlay), and (6) auto-zoom not accounting for the bottom sheet covering half the map.

Approach: minimal targeted changes per issue — no refactors beyond what's needed. Prefer DaisyUI classes over bespoke CSS.

Relevant Context

  • src/styles/main.css — mobile media query (@media (max-width: 767px)) and :root { --dock-height: 4rem }
  • src/index.html:38<body class="font-sans h-screen overflow-hidden">; navbar-center (mobile search) is lines 64-71; #map-controls is lines 341-406; #mobile-dock is lines 578-639
  • src/modules/notification-system.ts:35-36 — container created with class 'fixed bottom-4 left-4 z-50 space-y-2'
  • src/modules/basemap-control.ts:148-156 — inline style.cssText sets position: absolute; bottom: 40px; right: 10px; no z-index set inline
  • src/modules/bottom-sheet.ts:27-34 — constructor; mobile check window.innerWidth >= 768; resize listener at line 26
  • src/modules/search-controller.ts:33-50 — looks for both map-search and mobile-search inputs; createSearchResults() appends dropdown to #map-controls; click-outside handler checks #mobile-search on line 61
  • src/modules/map-controller.ts:388highlightStop calls flyTo with no padding object; flyToRoute line 515 calls fitBounds({ padding: 80 }); fitMapToTrip line 453 calls fitBounds({ padding: 50 }); fitToRoutes line 561 calls fitBounds({ padding: 50 })

MapLibre padding option: both flyTo and fitBounds accept padding as either a number (uniform) or { top, bottom, left, right } in pixels. For a single-point flyTo, bottom padding shifts the center point upward into the visible area above the sheet.

Phase 1: Body / Page Stability

Goal: prevent overscroll bounce when dragging the navbar, and prevent the browser chrome from appearing/disappearing during scroll.

  • In src/styles/main.css, add before the media query block:
    html, body {
      overscroll-behavior: none;
    }
    
    This is the minimal, non-invasive fix for elastic bounce without interfering with map touch events.

Gotcha: touch-action: none on body would break map panning — do NOT add it.

Phase 2: Dock Height Dynamic Measurement (Gap Fix)

Goal: eliminate the ~5-10px gap between the dock top edge and the bottom sheet.

The gap exists because --dock-height: 4rem (hardcoded CSS) doesn't match the dock's actual rendered height (which includes its border, padding, and DaisyUI-computed height).

  • In src/modules/bottom-sheet.ts, add a private method:
    private syncDockHeight(): void {
      const dock = document.getElementById('mobile-dock');
      if (!dock) return;
      const h = dock.getBoundingClientRect().height;
      if (h > 0) {
        document.documentElement.style.setProperty('--dock-height', `${h}px`);
      }
    }
    
  • Call this.syncDockHeight() at the end of the constructor (after the mobile early-return check), and inside the resize listener (before this.setSnap(...)).

Gotcha: call syncDockHeight AFTER setSnap('closed', false) in the constructor so the panel position is correct from the start.

Phase 3: Basemap Control Positioning on Mobile

Goal: move the basemap/projection/shape-toggle buttons above the dock (not hidden behind it), with consistent z-index below the dock/sheet layer.

The basemap-control div is appended to the map container via JS with inline CSS bottom: 40px; right: 10px and no z-index. On mobile, the dock is z-50 and the bottom sheet is z-50. The basemap buttons end up partially or fully behind the dock.

  • In src/styles/main.css, inside the @media (max-width: 767px) block, add:
    .basemap-control {
      bottom: calc(var(--dock-height) + 10px) !important;
      z-index: 40 !important;
    }
    
    The !important is required to override the inline style.cssText set in basemap-control.ts. z-index 40 puts these buttons below the dock/sheet (z-50) so the drawer slides over them naturally.

Gotcha: rebuildControl() calls createControl() which resets style.cssText on each basemap change — that's fine because we're overriding via the stylesheet (higher specificity wins when !important is used vs inline).

Phase 4: Search Box to Map Overlay + Notification Placement

Goal: move the search box from the mobile navbar to a map overlay at the top-left, visible on both mobile and desktop. Notifications move from bottom-left to below the search area.

HTML changes (src/index.html)

  • Remove the entire navbar-center div (lines 64-71):

    <div class="navbar-center flex md:hidden flex-1 px-2">
      <input type="text" id="mobile-search" ... />
    </div>
    
  • Replace #map-controls (lines 344-406) with a new layout:

    <div
      id="map-controls"
      class="absolute top-2 left-2 right-2 z-40 flex flex-row items-start gap-2"
    >
      <!-- Search -->
      <div class="flex-1 max-w-sm card card-bordered bg-base-100 shadow-lg p-2">
        <input
          type="text"
          id="map-search"
          placeholder="Search"
          class="input input-sm w-full"
        />
      </div>
    
      <!-- Map Tools -->
      <div class="card card-bordered bg-base-100 shadow-lg p-1 flex-shrink-0">
        <div class="join">
          <button
            id="pointer-btn"
            class="btn btn-sm btn-square join-item btn-primary tooltip"
            data-tip="Pointer"
          >
            <!-- same pointer SVG as before -->
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 2l12 10-6 1-3 6L6 2z" />
            </svg>
          </button>
          <button
            id="add-stop-btn"
            class="btn btn-sm btn-square join-item tooltip"
            data-tip="Add stop"
          >
            <!-- same plus SVG as before -->
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
            </svg>
          </button>
        </div>
      </div>
    </div>
    

    Key changes from current:

    • left-2 right-2 instead of right-2 only — spans full width
    • No more md: breakpoint differences — same layout on all screens
    • Search is flex-1 max-w-sm (~24rem max, ~2× the old w-48), visible on all screen sizes
    • Tools card is flex-shrink-0
  • Remove the desktop-only search card that was inside #map-controls (it's merged into the above)

src/modules/search-controller.ts

  • In initialize(), remove the mobile-search lookup entirely — only wire map-search:
    initialize(): void {
      const input = document.getElementById('map-search') as HTMLInputElement | null;
      if (!input) return;
      this.inputs = [input];
      this.createSearchResults();
      this.wireInput(input);
      document.addEventListener('click', (e) => {
        if (!(e.target as Element)?.closest('#map-controls')) {
          this.hideResults();
        }
      });
    }
    
  • Remove syncInputs() method (no longer needed with a single input).
  • Remove all remaining syncInputs call sites.

src/modules/notification-system.ts

  • Change the container class in initialize() from:
    this.container.className = 'fixed bottom-4 left-4 z-50 space-y-2';
    
    to:
    this.container.className = 'fixed top-28 left-2 z-[100] space-y-2 max-w-xs';
    
    top-28 (7rem) = 4rem navbar + ~3rem for search/controls height. z-[100] ensures notifications appear above the dock and sheet.

Gotcha: max-w-xs (20rem) keeps notifications from spanning the full width on desktop. On mobile the search is max-w-sm (24rem); notifications slightly narrower is fine.

Phase 5: Auto-Zoom Aware of Bottom Sheet

Goal: when navigating to a stop/route/trip on mobile, the map zooms so the feature appears in the visible area above the half-open sheet (not behind it).

MapLibre flyTo and fitBounds both accept padding: { top, bottom, left, right } in pixels. Adding bottom padding pushes the viewport center upward, keeping the feature visible above the sheet.

src/modules/map-controller.ts

  • Add a private field near the top of the class:
    private bottomPadding = 0;
    
  • Add a public setter:
    public setBottomPadding(px: number): void {
      this.bottomPadding = px;
    }
    
  • Update highlightStopflyTo call (line ~388) to use padding:
    this.map!.flyTo({
      center: [lon, lat],
      zoom: Math.max(this.map!.getZoom(), 13),
      duration: 1500,
      essential: true,
      padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 },
    });
    
  • Update flyToRoutefitBounds call (line ~515):
    this.map!.fitBounds(bounds, {
      padding: { top: 80, bottom: 80 + this.bottomPadding, left: 80, right: 80 },
      duration: 2000,
      essential: true,
    });
    
  • Update fitMapToTripfitBounds call (line ~453):
    this.map!.fitBounds(bounds, {
      padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 },
    });
    
  • Update fitToRoutesfitBounds call (line ~561):
    this.map!.fitBounds(bounds, {
      padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 },
    });
    
  • Update fitAllStopsfitBounds call (line ~333):
    this.map!.fitBounds(bounds, {
      padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 },
    });
    

src/index.ts

  • After the bottomSheet setup block (after line ~247), set the bottom padding on the map controller:
    if (window.innerWidth < 768) {
      this.mapController.setBottomPadding(Math.round(window.innerHeight * 0.45));
    }
    
    This is set once at init. If orientation changes, the resize handler in bottom-sheet.ts will update --dock-height but we don't need to update padding dynamically — the half-snap height is proportional so it scales with the viewport naturally.

Gotcha: setBottomPadding must be called after this.mapController is initialized. Check that this.mapController is set before this call in the init sequence.


Original Issue

Follow-up to #74 mobile support. Issues observed after Phase 3 of #74:

  • Notification placement should be under the nav bar
  • Whole page is slightly scrollable / elastic bounce when dragging navbar
  • Map style button z-level inconsistency with dock; all basemap buttons should be consistently above the dock (y-axis)
  • Gap between dock and bottom drawer (~5-10px visible)
  • Auto-zoom to features should center in visible map area above the sheet
  • Search box should move to over the map (top-left), with pointer/add-stop buttons to its right; propagate to desktop with max-width; notifications go below
## Summary Follow-up polish pass for mobile support (#74). Fixes six concrete issues: (1) page overscroll/bounce when dragging the navbar, (2) a ~5-10px gap between the dock and the bottom sheet caused by the dock's actual rendered height differing from the hardcoded `--dock-height` CSS variable, (3) the basemap/projection/shape-toggle buttons partially hidden behind the dock due to incorrect z/y positioning, (4) notifications appearing at the bottom-left (wrong place) instead of below the search area, (5) the search box being in the navbar on mobile (should be a map overlay), and (6) auto-zoom not accounting for the bottom sheet covering half the map. Approach: minimal targeted changes per issue — no refactors beyond what's needed. Prefer DaisyUI classes over bespoke CSS. ## Relevant Context - `src/styles/main.css` — mobile media query (`@media (max-width: 767px)`) and `:root { --dock-height: 4rem }` - `src/index.html:38` — `<body class="font-sans h-screen overflow-hidden">`; `navbar-center` (mobile search) is lines 64-71; `#map-controls` is lines 341-406; `#mobile-dock` is lines 578-639 - `src/modules/notification-system.ts:35-36` — container created with class `'fixed bottom-4 left-4 z-50 space-y-2'` - `src/modules/basemap-control.ts:148-156` — inline `style.cssText` sets `position: absolute; bottom: 40px; right: 10px`; no z-index set inline - `src/modules/bottom-sheet.ts:27-34` — constructor; mobile check `window.innerWidth >= 768`; resize listener at line 26 - `src/modules/search-controller.ts:33-50` — looks for both `map-search` and `mobile-search` inputs; `createSearchResults()` appends dropdown to `#map-controls`; click-outside handler checks `#mobile-search` on line 61 - `src/modules/map-controller.ts:388` — `highlightStop` calls `flyTo` with no padding object; `flyToRoute` line 515 calls `fitBounds({ padding: 80 })`; `fitMapToTrip` line 453 calls `fitBounds({ padding: 50 })`; `fitToRoutes` line 561 calls `fitBounds({ padding: 50 })` MapLibre `padding` option: both `flyTo` and `fitBounds` accept `padding` as either a number (uniform) or `{ top, bottom, left, right }` in pixels. For a single-point `flyTo`, bottom padding shifts the center point upward into the visible area above the sheet. ## Phase 1: Body / Page Stability Goal: prevent overscroll bounce when dragging the navbar, and prevent the browser chrome from appearing/disappearing during scroll. - [x] In `src/styles/main.css`, add before the media query block: ```css html, body { overscroll-behavior: none; } ``` This is the minimal, non-invasive fix for elastic bounce without interfering with map touch events. Gotcha: `touch-action: none` on body would break map panning — do NOT add it. ## Phase 2: Dock Height Dynamic Measurement (Gap Fix) Goal: eliminate the ~5-10px gap between the dock top edge and the bottom sheet. The gap exists because `--dock-height: 4rem` (hardcoded CSS) doesn't match the dock's actual rendered height (which includes its border, padding, and DaisyUI-computed height). - [x] In `src/modules/bottom-sheet.ts`, add a private method: ```typescript private syncDockHeight(): void { const dock = document.getElementById('mobile-dock'); if (!dock) return; const h = dock.getBoundingClientRect().height; if (h > 0) { document.documentElement.style.setProperty('--dock-height', `${h}px`); } } ``` - [x] Call `this.syncDockHeight()` at the end of the constructor (after the mobile early-return check), and inside the resize listener (before `this.setSnap(...)`). Gotcha: call `syncDockHeight` AFTER `setSnap('closed', false)` in the constructor so the panel position is correct from the start. ## Phase 3: Basemap Control Positioning on Mobile Goal: move the basemap/projection/shape-toggle buttons above the dock (not hidden behind it), with consistent z-index below the dock/sheet layer. The `basemap-control` div is appended to the map container via JS with inline CSS `bottom: 40px; right: 10px` and no z-index. On mobile, the dock is `z-50` and the bottom sheet is `z-50`. The basemap buttons end up partially or fully behind the dock. - [x] In `src/styles/main.css`, inside the `@media (max-width: 767px)` block, add: ```css .basemap-control { bottom: calc(var(--dock-height) + 10px) !important; z-index: 40 !important; } ``` The `!important` is required to override the inline `style.cssText` set in `basemap-control.ts`. z-index 40 puts these buttons below the dock/sheet (z-50) so the drawer slides over them naturally. Gotcha: `rebuildControl()` calls `createControl()` which resets `style.cssText` on each basemap change — that's fine because we're overriding via the stylesheet (higher specificity wins when `!important` is used vs inline). ## Phase 4: Search Box to Map Overlay + Notification Placement Goal: move the search box from the mobile navbar to a map overlay at the top-left, visible on both mobile and desktop. Notifications move from bottom-left to below the search area. ### HTML changes (`src/index.html`) - [x] Remove the entire `navbar-center` div (lines 64-71): ```html <div class="navbar-center flex md:hidden flex-1 px-2"> <input type="text" id="mobile-search" ... /> </div> ``` - [x] Replace `#map-controls` (lines 344-406) with a new layout: ```html <div id="map-controls" class="absolute top-2 left-2 right-2 z-40 flex flex-row items-start gap-2" > <!-- Search --> <div class="flex-1 max-w-sm card card-bordered bg-base-100 shadow-lg p-2"> <input type="text" id="map-search" placeholder="Search" class="input input-sm w-full" /> </div> <!-- Map Tools --> <div class="card card-bordered bg-base-100 shadow-lg p-1 flex-shrink-0"> <div class="join"> <button id="pointer-btn" class="btn btn-sm btn-square join-item btn-primary tooltip" data-tip="Pointer" > <!-- same pointer SVG as before --> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 2l12 10-6 1-3 6L6 2z" /> </svg> </button> <button id="add-stop-btn" class="btn btn-sm btn-square join-item tooltip" data-tip="Add stop" > <!-- same plus SVG as before --> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> </svg> </button> </div> </div> </div> ``` Key changes from current: - `left-2 right-2` instead of `right-2` only — spans full width - No more `md:` breakpoint differences — same layout on all screens - Search is `flex-1 max-w-sm` (~24rem max, ~2× the old `w-48`), visible on all screen sizes - Tools card is `flex-shrink-0` - [x] Remove the desktop-only search card that was inside `#map-controls` (it's merged into the above) ### `src/modules/search-controller.ts` - [x] In `initialize()`, remove the `mobile-search` lookup entirely — only wire `map-search`: ```typescript initialize(): void { const input = document.getElementById('map-search') as HTMLInputElement | null; if (!input) return; this.inputs = [input]; this.createSearchResults(); this.wireInput(input); document.addEventListener('click', (e) => { if (!(e.target as Element)?.closest('#map-controls')) { this.hideResults(); } }); } ``` - [x] Remove `syncInputs()` method (no longer needed with a single input). - [x] Remove all remaining `syncInputs` call sites. ### `src/modules/notification-system.ts` - [x] Change the container class in `initialize()` from: ```typescript this.container.className = 'fixed bottom-4 left-4 z-50 space-y-2'; ``` to: ```typescript this.container.className = 'fixed top-28 left-2 z-[100] space-y-2 max-w-xs'; ``` `top-28` (7rem) = 4rem navbar + ~3rem for search/controls height. `z-[100]` ensures notifications appear above the dock and sheet. Gotcha: `max-w-xs` (20rem) keeps notifications from spanning the full width on desktop. On mobile the search is `max-w-sm` (24rem); notifications slightly narrower is fine. ## Phase 5: Auto-Zoom Aware of Bottom Sheet Goal: when navigating to a stop/route/trip on mobile, the map zooms so the feature appears in the visible area above the half-open sheet (not behind it). MapLibre `flyTo` and `fitBounds` both accept `padding: { top, bottom, left, right }` in pixels. Adding `bottom` padding pushes the viewport center upward, keeping the feature visible above the sheet. ### `src/modules/map-controller.ts` - [x] Add a private field near the top of the class: ```typescript private bottomPadding = 0; ``` - [x] Add a public setter: ```typescript public setBottomPadding(px: number): void { this.bottomPadding = px; } ``` - [x] Update `highlightStop` → `flyTo` call (line ~388) to use padding: ```typescript this.map!.flyTo({ center: [lon, lat], zoom: Math.max(this.map!.getZoom(), 13), duration: 1500, essential: true, padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 }, }); ``` - [x] Update `flyToRoute` → `fitBounds` call (line ~515): ```typescript this.map!.fitBounds(bounds, { padding: { top: 80, bottom: 80 + this.bottomPadding, left: 80, right: 80 }, duration: 2000, essential: true, }); ``` - [x] Update `fitMapToTrip` → `fitBounds` call (line ~453): ```typescript this.map!.fitBounds(bounds, { padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 }, }); ``` - [x] Update `fitToRoutes` → `fitBounds` call (line ~561): ```typescript this.map!.fitBounds(bounds, { padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 }, }); ``` - [x] Update `fitAllStops` → `fitBounds` call (line ~333): ```typescript this.map!.fitBounds(bounds, { padding: { top: 50, bottom: 50 + this.bottomPadding, left: 50, right: 50 }, }); ``` ### `src/index.ts` - [x] After the `bottomSheet` setup block (after line ~247), set the bottom padding on the map controller: ```typescript if (window.innerWidth < 768) { this.mapController.setBottomPadding(Math.round(window.innerHeight * 0.45)); } ``` This is set once at init. If orientation changes, the resize handler in `bottom-sheet.ts` will update `--dock-height` but we don't need to update padding dynamically — the half-snap height is proportional so it scales with the viewport naturally. Gotcha: `setBottomPadding` must be called after `this.mapController` is initialized. Check that `this.mapController` is set before this call in the init sequence. --- ## Original Issue Follow-up to #74 mobile support. Issues observed after Phase 3 of #74: - Notification placement should be under the nav bar - Whole page is slightly scrollable / elastic bounce when dragging navbar - Map style button z-level inconsistency with dock; all basemap buttons should be consistently above the dock (y-axis) - Gap between dock and bottom drawer (~5-10px visible) - Auto-zoom to features should center in visible map area above the sheet - Search box should move to over the map (top-left), with pointer/add-stop buttons to its right; propagate to desktop with max-width; notifications go below
maxtkc self-assigned this 2026-04-08 14:56:06 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#75
No description provided.