feat(web): board and calendar views, saved filters and label pages

The board is one column per section with native drag-and-drop, which
works with a trackpad, a mouse and a screen reader without any of them
being reimplemented. Dropping is optimistic: a card that snaps back for
200ms reads as a failed drop.

Weeks in the calendar start on Monday. That is ISO 8601 and what every
Danish calendar does; a Sunday-first grid makes a Dane misread the month
at a glance. On a phone the chips give way to coloured bars — at that
width the month's shape is the useful part, not the titles.

Label pages run the filter language rather than a bespoke endpoint, so a
label page and a saved filter can never disagree about what a label
selects.

fix(recurrence): "hver mandag", not "hver uge mandag"

A weekly rule that names its days does not also need the word for the
interval. That is how the phrase is said, and it is what the parser
accepts back — the round trip through Describe now holds for weekday
rules too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 07:10:51 +02:00
co-authored by Claude Opus 5
parent 7e4c730049
commit 0ee72d399c
11 changed files with 867 additions and 31 deletions
+4 -2
View File
@@ -270,8 +270,9 @@ func TestDescribe(t *testing.T) {
"FREQ=DAILY;INTERVAL=3": "hver 3. dag",
"FREQ=WEEKLY": "hver uge",
"FREQ=WEEKLY;INTERVAL=2": "hver anden uge",
"FREQ=WEEKLY;BYDAY=MO": "hver uge mandag",
"FREQ=WEEKLY;BYDAY=MO,TH": "hver uge mandag og torsdag",
"FREQ=WEEKLY;BYDAY=MO": "hver mandag",
"FREQ=WEEKLY;BYDAY=MO,TH": "hver mandag og torsdag",
"FREQ=WEEKLY;BYDAY=FR;INTERVAL=2": "hver anden uge fredag",
"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR": "hverdage",
"FREQ=WEEKLY;BYDAY=SA,SU": "hver weekend",
"FREQ=MONTHLY": "hver måned",
@@ -293,6 +294,7 @@ func TestDescribeRoundTrip(t *testing.T) {
phrases := []string{
"hver dag", "hver anden dag", "hver uge", "hver anden uge",
"hverdage", "hver weekend", "hver måned", "hvert år",
"hver mandag", "hver mandag og torsdag",
}
for _, phrase := range phrases {
t.Run(phrase, func(t *testing.T) {