feat(upcoming): look up to 30 days ahead, and choose how far

Kommendes liste has always been the next seven days and nothing else. Seven days
is the right walk to take in the morning, and it is still what the view opens on
— but "when does this month get busy" is a different question, and the month grid
answers it with shape rather than in order. So the list now takes a horizon: 7,
14 or 30 days, chosen with a switcher beside the view buttons.

Only the list gets one. A week grid is a week and a month grid is a month; there
is no fortnight for them to show, so the switcher is hidden in those modes.

The choice lives in localStorage next to the mode, under its own key. It is the
same kind of choice as list-or-grid — how you want to look, not a fact about the
work — and it should survive a reload without following you to a phone where
thirty day headings is a very long scroll.

Once the strip is longer than a week the day headings carry the week number.
Thirty days repeat every weekday four times, and "mandag 6. okt" alone does not
say which mandag.

Server side the endpoint already took `?days=` and capped it at 31, so the window
needed nothing — but its row limit was a constant 1000 written for a week. A
month asked for with a week's ceiling would truncate the last days silently, and
a day cut off the end of the answer looks exactly like a day with nothing due, so
the limit now follows the window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SA1TDmgQZw3AufJqkbwezx
This commit is contained in:
kristianwind
2026-09-07 14:46:49 +02:00
co-authored by Claude Opus 5
parent f60758b770
commit 094ea5cdd8
9 changed files with 202 additions and 20 deletions
+36 -12
View File
@@ -865,8 +865,8 @@ class SidebarLayout {
export const sidebar = new SidebarLayout();
/**
* Whether Kommende shows the next seven days as a list, one week as a grid, or the
* whole month.
* Whether Kommende shows a run of days as a list, one week as a grid, or the whole
* month.
*
* In localStorage rather than on the account, and for the same reason as the
* sidebar's width: a grid of seven columns needs room, and the answer on a phone is
@@ -878,24 +878,48 @@ export const sidebar = new SidebarLayout();
*/
const UPCOMING_MODES = ['list', 'week', 'calendar'];
/**
* How far the list looks ahead, in days.
*
* Seven days is a week's horizon and answers "what is coming"; thirty answers
* "when does this month get busy", which is the question a month grid answers by
* shape but not in order. The list is the only mode that takes a horizon — the
* grids are anchored to a week and a month, and a fortnight is neither.
*
* The server caps the window at 31 days, so nothing here may exceed that.
*/
export const UPCOMING_DAYS = [7, 14, 30];
class UpcomingView {
mode = $state(read());
mode = $state(readChoice('verdande:upcoming', UPCOMING_MODES, 'list'));
days = $state(readChoice('verdande:upcoming-days', UPCOMING_DAYS, 7, Number));
set(next) {
if (!UPCOMING_MODES.includes(next)) return;
this.mode = next;
try {
localStorage.setItem('verdande:upcoming', next);
} catch {
// Private browsing; the choice simply will not persist.
}
writeChoice('verdande:upcoming', next);
}
setDays(next) {
const n = Number(next);
if (!UPCOMING_DAYS.includes(n)) return;
this.days = n;
writeChoice('verdande:upcoming-days', n);
}
}
function read() {
if (typeof localStorage === 'undefined') return 'list';
const stored = localStorage.getItem('verdande:upcoming');
return UPCOMING_MODES.includes(stored) ? stored : 'list';
function readChoice(key, allowed, fallback, parse = String) {
if (typeof localStorage === 'undefined') return fallback;
const stored = parse(localStorage.getItem(key));
return allowed.includes(stored) ? stored : fallback;
}
function writeChoice(key, value) {
try {
localStorage.setItem(key, String(value));
} catch {
// Private browsing; the choice simply will not persist.
}
}
export const upcomingView = new UpcomingView();