Compare commits

..

12 Commits

Author SHA1 Message Date
ba0cdbab64 Add all 5 transit stops to sites config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:07:51 +01:00
57cd9809e0 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>
2026-02-15 23:07:10 +01:00
5f60ed88c8 chore: remove unused legacy departure card styles and dead code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 20:04:09 +01:00
4a6012b097 feat: Swedish weather bar with wind speed display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 20:01:39 +01:00
f0b04a7a0d fix: use 2-column grid for normal mode on large screens
Change from 4-column to 2-column grid in the @media (min-width: 1200px)
block for body.normal, giving each departure card ~580px width on 1080p
monitors. Also add font-size increases for destination text (1.1em),
countdown numbers (1.4em), and next departures (0.9em) to improve
readability at the wider card size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 20:00:26 +01:00
84ce6efb2d fix: reduce clock and site header visual weight for better information hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:59:26 +01:00
92166cea6e feat: improve departure information hierarchy with clearer time display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:57:53 +01:00
98441bc906 feat: full-width departure rows for landscape kiosk mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:55:33 +01:00
3c9ae03cb6 fix: replace noisy striped direction arrows with clean circular indicators
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:53:56 +01:00
1b1460fd45 feat: translate all user-facing text to Swedish for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:51:49 +01:00
2b7fc6b016 refactor: consolidate hardcoded colors into CSS custom properties
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:49:39 +01:00
1e776c1c9a feat: unify typography with tabular-nums for stable number alignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:45:52 +01:00
6 changed files with 356 additions and 281 deletions

View File

