Improve mobile usability with touch-optimized defaults and targets

- Add mobile-specific default layers (hotspots, conflicts, outages only)
  to reduce visual clutter on small screens
- Default to MENA view on mobile devices for better regional focus
- Add 44x44px touch targets using ::before pseudo-elements for all
  map markers (hotspots, bases, earthquakes, etc.)
- Update MapPopup to center horizontally on mobile for better visibility
- Add comprehensive mobile CSS with responsive controls and buttons
- Update mobile warning modal messaging to reflect new mobile experience
- Add extra-small screen (480px) specific layout adjustments

Fixes touch target accuracy issues where clicking one marker would
open a different marker's popup.
This commit is contained in:
Claude
2026-01-11 08:33:24 +00:00
parent 601ddc6b9b
commit a430337476
5 changed files with 258 additions and 13 deletions
+23 -3
View File
@@ -8,6 +8,7 @@ import {
REFRESH_INTERVALS,
DEFAULT_PANELS,
DEFAULT_MAP_LAYERS,
MOBILE_DEFAULT_MAP_LAYERS,
STORAGE_KEYS,
} from '@/config';
import { fetchCategoryFeeds, fetchMultipleStocks, fetchCrypto, fetchPredictions, fetchEarthquakes, fetchWeatherAlerts, fetchFredData, fetchInternetOutages, isOutagesConfigured, fetchAisSignals, initAisStream, getAisStatus, disconnectAisStream, isAisConfigured, fetchCableActivity, fetchProtestEvents, getProtestStatus, fetchFlightDelays, fetchMilitaryFlights, fetchMilitaryVessels, initMilitaryVesselStream, isMilitaryVesselTrackingConfigured, initDB, updateBaseline, calculateDeviation, addToSignalHistory, saveSnapshot, cleanOldSnapshots, analysisWorker, fetchPizzIntStatus, fetchGdeltTensions } from '@/services';
@@ -38,6 +39,13 @@ import { AI_DATA_CENTERS } from '@/config/ai-datacenters';
import { GAMMA_IRRADIATORS } from '@/config/irradiators';
import type { PredictionMarket, MarketData, ClusteredEvent } from '@/types';
// Helper to detect mobile devices
function isMobileDevice(): boolean {
const isMobileWidth = window.innerWidth < 768;
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches;
return isMobileWidth || isTouchDevice;
}
export class App {
private container: HTMLElement;
private map: MapComponent | null = null;
@@ -60,18 +68,23 @@ export class App {
private isPlaybackMode = false;
private initialUrlState: ParsedMapUrlState | null = null;
private inFlight: Set<string> = new Set();
private isMobile: boolean;
constructor(containerId: string) {
const el = document.getElementById(containerId);
if (!el) throw new Error(`Container ${containerId} not found`);
this.container = el;
this.isMobile = isMobileDevice();
this.monitors = loadFromStorage<Monitor[]>(STORAGE_KEYS.monitors, []);
this.panelSettings = loadFromStorage<Record<string, PanelConfig>>(
STORAGE_KEYS.panels,
DEFAULT_PANELS
);
this.mapLayers = loadFromStorage<MapLayers>(STORAGE_KEYS.mapLayers, DEFAULT_MAP_LAYERS);
// Use mobile-specific defaults on first load (no saved layers)
const defaultLayers = this.isMobile ? MOBILE_DEFAULT_MAP_LAYERS : DEFAULT_MAP_LAYERS;
this.mapLayers = loadFromStorage<MapLayers>(STORAGE_KEYS.mapLayers, defaultLayers);
this.initialUrlState = parseMapUrlState(window.location.search, this.mapLayers);
if (this.initialUrlState.layers) {
this.mapLayers = this.initialUrlState.layers;
@@ -561,11 +574,12 @@ export class App {
const panelsGrid = document.getElementById('panelsGrid')!;
// Initialize map in the map section
// Default to MENA view on mobile for better focus
const mapContainer = document.getElementById('mapContainer') as HTMLElement;
this.map = new MapComponent(mapContainer, {
zoom: 1.5,
zoom: this.isMobile ? 2.5 : 1.5,
pan: { x: 0, y: 0 },
view: 'global',
view: this.isMobile ? 'mena' : 'global',
layers: this.mapLayers,
timeRange: '7d',
});
@@ -678,6 +692,12 @@ export class App {
this.applyPanelSettings();
this.applyInitialUrlState();
// Set correct view button state (especially for mobile defaults)
const currentView = this.map?.getState().view;
if (currentView) {
this.setActiveViewButton(currentView);
}
}
private applyInitialUrlState(): void {
+15 -5
View File
@@ -33,11 +33,21 @@ export class MapPopup {
const content = this.renderContent(data);
this.popup.innerHTML = content;
// Position popup
const maxX = this.container.clientWidth - 400;
const maxY = this.container.clientHeight - 300;
this.popup.style.left = `${Math.min(data.x + 20, maxX)}px`;
this.popup.style.top = `${Math.min(data.y - 20, maxY)}px`;
// Detect mobile/touch devices
const isMobile = window.innerWidth < 768 || window.matchMedia('(pointer: coarse)').matches;
if (isMobile) {
// On mobile, center the popup horizontally and position in lower half
this.popup.style.left = '50%';
this.popup.style.transform = 'translateX(-50%)';
this.popup.style.top = `${Math.max(60, Math.min(data.y, this.container.clientHeight * 0.4))}px`;
} else {
// Desktop: position near click with bounds checking
const maxX = this.container.clientWidth - 400;
const maxY = this.container.clientHeight - 300;
this.popup.style.left = `${Math.min(data.x + 20, maxX)}px`;
this.popup.style.top = `${Math.min(data.y - 20, maxY)}px`;
}
this.container.appendChild(this.popup);
+5 -5
View File
@@ -9,19 +9,19 @@ export class MobileWarningModal {
this.element.innerHTML = `
<div class="mobile-warning-modal">
<div class="mobile-warning-header">
<span class="mobile-warning-icon">🖥️</span>
<span class="mobile-warning-title">Desktop Recommended</span>
<span class="mobile-warning-icon">📱</span>
<span class="mobile-warning-title">Mobile View</span>
</div>
<div class="mobile-warning-content">
<p>This dashboard is optimized for desktop viewing. Some features may not work as expected on mobile devices.</p>
<p>For the best experience, please visit on a desktop or laptop computer.</p>
<p>You're viewing a simplified mobile version focused on MENA region with essential layers enabled.</p>
<p>Tip: Use the view buttons (GLOBAL/US/MENA) to switch regions. Tap markers to see details.</p>
</div>
<div class="mobile-warning-footer">
<label class="mobile-warning-remember">
<input type="checkbox" id="mobileWarningRemember">
<span>Don't show again</span>
</label>
<button class="mobile-warning-btn">Continue Anyway</button>
<button class="mobile-warning-btn">Got it</button>
</div>
</div>
`;
+23
View File
@@ -43,6 +43,29 @@ export const DEFAULT_MAP_LAYERS: MapLayers = {
military: false,
};
// Mobile-specific defaults: minimal layers for better usability
export const MOBILE_DEFAULT_MAP_LAYERS: MapLayers = {
conflicts: true,
bases: false,
cables: false,
pipelines: false,
hotspots: true,
ais: false,
nuclear: false,
irradiators: false,
sanctions: false,
earthquakes: false,
weather: false,
economic: false,
countries: false,
waterways: false,
outages: true,
datacenters: false,
protests: false,
flights: false,
military: false,
};
export const MONITOR_COLORS = [
'#44ff88',
'#ff8844',
+192
View File
@@ -4529,3 +4529,195 @@ body.playback-mode .status-dot {
.mobile-warning-btn:active {
transform: translateY(0);
}
/* ==========================================================================
Mobile Touch Optimization
========================================================================== */
/* Touch device detection - applies to both small screens and touch devices */
@media (pointer: coarse), (max-width: 768px) {
/* Expand touch targets for all map markers using ::before pseudo-element */
/* This creates an invisible touch area around small markers */
.hotspot::before,
.base-marker::before,
.earthquake-marker::before,
.nuclear-marker::before,
.economic-marker::before,
.datacenter-marker::before,
.protest-marker::before,
.flight-marker::before,
.outage-marker::before,
.weather-marker::before,
.military-flight-marker::before,
.military-vessel-marker::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
border-radius: 50%;
/* Uncomment to debug: background: rgba(255, 0, 0, 0.2); */
}
/* Ensure hotspot container is large enough for touch */
.hotspot {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Increase marker visual sizes slightly for better visibility */
.hotspot-marker {
width: 16px;
height: 16px;
}
.base-marker {
width: 12px;
height: 12px;
}
/* Add spacing to prevent marker overlap on dense areas */
.hotspot,
.base-marker,
.nuclear-marker,
.economic-marker {
z-index: 50;
}
/* Ensure labels don't interfere with touch targets */
.hotspot-label,
.base-label,
.earthquake-label,
.nuclear-label,
.economic-label {
pointer-events: none;
}
/* Make layer toggle buttons larger for touch */
.layer-toggle {
padding: 8px 12px;
font-size: 10px;
min-height: 36px;
min-width: 60px;
}
.layer-toggles {
gap: 6px;
flex-wrap: wrap;
padding: 8px;
}
/* Larger map controls */
.map-control-btn {
width: 44px;
height: 44px;
font-size: 20px;
}
/* Time slider buttons */
.time-btn {
padding: 8px 12px;
min-height: 36px;
}
/* Map popup positioning for mobile - ensure it's visible */
.map-popup {
max-width: calc(100vw - 32px);
max-height: 60vh;
overflow-y: auto;
}
/* Hide some UI elements that clutter mobile view */
.map-timestamp {
font-size: 9px;
}
.map-legend {
display: none;
}
/* Reduce header clutter on mobile */
.header-center {
gap: 4px;
}
.view-btn {
padding: 6px 10px;
font-size: 10px;
min-height: 32px;
}
/* Hide search button keyboard shortcut on mobile */
.search-btn kbd {
display: none;
}
/* Simplify layer help popup for mobile */
.layer-help-popup {
max-width: calc(100vw - 20px);
max-height: 70vh;
}
/* Ensure conflict click areas are large enough */
.conflict-click-area {
cursor: pointer;
pointer-events: auto;
}
}
/* Extra small screens */
@media (max-width: 480px) {
.layer-toggles {
max-height: 80px;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
}
.layer-toggle {
flex-shrink: 0;
padding: 6px 10px;
font-size: 9px;
min-width: 50px;
}
.map-popup {
left: 10px !important;
right: 10px !important;
width: auto !important;
max-width: none;
}
/* Stack header on very small screens */
.header {
flex-wrap: wrap;
padding: 8px;
}
.header-left,
.header-center,
.header-right {
flex-basis: 100%;
justify-content: center;
}
.header-center {
order: -1;
margin-bottom: 8px;
}
/* Hide non-essential header items */
.copy-link-btn,
.time-display {
display: none;
}
}