Landscape kiosk overhaul: 3-column layout, resilient updates, visual polish
- Add 3-column balanced site distribution using greedy weight algorithm - Build new DOM off-screen in DocumentFragment, swap atomically (no flash) - Skip empty API responses and preserve display on transient errors - Remove news ticker from UI and grid layout - Add blue-to-red gradient on site header bars - Bump font sizes: destinations 1.4em, countdowns 1.5em, line numbers 1.6em - Add breathing pulse animation on daylight bar sun/moon icons - Fix daylight bar indicator snapping to position on first render - Make config button visible in landscape with semi-transparent background - Add weather forecast strip as grid row 4 with compact styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = `<span class="site-name">${site.siteName || siteConfig.name}</span>`;
|
||||
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 = `
|
||||
<div class="error">
|
||||
<p>Failed to load departures. Please try again later.</p>
|
||||
<p>Error: ${error.message}</p>
|
||||
<p>Make sure the Node.js server is running: <code>node server.js</code></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:', {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user