@@ -1,8 +1,8 @@
{
"orientation": "landscape",
"darkMode": "auto",
"darkMode": "off",
"backgroundImage": "https://images.unsplash.com/photo-1509356843151-3e7d96241e11?q=80&w=1000",
"backgroundOpacity": 0.45,
"backgroundOpacity": 0.25,
"sites": [
{
"id": "1411",
@@ -23,6 +23,16 @@
"id": "1110",
"name": "Radiohuset",
"enabled": true
},
{
"id": "1007",
"name": "Cityterminalen (på Kungsbron)",
"enabled": true
},
{
"id": "9636",
"name": "Bråvallavägen",
"enabled": true
}
],
"combineSameDirection": true

View File

@@ -1,57 +1,25 @@
/* Clock styles - Modern entryway kiosk design with blue glow - Ribbon banner */
/* Clock styles - Subtle header clock */
.clock-container {
background: linear-gradient(135deg, #003366 0%, #004080 50%, #0059b3 100%);
color: var(--color-surface);
padding: 8px 20px;
border-radius: 8px;
background: linear-gradient(135deg, #003366 0%, #004d80 100%);
color: white;
padding: 6px 20px;
border-radius: 4px;
margin-bottom: 8px;
text-align: center;
box-shadow: 0 0 20px rgba(0, 89, 179, 0.6),
0 0 40px rgba(0, 89, 179, 0.4),
0 4px 12px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
transition: box-shadow 0.3s ease;
border: 2px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.clock-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.1) 100%);
pointer-events: none;
}
.clock-container:hover {
box-shadow: 0 0 30px rgba(0, 89, 179, 0.8),
0 0 60px rgba(0, 89, 179, 0.5),
0 4px 12px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: box-shadow 0.3s ease;
}
body.dark-mode .clock-container {
background: linear-gradient(135deg, #001a33 0%, #002d5c 50%, #004080 100%);
box-shadow: 0 0 25px rgba(0, 89, 179, 0.7),
0 0 50px rgba(0, 89, 179, 0.4),
0 4px 12px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
body.dark-mode .clock-container:hover {
box-shadow: 0 0 35px rgba(0, 89, 179, 0.9),
0 0 70px rgba(0, 89, 179, 0.6),
0 4px 12px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
background: linear-gradient(135deg, #001a33 0%, #002d5c 100%);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
/* Configuration button styles */
@@ -88,6 +56,12 @@ body.dark-mode .clock-container:hover {
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;
@@ -350,7 +324,7 @@ body.dark-mode .site-search-result div:first-child {
}
body.dark-mode .site-search-result div:last-child {
color: var(--color-text-muted-dark);
color: var(--color-text-secondary);
}
#search-site-button {
@@ -469,10 +443,20 @@ body.dark-mode #config-cancel-button:hover {
background-color: var(--color-text-muted);
}
/* Tabular figures for all numeric displays */
.clock-time,
.countdown-large,
.next-departures,
#custom-weather .temperature,
#custom-weather .forecast-hour .temp {
font-variant-numeric: tabular-nums;
font-family: var(--font-numbers);
}
.clock-time {
font-size: 2.2em;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
font-family: var(--font-numbers);
white-space: nowrap;
display: inline-block;
letter-spacing: 1px;
@@ -488,7 +472,7 @@ body.dark-mode #config-cancel-button:hover {
.clock-date {
font-size: 2.2em;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
font-family: var(--font-primary);
display: inline-block;
opacity: 0.98;
letter-spacing: 0.5px;
@@ -673,65 +657,27 @@ body.dark-mode .departure-card {
/* Direction arrow indicator */
.direction-arrow-box {
width: 32px;
height: 32px;
border-radius: 4px;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4em;
font-size: 1.1em;
font-weight: bold;
flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
border: 1.5px solid rgba(255, 255, 255, 0.3);
border: none;
box-shadow: none;
}
.direction-arrow-box.left {
background: repeating-linear-gradient(
45deg,
#fff5e6,
#fff5e6 4px,
#ffe6cc 4px,
#ffe6cc 8px
);
color: #ff6600;
border-color: #ff6600;
background: rgba(255, 140, 0, 0.2);
color: #ff9800;
}
.direction-arrow-box.right {
background: repeating-linear-gradient(
45deg,
#e6f2ff,
#e6f2ff 4px,
#cce6ff 4px,
#cce6ff 8px
);
color: #0066cc;
border-color: #0066cc;
}
body.dark-mode .direction-arrow-box.left {
background: repeating-linear-gradient(
45deg,
#664422,
#664422 4px,
#553311 4px,
#553311 8px
);
color: #ff8800;
border-color: #ff8800;
}
body.dark-mode .direction-arrow-box.right {
background: repeating-linear-gradient(
45deg,
#223366,
#223366 4px,
#112255 4px,
#112255 8px
);
background: rgba(79, 195, 247, 0.2);
color: var(--color-accent);
border-color: var(--color-accent);
}
/* Destination text */
@@ -755,20 +701,10 @@ body.dark-mode .direction-destination {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
min-width: 110px;
gap: 2px;
flex-shrink: 0;
max-width: 110px;
}
/* Time display */
.time-display {
display: flex;
align-items: baseline;
gap: 4px;
white-space: nowrap;
font-size: 0.95em;
}
/* Pulse animation for urgent and now states */
@keyframes pulse-glow {
@@ -838,19 +774,7 @@ body.landscape .countdown-large.soon {
text-shadow: 0 0 8px #f39c12, 0 0 12px #f39c12, 0 0 16px rgba(243, 156, 18, 0.8);
}
/* Time range (legacy) */
.time-range {
font-size: 0.85em;
color: var(--color-text-muted);
font-weight: 500;
white-space: nowrap;
}
body.dark-mode .time-range {
color: var(--color-text-muted-dark);
}
/* Next departures (replaces time-range) */
/* Next departures */
.next-departures {
font-size: 0.8em;
color: var(--color-text-muted);
@@ -1128,15 +1052,16 @@ body.landscape .site-header {
body.landscape .site-name {
display: block;
background: linear-gradient(135deg, #0061a1 0%, #003d66 100%);
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: 1px;
padding: 6px 16px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 97, 161, 0.4);
font-size: 0.85em;
letter-spacing: 2px;
padding: 5px 16px;
border-radius: 2px;
box-shadow: none;
font-size: 0.95em;
border-left: 4px solid var(--color-accent);
}
/* Compact weather bar - hidden by default, shown in landscape */
@@ -1149,7 +1074,7 @@ body.landscape #compact-weather-bar {
align-items: center;
justify-content: center;
gap: 16px;
background: rgba(0, 0, 0, 0.5);
background: var(--color-bar-bg);
color: #ddd;
padding: 6px 20px;
border-radius: 4px;
@@ -1173,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 {
@@ -1222,7 +1141,7 @@ body.landscape #news-ticker {
left: 0;
width: 100%;
height: 100%;
background-color: #191970;
background-color: var(--color-daylight-night);
background-image: var(--daylight-gradient, none);
}
@@ -1236,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;
@@ -1459,18 +1393,6 @@ body.dark-mode .config-search-input:focus-visible {
color: #ccc;
}
.site-search-result div:last-child {
color: #777;
}
body.dark-mode .site-search-result div:last-child {
color: #bbb;
}
body.dark-mode .time-range {
color: #bbb;
}
/* ========================================
Responsive Design - Departure cards
======================================== */
@@ -1501,14 +1423,11 @@ body.dark-mode .time-range {
max-width: 90px;
}
.time-display {
font-size: 0.85em;
}
.direction-arrow-box {
width: 26px;
height: 26px;
font-size: 1.1em;
width: 24px;
height: 24px;
font-size: 1em;
}
/* Weather responsive */
@@ -1594,13 +1513,9 @@ body.dark-mode .time-range {
max-width: 75px;
}
.time-range {
font-size: 0.75em;
}
.direction-arrow-box {
width: 22px;
height: 22px;
width: 20px;
height: 20px;
font-size: 0.9em;
}

View File

@@ -34,13 +34,24 @@
--ticker-height: 36px;
--ticker-speed: 30s;
--ticker-bg: rgba(0, 0, 0, 0.85);
--font-primary: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
--font-numbers: 'Segoe UI', 'Roboto Mono', 'SF Mono', 'Consolas', monospace;
--color-bg-kiosk: #1a1a2e;
--color-surface-kiosk: rgba(255, 255, 255, 0.08);
--color-surface-kiosk-hover: rgba(255, 255, 255, 0.12);
--color-text-secondary: #bbb;
--color-text-tertiary: #888;
--color-bar-bg: rgba(0, 0, 0, 0.5);
--color-daylight-night: #191970;
--color-daylight-dawn: #FF6B35;
--color-daylight-day: #FFEB3B;
}
/* ========================================
Base Styles
======================================== */
body {
font-family: Arial, sans-serif;
font-family: var(--font-primary);
margin: 0;
padding: 0;
background-color: var(--color-bg);
@@ -116,6 +127,7 @@ body {
margin: 0;
max-width: 100%;
}
}
/* ========================================
@@ -150,7 +162,7 @@ body.normal .departure-container {
body.landscape {
max-width: 100%;
padding: 10px 20px 0 20px;
background-color: #1a1a2e;
background-color: var(--color-bg-kiosk);
color: var(--color-text-light);
}
@@ -162,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;
@@ -173,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 {
@@ -185,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: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: var(--kiosk-gap);
display: flex;
gap: 12px;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
@@ -201,46 +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: 65px;
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.06);
}
body.landscape .line-number-box {
min-width: 90px;
width: 90px;
}
body.landscape .line-number-large {
font-size: 3.5em;
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;
}
@@ -254,37 +321,47 @@ body.landscape .countdown-large {
color: var(--color-text-light);
}
body.landscape .time-range,
body.landscape .next-departures {
color: #bbb;
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: var(--kiosk-countdown-size);
font-size: 1.5em;
}
body.landscape .times-container {
min-width: 130px;
max-width: 160px;
min-width: 90px;
max-width: 140px;
}
body.landscape .next-departures {
font-size: 0.7em;
color: #bbb;
font-size: 0.75em;
color: var(--color-text-secondary);
white-space: nowrap;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
}
/* ========================================

View File

@@ -119,16 +119,16 @@ class DeparturesManager {
if (diffMinutes <= 0) {
return 'Nu';
} else if (diffMinutes === 1) {
return 'In 1 minute';
return 'Om 1 minut';
} else if (diffMinutes < 60) {
return `In ${diffMinutes} minutes`;
return `Om ${diffMinutes} minuter`;
} else {
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
if (minutes === 0) {
return `In ${hours} hour${hours > 1 ? 's' : ''}`;
return `Om ${hours} timm${hours > 1 ? 'ar' : 'e'}`;
} else {
return `In ${hours} hour${hours > 1 ? 's' : ''} and ${minutes} minute${minutes > 1 ? 's' : ''}`;
return `Om ${hours} timm${hours > 1 ? 'ar' : 'e'} och ${minutes} minut${minutes > 1 ? 'er' : ''}`;
}
}
}
@@ -244,6 +244,8 @@ class DeparturesManager {
return { countdownText, countdownClass };
}
// --- Legacy single-site display methods (fallback if API returns data.departures instead of data.sites) ---
/**
* Create a departure card element (legacy format)
* @param {Object} departure - Departure object
@@ -359,33 +361,25 @@ class DeparturesManager {
timesContainer.className = 'times-container';
const firstDeparture = direction.departures[0];
const secondDeparture = direction.departures[1];
if (firstDeparture) {
const displayTime = firstDeparture.display;
const departureTime = this.getDepartureTime(firstDeparture);
const timeDisplay = DeparturesManager.formatDateTime(departureTime);
const { countdownText, countdownClass } = this.getCountdownInfo(firstDeparture);
const timeDisplayElement = document.createElement('div');
timeDisplayElement.className = 'time-display';
// Primary countdown - big and prominent
const countdownSpan = document.createElement('span');
countdownSpan.className = `countdown-large ${countdownClass}`;
countdownSpan.textContent = countdownText;
timesContainer.appendChild(countdownSpan);
timeDisplayElement.appendChild(countdownSpan);
// Show next 2-3 absolute times as small text
const nextTimesSpan = document.createElement('span');
nextTimesSpan.className = 'next-departures';
const upcomingTimes = direction.departures.slice(0, 3)
// Next departures - separate line, excludes current departure
const upcomingTimes = direction.departures.slice(1, 4)
.map(d => DeparturesManager.formatDateTime(this.getDepartureTime(d)));
nextTimesSpan.textContent = upcomingTimes.join(' ');
timeDisplayElement.appendChild(nextTimesSpan);
timesContainer.appendChild(timeDisplayElement);
if (upcomingTimes.length > 0) {
const nextTimesDiv = document.createElement('div');
nextTimesDiv.className = 'next-departures';
nextTimesDiv.textContent = 'Sedan: ' + upcomingTimes.join(' ');
timesContainer.appendChild(nextTimesDiv);
}
}
directionRow.appendChild(timesContainer);
@@ -397,8 +391,11 @@ class DeparturesManager {
});
}
// --- Legacy single-site display methods (continued) ---
/**
* Display departures in the UI
* Display departures in the UI (legacy single-site path)
* Called when API returns data.departures instead of data.sites.
* @param {Array} departures - Array of departure objects
*/
displayDepartures(departures) {
@@ -422,7 +419,7 @@ class DeparturesManager {
}
/**
* Update existing cards or add new ones
* Update existing cards or add new ones (legacy single-site path)
* @param {Array} newDepartures - Array of new departure objects
*/
updateExistingCards(newDepartures) {
@@ -468,7 +465,7 @@ class DeparturesManager {
}
/**
* Update card content
* Update card content (legacy single-site path)
* @param {HTMLElement} card - Card element
* @param {Object} departure - Departure object
*/
@@ -490,6 +487,8 @@ class DeparturesManager {
}
}
// --- End of legacy single-site display methods ---
/**
* Add highlight effect to element
* @param {HTMLElement} element - Element to highlight
@@ -509,32 +508,80 @@ class DeparturesManager {
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);
}
/**
@@ -578,13 +625,14 @@ class DeparturesManager {
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);
}
}
}

View File

@@ -5,6 +5,24 @@
*/
class WeatherManager {
static WEATHER_CONDITIONS_SV = {
'Clear': 'Klart',
'Clouds': 'Molnigt',
'Rain': 'Regn',
'Drizzle': 'Duggregn',
'Thunderstorm': 'Åska',
'Snow': 'Snö',
'Mist': 'Dimma',
'Fog': 'Dimma',
'Haze': 'Dis',
'Smoke': 'Rök',
'Dust': 'Damm',
'Sand': 'Sand',
'Ash': 'Aska',
'Squall': 'Byar',
'Tornado': 'Tornado'
};
constructor(options = {}) {
// Default options
// Get API key from options, window (injected by server from .env), or fallback
@@ -297,7 +315,7 @@ class WeatherManager {
const conditionElement = document.querySelector('#custom-weather .weather-icon div');
if (conditionElement) {
conditionElement.textContent = this.weatherData.condition;
conditionElement.textContent = WeatherManager.WEATHER_CONDITIONS_SV[this.weatherData.condition] || this.weatherData.condition;
}
const iconElement = document.querySelector('#custom-weather .weather-icon img');
@@ -364,7 +382,7 @@ class WeatherManager {
if (sunTimesElement && this.sunTimes) {
const sunriseTime = this.formatTime(this.sunTimes.today.sunrise);
const sunsetTime = this.formatTime(this.sunTimes.today.sunset);
sunTimesElement.textContent = `☀️ Sunrise: ${sunriseTime} | 🌙 Sunset: ${sunsetTime}`;
sunTimesElement.textContent = `☀️ Soluppgång: ${sunriseTime} | 🌙 Solnedgång: ${sunsetTime}`;
}
// Update daylight hours bar
@@ -381,7 +399,7 @@ class WeatherManager {
/**
* Render compact weather bar for landscape mode
* Shows: [icon] temp condition | Sunrise HH:MM | Sunset HH:MM
* Shows: [icon] temp condition 💨 wind | ☀️ Sol ↑ HH:MM 🌙 Sol ↓ HH:MM
*/
renderCompactWeatherBar() {
const bar = document.getElementById('compact-weather-bar');
@@ -399,13 +417,21 @@ class WeatherManager {
const strong = document.createElement('strong');
strong.textContent = `${this.weatherData.temperature}\u00B0C`;
tempSpan.appendChild(strong);
tempSpan.appendChild(document.createTextNode(` ${this.weatherData.condition || ''}`));
const conditionSv = WeatherManager.WEATHER_CONDITIONS_SV[this.weatherData.condition] || this.weatherData.condition;
tempSpan.appendChild(document.createTextNode(` ${conditionSv || ''}`));
bar.appendChild(tempSpan);
const sep1 = document.createElement('span');
sep1.className = 'weather-bar-sep';
sep1.textContent = '|';
bar.appendChild(sep1);
// Wind speed
if (this.weatherData.wind) {
const windSpan = document.createElement('span');
windSpan.textContent = `💨 ${this.weatherData.wind.speed} km/h`;
bar.appendChild(windSpan);
}
const sep = document.createElement('span');
sep.className = 'weather-bar-sep';
sep.textContent = '|';
bar.appendChild(sep);
let sunriseStr = '--:--';
let sunsetStr = '--:--';
@@ -413,19 +439,9 @@ class WeatherManager {
sunriseStr = this.formatTime(this.sunTimes.today.sunrise);
sunsetStr = this.formatTime(this.sunTimes.today.sunset);
}
const sunriseSpan = document.createElement('span');
sunriseSpan.textContent = `\u2600\uFE0F Sunrise ${sunriseStr}`;
bar.appendChild(sunriseSpan);
const sep2 = document.createElement('span');
sep2.className = 'weather-bar-sep';
sep2.textContent = '|';
bar.appendChild(sep2);
const sunsetSpan = document.createElement('span');
sunsetSpan.textContent = `\uD83C\uDF19 Sunset ${sunsetStr}`;
bar.appendChild(sunsetSpan);
const sunSpan = document.createElement('span');
sunSpan.textContent = `☀️ Sol ↑ ${sunriseStr} 🌙 Sol ↓ ${sunsetStr}`;
bar.appendChild(sunSpan);
}
/**
@@ -698,7 +714,17 @@ class WeatherManager {
}
// Position current hour indicator
// 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:', {

View File

@@ -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 => {