diff --git a/public/css/components.css b/public/css/components.css index 8d358ee..d817132 100644 --- a/public/css/components.css +++ b/public/css/components.css @@ -56,6 +56,12 @@ body.dark-mode .clock-container { opacity: 1; } +body.landscape .config-button { + background-color: rgba(255, 255, 255, 0.15); + opacity: 0.7; + bottom: 50px; +} + /* Configuration modal styles */ .config-modal { display: none; @@ -1046,16 +1052,16 @@ body.landscape .site-header { body.landscape .site-name { display: block; - background: rgba(0, 97, 161, 0.3); - color: var(--color-text-secondary); - font-weight: 600; + background: linear-gradient(90deg, rgba(0, 97, 161, 0.45), rgba(200, 30, 50, 0.45)); + color: #fff; + font-weight: 700; text-transform: uppercase; letter-spacing: 2px; - padding: 4px 16px; + padding: 5px 16px; border-radius: 2px; box-shadow: none; - font-size: 0.8em; - border-left: 3px solid var(--color-primary-light); + font-size: 0.95em; + border-left: 4px solid var(--color-accent); } /* Compact weather bar - hidden by default, shown in landscape */ @@ -1092,13 +1098,7 @@ body.landscape #compact-weather-bar { } body.landscape #news-ticker { - display: block; - overflow: hidden; - background: var(--ticker-bg); - height: var(--ticker-height); - line-height: var(--ticker-height); - border-radius: 4px; - position: relative; + display: none; } #news-ticker .ticker-content { @@ -1155,13 +1155,28 @@ body.landscape #news-ticker { pointer-events: none; } -#daylight-hours-bar .sun-icon { +#daylight-hours-bar .sun-icon, +#daylight-hours-bar .moon-icon { font-size: 18px; display: block; + animation: celestial-pulse 3s ease-in-out infinite; +} + +#daylight-hours-bar .sun-icon { filter: drop-shadow(0 0 4px rgba(255, 215, 0, 0.8)); text-shadow: 0 0 8px rgba(255, 215, 0, 0.6); } +#daylight-hours-bar .moon-icon { + filter: drop-shadow(0 0 4px rgba(180, 200, 255, 0.8)); + text-shadow: 0 0 8px rgba(180, 200, 255, 0.6); +} + +@keyframes celestial-pulse { + 0%, 100% { transform: scale(1); opacity: 0.85; } + 50% { transform: scale(1.3); opacity: 1; } +} + /* Landscape: daylight bar sits in grid instead of fixed overlay */ body.landscape #daylight-hours-bar { position: relative; diff --git a/public/css/main.css b/public/css/main.css index 7811bbc..7dac6a1 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -101,7 +101,7 @@ body { body.normal .departure-container { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 0; width: 100%; @@ -128,17 +128,6 @@ body { max-width: 100%; } - body.normal .direction-destination { - font-size: 1.1em; - } - - body.normal .countdown-large { - font-size: 1.4em; - } - - body.normal .next-departures { - font-size: 0.9em; - } } /* ======================================== @@ -185,7 +174,7 @@ body.landscape #background-overlay { body.landscape #content-wrapper { display: grid; grid-template-rows: auto auto 1fr auto auto; - gap: var(--kiosk-gap); + gap: 4px; height: 100vh; max-height: 100vh; overflow: hidden; @@ -196,9 +185,11 @@ body.landscape .clock-container { margin-bottom: 0; } -/* Compact weather bar sits in row 2 */ +/* Compact weather bar in row 2 */ body.landscape #compact-weather-bar { grid-row: 2; + font-size: 0.85em; + padding: 2px 16px; } body.landscape .main-content-grid { @@ -208,15 +199,62 @@ body.landscape .main-content-grid { min-height: 0; } -/* Hide the full weather widget in landscape */ +/* Weather forecast strip at bottom - row 4 */ body.landscape .weather-section { + grid-row: 4; + overflow: hidden; +} + +body.landscape .weather-container { + overflow: hidden; + max-height: none; + position: static; +} + +body.landscape #custom-weather { + background: var(--color-bar-bg); + border-radius: 4px; + padding: 2px 8px; +} + +body.landscape #custom-weather .current-weather { + display: none; +} + +body.landscape #custom-weather .forecast { + display: flex; + gap: 0; + overflow-x: auto; + overflow-y: hidden; + justify-content: center; +} + +body.landscape #custom-weather .forecast-hour { + flex-shrink: 0; + padding: 2px 8px; + min-width: auto; +} + +body.landscape #custom-weather .forecast-hour .time { + font-size: 0.65em; +} + +body.landscape #custom-weather .forecast-hour .icon img { + width: 20px; + height: 20px; +} + +body.landscape #custom-weather .forecast-hour .temp { + font-size: 0.65em; +} + +body.landscape #custom-weather .attribution { display: none; } body.landscape .departure-container { display: flex; - flex-direction: column; - gap: var(--kiosk-gap); + gap: 12px; overflow-y: auto; overflow-x: hidden; padding-right: 4px; @@ -224,49 +262,52 @@ body.landscape .departure-container { height: 100%; } -body.landscape .weather-container { - overflow-y: auto; - overflow-x: hidden; - max-height: 100%; - position: sticky; - top: 0; - align-self: start; -} - -body.landscape .departure-card { - min-height: 80px; - background-color: var(--color-surface-kiosk); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 4px; - width: 100%; - flex-shrink: 0; -} - -body.landscape .line-number-box { - min-width: 110px; - width: 110px; -} - -body.landscape .line-number-large { - font-size: 3em; +body.landscape .departure-column { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; } body.landscape .site-container { - margin-bottom: 6px; + margin-bottom: 0; +} + +body.landscape .departure-card { + min-height: 0; + background-color: var(--color-surface-kiosk); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 3px; + margin-bottom: 0; +} + +body.landscape .line-number-box { + min-width: 48px; + width: 48px; + padding: 2px; +} + +body.landscape .line-number-large { + font-size: 1.6em; +} + +body.landscape .transport-mode-icon .transport-icon { + width: 14px; + height: 14px; +} + +body.landscape .site-container { + margin-bottom: 2px; } body.landscape .site-header { - font-size: 1em; + font-size: 0.8em; padding: 0; - margin-bottom: 6px; + margin-bottom: 2px; } -/* News ticker sits in row 4 */ -body.landscape #news-ticker { - grid-row: 4; -} - -/* Daylight bar sits in row 5 */ +/* Daylight bar in row 5 */ body.landscape #daylight-hours-bar { grid-row: 5; } @@ -274,7 +315,6 @@ body.landscape #daylight-hours-bar { /* Dark card surfaces for landscape */ body.landscape .direction-destination { color: var(--color-text-light); - font-size: 1.3em; } body.landscape .countdown-large { @@ -285,32 +325,43 @@ body.landscape .next-departures { color: var(--color-text-secondary); } -/* Tighter card spacing in landscape */ +/* Compact card spacing in landscape */ body.landscape .directions-wrapper { - padding: 4px 8px; - gap: 2px; + padding: 3px 6px; + gap: 1px; } body.landscape .direction-row { - min-height: 28px; - gap: 6px; + min-height: 30px; + gap: 4px; } -/* Hero countdown in landscape */ +body.landscape .direction-destination { + font-size: 1.4em; + color: #eee; +} + +body.landscape .direction-arrow-box { + width: 22px; + height: 22px; + font-size: 0.85em; +} + +/* Countdown in landscape */ body.landscape .countdown-large { - font-size: 2.5em; + font-size: 1.5em; } body.landscape .times-container { - min-width: 200px; - max-width: 280px; + min-width: 90px; + max-width: 140px; } body.landscape .next-departures { - font-size: 1em; + font-size: 0.75em; color: var(--color-text-secondary); white-space: nowrap; - letter-spacing: 1px; + letter-spacing: 0.3px; } /* ======================================== diff --git a/public/js/components/DeparturesManager.js b/public/js/components/DeparturesManager.js index 7970649..8612840 100644 --- a/public/js/components/DeparturesManager.js +++ b/public/js/components/DeparturesManager.js @@ -507,33 +507,81 @@ class DeparturesManager { displayMultipleSites(sites) { const config = this.getConfig(); const enabledSites = config.sites.filter(site => site.enabled); - - this.container.innerHTML = ''; - + + // Build new content off-DOM first, then swap in one operation + const fragment = document.createDocumentFragment(); + + // Build site containers + const siteElements = []; sites.forEach(site => { const siteConfig = enabledSites.find(s => s.id === site.siteId); if (!siteConfig) return; - + + // Skip sites that returned empty departures (API hiccup) + // but keep sites with explicit errors so user sees feedback + if (site.data && site.data.departures && site.data.departures.length === 0 && !site.error) { + return; + } + const siteContainer = document.createElement('div'); siteContainer.className = 'site-container'; - + const siteHeader = document.createElement('div'); siteHeader.className = 'site-header'; - siteHeader.innerHTML = `${site.siteName || siteConfig.name}`; + const siteName = document.createElement('span'); + siteName.className = 'site-name'; + siteName.textContent = site.siteName || siteConfig.name; + siteHeader.appendChild(siteName); siteContainer.appendChild(siteHeader); - - if (site.data && site.data.departures) { + + let cardCount = 0; + if (site.data && site.data.departures && site.data.departures.length > 0) { const lineGroups = this.groupDeparturesByLineNumber(site.data.departures); this.displayGroupedDeparturesByLine(lineGroups, siteContainer); + cardCount = Object.keys(lineGroups).length; } else if (site.error) { const errorElement = document.createElement('div'); errorElement.className = 'error'; errorElement.textContent = `Error loading departures for ${site.siteName}: ${site.error}`; siteContainer.appendChild(errorElement); + cardCount = 1; } - - this.container.appendChild(siteContainer); + + siteElements.push({ element: siteContainer, weight: cardCount + 1 }); }); + + // If no sites have data at all, keep existing display (don't flash empty) + if (siteElements.length === 0) return; + + // In landscape mode, distribute sites into balanced columns + if (document.body.classList.contains('landscape') && siteElements.length > 1) { + const numCols = Math.min(3, siteElements.length); + const columns = []; + const weights = []; + for (let i = 0; i < numCols; i++) { + const col = document.createElement('div'); + col.className = 'departure-column'; + columns.push(col); + weights.push(0); + } + + // Greedy: assign each site to the lightest column + siteElements.forEach(({ element, weight }) => { + const minIdx = weights.indexOf(Math.min(...weights)); + columns[minIdx].appendChild(element); + weights[minIdx] += weight; + }); + + columns.forEach(col => fragment.appendChild(col)); + } else { + siteElements.forEach(({ element }) => { + fragment.appendChild(element); + }); + } + + // Swap old content for new in one operation (no flash) + this.container.textContent = ''; + this.container.appendChild(fragment); } /** @@ -576,14 +624,15 @@ class DeparturesManager { } else { console.error('Error fetching departures:', error); } - - this.container.innerHTML = ` -
-

Failed to load departures. Please try again later.

-

Error: ${error.message}

-

Make sure the Node.js server is running: node server.js

-
- `; + + // On transient errors, keep existing data on screen. + // Only show error if we have no data at all yet. + if (!this.container.children.length) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error'; + errorDiv.textContent = `Failed to load departures: ${error.message}`; + this.container.appendChild(errorDiv); + } } } diff --git a/public/js/components/WeatherManager.js b/public/js/components/WeatherManager.js index 6f1b068..cdfafce 100644 --- a/public/js/components/WeatherManager.js +++ b/public/js/components/WeatherManager.js @@ -714,7 +714,17 @@ class WeatherManager { } // Position current hour indicator - indicatorElement.style.left = `${currentPos}%`; + // On first render, skip transition so icon appears instantly at correct position + if (!this._daylightBarInitialized) { + indicatorElement.style.transition = 'none'; + indicatorElement.style.left = `${currentPos}%`; + // Force reflow, then re-enable transition for smooth minute-to-minute movement + indicatorElement.offsetLeft; + indicatorElement.style.transition = ''; + this._daylightBarInitialized = true; + } else { + indicatorElement.style.left = `${currentPos}%`; + } // Debug logging console.log('Daylight bar positions:', { diff --git a/public/js/main.js b/public/js/main.js index a7ad688..f529c76 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -78,8 +78,7 @@ document.addEventListener('DOMContentLoaded', async function() { lastUpdatedId: 'last-updated' }); - // Initialize NewsTicker (visible in landscape mode only via CSS) - window.newsTicker = new NewsTicker(); + // NewsTicker disabled - ticker removed from UI // Set up event listeners document.addEventListener('darkModeChanged', event => {