diff --git a/src/App.jsx b/src/App.jsx
index b10ab3b..8a5f05e 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -572,6 +572,439 @@ const App = () => {
35
+ ) : config.layout === 'tablet' ? (
+ /* TABLET LAYOUT - Optimized for 7-10" widescreen displays (16:9) */
+
{
+ // Configuration state - initially use defaults, then load from server
+ const [config, setConfig] = useState(loadConfig);
+ const [configLoaded, setConfigLoaded] = useState(false);
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [startTime] = useState(Date.now());
+ const [uptime, setUptime] = useState('0d 0h 0m');
+
+ // Load server configuration on startup (only matters for first-time users)
+ useEffect(() => {
+ const initConfig = async () => {
+ // Fetch server config (provides defaults for new users without localStorage)
+ await fetchServerConfig();
+
+ // Load config - localStorage takes priority over server config
+ const loadedConfig = loadConfig();
+ setConfig(loadedConfig);
+ setConfigLoaded(true);
+
+ // Only show settings if user has no saved config AND no valid callsign
+ // This prevents the popup from appearing every refresh
+ const hasLocalStorage = localStorage.getItem('openhamclock_config');
+ if (!hasLocalStorage && loadedConfig.callsign === 'N0CALL') {
+ setShowSettings(true);
+ }
+ };
+ initConfig();
+ }, []);
+
+ // DX Location with localStorage persistence
+ const [dxLocation, setDxLocation] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_dxLocation');
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ if (parsed.lat && parsed.lon) return parsed;
+ }
+ } catch (e) {}
+ return config.defaultDX;
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_dxLocation', JSON.stringify(dxLocation));
+ } catch (e) {}
+ }, [dxLocation]);
+
+ // UI state
+ const [showSettings, setShowSettings] = useState(false);
+ const [showDXFilters, setShowDXFilters] = useState(false);
+ const [showPSKFilters, setShowPSKFilters] = useState(false);
+ const [weatherExpanded, setWeatherExpanded] = useState(() => {
+ try { return localStorage.getItem('openhamclock_weatherExpanded') === 'true'; } catch { return false; }
+ });
+ const [tempUnit, setTempUnit] = useState(() => {
+ try { return localStorage.getItem('openhamclock_tempUnit') || 'F'; } catch { return 'F'; }
+ });
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ // Map layer visibility
+ const [mapLayers, setMapLayers] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_mapLayers');
+ const defaults = { showDXPaths: true, showDXLabels: true, showPOTA: true, showSatellites: false, showPSKReporter: true, showWSJTX: true };
+ return stored ? { ...defaults, ...JSON.parse(stored) } : defaults;
+ } catch (e) { return { showDXPaths: true, showDXLabels: true, showPOTA: true, showSatellites: false, showPSKReporter: true, showWSJTX: true }; }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_mapLayers', JSON.stringify(mapLayers));
+ } catch (e) {}
+ }, [mapLayers]);
+
+ const [hoveredSpot, setHoveredSpot] = useState(null);
+
+ const toggleDXPaths = useCallback(() => setMapLayers(prev => ({ ...prev, showDXPaths: !prev.showDXPaths })), []);
+ const toggleDXLabels = useCallback(() => setMapLayers(prev => ({ ...prev, showDXLabels: !prev.showDXLabels })), []);
+ const togglePOTA = useCallback(() => setMapLayers(prev => ({ ...prev, showPOTA: !prev.showPOTA })), []);
+ const toggleSatellites = useCallback(() => setMapLayers(prev => ({ ...prev, showSatellites: !prev.showSatellites })), []);
+ const togglePSKReporter = useCallback(() => setMapLayers(prev => ({ ...prev, showPSKReporter: !prev.showPSKReporter })), []);
+ const toggleWSJTX = useCallback(() => setMapLayers(prev => ({ ...prev, showWSJTX: !prev.showWSJTX })), []);
+
+ // 12/24 hour format
+ const [use12Hour, setUse12Hour] = useState(() => {
+ try {
+ return localStorage.getItem('openhamclock_use12Hour') === 'true';
+ } catch (e) { return false; }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_use12Hour', use12Hour.toString());
+ } catch (e) {}
+ }, [use12Hour]);
+
+ const handleTimeFormatToggle = useCallback(() => setUse12Hour(prev => !prev), []);
+
+ // Fullscreen
+ const handleFullscreenToggle = useCallback(() => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen().then(() => setIsFullscreen(true)).catch(() => {});
+ } else {
+ document.exitFullscreen().then(() => setIsFullscreen(false)).catch(() => {});
+ }
+ }, []);
+
+ useEffect(() => {
+ const handler = () => setIsFullscreen(!!document.fullscreenElement);
+ document.addEventListener('fullscreenchange', handler);
+ return () => document.removeEventListener('fullscreenchange', handler);
+ }, []);
+
+ useEffect(() => {
+ applyTheme(config.theme || 'dark');
+ }, []);
+
+ // Config save handler - persists to localStorage
+ const handleSaveConfig = (newConfig) => {
+ setConfig(newConfig);
+ saveConfig(newConfig);
+ applyTheme(newConfig.theme || 'dark');
+ console.log('[Config] Saved to localStorage:', newConfig.callsign);
+ };
+
+ // Data hooks
+ const spaceWeather = useSpaceWeather();
+ const bandConditions = useBandConditions(spaceWeather.data);
+ const solarIndices = useSolarIndices();
+ const potaSpots = usePOTASpots();
+
+ // DX Filters
+ const [dxFilters, setDxFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_dxFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) { return {}; }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_dxFilters', JSON.stringify(dxFilters));
+ } catch (e) {}
+ }, [dxFilters]);
+
+ // PSKReporter Filters
+ const [pskFilters, setPskFilters] = useState(() => {
+ try {
+ const stored = localStorage.getItem('openhamclock_pskFilters');
+ return stored ? JSON.parse(stored) : {};
+ } catch (e) { return {}; }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem('openhamclock_pskFilters', JSON.stringify(pskFilters));
+ } catch (e) {}
+ }, [pskFilters]);
+
+ const dxCluster = useDXCluster(config.dxClusterSource || 'auto', dxFilters);
+ const dxPaths = useDXPaths();
+ const dxpeditions = useDXpeditions();
+ const contests = useContests();
+ const propagation = usePropagation(config.location, dxLocation);
+ const mySpots = useMySpots(config.callsign);
+ const satellites = useSatellites(config.location);
+ const localWeather = useLocalWeather(config.location, tempUnit);
+ const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' });
+ const wsjtx = useWSJTX();
+
+ // Filter PSKReporter spots for map display
+ const filteredPskSpots = useMemo(() => {
+ const allSpots = [...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])];
+ if (!pskFilters?.bands?.length && !pskFilters?.grids?.length && !pskFilters?.modes?.length) {
+ return allSpots;
+ }
+ return allSpots.filter(spot => {
+ if (pskFilters?.bands?.length && !pskFilters.bands.includes(spot.band)) return false;
+ if (pskFilters?.modes?.length && !pskFilters.modes.includes(spot.mode)) return false;
+ if (pskFilters?.grids?.length) {
+ const grid = spot.receiverGrid || spot.senderGrid;
+ if (!grid) return false;
+ const gridPrefix = grid.substring(0, 2).toUpperCase();
+ if (!pskFilters.grids.includes(gridPrefix)) return false;
+ }
+ return true;
+ });
+ }, [pskReporter.txReports, pskReporter.rxReports, pskFilters]);
+
+ // Filter WSJT-X decodes for map display (only those with lat/lon from grid)
+ const wsjtxMapSpots = useMemo(() => {
+ return wsjtx.decodes.filter(d => d.lat && d.lon && d.type === 'CQ');
+ }, [wsjtx.decodes]);
+
+ // Computed values
+ const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]);
+ const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]);
+ const deSunTimes = useMemo(() => calculateSunTimes(config.location.lat, config.location.lon, currentTime), [config.location, currentTime]);
+ const dxSunTimes = useMemo(() => calculateSunTimes(dxLocation.lat, dxLocation.lon, currentTime), [dxLocation, currentTime]);
+
+ // Time update
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ const elapsed = Date.now() - startTime;
+ const d = Math.floor(elapsed / 86400000);
+ const h = Math.floor((elapsed % 86400000) / 3600000);
+ const m = Math.floor((elapsed % 3600000) / 60000);
+ setUptime(`${d}d ${h}h ${m}m`);
+ }, 1000);
+ return () => clearInterval(timer);
+ }, [startTime]);
+
+ const handleDXChange = useCallback((coords) => {
+ setDxLocation({ lat: coords.lat, lon: coords.lon });
+ }, []);
+
+ // Format times — use explicit timezone if configured (fixes privacy browsers like Librewolf
+ // that spoof timezone to UTC via privacy.resistFingerprinting)
+ const utcTime = currentTime.toISOString().substr(11, 8);
+ const utcDate = currentTime.toISOString().substr(0, 10);
+ const localTimeOpts = { hour12: use12Hour };
+ const localDateOpts = { weekday: 'short', month: 'short', day: 'numeric' };
+ if (config.timezone) {
+ localTimeOpts.timeZone = config.timezone;
+ localDateOpts.timeZone = config.timezone;
+ }
+ const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts);
+ const localDate = currentTime.toLocaleDateString('en-US', localDateOpts);
+
+ // Scale for small screens
+ const [scale, setScale] = useState(1);
+ useEffect(() => {
+ const calculateScale = () => {
+ const minWidth = 1200;
+ const minHeight = 800;
+ const scaleX = window.innerWidth / minWidth;
+ const scaleY = window.innerHeight / minHeight;
+ setScale(Math.min(scaleX, scaleY, 1));
+ };
+ calculateScale();
+ window.addEventListener('resize', calculateScale);
+ return () => window.removeEventListener('resize', calculateScale);
+ }, []);
+
+ return (
+
+ {config.layout === 'classic' ? (
+ /* CLASSIC HAMCLOCK-STYLE LAYOUT */
+
+ {/* TOP BAR - HamClock style */}
+
+ {/* Callsign & Time */}
+
+
setShowSettings(true)}
+ title="Click for settings"
+ >
+ {config.callsign}
+
+
+ Up 35d 18h • v4.20
+
+
+
+ {utcTime}:{String(new Date().getUTCSeconds()).padStart(2, '0')}
+
+
+ {utcDate} UTC
+
+
+
+
+ {/* Solar Indices - SSN & SFI */}
+
+ {/* SSN */}
+
+
Sunspot Number
+
+
+ {solarIndices?.data?.ssn?.history?.length > 0 && (
+
+ )}
+
+
+ {solarIndices?.data?.ssn?.current || '--'}
+
+
+
-30 Days
+
+
+ {/* SFI */}
+
+
10.7 cm Solar flux
+
+
+ {solarIndices?.data?.sfi?.history?.length > 0 && (
+
+ )}
+
+
+ {solarIndices?.data?.sfi?.current || '--'}
+
+
+
-30 Days +7
+
+
+
+ {/* Live Spots & Indices */}
+
+ {/* Live Spots by Band */}
+
+
Live Spots
+
of {deGrid} - 15 mins
+
+ {[
+ { band: '160m', color: '#ff6666' },
+ { band: '80m', color: '#ff9966' },
+ { band: '60m', color: '#ffcc66' },
+ { band: '40m', color: '#ccff66' },
+ { band: '30m', color: '#66ff99' },
+ { band: '20m', color: '#66ffcc' },
+ { band: '17m', color: '#66ccff' },
+ { band: '15m', color: '#6699ff' },
+ { band: '12m', color: '#9966ff' },
+ { band: '10m', color: '#cc66ff' },
+ ].map(b => (
+
+ {b.band}
+
+ {dxCluster.data?.filter(s => {
+ const freq = parseFloat(s.freq);
+ const bands = {
+ '160m': [1.8, 2], '80m': [3.5, 4], '60m': [5.3, 5.4], '40m': [7, 7.3],
+ '30m': [10.1, 10.15], '20m': [14, 14.35], '17m': [18.068, 18.168],
+ '15m': [21, 21.45], '12m': [24.89, 24.99], '10m': [28, 29.7]
+ };
+ const r = bands[b.band];
+ return r && freq >= r[0] && freq <= r[1];
+ }).length || 0}
+
+
+ ))}
+
+
+
+ {/* Space Weather Indices */}
+
+
+
+
Kp
+
{solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'}
+
+
+
+
+
+
+
+ {/* MAIN AREA */}
+
+ {/* DX Cluster List */}
+
+
+ Cluster
+ dxspider.co.uk:7300
+
+
+ {dxCluster.data?.slice(0, 25).map((spot, i) => (
+
setHoveredSpot(spot)}
+ onMouseLeave={() => setHoveredSpot(null)}
+ >
+ {parseFloat(spot.freq).toFixed(1)}
+ {spot.call}
+ {spot.time || '--'}
+
+ ))}
+
+
+
+ {/* Map */}
+
+
+
+ {/* Settings button overlay */}
+
+
+
+
+ {/* BOTTOM - Frequency Scale */}
+
+ MHz
+ 5
+ 10
+ 15
+ 20
+ 25
+ 30
+ 35
+
+
+ ) : (
+ /* MODERN LAYOUT */
+
+ {/* TOP BAR */}
+
setShowSettings(true)}
+ onFullscreenToggle={handleFullscreenToggle}
+ isFullscreen={isFullscreen}
+ />
+
+ {/* LEFT SIDEBAR */}
+
+ {/* DE Location + Weather */}
+
+
📍 DE - YOUR LOCATION
+
+
{deGrid}
+
{config.location.lat.toFixed(4)}°, {config.location.lon.toFixed(4)}°
+
+ ☀
+ {deSunTimes.sunrise}
+ →
+ {deSunTimes.sunset}
+
+
+
+ {/* Local Weather — compact by default, click to expand */}
+ {localWeather.data && (() => {
+ const w = localWeather.data;
+ const deg = `°${w.tempUnit || tempUnit}`;
+ const wind = w.windUnit || 'mph';
+ const vis = w.visUnit || 'mi';
+ return (
+
+ {/* Compact summary row — always visible */}
+
+
{ const next = !weatherExpanded; setWeatherExpanded(next); try { localStorage.setItem('openhamclock_weatherExpanded', next.toString()); } catch {} }}
+ style={{
+ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
+ userSelect: 'none', flex: 1, minWidth: 0,
+ }}
+ >
+ {w.icon}
+
+ {w.temp}{deg}
+
+ {w.description}
+
+ 💨{w.windSpeed}
+
+ ▼
+
+ {/* F/C toggle */}
+
+
+
+ {/* Expanded details */}
+ {weatherExpanded && (
+
+ {/* Feels like + hi/lo */}
+
+ {w.feelsLike !== w.temp && (
+ Feels like {w.feelsLike}{deg}
+ )}
+ {w.todayHigh != null && (
+
+ ▲{w.todayHigh}°
+ {' '}
+ ▼{w.todayLow}°
+
+ )}
+
+
+ {/* Detail grid */}
+
+
+ 💨 Wind
+ {w.windDir} {w.windSpeed} {wind}
+
+
+ 💧 Humidity
+ {w.humidity}%
+
+ {w.windGusts > w.windSpeed + 5 && (
+
+ 🌬️ Gusts
+ {w.windGusts} {wind}
+
+ )}
+
+ 🌡️ Dew Pt
+ {w.dewPoint}{deg}
+
+ {w.pressure && (
+
+ 🔵 Pressure
+ {w.pressure} hPa
+
+ )}
+
+ ☁️ Clouds
+ {w.cloudCover}%
+
+ {w.visibility && (
+
+ 👁️ Vis
+ {w.visibility} {vis}
+
+ )}
+ {w.uvIndex > 0 && (
+
+ ☀️ UV
+ = 8 ? '#ef4444' : w.uvIndex >= 6 ? '#f97316' : w.uvIndex >= 3 ? '#eab308' : 'var(--text-secondary)' }}>
+ {w.uvIndex.toFixed(1)}
+
+
+ )}
+
+
+ {/* 3-Day Forecast */}
+ {w.daily?.length > 0 && (
+
+
FORECAST
+
+ {w.daily.map((day, i) => (
+
+
{i === 0 ? 'Today' : day.date}
+
{day.icon}
+
+ {day.high}°
+ /
+ {day.low}°
+
+ {day.precipProb > 0 && (
+
+ 💧{day.precipProb}%
+
+ )}
+
+ ))}
+
+
+ )}
+
+ )}
+
+ );
+ })()}
+
+
+ {/* DX Location */}
+
+
🎯 DX - TARGET
+
+
{dxGrid}
+
{dxLocation.lat.toFixed(4)}°, {dxLocation.lon.toFixed(4)}°
+
+ ☀
+ {dxSunTimes.sunrise}
+ →
+ {dxSunTimes.sunset}
+
+
+
+
+ {/* Solar Panel */}
+
+
+ {/* VOACAP/Propagation Panel */}
+
+
+
+ {/* CENTER - MAP */}
+
+
+
+ Click map to set DX • 73 de {config.callsign}
+
+
+
+ {/* RIGHT SIDEBAR */}
+
+ {/* DX Cluster - primary panel, takes most space */}
+
+ setShowDXFilters(true)}
+ onHoverSpot={setHoveredSpot}
+ hoveredSpot={hoveredSpot}
+ showOnMap={mapLayers.showDXPaths}
+ onToggleMap={toggleDXPaths}
+ />
+
+
+ {/* PSKReporter + WSJT-X - digital mode spots */}
+
+
setShowPSKFilters(true)}
+ onShowOnMap={(report) => {
+ if (report.lat && report.lon) {
+ setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender });
+ }
+ }}
+ wsjtxDecodes={wsjtx.decodes}
+ wsjtxClients={wsjtx.clients}
+ wsjtxQsos={wsjtx.qsos}
+ wsjtxStats={wsjtx.stats}
+ wsjtxLoading={wsjtx.loading}
+ wsjtxEnabled={wsjtx.enabled}
+ wsjtxPort={wsjtx.port}
+ wsjtxRelayEnabled={wsjtx.relayEnabled}
+ wsjtxRelayConnected={wsjtx.relayConnected}
+ wsjtxSessionId={wsjtx.sessionId}
+ showWSJTXOnMap={mapLayers.showWSJTX}
+ onToggleWSJTXMap={toggleWSJTX}
+ />
+
+
+ {/* DXpeditions */}
+
+
+
+
+ {/* POTA */}
+
+
+ {/* Contests - at bottom, compact */}
+
+
+
+
+
+ )}
+
+ {/* Modals */}
+
setShowSettings(false)}
+ config={config}
+ onSave={handleSaveConfig}
+ />
+ setShowDXFilters(false)}
+ />
+ setShowPSKFilters(false)}
+ />
+
+ );
+};
+
+export default App;
diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx
index 54bb296..d10eb61 100644
--- a/src/components/SettingsPanel.jsx
+++ b/src/components/SettingsPanel.jsx
@@ -176,7 +176,9 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const layoutDescriptions = {
modern: t('station.settings.layout.modern.describe'),
- classic: t('station.settings.layout.classic.describe')
+ classic: t('station.settings.layout.classic.describe'),
+ tablet: t('station.settings.layout.tablet.describe'),
+ compact: t('station.settings.layout.compact.describe')
};
return (
@@ -461,7 +463,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{t('station.settings.layout')}
- {['modern', 'classic'].map((l) => (
+ {['modern', 'classic', 'tablet', 'compact'].map((l) => (
))}
diff --git a/src/lang/de.json b/src/lang/de.json
index 7f23a69..bd17759 100644
--- a/src/lang/de.json
+++ b/src/lang/de.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "Live-USGS-Erdbebendaten (M2,5+ der letzten 24 Stunden)",
"plugins.layers.wxradar.name": "Wetterradar",
"plugins.layers.wxradar.description": "NEXRAD-Wetterradar-Überlagerung für Nordamerika",
- "plugins.layers.wxradar.attribution": "Wetterdaten © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Wetterdaten © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/en.json b/src/lang/en.json
index 49a03bd..08a0d2a 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -28,6 +28,10 @@
"station.settings.layout.classic.describe": "→ Original HamClock-style layout",
"station.settings.layout.modern": "Modern",
"station.settings.layout.modern.describe": "→ Modern responsive grid layout",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens",
"station.settings.latitude": "Latitude",
"station.settings.locator": "Grid Square (or enter Lat/Lon below)",
"station.settings.longitude": "Longitude",
diff --git a/src/lang/es.json b/src/lang/es.json
index 4bda44d..dcd99e0 100644
--- a/src/lang/es.json
+++ b/src/lang/es.json
@@ -54,5 +54,9 @@
"plugins.layers.earthquakes.description": "Datos sísmicos en vivo del USGS (M2.5+ de las últimas 24 horas)",
"plugins.layers.wxradar.name": "Radar meteorológico",
"plugins.layers.wxradar.description": "Superposición del radar meteorológico NEXRAD para Norteamérica",
- "plugins.layers.wxradar.attribution": "Datos meteorológicos © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Datos meteorológicos © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/fr.json b/src/lang/fr.json
index cae97fc..5d33094 100644
--- a/src/lang/fr.json
+++ b/src/lang/fr.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "Données sismiques USGS en direct (M2,5+ sur les dernières 24 heures)",
"plugins.layers.wxradar.name": "Radar météo",
"plugins.layers.wxradar.description": "Surcouche du radar météo NEXRAD pour l’Amérique du Nord",
- "plugins.layers.wxradar.attribution": "Données météo © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Données météo © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/it.json b/src/lang/it.json
index b18d457..d2d24fa 100644
--- a/src/lang/it.json
+++ b/src/lang/it.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "Dati sismici USGS in tempo reale (M2,5+ delle ultime 24 ore)",
"plugins.layers.wxradar.name": "Radar meteorologico",
"plugins.layers.wxradar.description": "Sovrapposizione del radar meteorologico NEXRAD per il Nord America",
- "plugins.layers.wxradar.attribution": "Dati meteo © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Dati meteo © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/ja.json b/src/lang/ja.json
index 59d4447..1407a08 100644
--- a/src/lang/ja.json
+++ b/src/lang/ja.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "USGSのリアルタイム地震データ(過去24時間のM2.5以上)",
"plugins.layers.wxradar.name": "気象レーダー",
"plugins.layers.wxradar.description": "北米向けNEXRAD気象レーダーのオーバーレイ",
- "plugins.layers.wxradar.attribution": "気象データ © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "気象データ © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/ko.json b/src/lang/ko.json
index 8d703c0..21db534 100644
--- a/src/lang/ko.json
+++ b/src/lang/ko.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "USGS 실시간 지진 데이터 (지난 24시간 동안 일어난 규모 M2.5 이상의 지진)",
"plugins.layers.wxradar.name": "기상 레이더",
"plugins.layers.wxradar.description": "북아메리카 지역 NEXRAD 기상 레이더 오버레이",
- "plugins.layers.wxradar.attribution": "기상 데이터 © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "기상 데이터 © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/nl.json b/src/lang/nl.json
index b45fcb5..b46b5d9 100644
--- a/src/lang/nl.json
+++ b/src/lang/nl.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "Live USGS-aardbevingsgegevens (M2,5+ van de afgelopen 24 uur)",
"plugins.layers.wxradar.name": "Weerradar",
"plugins.layers.wxradar.description": "NEXRAD-weerradaroverlay voor Noord-Amerika",
- "plugins.layers.wxradar.attribution": "Weergegevens © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Weergegevens © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/lang/pt.json b/src/lang/pt.json
index 8091927..4cd8cb2 100644
--- a/src/lang/pt.json
+++ b/src/lang/pt.json
@@ -53,5 +53,9 @@
"plugins.layers.earthquakes.description": "Dados sísmicos do USGS ao vivo (M2,5+ das últimas 24 horas)",
"plugins.layers.wxradar.name": "Radar meteorológico",
"plugins.layers.wxradar.description": "Sobreposição do radar meteorológico NEXRAD para a América do Norte",
- "plugins.layers.wxradar.attribution": "Dados meteorológicos © Iowa State University Mesonet"
+ "plugins.layers.wxradar.attribution": "Dados meteorológicos © Iowa State University Mesonet",
+ "station.settings.layout.tablet": "Tablet",
+ "station.settings.layout.tablet.describe": "→ Optimized for 7-10\" widescreen displays (16:9)",
+ "station.settings.layout.compact": "Compact",
+ "station.settings.layout.compact.describe": "→ Data-first layout for 4:3 and smaller screens"
}
diff --git a/src/styles/main.css b/src/styles/main.css
index ad8b710..eda3aa7 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -905,3 +905,105 @@ body::before {
position: relative;
z-index: 10000 !important;
}
+
+/* ============================================
+ RESPONSIVE: Phone & Small Screen Overrides
+ ============================================ */
+
+/* Tablets and small screens (under 1024px) */
+@media (max-width: 1024px) {
+ .panel {
+ padding: 6px !important;
+ border-radius: 4px !important;
+ }
+
+ .panel-header {
+ font-size: 11px !important;
+ padding: 4px 6px !important;
+ }
+}
+
+/* Phone landscape and small tablets (under 768px) */
+@media (max-width: 768px) {
+ /* Force single-column on modern layout */
+ .ohc-mobile-stack {
+ display: flex !important;
+ flex-direction: column !important;
+ grid-template-columns: unset !important;
+ }
+
+ .panel {
+ padding: 4px !important;
+ margin-bottom: 2px !important;
+ border-radius: 3px !important;
+ }
+
+ .panel-header {
+ font-size: 10px !important;
+ padding: 3px 6px !important;
+ }
+
+ /* Prevent horizontal overflow */
+ body {
+ overflow-x: hidden;
+ }
+}
+
+/* Phone portrait (under 480px) */
+@media (max-width: 480px) {
+ /* Extra compact for phones */
+ .panel {
+ padding: 3px !important;
+ font-size: 11px !important;
+ }
+
+ .panel-header {
+ font-size: 9px !important;
+ }
+
+ /* Reduce font sizes globally for very small screens */
+ body {
+ font-size: 12px;
+ }
+}
+
+/* Short screens (Pi 7" touchscreen at 480px height) */
+@media (max-height: 520px) {
+ .panel {
+ padding: 3px !important;
+ }
+
+ .panel-header {
+ padding: 2px 4px !important;
+ font-size: 10px !important;
+ }
+}
+
+/* Touch device improvements */
+@media (pointer: coarse) {
+ /* Larger tap targets for touchscreens */
+ button {
+ min-height: 36px;
+ min-width: 36px;
+ }
+
+ /* Prevent text selection on tap */
+ .panel-header {
+ user-select: none;
+ -webkit-user-select: none;
+ }
+
+ /* Scrollbar styling for touch */
+ ::-webkit-scrollbar {
+ width: 4px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: 2px;
+ }
+}