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:
+23
-3
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user