diff --git a/docs/weerscherm-spec.md b/docs/weerscherm-spec.md index 29c98d0..6b9b5c5 100644 --- a/docs/weerscherm-spec.md +++ b/docs/weerscherm-spec.md @@ -252,6 +252,10 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`, - Daaronder de gradiëntbalk van `--cold` naar `--warm`. De marker staat op `(temp − min) / (max − min)`, begrensd op 0–100%. Bij `max == min` staat hij op 50%. - Onder de balk: "9,2° laagst om 06:10" en "19,8° hoogst om 13:05". - **Binnen (rechts):** 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in `--muted`. + _Toegevoegd 2026-09-13: een module is tikbaar. Een tik zet die module bovenaan de lijst (de overige twee + behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken + op de bovenste (vastgezette) module maakt hem weer los. De keuze blijft staan bij de 5-minuten-verversing en + reset bij een volledige herlaad. Zie `pinRoom()` in `public/format.js`._ - **Regen (linksonder):** - "Vandaag" groot, in mm met 1 decimaal. - Daarnaast 4 staafjes: de 3 vorige dagen (labels "Di", "Wo", "Do") en vandaag, uitgelicht. De hoogte is `mm / max(4 waarden) × 70px`, met een minimum van 2px. Zijn alle waarden 0, dan zijn alle staafjes 2px. diff --git a/public/app.js b/public/app.js index 903dfe3..12dfe15 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ import { f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel, formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil, co2Level, fPct, fHpa, seriesRange, sparklinePath, - windArrowDeg, fKmh, formatHour, formatFullDate, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, } from './format.js'; import { icon, windArrow } from './icons.js'; @@ -22,10 +22,11 @@ const stage = $('stage'); const viewport = $('viewport'); const state = { - data: null, // laatste payload, ook als die uit de SW-cache komt + data: null, // laatste payload, ook als die uit de SW-cache komt tz: FALLBACK_TZ, - lastOk: null, // Date van de laatste geslaagde live-fetch - offlineSince: null // Date vanaf wanneer er geen verbinding is + lastOk: null, // Date van de laatste geslaagde live-fetch + offlineSince: null, // Date vanaf wanneer er geen verbinding is + pinnedRoomId: null, // id van de module die vastgezet is bovenaan; blijft staan tot opnieuw getikt of een volledige herlaad }; /* ---------- schaal ---------- */ @@ -71,11 +72,13 @@ function renderOutdoor(o) { } function renderRooms(rooms) { - const html = rooms.map((r) => { + const ordered = pinRoom(rooms, state.pinnedRoomId); + const html = ordered.map((r) => { const off = !r.reachable || r.temp === null; const co2 = co2Level(off ? null : r.co2); const pill = `${co2.level === 'unknown' ? '–' : r.co2 + ' ppm'}`; - return `
+ const pinned = r.id === state.pinnedRoomId; + return `

${esc(r.name)}${pill}

${off ? '–' : f1(r.temp) + '°'}

↓ ${off ? '–' : f1(r.min) + '°'}↑ ${off ? '–' : f1(r.max) + '°'}RV ${off ? '–' : fPct(r.humidity)}

@@ -318,6 +321,13 @@ $('forecast').addEventListener('click', (e) => { }); $('hour-overlay').addEventListener('click', closeHourOverlay); +$('rooms').addEventListener('click', (e) => { + const room = e.target.closest('.a-room'); + if (!room || !state.data) return; + state.pinnedRoomId = state.pinnedRoomId === room.dataset.id ? null : room.dataset.id; + renderRooms(state.data.indoor); +}); + window.addEventListener('resize', fit); window.addEventListener('orientationchange', () => setTimeout(fit, 50)); document.addEventListener('visibilitychange', () => { diff --git a/public/format.js b/public/format.js index d823be5..d2c1e11 100644 --- a/public/format.js +++ b/public/format.js @@ -205,3 +205,17 @@ export function formatFullDate(dateStr, tz) { if (Number.isNaN(d.getTime())) return DASH; return formatDate(d, tz); } + +/** + * Herschikt kamers zodat de kamer met `pinnedId` vooraan staat; de rest behoudt de onderlinge volgorde. + * Zonder pinnedId, of als die niet voorkomt of al vooraan staat, blijft de oorspronkelijke array (referentie) terug. + */ +export function pinRoom(rooms, pinnedId) { + if (!pinnedId) return rooms; + const index = rooms.findIndex((r) => r.id === pinnedId); + if (index <= 0) return rooms; + const copy = rooms.slice(); + const [pinned] = copy.splice(index, 1); + copy.unshift(pinned); + return copy; +} diff --git a/public/styles.css b/public/styles.css index 05c08ad..798f6c8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -108,8 +108,9 @@ html, body { .pill.medium { background: var(--warm); } .pill.bad { background: var(--bad); } .pill.unknown { background: var(--line); color: var(--muted); } -.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; } +.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; transition: font-size .25s ease; } .a-room.off .a-rtemp { color: var(--muted); } +.a-room.pinned .a-rtemp { font-size: 92px; } .a-rmm { font-size: 17px; display: flex; gap: 18px; } .a-rmm .rv { color: var(--muted); } diff --git a/public/sw.js b/public/sw.js index 042f5d9..f5ab7c7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ -const VERSION = 'weerscherm-v5'; +const VERSION = 'weerscherm-v6'; const SHELL = [ './', './index.html', diff --git a/tests/js/format.test.js b/tests/js/format.test.js index ed81746..28d8cda 100644 --- a/tests/js/format.test.js +++ b/tests/js/format.test.js @@ -121,7 +121,7 @@ test('msUntil computes the wait until the next local time', () => { import { co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints, - windArrowDeg, fKmh, formatHour, formatFullDate, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, } from '../../public/format.js'; test('co2Level uses the Netatmo thresholds', () => { @@ -187,3 +187,24 @@ test('formatFullDate matches formatDate for the same calendar date', () => { assert.equal(formatFullDate('2026-09-14', TZ), 'Maandag 14 september'); assert.equal(formatFullDate('kapot', TZ), '–'); }); + +test('pinRoom moves the matching room to the front, keeping the others in order', () => { + const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]; + assert.deepEqual(pinRoom(rooms, 'b').map((r) => r.id), ['b', 'a', 'c']); + assert.deepEqual(pinRoom(rooms, 'c').map((r) => r.id), ['c', 'a', 'b']); +}); + +test('pinRoom returns the original array unchanged without a match, id, or when already first', () => { + const rooms = [{ id: 'a' }, { id: 'b' }]; + assert.equal(pinRoom(rooms, null), rooms); + assert.equal(pinRoom(rooms, undefined), rooms); + assert.equal(pinRoom(rooms, 'x'), rooms, 'onbekend id: geen match, ongewijzigd terug'); + assert.equal(pinRoom(rooms, 'a'), rooms, 'al vooraan: geen kopie nodig'); +}); + +test('pinRoom does not mutate the input array', () => { + const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]; + const original = rooms.slice(); + pinRoom(rooms, 'c'); + assert.deepEqual(rooms, original); +});